entitysdk 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {entitysdk-0.3.0/src/entitysdk.egg-info → entitysdk-0.3.2}/PKG-INFO +5 -7
- {entitysdk-0.3.0 → entitysdk-0.3.2}/README.md +4 -6
- {entitysdk-0.3.0 → entitysdk-0.3.2}/examples/02_morphology.ipynb +10 -13
- entitysdk-0.3.2/src/entitysdk/__init__.py +6 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/client.py +63 -18
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/core.py +5 -29
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/mixin.py +1 -1
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/asset.py +2 -2
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/mtype.py +1 -1
- entitysdk-0.3.2/src/entitysdk/schemas/__init__.py +1 -0
- entitysdk-0.3.2/src/entitysdk/schemas/asset.py +13 -0
- entitysdk-0.3.2/src/entitysdk/schemas/base.py +13 -0
- entitysdk-0.3.2/src/entitysdk/utils/__init__.py +1 -0
- entitysdk-0.3.2/src/entitysdk/utils/asset.py +31 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2/src/entitysdk.egg-info}/PKG-INFO +5 -7
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk.egg-info/SOURCES.txt +7 -1
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/data/electrical_cell_recording.json +8 -4
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/data/memodel_calibration_result.json +3 -2
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/data/validation_result.json +3 -2
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_electrical_cell_recording.py +0 -1
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_ion_channel_model.py +0 -1
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_memodel_calibration_result.py +0 -1
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_morphology.py +0 -1
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_validation_result.py +0 -1
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_client.py +180 -13
- entitysdk-0.3.2/tests/unit/utils/test_asset.py +78 -0
- entitysdk-0.3.0/src/entitysdk/__init__.py +0 -1
- {entitysdk-0.3.0 → entitysdk-0.3.2}/.github/workflows/sdist.yml +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/.github/workflows/tox.yml +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/.gitignore +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/CHANGELOG.rst +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/CONTRIBUTING.md +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/LICENSE.txt +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/examples/01_searching.ipynb +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/examples/03_contribution.ipynb +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/pyproject.toml +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/setup.cfg +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/common.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/config.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/exception.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/__init__.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/agent.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/base.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/brain_location.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/brain_region.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/contribution.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/core.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/electrical_cell_recording.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/emodel.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/entity.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/etype.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/ion_channel_model.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/license.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/memodel.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/memodelcalibrationresult.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/morphology.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/response.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/subject.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/taxonomy.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/validation_result.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/result.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/route.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/serdes.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/token_manager.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/types.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/util.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk.egg-info/dependency_links.txt +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk.egg-info/requires.txt +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk.egg-info/top_level.txt +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/__init__.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/integration/__init__.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/integration/conftest.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/integration/test_searching.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/__init__.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/conftest.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/__init__.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/data/.gitignore +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/data/ion_channel_model.json +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/data/reconstruction_morphology.json +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_agent.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_asset.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_brain_region.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_contribution.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_init.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_base.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_common.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_config.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_result.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_route.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_serdes.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_token_manager.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_util.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/util.py +0 -0
- {entitysdk-0.3.0 → entitysdk-0.3.2}/tox.ini +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: entitysdk
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.2
|
4
4
|
Summary: Python library for interacting with the entitycore service
|
5
5
|
Author-email: Open Brain Institute <info@openbraininstitute.org>
|
6
6
|
Maintainer-email: Open Brain Institute <info@openbraininstitute.org>
|
@@ -58,9 +58,7 @@ token = get_token(environment="staging")
|
|
58
58
|
|
59
59
|
```python
|
60
60
|
from uuid import UUID
|
61
|
-
from entitysdk
|
62
|
-
from entitysdk.common import ProjectContext
|
63
|
-
from entitysdk.models.morphology import ReconstructionMorphology
|
61
|
+
from entitysdk import Client, ProjectContext, models
|
64
62
|
|
65
63
|
# Initialize client
|
66
64
|
client = Client(
|
@@ -73,7 +71,7 @@ client = Client(
|
|
73
71
|
|
74
72
|
# Search for morphologies
|
75
73
|
iterator = client.search_entity(
|
76
|
-
entity_type=ReconstructionMorphology,
|
74
|
+
entity_type=models.ReconstructionMorphology,
|
77
75
|
query={"mtype__pref_label": "L5_TPC:A"},
|
78
76
|
token=token,
|
79
77
|
limit=1,
|
@@ -83,7 +81,7 @@ morphology = next(iterator)
|
|
83
81
|
# Upload an asset
|
84
82
|
client.upload_file(
|
85
83
|
entity_id=morphology.id,
|
86
|
-
entity_type=ReconstructionMorphology,
|
84
|
+
entity_type=models.ReconstructionMorphology,
|
87
85
|
file_path="path/to/file.swc",
|
88
86
|
file_content_type="application/swc",
|
89
87
|
token="your-token"
|
@@ -99,7 +97,7 @@ client.upload_file(
|
|
99
97
|
Example configuration:
|
100
98
|
```python
|
101
99
|
from uuid import UUID
|
102
|
-
from entitysdk
|
100
|
+
from entitysdk import ProjectContext
|
103
101
|
|
104
102
|
project_context = ProjectContext(
|
105
103
|
project_id=UUID("12345678-1234-1234-1234-123456789012"),
|
@@ -37,9 +37,7 @@ token = get_token(environment="staging")
|
|
37
37
|
|
38
38
|
```python
|
39
39
|
from uuid import UUID
|
40
|
-
from entitysdk
|
41
|
-
from entitysdk.common import ProjectContext
|
42
|
-
from entitysdk.models.morphology import ReconstructionMorphology
|
40
|
+
from entitysdk import Client, ProjectContext, models
|
43
41
|
|
44
42
|
# Initialize client
|
45
43
|
client = Client(
|
@@ -52,7 +50,7 @@ client = Client(
|
|
52
50
|
|
53
51
|
# Search for morphologies
|
54
52
|
iterator = client.search_entity(
|
55
|
-
entity_type=ReconstructionMorphology,
|
53
|
+
entity_type=models.ReconstructionMorphology,
|
56
54
|
query={"mtype__pref_label": "L5_TPC:A"},
|
57
55
|
token=token,
|
58
56
|
limit=1,
|
@@ -62,7 +60,7 @@ morphology = next(iterator)
|
|
62
60
|
# Upload an asset
|
63
61
|
client.upload_file(
|
64
62
|
entity_id=morphology.id,
|
65
|
-
entity_type=ReconstructionMorphology,
|
63
|
+
entity_type=models.ReconstructionMorphology,
|
66
64
|
file_path="path/to/file.swc",
|
67
65
|
file_content_type="application/swc",
|
68
66
|
token="your-token"
|
@@ -78,7 +76,7 @@ client.upload_file(
|
|
78
76
|
Example configuration:
|
79
77
|
```python
|
80
78
|
from uuid import UUID
|
81
|
-
from entitysdk
|
79
|
+
from entitysdk import ProjectContext
|
82
80
|
|
83
81
|
project_context = ProjectContext(
|
84
82
|
project_id=UUID("12345678-1234-1234-1234-123456789012"),
|
@@ -302,19 +302,16 @@
|
|
302
302
|
"metadata": {},
|
303
303
|
"outputs": [],
|
304
304
|
"source": [
|
305
|
-
"
|
306
|
-
"
|
307
|
-
"
|
308
|
-
"
|
309
|
-
"
|
310
|
-
"
|
311
|
-
"
|
312
|
-
"
|
313
|
-
"
|
314
|
-
"
|
315
|
-
" entity_id=fetched.id, entity_type=type(fetched), asset_id=asset.id, token=token\n",
|
316
|
-
" )\n",
|
317
|
-
" break\n",
|
305
|
+
"downloaded_asset = client.download_assets(\n",
|
306
|
+
" fetched.assets,\n",
|
307
|
+
" selection={\"content_type\": \"application/swc\"},\n",
|
308
|
+
" output_path=\"./my-file.h5\",\n",
|
309
|
+
" token=token,\n",
|
310
|
+
").one()\n",
|
311
|
+
"\n",
|
312
|
+
"content = client.download_content(\n",
|
313
|
+
" entity_id=fetched.id, entity_type=type(fetched), asset_id=downloaded_asset.id, token=token\n",
|
314
|
+
")\n",
|
318
315
|
"\n",
|
319
316
|
"print(content)\n",
|
320
317
|
"print(Path(\"my-file.h5\").read_text())"
|
@@ -3,6 +3,7 @@
|
|
3
3
|
import io
|
4
4
|
import os
|
5
5
|
from pathlib import Path
|
6
|
+
from typing import Any, cast
|
6
7
|
|
7
8
|
import httpx
|
8
9
|
|
@@ -11,7 +12,9 @@ from entitysdk.common import ProjectContext
|
|
11
12
|
from entitysdk.exception import EntitySDKError
|
12
13
|
from entitysdk.models.asset import Asset, LocalAssetMetadata
|
13
14
|
from entitysdk.models.core import Identifiable
|
15
|
+
from entitysdk.models.entity import Entity
|
14
16
|
from entitysdk.result import IteratorResult
|
17
|
+
from entitysdk.schemas.asset import DownloadedAsset
|
15
18
|
from entitysdk.token_manager import TokenManager
|
16
19
|
from entitysdk.types import ID, DeploymentEnvironment
|
17
20
|
from entitysdk.util import (
|
@@ -19,6 +22,7 @@ from entitysdk.util import (
|
|
19
22
|
create_intermediate_directories,
|
20
23
|
validate_filename_extension_consistency,
|
21
24
|
)
|
25
|
+
from entitysdk.utils.asset import filter_assets
|
22
26
|
|
23
27
|
|
24
28
|
class Client:
|
@@ -103,7 +107,6 @@ class Client:
|
|
103
107
|
entity_id: ID,
|
104
108
|
*,
|
105
109
|
entity_type: type[Identifiable],
|
106
|
-
with_assets: bool = True,
|
107
110
|
project_context: ProjectContext | None = None,
|
108
111
|
token: str | None = None,
|
109
112
|
) -> Identifiable:
|
@@ -126,28 +129,13 @@ class Client:
|
|
126
129
|
)
|
127
130
|
token = self._get_token(override_token=token)
|
128
131
|
context = self._optional_user_context(override_context=project_context)
|
129
|
-
|
132
|
+
return core.get_entity(
|
130
133
|
url=url,
|
131
134
|
token=token,
|
132
135
|
entity_type=entity_type,
|
133
136
|
project_context=context,
|
134
137
|
http_client=self._http_client,
|
135
138
|
)
|
136
|
-
if with_assets and "assets" in entity_type.model_fields:
|
137
|
-
url = route.get_assets_endpoint(
|
138
|
-
api_url=self.api_url,
|
139
|
-
entity_type=entity_type,
|
140
|
-
entity_id=entity_id,
|
141
|
-
)
|
142
|
-
assets = core.get_entity_assets(
|
143
|
-
url=url,
|
144
|
-
token=token,
|
145
|
-
project_context=context,
|
146
|
-
http_client=self._http_client,
|
147
|
-
)
|
148
|
-
entity = entity.evolve(assets=assets)
|
149
|
-
|
150
|
-
return entity
|
151
139
|
|
152
140
|
def search_entity(
|
153
141
|
self,
|
@@ -361,7 +349,7 @@ class Client:
|
|
361
349
|
output_path: os.PathLike,
|
362
350
|
project_context: ProjectContext | None = None,
|
363
351
|
token: str | None = None,
|
364
|
-
) ->
|
352
|
+
) -> Path:
|
365
353
|
"""Download asset file to a file path.
|
366
354
|
|
367
355
|
Args:
|
@@ -371,6 +359,9 @@ class Client:
|
|
371
359
|
output_path: Either be a file path to write the file to or an output directory.
|
372
360
|
project_context: Optional project context.
|
373
361
|
token: Authorization access token.
|
362
|
+
|
363
|
+
Returns:
|
364
|
+
Output file path.
|
374
365
|
"""
|
375
366
|
asset_endpoint = route.get_assets_endpoint(
|
376
367
|
api_url=self.api_url,
|
@@ -402,6 +393,60 @@ class Client:
|
|
402
393
|
http_client=self._http_client,
|
403
394
|
)
|
404
395
|
|
396
|
+
def download_assets(
|
397
|
+
self,
|
398
|
+
entity_or_id: Entity | tuple[ID, type[Entity]],
|
399
|
+
*,
|
400
|
+
selection: dict[str, Any] | None = None,
|
401
|
+
output_path: Path,
|
402
|
+
project_context: ProjectContext | None = None,
|
403
|
+
token: str | None = None,
|
404
|
+
) -> IteratorResult:
|
405
|
+
"""Download assets."""
|
406
|
+
|
407
|
+
def _download_entity_asset(asset):
|
408
|
+
if asset.is_directory:
|
409
|
+
raise NotImplementedError("Downloading asset directories is not supported yet.")
|
410
|
+
else:
|
411
|
+
path = self.download_file(
|
412
|
+
entity_id=entity.id,
|
413
|
+
entity_type=type(entity),
|
414
|
+
asset_id=asset.id,
|
415
|
+
output_path=output_path,
|
416
|
+
project_context=context,
|
417
|
+
token=token,
|
418
|
+
)
|
419
|
+
|
420
|
+
return DownloadedAsset(
|
421
|
+
asset=asset,
|
422
|
+
output_path=path,
|
423
|
+
)
|
424
|
+
|
425
|
+
token = self._get_token(override_token=token)
|
426
|
+
context = self._optional_user_context(override_context=project_context)
|
427
|
+
if isinstance(entity_or_id, tuple):
|
428
|
+
entity_id, entity_type = entity_or_id
|
429
|
+
entity = self.get_entity(
|
430
|
+
entity_id=entity_id,
|
431
|
+
entity_type=entity_type,
|
432
|
+
project_context=context,
|
433
|
+
token=token,
|
434
|
+
)
|
435
|
+
else:
|
436
|
+
entity = entity_or_id
|
437
|
+
|
438
|
+
if not issubclass(type(entity), Entity):
|
439
|
+
raise EntitySDKError(f"Type {type(entity)} has no assets.")
|
440
|
+
|
441
|
+
# make mypy happy as it doesn't get the correct type :(
|
442
|
+
entity = cast(Entity, entity)
|
443
|
+
|
444
|
+
if not entity.assets:
|
445
|
+
raise EntitySDKError(f"Entity {entity.id} ({entity.name}) has no assets.")
|
446
|
+
|
447
|
+
assets = filter_assets(entity.assets, selection) if selection else entity.assets
|
448
|
+
return IteratorResult(map(_download_entity_asset, assets))
|
449
|
+
|
405
450
|
def delete_asset(
|
406
451
|
self,
|
407
452
|
*,
|
@@ -76,34 +76,6 @@ def get_entity(
|
|
76
76
|
return serdes.deserialize_entity(response.json(), entity_type)
|
77
77
|
|
78
78
|
|
79
|
-
def get_entity_assets(
|
80
|
-
url: str,
|
81
|
-
*,
|
82
|
-
project_context: ProjectContext | None = None,
|
83
|
-
token: str,
|
84
|
-
http_client: httpx.Client | None = None,
|
85
|
-
) -> list[Asset]:
|
86
|
-
"""Get entity assets.
|
87
|
-
|
88
|
-
Args:
|
89
|
-
url: URL of the resource.
|
90
|
-
project_context: Project context.
|
91
|
-
token: Authorization access token.
|
92
|
-
http_client: HTTP client.
|
93
|
-
|
94
|
-
Returns:
|
95
|
-
List of assets.
|
96
|
-
"""
|
97
|
-
response = make_db_api_request(
|
98
|
-
url=url,
|
99
|
-
method="GET",
|
100
|
-
project_context=project_context,
|
101
|
-
token=token,
|
102
|
-
http_client=http_client,
|
103
|
-
)
|
104
|
-
return [serdes.deserialize_entity(asset, Asset) for asset in response.json()["data"]]
|
105
|
-
|
106
|
-
|
107
79
|
def register_entity(
|
108
80
|
url: str,
|
109
81
|
*,
|
@@ -211,7 +183,7 @@ def download_asset_file(
|
|
211
183
|
project_context: ProjectContext | None = None,
|
212
184
|
token: str,
|
213
185
|
http_client: httpx.Client | None = None,
|
214
|
-
) ->
|
186
|
+
) -> Path:
|
215
187
|
"""Download asset file to a file path.
|
216
188
|
|
217
189
|
Args:
|
@@ -220,6 +192,9 @@ def download_asset_file(
|
|
220
192
|
project_context: Project context.
|
221
193
|
token: Authorization access token.
|
222
194
|
http_client: HTTP client.
|
195
|
+
|
196
|
+
Returns:
|
197
|
+
Output file path.
|
223
198
|
"""
|
224
199
|
bytes_content = download_asset_content(
|
225
200
|
url=url,
|
@@ -228,6 +203,7 @@ def download_asset_file(
|
|
228
203
|
http_client=http_client,
|
229
204
|
)
|
230
205
|
output_path.write_bytes(bytes_content)
|
206
|
+
return output_path
|
231
207
|
|
232
208
|
|
233
209
|
def download_asset_content(
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Local to entitysdk schemas."""
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Utilities."""
|
@@ -0,0 +1,31 @@
|
|
1
|
+
"""Asset related utitilies."""
|
2
|
+
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from entitysdk.exception import EntitySDKError
|
6
|
+
from entitysdk.models.asset import Asset
|
7
|
+
|
8
|
+
|
9
|
+
def filter_assets(assets: list[Asset], selection: dict[str, Any]) -> list[Asset]:
|
10
|
+
"""Filter assets according to selection dictionary."""
|
11
|
+
if not assets:
|
12
|
+
return []
|
13
|
+
|
14
|
+
if not selection:
|
15
|
+
return assets
|
16
|
+
|
17
|
+
if not selection.keys() <= Asset.model_fields.keys():
|
18
|
+
raise EntitySDKError(
|
19
|
+
"Selection keys are not matching asset metadata keys.\n"
|
20
|
+
f"Selection: {sorted(selection.keys())}\n"
|
21
|
+
f"Available: {sorted(Asset.model_fields.keys())}"
|
22
|
+
)
|
23
|
+
|
24
|
+
def _selection_predicate(asset: Asset) -> bool:
|
25
|
+
attributes = vars(asset)
|
26
|
+
for key, value in selection.items():
|
27
|
+
if attributes[key] != value:
|
28
|
+
return False
|
29
|
+
return True
|
30
|
+
|
31
|
+
return [asset for asset in assets if _selection_predicate(asset)]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: entitysdk
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.2
|
4
4
|
Summary: Python library for interacting with the entitycore service
|
5
5
|
Author-email: Open Brain Institute <info@openbraininstitute.org>
|
6
6
|
Maintainer-email: Open Brain Institute <info@openbraininstitute.org>
|
@@ -58,9 +58,7 @@ token = get_token(environment="staging")
|
|
58
58
|
|
59
59
|
```python
|
60
60
|
from uuid import UUID
|
61
|
-
from entitysdk
|
62
|
-
from entitysdk.common import ProjectContext
|
63
|
-
from entitysdk.models.morphology import ReconstructionMorphology
|
61
|
+
from entitysdk import Client, ProjectContext, models
|
64
62
|
|
65
63
|
# Initialize client
|
66
64
|
client = Client(
|
@@ -73,7 +71,7 @@ client = Client(
|
|
73
71
|
|
74
72
|
# Search for morphologies
|
75
73
|
iterator = client.search_entity(
|
76
|
-
entity_type=ReconstructionMorphology,
|
74
|
+
entity_type=models.ReconstructionMorphology,
|
77
75
|
query={"mtype__pref_label": "L5_TPC:A"},
|
78
76
|
token=token,
|
79
77
|
limit=1,
|
@@ -83,7 +81,7 @@ morphology = next(iterator)
|
|
83
81
|
# Upload an asset
|
84
82
|
client.upload_file(
|
85
83
|
entity_id=morphology.id,
|
86
|
-
entity_type=ReconstructionMorphology,
|
84
|
+
entity_type=models.ReconstructionMorphology,
|
87
85
|
file_path="path/to/file.swc",
|
88
86
|
file_content_type="application/swc",
|
89
87
|
token="your-token"
|
@@ -99,7 +97,7 @@ client.upload_file(
|
|
99
97
|
Example configuration:
|
100
98
|
```python
|
101
99
|
from uuid import UUID
|
102
|
-
from entitysdk
|
100
|
+
from entitysdk import ProjectContext
|
103
101
|
|
104
102
|
project_context = ProjectContext(
|
105
103
|
project_id=UUID("12345678-1234-1234-1234-123456789012"),
|
@@ -50,6 +50,11 @@ src/entitysdk/models/response.py
|
|
50
50
|
src/entitysdk/models/subject.py
|
51
51
|
src/entitysdk/models/taxonomy.py
|
52
52
|
src/entitysdk/models/validation_result.py
|
53
|
+
src/entitysdk/schemas/__init__.py
|
54
|
+
src/entitysdk/schemas/asset.py
|
55
|
+
src/entitysdk/schemas/base.py
|
56
|
+
src/entitysdk/utils/__init__.py
|
57
|
+
src/entitysdk/utils/asset.py
|
53
58
|
tests/__init__.py
|
54
59
|
tests/integration/__init__.py
|
55
60
|
tests/integration/conftest.py
|
@@ -82,4 +87,5 @@ tests/unit/models/data/electrical_cell_recording.json
|
|
82
87
|
tests/unit/models/data/ion_channel_model.json
|
83
88
|
tests/unit/models/data/memodel_calibration_result.json
|
84
89
|
tests/unit/models/data/reconstruction_morphology.json
|
85
|
-
tests/unit/models/data/validation_result.json
|
90
|
+
tests/unit/models/data/validation_result.json
|
91
|
+
tests/unit/utils/test_asset.py
|
@@ -38,7 +38,8 @@
|
|
38
38
|
"update_date": "2025-05-17T15:43:33.885603Z",
|
39
39
|
"name": "Rattus norvegicus",
|
40
40
|
"taxonomy_id": "NCBITaxon:10116"
|
41
|
-
}
|
41
|
+
},
|
42
|
+
"assets": []
|
42
43
|
},
|
43
44
|
"brain_region": {
|
44
45
|
"creation_date": "2025-05-17T15:41:56.308111Z",
|
@@ -98,7 +99,8 @@
|
|
98
99
|
"description": "",
|
99
100
|
"injection_type": "current_clamp",
|
100
101
|
"shape": "step",
|
101
|
-
"authorized_public": false
|
102
|
+
"authorized_public": false,
|
103
|
+
"assets": []
|
102
104
|
},
|
103
105
|
{
|
104
106
|
"type": "electrical_recording_stimulus",
|
@@ -109,7 +111,8 @@
|
|
109
111
|
"description": "",
|
110
112
|
"injection_type": "current_clamp",
|
111
113
|
"shape": "step",
|
112
|
-
"authorized_public": false
|
114
|
+
"authorized_public": false,
|
115
|
+
"assets": []
|
113
116
|
},
|
114
117
|
{
|
115
118
|
"type": "electrical_recording_stimulus",
|
@@ -120,7 +123,8 @@
|
|
120
123
|
"description": "",
|
121
124
|
"injection_type": "current_clamp",
|
122
125
|
"shape": "step",
|
123
|
-
"authorized_public": false
|
126
|
+
"authorized_public": false,
|
127
|
+
"assets": []
|
124
128
|
}
|
125
129
|
]
|
126
130
|
}
|
@@ -3,5 +3,6 @@
|
|
3
3
|
"id" : "6fa52c8b-e9df-4f10-bdee-07c9af59a5ba",
|
4
4
|
"passed" : true,
|
5
5
|
"name" : "Simulatable Neuron Spiking Validation",
|
6
|
-
"validated_entity_id" : "54004e96-cef0-4f1c-8c89-d9cdf7b94e43"
|
7
|
-
|
6
|
+
"validated_entity_id" : "54004e96-cef0-4f1c-8c89-d9cdf7b94e43",
|
7
|
+
"assets": []
|
8
|
+
}
|
@@ -26,7 +26,6 @@ def test_read_ion_channel_model(client, httpx_mock, auth_token, json_ion_channel
|
|
26
26
|
entity_id=MOCK_UUID,
|
27
27
|
entity_type=IonChannelModel,
|
28
28
|
token=auth_token,
|
29
|
-
with_assets=False,
|
30
29
|
)
|
31
30
|
assert entity.model_dump(mode="json") == json_ion_channel_expanded | {"legacy_id": None}
|
32
31
|
|
@@ -28,7 +28,6 @@ def test_read_reconstruction_morphology(client, httpx_mock, auth_token, json_mor
|
|
28
28
|
entity_id=MOCK_UUID,
|
29
29
|
entity_type=ReconstructionMorphology,
|
30
30
|
token=auth_token,
|
31
|
-
with_assets=False,
|
32
31
|
)
|
33
32
|
assert entity.model_dump(mode="json") == json_morphology_expanded | {"legacy_id": None}
|
34
33
|
|
@@ -9,6 +9,7 @@ import pytest
|
|
9
9
|
from entitysdk.client import Client
|
10
10
|
from entitysdk.config import settings
|
11
11
|
from entitysdk.exception import EntitySDKError
|
12
|
+
from entitysdk.models import Asset, MTypeClass
|
12
13
|
from entitysdk.models.core import Identifiable
|
13
14
|
from entitysdk.models.entity import Entity
|
14
15
|
from entitysdk.types import DeploymentEnvironment
|
@@ -134,6 +135,13 @@ def test_client_nupdate(mocked_route, client, httpx_mock, auth_token):
|
|
134
135
|
assert res.name == new_name
|
135
136
|
|
136
137
|
|
138
|
+
def _mock_entity_response(entity_id):
|
139
|
+
return {
|
140
|
+
"id": str(entity_id),
|
141
|
+
"description": "my-entity",
|
142
|
+
}
|
143
|
+
|
144
|
+
|
137
145
|
def _mock_asset_response(asset_id):
|
138
146
|
return {
|
139
147
|
"id": str(asset_id),
|
@@ -423,32 +431,28 @@ def test_client_get(
|
|
423
431
|
asset_id1 = uuid.uuid4()
|
424
432
|
asset_id2 = uuid.uuid4()
|
425
433
|
|
426
|
-
class EntityWithAssets(Entity):
|
427
|
-
"""Entity plus assets."""
|
428
|
-
|
429
434
|
mock_route.return_value = "entity"
|
430
435
|
|
431
436
|
httpx_mock.add_response(
|
432
437
|
method="GET",
|
433
438
|
url=f"{api_url}/entity/{entity_id}",
|
434
439
|
match_headers=request_headers,
|
435
|
-
json={"id": str(entity_id), "name": "foo", "description": "bar", "type": "entity"},
|
436
|
-
)
|
437
|
-
httpx_mock.add_response(
|
438
|
-
method="GET",
|
439
|
-
url=f"{api_url}/entity/{entity_id}/assets",
|
440
|
-
match_headers=request_headers,
|
441
440
|
json={
|
442
|
-
"
|
443
|
-
"
|
441
|
+
"id": str(entity_id),
|
442
|
+
"name": "foo",
|
443
|
+
"description": "bar",
|
444
|
+
"type": "entity",
|
445
|
+
"assets": [
|
446
|
+
_mock_asset_response(asset_id1),
|
447
|
+
_mock_asset_response(asset_id2),
|
448
|
+
],
|
444
449
|
},
|
445
450
|
)
|
446
451
|
|
447
452
|
res = client.get_entity(
|
448
453
|
entity_id=str(entity_id),
|
449
|
-
entity_type=
|
454
|
+
entity_type=Entity,
|
450
455
|
token=auth_token,
|
451
|
-
with_assets=True,
|
452
456
|
)
|
453
457
|
assert res.id == entity_id
|
454
458
|
assert len(res.assets) == 2
|
@@ -547,3 +551,166 @@ def test_client_update_asset(
|
|
547
551
|
|
548
552
|
assert res.id == asset_id
|
549
553
|
assert res.status == "created"
|
554
|
+
|
555
|
+
|
556
|
+
def test_client_download_assets(
|
557
|
+
tmp_path, api_url, client, project_context, auth_token, request_headers, httpx_mock
|
558
|
+
):
|
559
|
+
entity_id = uuid.uuid4()
|
560
|
+
asset1_id = uuid.uuid4()
|
561
|
+
asset2_id = uuid.uuid4()
|
562
|
+
|
563
|
+
httpx_mock.add_response(
|
564
|
+
method="GET",
|
565
|
+
url=f"{api_url}/entity/{entity_id}",
|
566
|
+
match_headers=request_headers,
|
567
|
+
json=_mock_entity_response(entity_id)
|
568
|
+
| {
|
569
|
+
"assets": [
|
570
|
+
_mock_asset_response(asset1_id)
|
571
|
+
| {"path": "foo/bar/bar.h5", "content_type": "application/hdf5"},
|
572
|
+
_mock_asset_response(asset2_id)
|
573
|
+
| {"path": "foo/bar/bar.swc", "content_type": "application/swc"},
|
574
|
+
]
|
575
|
+
},
|
576
|
+
)
|
577
|
+
httpx_mock.add_response(
|
578
|
+
method="GET",
|
579
|
+
url=f"{api_url}/entity/{entity_id}/assets/{asset2_id}",
|
580
|
+
match_headers=request_headers,
|
581
|
+
json=_mock_asset_response(asset2_id)
|
582
|
+
| {"path": "foo/bar/bar.swc", "content_type": "application/swc"},
|
583
|
+
)
|
584
|
+
httpx_mock.add_response(
|
585
|
+
method="GET",
|
586
|
+
url=f"{api_url}/entity/{entity_id}/assets/{asset2_id}/download",
|
587
|
+
match_headers=request_headers,
|
588
|
+
content=b"bar",
|
589
|
+
)
|
590
|
+
|
591
|
+
res = client.download_assets(
|
592
|
+
(entity_id, Entity),
|
593
|
+
selection={"content_type": "application/swc"},
|
594
|
+
output_path=tmp_path,
|
595
|
+
project_context=project_context,
|
596
|
+
token=auth_token,
|
597
|
+
).one()
|
598
|
+
|
599
|
+
assert res.asset.path == "foo/bar/bar.swc"
|
600
|
+
assert res.output_path == tmp_path / "foo/bar/bar.swc"
|
601
|
+
assert res.output_path.read_bytes() == b"bar"
|
602
|
+
|
603
|
+
|
604
|
+
def test_client_download_assets__no_assets_raise(
|
605
|
+
tmp_path, api_url, client, project_context, auth_token, request_headers, httpx_mock
|
606
|
+
):
|
607
|
+
entity_id = uuid.uuid4()
|
608
|
+
|
609
|
+
httpx_mock.add_response(
|
610
|
+
method="GET",
|
611
|
+
url=f"{api_url}/entity/{entity_id}",
|
612
|
+
match_headers=request_headers,
|
613
|
+
json=_mock_entity_response(entity_id) | {"assets": []},
|
614
|
+
)
|
615
|
+
|
616
|
+
with pytest.raises(EntitySDKError, match="has no assets"):
|
617
|
+
client.download_assets(
|
618
|
+
(entity_id, Entity),
|
619
|
+
selection={"content_type": "application/swc"},
|
620
|
+
output_path=tmp_path,
|
621
|
+
project_context=project_context,
|
622
|
+
token=auth_token,
|
623
|
+
).one()
|
624
|
+
|
625
|
+
|
626
|
+
def test_client_download_assets__non_entity(
|
627
|
+
tmp_path, api_url, client, project_context, auth_token, request_headers, httpx_mock
|
628
|
+
):
|
629
|
+
entity_id = uuid.uuid4()
|
630
|
+
|
631
|
+
httpx_mock.add_response(
|
632
|
+
method="GET",
|
633
|
+
url=f"{api_url}/mtype/{entity_id}",
|
634
|
+
match_headers=request_headers,
|
635
|
+
json=_mock_entity_response(entity_id) | {"pref_label": "foo", "definition": "bar"},
|
636
|
+
)
|
637
|
+
|
638
|
+
with pytest.raises(EntitySDKError, match="has no assets"):
|
639
|
+
client.download_assets(
|
640
|
+
(entity_id, MTypeClass),
|
641
|
+
selection={"content_type": "application/swc"},
|
642
|
+
output_path=tmp_path,
|
643
|
+
project_context=project_context,
|
644
|
+
token=auth_token,
|
645
|
+
).one()
|
646
|
+
|
647
|
+
|
648
|
+
def test_client_download_assets__directory_not_supported(
|
649
|
+
tmp_path, api_url, client, project_context, auth_token, request_headers, httpx_mock
|
650
|
+
):
|
651
|
+
entity_id = uuid.uuid4()
|
652
|
+
asset_id = uuid.uuid4()
|
653
|
+
|
654
|
+
httpx_mock.add_response(
|
655
|
+
method="GET",
|
656
|
+
url=f"{api_url}/entity/{entity_id}",
|
657
|
+
match_headers=request_headers,
|
658
|
+
json=_mock_entity_response(entity_id)
|
659
|
+
| {"assets": [_mock_asset_response(asset_id) | {"is_directory": True}]},
|
660
|
+
)
|
661
|
+
|
662
|
+
with pytest.raises(
|
663
|
+
NotImplementedError, match="Downloading asset directories is not supported yet."
|
664
|
+
):
|
665
|
+
client.download_assets(
|
666
|
+
(entity_id, Entity),
|
667
|
+
output_path=tmp_path,
|
668
|
+
project_context=project_context,
|
669
|
+
token=auth_token,
|
670
|
+
).one()
|
671
|
+
|
672
|
+
|
673
|
+
def test_client_download_assets__entity(
|
674
|
+
tmp_path, api_url, client, project_context, auth_token, request_headers, httpx_mock
|
675
|
+
):
|
676
|
+
entity_id = uuid.uuid4()
|
677
|
+
asset_id = uuid.uuid4()
|
678
|
+
|
679
|
+
entity = Entity(
|
680
|
+
id=entity_id,
|
681
|
+
name="foo",
|
682
|
+
description="bar",
|
683
|
+
assets=[
|
684
|
+
Asset(
|
685
|
+
id=asset_id,
|
686
|
+
path="foo.json",
|
687
|
+
full_path="/foo/asset1",
|
688
|
+
is_directory=False,
|
689
|
+
content_type="application/json",
|
690
|
+
size=1,
|
691
|
+
),
|
692
|
+
],
|
693
|
+
)
|
694
|
+
httpx_mock.add_response(
|
695
|
+
method="GET",
|
696
|
+
url=f"{api_url}/entity/{entity_id}/assets/{asset_id}",
|
697
|
+
match_headers=request_headers,
|
698
|
+
json=_mock_asset_response(asset_id)
|
699
|
+
| {"path": "foo.json", "content_type": "application/json"},
|
700
|
+
)
|
701
|
+
httpx_mock.add_response(
|
702
|
+
method="GET",
|
703
|
+
url=f"{api_url}/entity/{entity_id}/assets/{asset_id}/download",
|
704
|
+
match_headers=request_headers,
|
705
|
+
content=b"bar",
|
706
|
+
)
|
707
|
+
|
708
|
+
res = client.download_assets(
|
709
|
+
entity,
|
710
|
+
selection={"content_type": "application/json"},
|
711
|
+
output_path=tmp_path,
|
712
|
+
project_context=project_context,
|
713
|
+
token=auth_token,
|
714
|
+
).all()
|
715
|
+
|
716
|
+
assert len(res) == 1
|
@@ -0,0 +1,78 @@
|
|
1
|
+
import pytest
|
2
|
+
|
3
|
+
from entitysdk.exception import EntitySDKError
|
4
|
+
from entitysdk.models import Asset
|
5
|
+
from entitysdk.utils import asset as test_module
|
6
|
+
|
7
|
+
|
8
|
+
@pytest.fixture
|
9
|
+
def assets():
|
10
|
+
return [
|
11
|
+
Asset(
|
12
|
+
path="foo/asset1",
|
13
|
+
full_path="/foo/asset1",
|
14
|
+
is_directory=False,
|
15
|
+
content_type="application/json",
|
16
|
+
size=1,
|
17
|
+
),
|
18
|
+
Asset(
|
19
|
+
path="foo/asset2",
|
20
|
+
full_path="/foo/asset2",
|
21
|
+
is_directory=False,
|
22
|
+
content_type="application/csv",
|
23
|
+
size=1,
|
24
|
+
),
|
25
|
+
Asset(
|
26
|
+
path="foo/asset3",
|
27
|
+
full_path="/foo/asset3",
|
28
|
+
is_directory=False,
|
29
|
+
content_type="application/csv",
|
30
|
+
size=1,
|
31
|
+
),
|
32
|
+
]
|
33
|
+
|
34
|
+
|
35
|
+
def test_filter_assets__none(assets):
|
36
|
+
res = test_module.filter_assets(assets, selection={"content_type": "application/swc"})
|
37
|
+
assert res == []
|
38
|
+
|
39
|
+
|
40
|
+
def test_filter_assets__one(assets):
|
41
|
+
res = test_module.filter_assets(assets, selection={"content_type": "application/json"})
|
42
|
+
assert len(res) == 1
|
43
|
+
assert res[0].path == "foo/asset1"
|
44
|
+
|
45
|
+
|
46
|
+
def test_filter_assets__multiple_matches(assets):
|
47
|
+
res = test_module.filter_assets(assets, selection={"content_type": "application/csv"})
|
48
|
+
assert len(res) == 2
|
49
|
+
assert res[0].path == "foo/asset2"
|
50
|
+
assert res[1].path == "foo/asset3"
|
51
|
+
|
52
|
+
|
53
|
+
def test_filter_assets__empty_assets():
|
54
|
+
res = test_module.filter_assets([], selection={"content_type": "application/csv"})
|
55
|
+
assert res == []
|
56
|
+
|
57
|
+
|
58
|
+
def test_filter_assets__multiple_selections(assets):
|
59
|
+
res = test_module.filter_assets(
|
60
|
+
assets, selection={"content_type": "application/csv", "size": 1, "path": "foo/asset2"}
|
61
|
+
)
|
62
|
+
assert len(res) == 1
|
63
|
+
assert res[0].path == "foo/asset2"
|
64
|
+
|
65
|
+
|
66
|
+
def test_filter_assets__empty_selection(assets):
|
67
|
+
res = test_module.filter_assets(assets, selection={})
|
68
|
+
assert res == assets
|
69
|
+
|
70
|
+
|
71
|
+
def test_filter_assets__invalid_keys(assets):
|
72
|
+
with pytest.raises(EntitySDKError, match="Selection keys are not matching asset metadata keys"):
|
73
|
+
test_module.filter_assets(assets, selection={"foo": "bar"})
|
74
|
+
|
75
|
+
with pytest.raises(EntitySDKError, match="Selection keys are not matching asset metadata keys"):
|
76
|
+
test_module.filter_assets(
|
77
|
+
assets, selection={"content_type": "application/json", "foo": "bar"}
|
78
|
+
)
|
@@ -1 +0,0 @@
|
|
1
|
-
"""entitysdk."""
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|