Skip to content

Commit 04289c6

Browse files
Add support for admin endpoints (#128)
1 parent c36893a commit 04289c6

File tree

9 files changed

+364
-44
lines changed

9 files changed

+364
-44
lines changed

src/entitysdk/client.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
)
3939
from entitysdk.utils.asset import filter_assets
4040

41+
TEntity = TypeVar("TEntity", bound=Entity)
4142
TIdentifiable = TypeVar("TIdentifiable", bound=Identifiable)
4243

4344

@@ -112,13 +113,15 @@ def get_entity(
112113
*,
113114
entity_type: type[TIdentifiable],
114115
project_context: ProjectContext | None = None,
116+
admin: bool = False,
115117
) -> TIdentifiable:
116118
"""Get entity from resource id.
117119
118120
Args:
119121
entity_id: Resource id of the entity.
120122
entity_type: Type of the entity.
121123
project_context: Optional project context.
124+
admin: Whether to use the admin endpoint or not.
122125
123126
Returns:
124127
entity_type instantiated by deserializing the response.
@@ -127,8 +130,11 @@ def get_entity(
127130
api_url=self.api_url,
128131
entity_type=entity_type,
129132
entity_id=entity_id,
133+
admin=admin,
134+
)
135+
context = (
136+
self._optional_user_context(override_context=project_context) if not admin else None
130137
)
131-
context = self._optional_user_context(override_context=project_context)
132138
return core.get_entity(
133139
url=url,
134140
entity_type=entity_type,
@@ -208,13 +214,36 @@ def register_entity(
208214
token=self._token_manager.get_token(),
209215
)
210216

217+
def get_entity_assets(
218+
self,
219+
entity_id: ID,
220+
*,
221+
entity_type: type[TEntity],
222+
project_context: ProjectContext | None = None,
223+
admin: bool = False,
224+
) -> IteratorResult[Asset]:
225+
"""Get all assets of an entity."""
226+
context = (
227+
self._optional_user_context(override_context=project_context) if not admin else None
228+
)
229+
return core.get_entity_assets(
230+
api_url=self.api_url,
231+
entity_id=entity_id,
232+
entity_type=entity_type,
233+
project_context=context,
234+
http_client=self._http_client,
235+
token=self._token_manager.get_token(),
236+
admin=admin,
237+
)
238+
211239
def update_entity(
212240
self,
213241
entity_id: ID,
214242
entity_type: type[TIdentifiable],
215243
attrs_or_entity: dict | Identifiable,
216244
*,
217245
project_context: ProjectContext | None = None,
246+
admin: bool = False,
218247
) -> TIdentifiable:
219248
"""Update an entity.
220249
@@ -223,13 +252,17 @@ def update_entity(
223252
entity_type: Type of the entity.
224253
attrs_or_entity: Attributes or entity to update.
225254
project_context: Optional project context.
255+
admin: whether to use the admin endpoint or not.
226256
"""
227257
url = route.get_entities_endpoint(
228258
api_url=self.api_url,
229259
entity_type=entity_type,
230260
entity_id=entity_id,
261+
admin=admin,
262+
)
263+
context = (
264+
self._optional_user_context(override_context=project_context) if not admin else None
231265
)
232-
context = self._required_user_context(override_context=project_context)
233266
return core.update_entity(
234267
url=url,
235268
project_context=context,
@@ -239,6 +272,27 @@ def update_entity(
239272
token=self._token_manager.get_token(),
240273
)
241274

275+
def delete_entity(
276+
self,
277+
entity_id: ID,
278+
entity_type: type[Identifiable],
279+
*,
280+
admin: bool = False,
281+
) -> None:
282+
"""Delete an entity."""
283+
url = route.get_entities_endpoint(
284+
api_url=self.api_url,
285+
entity_type=entity_type,
286+
entity_id=entity_id,
287+
admin=admin,
288+
)
289+
core.delete_entity(
290+
url=url,
291+
entity_type=entity_type,
292+
http_client=self._http_client,
293+
token=self._token_manager.get_token(),
294+
)
295+
242296
def upload_file(
243297
self,
244298
*,
@@ -617,15 +671,19 @@ def delete_asset(
617671
entity_type: type[Identifiable],
618672
asset_id: ID,
619673
project_context: ProjectContext | None = None,
674+
admin: bool = False,
620675
) -> Asset:
621676
"""Delete an entity's asset."""
622677
url = route.get_assets_endpoint(
623678
api_url=self.api_url,
624679
entity_type=entity_type,
625680
entity_id=entity_id,
626681
asset_id=asset_id,
682+
admin=admin,
683+
)
684+
context = (
685+
self._required_user_context(override_context=project_context) if not admin else None
627686
)
628-
context = self._required_user_context(override_context=project_context)
629687
return core.delete_asset(
630688
url=url,
631689
project_context=context,

src/entitysdk/core.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from entitysdk.models.core import Identifiable
2222
from entitysdk.models.entity import Entity
2323
from entitysdk.result import IteratorResult
24-
from entitysdk.route import get_entity_derivations_endpoint
24+
from entitysdk.route import get_assets_endpoint, get_entity_derivations_endpoint
2525
from entitysdk.types import ID, AssetLabel, DerivationType
2626
from entitysdk.util import make_db_api_request, stream_paginated_request
2727

@@ -74,13 +74,15 @@ def get_entity(
7474
entity_type: type[TIdentifiable],
7575
project_context: ProjectContext | None = None,
7676
token: str,
77+
options: dict | None = None,
7778
http_client: httpx.Client | None = None,
7879
) -> TIdentifiable:
7980
"""Instantiate entity with model ``entity_type`` from resource id."""
8081
response = make_db_api_request(
8182
url=url,
8283
method="GET",
8384
json=None,
85+
parameters=options,
8486
project_context=project_context,
8587
token=token,
8688
http_client=http_client,
@@ -121,6 +123,36 @@ def get_entity_derivations(
121123
)
122124

123125

126+
def get_entity_assets(
127+
*,
128+
api_url: str,
129+
entity_id: ID,
130+
entity_type: type[Entity],
131+
project_context: ProjectContext | None,
132+
token: str,
133+
http_client: httpx.Client | None = None,
134+
admin: bool = False,
135+
):
136+
"""Get all assets of an entity."""
137+
url = get_assets_endpoint(
138+
api_url=api_url,
139+
entity_type=entity_type,
140+
entity_id=entity_id,
141+
asset_id=None,
142+
admin=admin,
143+
)
144+
response = make_db_api_request(
145+
url=url,
146+
method="GET",
147+
project_context=project_context,
148+
token=token,
149+
http_client=http_client,
150+
)
151+
return IteratorResult(
152+
serdes.deserialize_model(json_data, Asset) for json_data in response.json()["data"]
153+
)
154+
155+
124156
def register_entity(
125157
url: str,
126158
*,
@@ -148,7 +180,7 @@ def update_entity(
148180
*,
149181
entity_type: type[TIdentifiable],
150182
attrs_or_entity: dict | Identifiable,
151-
project_context: ProjectContext,
183+
project_context: ProjectContext | None,
152184
token: str,
153185
http_client: httpx.Client | None = None,
154186
) -> TIdentifiable:
@@ -172,6 +204,22 @@ def update_entity(
172204
return serdes.deserialize_model(json_data, entity_type)
173205

174206

207+
def delete_entity(
208+
url: str,
209+
*,
210+
entity_type: type[Identifiable],
211+
token: str,
212+
http_client: httpx.Client | None = None,
213+
):
214+
"""Delete entity."""
215+
make_db_api_request(
216+
url=url,
217+
method="DELETE",
218+
token=token,
219+
http_client=http_client,
220+
)
221+
222+
175223
def upload_asset_file(
176224
url: str,
177225
*,
@@ -372,7 +420,7 @@ def download_asset_content(
372420
def delete_asset(
373421
url: str,
374422
*,
375-
project_context: ProjectContext,
423+
project_context: ProjectContext | None,
376424
token: str,
377425
http_client: httpx.Client | None = None,
378426
) -> Asset:

src/entitysdk/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from entitysdk.models.ion_channel_model import IonChannelModel, NeuronBlock, UseIon
2424
from entitysdk.models.ion_channel_recording import IonChannelRecording
2525
from entitysdk.models.license import License
26+
from entitysdk.models.measurement_annotation import MeasurementAnnotation
2627
from entitysdk.models.memodel import MEModel
2728
from entitysdk.models.memodelcalibrationresult import MEModelCalibrationResult
2829
from entitysdk.models.mtype import MTypeClass
@@ -63,6 +64,7 @@
6364
"IonChannelModel",
6465
"IonChannelRecording",
6566
"License",
67+
"MeasurementAnnotation",
6668
"MEModel",
6769
"MEModelCalibrationResult",
6870
"MTypeClass",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Measurement annotation."""
2+
3+
from entitysdk.models.base import BaseModel
4+
from entitysdk.models.core import Identifiable
5+
from entitysdk.types import (
6+
ID,
7+
MeasurableEntity,
8+
MeasurementStatistic,
9+
MeasurementUnit,
10+
StructuralDomain,
11+
)
12+
13+
14+
class MeasurementItem(BaseModel):
15+
"""Measurement item."""
16+
17+
name: MeasurementStatistic | None
18+
unit: MeasurementUnit | None
19+
value: float | None
20+
21+
22+
class MeasurementKind(BaseModel):
23+
"""Measurement kind."""
24+
25+
structural_domain: StructuralDomain
26+
measurement_items: list[MeasurementItem]
27+
pref_label: str
28+
29+
30+
class MeasurementAnnotation(Identifiable):
31+
"""Measurement annotation."""
32+
33+
entity_id: ID
34+
entity_type: MeasurableEntity
35+
measurement_kinds: list[MeasurementKind]

src/entitysdk/route.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"IonChannelModel": "ion-channel-model",
3030
"IonChannelRecording": "ion-channel-recording",
3131
"License": "license",
32+
"MeasurementAnnotation": "measurement-annotation",
3233
"MEModel": "memodel",
3334
"MEModelCalibrationResult": "memodel-calibration-result",
3435
"MTypeClassification": "mtype-classification",
@@ -79,9 +80,14 @@ def get_entities_endpoint(
7980
api_url: str,
8081
entity_type: type[Identifiable],
8182
entity_id: str | ID | None = None,
83+
admin: bool = False,
8284
) -> str:
8385
"""Get the API endpoint for an entity type."""
8486
route_name = get_route_name(entity_type)
87+
88+
if admin:
89+
route_name = f"admin/{route_name}"
90+
8591
endpoint = route_name if entity_id is None else f"{route_name}/{entity_id}"
8692
return f"{api_url}/{endpoint}"
8793

@@ -103,6 +109,7 @@ def get_assets_endpoint(
103109
entity_type: type[Identifiable],
104110
entity_id: str | ID,
105111
asset_id: str | ID | None = None,
112+
admin: bool = False,
106113
) -> str:
107114
"""Return the endpoint for the assets of an entity.
108115
@@ -111,10 +118,13 @@ def get_assets_endpoint(
111118
entity_type: The type of the entity.
112119
entity_id: The ID of the entity.
113120
asset_id: The ID of the asset.
121+
admin: If true route is prefixed by admin, e.g. /admin/entity
114122
115123
Returns:
116124
The endpoint for the assets of an entity.
117125
"""
118-
base_url = get_entities_endpoint(api_url=api_url, entity_type=entity_type, entity_id=entity_id)
126+
base_url = get_entities_endpoint(
127+
api_url=api_url, entity_type=entity_type, entity_id=entity_id, admin=admin
128+
)
119129
asset_path = "assets" if asset_id is None else f"assets/{asset_id}"
120130
return f"{base_url}/{asset_path}"

src/entitysdk/types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
from entitysdk._server_schemas import EMCellMeshGenerationMethod as EMCellMeshGenerationMethod
3333
from entitysdk._server_schemas import EMCellMeshType as EMCellMeshType
3434
from entitysdk._server_schemas import EntityType as EntityType
35+
from entitysdk._server_schemas import MeasurableEntity as MeasurableEntity
36+
from entitysdk._server_schemas import MeasurementStatistic as MeasurementStatistic
37+
from entitysdk._server_schemas import MeasurementUnit as MeasurementUnit
3538
from entitysdk._server_schemas import ModifiedMorphologyMethodType as ModifiedMorphologyMethodType
3639
from entitysdk._server_schemas import PublicationType as PublicationType
3740
from entitysdk._server_schemas import Sex as Sex

tests/unit/conftest.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import uuid
2+
from typing import NamedTuple
23

34
import pytest
45

@@ -7,6 +8,11 @@
78
from tests.unit.util import PROJECT_ID, VIRTUAL_LAB_ID
89

910

11+
class Clients(NamedTuple):
12+
with_context: Client
13+
wout_context: Client
14+
15+
1016
@pytest.fixture(scope="session")
1117
def api_url():
1218
return "http://mock-host:8000"
@@ -46,6 +52,19 @@ def client(project_context, api_url, auth_token):
4652
return Client(api_url=api_url, project_context=project_context, token_manager=auth_token)
4753

4854

55+
@pytest.fixture
56+
def client_no_context(api_url, auth_token):
57+
return Client(api_url=api_url, token_manager=auth_token)
58+
59+
60+
@pytest.fixture
61+
def clients(client, client_no_context):
62+
return Clients(
63+
with_context=client,
64+
wout_context=client_no_context,
65+
)
66+
67+
4968
@pytest.fixture
5069
def random_uuid():
5170
return uuid.uuid4()

0 commit comments

Comments
 (0)