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.
Files changed (94) hide show
  1. {entitysdk-0.3.0/src/entitysdk.egg-info → entitysdk-0.3.2}/PKG-INFO +5 -7
  2. {entitysdk-0.3.0 → entitysdk-0.3.2}/README.md +4 -6
  3. {entitysdk-0.3.0 → entitysdk-0.3.2}/examples/02_morphology.ipynb +10 -13
  4. entitysdk-0.3.2/src/entitysdk/__init__.py +6 -0
  5. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/client.py +63 -18
  6. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/core.py +5 -29
  7. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/mixin.py +1 -1
  8. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/asset.py +2 -2
  9. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/mtype.py +1 -1
  10. entitysdk-0.3.2/src/entitysdk/schemas/__init__.py +1 -0
  11. entitysdk-0.3.2/src/entitysdk/schemas/asset.py +13 -0
  12. entitysdk-0.3.2/src/entitysdk/schemas/base.py +13 -0
  13. entitysdk-0.3.2/src/entitysdk/utils/__init__.py +1 -0
  14. entitysdk-0.3.2/src/entitysdk/utils/asset.py +31 -0
  15. {entitysdk-0.3.0 → entitysdk-0.3.2/src/entitysdk.egg-info}/PKG-INFO +5 -7
  16. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk.egg-info/SOURCES.txt +7 -1
  17. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/data/electrical_cell_recording.json +8 -4
  18. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/data/memodel_calibration_result.json +3 -2
  19. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/data/validation_result.json +3 -2
  20. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_electrical_cell_recording.py +0 -1
  21. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_ion_channel_model.py +0 -1
  22. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_memodel_calibration_result.py +0 -1
  23. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_morphology.py +0 -1
  24. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_validation_result.py +0 -1
  25. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_client.py +180 -13
  26. entitysdk-0.3.2/tests/unit/utils/test_asset.py +78 -0
  27. entitysdk-0.3.0/src/entitysdk/__init__.py +0 -1
  28. {entitysdk-0.3.0 → entitysdk-0.3.2}/.github/workflows/sdist.yml +0 -0
  29. {entitysdk-0.3.0 → entitysdk-0.3.2}/.github/workflows/tox.yml +0 -0
  30. {entitysdk-0.3.0 → entitysdk-0.3.2}/.gitignore +0 -0
  31. {entitysdk-0.3.0 → entitysdk-0.3.2}/CHANGELOG.rst +0 -0
  32. {entitysdk-0.3.0 → entitysdk-0.3.2}/CONTRIBUTING.md +0 -0
  33. {entitysdk-0.3.0 → entitysdk-0.3.2}/LICENSE.txt +0 -0
  34. {entitysdk-0.3.0 → entitysdk-0.3.2}/examples/01_searching.ipynb +0 -0
  35. {entitysdk-0.3.0 → entitysdk-0.3.2}/examples/03_contribution.ipynb +0 -0
  36. {entitysdk-0.3.0 → entitysdk-0.3.2}/pyproject.toml +0 -0
  37. {entitysdk-0.3.0 → entitysdk-0.3.2}/setup.cfg +0 -0
  38. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/common.py +0 -0
  39. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/config.py +0 -0
  40. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/exception.py +0 -0
  41. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/__init__.py +0 -0
  42. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/agent.py +0 -0
  43. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/base.py +0 -0
  44. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/brain_location.py +0 -0
  45. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/brain_region.py +0 -0
  46. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/contribution.py +0 -0
  47. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/core.py +0 -0
  48. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/electrical_cell_recording.py +0 -0
  49. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/emodel.py +0 -0
  50. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/entity.py +0 -0
  51. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/etype.py +0 -0
  52. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/ion_channel_model.py +0 -0
  53. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/license.py +0 -0
  54. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/memodel.py +0 -0
  55. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/memodelcalibrationresult.py +0 -0
  56. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/morphology.py +0 -0
  57. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/response.py +0 -0
  58. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/subject.py +0 -0
  59. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/taxonomy.py +0 -0
  60. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/models/validation_result.py +0 -0
  61. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/result.py +0 -0
  62. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/route.py +0 -0
  63. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/serdes.py +0 -0
  64. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/token_manager.py +0 -0
  65. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/types.py +0 -0
  66. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk/util.py +0 -0
  67. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk.egg-info/dependency_links.txt +0 -0
  68. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk.egg-info/requires.txt +0 -0
  69. {entitysdk-0.3.0 → entitysdk-0.3.2}/src/entitysdk.egg-info/top_level.txt +0 -0
  70. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/__init__.py +0 -0
  71. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/integration/__init__.py +0 -0
  72. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/integration/conftest.py +0 -0
  73. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/integration/test_searching.py +0 -0
  74. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/__init__.py +0 -0
  75. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/conftest.py +0 -0
  76. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/__init__.py +0 -0
  77. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/data/.gitignore +0 -0
  78. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/data/ion_channel_model.json +0 -0
  79. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/data/reconstruction_morphology.json +0 -0
  80. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_agent.py +0 -0
  81. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_asset.py +0 -0
  82. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_brain_region.py +0 -0
  83. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_contribution.py +0 -0
  84. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/models/test_init.py +0 -0
  85. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_base.py +0 -0
  86. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_common.py +0 -0
  87. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_config.py +0 -0
  88. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_result.py +0 -0
  89. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_route.py +0 -0
  90. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_serdes.py +0 -0
  91. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_token_manager.py +0 -0
  92. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/test_util.py +0 -0
  93. {entitysdk-0.3.0 → entitysdk-0.3.2}/tests/unit/util.py +0 -0
  94. {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.0
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.client import Client
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.common import ProjectContext
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.client import Client
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.common import ProjectContext
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
- "for asset in fetched.assets:\n",
306
- " if asset.content_type == \"application/swc\":\n",
307
- " client.download_file(\n",
308
- " entity_id=fetched.id,\n",
309
- " entity_type=type(fetched),\n",
310
- " asset_id=asset.id,\n",
311
- " output_path=\"./my-file.h5\",\n",
312
- " token=token,\n",
313
- " )\n",
314
- " content = client.download_content(\n",
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())"
@@ -0,0 +1,6 @@
1
+ """entitysdk."""
2
+
3
+ from entitysdk.client import Client
4
+ from entitysdk.common import ProjectContext
5
+
6
+ __all__ = ["Client", "ProjectContext"]
@@ -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
- entity = core.get_entity(
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
- ) -> None:
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
- ) -> None:
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(
@@ -8,4 +8,4 @@ from entitysdk.models.asset import Asset
8
8
  class HasAssets(BaseModel):
9
9
  """Mixin class for entities that have assets."""
10
10
 
11
- assets: list[Asset] | None = None
11
+ assets: list[Asset] = []
@@ -56,9 +56,9 @@ class Asset(Identifiable):
56
56
  ),
57
57
  ] = None
58
58
  meta: Annotated[
59
- dict | None,
59
+ dict,
60
60
  Field(description="Asset json metadata."),
61
- ] = None
61
+ ] = {}
62
62
 
63
63
 
64
64
  class LocalAssetMetadata(BaseModel):
@@ -25,4 +25,4 @@ class MTypeClass(Identifiable):
25
25
  alt_label: Annotated[
26
26
  str | None,
27
27
  Field(description="The alternative label of th mtype class."),
28
- ]
28
+ ] = None
@@ -0,0 +1 @@
1
+ """Local to entitysdk schemas."""
@@ -0,0 +1,13 @@
1
+ """Asset related schemas."""
2
+
3
+ from pathlib import Path
4
+
5
+ from entitysdk.models.asset import Asset
6
+ from entitysdk.schemas.base import Schema
7
+
8
+
9
+ class DownloadedAsset(Schema):
10
+ """Downloaded asset."""
11
+
12
+ asset: Asset
13
+ output_path: Path
@@ -0,0 +1,13 @@
1
+ """Base schema model."""
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+
6
+ class Schema(BaseModel):
7
+ """Base model."""
8
+
9
+ model_config = ConfigDict(
10
+ frozen=True,
11
+ from_attributes=True,
12
+ extra="forbid",
13
+ )
@@ -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.0
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.client import Client
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.common import ProjectContext
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
  }
@@ -4,5 +4,6 @@
4
4
  "holding_current" : 0.0,
5
5
  "threshold_current" : 0.06554153953301577,
6
6
  "rin" : 100.03205800593804,
7
- "calibrated_entity_id" : "54004e96-cef0-4f1c-8c89-d9cdf7b94e43"
8
- }
7
+ "calibrated_entity_id" : "54004e96-cef0-4f1c-8c89-d9cdf7b94e43",
8
+ "assets": []
9
+ }
@@ -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
+ }
@@ -28,7 +28,6 @@ def test_read(client, httpx_mock, auth_token, json_data):
28
28
  entity_id=MOCK_UUID,
29
29
  entity_type=Model,
30
30
  token=auth_token,
31
- with_assets=False,
32
31
  )
33
32
  assert entity.model_dump(mode="json", exclude_none=True) == json_data
34
33
 
@@ -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(client, httpx_mock, auth_token, json_data):
28
28
  entity_id=MOCK_UUID,
29
29
  entity_type=Model,
30
30
  token=auth_token,
31
- with_assets=False,
32
31
  )
33
32
  assert entity.model_dump(mode="json", exclude_none=True) == json_data
34
33
 
@@ -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
 
@@ -28,7 +28,6 @@ def test_read(client, httpx_mock, auth_token, json_data):
28
28
  entity_id=MOCK_UUID,
29
29
  entity_type=Model,
30
30
  token=auth_token,
31
- with_assets=False,
32
31
  )
33
32
  assert entity.model_dump(mode="json", exclude_none=True) == json_data
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
- "data": [_mock_asset_response(asset_id1), _mock_asset_response(asset_id2)],
443
- "pagination": {"page": 1, "page_size": 10, "total_items": 2},
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=EntityWithAssets,
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