entitysdk 0.6.2__tar.gz → 0.7.1__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 (149) hide show
  1. {entitysdk-0.6.2/src/entitysdk.egg-info → entitysdk-0.7.1}/PKG-INFO +1 -1
  2. {entitysdk-0.6.2 → entitysdk-0.7.1}/pyproject.toml +1 -0
  3. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/client.py +59 -30
  4. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/core.py +1 -1
  5. entitysdk-0.7.1/src/entitysdk/dependencies/__init__.py +1 -0
  6. entitysdk-0.7.1/src/entitysdk/dependencies/entity.py +18 -0
  7. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/downloaders/emodel.py +1 -1
  8. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/downloaders/ion_channel_model.py +1 -1
  9. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/downloaders/morphology.py +1 -1
  10. entitysdk-0.7.1/src/entitysdk/downloaders/simulation.py +78 -0
  11. entitysdk-0.7.1/src/entitysdk/downloaders/simulation_result.py +61 -0
  12. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/exception.py +8 -0
  13. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/asset.py +5 -2
  14. entitysdk-0.7.1/src/entitysdk/schemas/asset.py +20 -0
  15. entitysdk-0.7.1/src/entitysdk/staging/__init__.py +7 -0
  16. entitysdk-0.7.1/src/entitysdk/staging/circuit.py +44 -0
  17. entitysdk-0.7.1/src/entitysdk/staging/simulation.py +142 -0
  18. entitysdk-0.7.1/src/entitysdk/staging/simulation_result.py +72 -0
  19. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/types.py +6 -0
  20. entitysdk-0.7.1/src/entitysdk/utils/io.py +16 -0
  21. {entitysdk-0.6.2 → entitysdk-0.7.1/src/entitysdk.egg-info}/PKG-INFO +1 -1
  22. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk.egg-info/SOURCES.txt +24 -0
  23. entitysdk-0.7.1/tests/unit/dependencies/test_entity.py +27 -0
  24. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/downloaders/test_emodel.py +1 -0
  25. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/downloaders/test_ion_channel_model.py +1 -0
  26. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/downloaders/test_memodel.py +3 -0
  27. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/downloaders/test_morphology.py +1 -0
  28. entitysdk-0.7.1/tests/unit/models/data/.gitignore +0 -0
  29. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/data/electrical_cell_recording.json +1 -0
  30. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/data/ion_channel_model.json +2 -1
  31. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/data/reconstruction_morphology.json +2 -0
  32. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/test_asset.py +4 -2
  33. entitysdk-0.7.1/tests/unit/staging/__init__.py +0 -0
  34. entitysdk-0.7.1/tests/unit/staging/conftest.py +281 -0
  35. entitysdk-0.7.1/tests/unit/staging/data/SomaVoltRec 1.h5 +0 -0
  36. entitysdk-0.7.1/tests/unit/staging/data/SomaVoltRec 2.h5 +0 -0
  37. entitysdk-0.7.1/tests/unit/staging/data/circuit/circuit_config.json +36 -0
  38. entitysdk-0.7.1/tests/unit/staging/data/circuit/edges.h5 +0 -0
  39. entitysdk-0.7.1/tests/unit/staging/data/circuit/nodes.h5 +0 -0
  40. entitysdk-0.7.1/tests/unit/staging/data/node_sets.json +67 -0
  41. entitysdk-0.7.1/tests/unit/staging/data/simulation_config.json +128 -0
  42. entitysdk-0.7.1/tests/unit/staging/data/spike_replays.h5 +0 -0
  43. entitysdk-0.7.1/tests/unit/staging/data/spikes.h5 +0 -0
  44. entitysdk-0.7.1/tests/unit/staging/test_simulation.py +98 -0
  45. entitysdk-0.7.1/tests/unit/staging/test_simulation_result.py +106 -0
  46. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/test_client.py +79 -26
  47. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/utils/test_asset.py +8 -0
  48. entitysdk-0.6.2/src/entitysdk/schemas/asset.py +0 -13
  49. {entitysdk-0.6.2 → entitysdk-0.7.1}/.github/workflows/sdist.yml +0 -0
  50. {entitysdk-0.6.2 → entitysdk-0.7.1}/.github/workflows/tox.yml +0 -0
  51. {entitysdk-0.6.2 → entitysdk-0.7.1}/.gitignore +0 -0
  52. {entitysdk-0.6.2 → entitysdk-0.7.1}/CHANGELOG.rst +0 -0
  53. {entitysdk-0.6.2 → entitysdk-0.7.1}/CONTRIBUTING.md +0 -0
  54. {entitysdk-0.6.2 → entitysdk-0.7.1}/LICENSE.txt +0 -0
  55. {entitysdk-0.6.2 → entitysdk-0.7.1}/README.md +0 -0
  56. {entitysdk-0.6.2 → entitysdk-0.7.1}/examples/01_searching.ipynb +0 -0
  57. {entitysdk-0.6.2 → entitysdk-0.7.1}/examples/02_morphology.ipynb +0 -0
  58. {entitysdk-0.6.2 → entitysdk-0.7.1}/examples/03_circuit.ipynb +0 -0
  59. {entitysdk-0.6.2 → entitysdk-0.7.1}/examples/04_simulation_campaign.ipynb +0 -0
  60. {entitysdk-0.6.2 → entitysdk-0.7.1}/examples/utils.py +0 -0
  61. {entitysdk-0.6.2 → entitysdk-0.7.1}/setup.cfg +0 -0
  62. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/__init__.py +0 -0
  63. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/common.py +0 -0
  64. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/config.py +0 -0
  65. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/downloaders/__init__.py +0 -0
  66. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/downloaders/memodel.py +0 -0
  67. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/mixin.py +0 -0
  68. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/__init__.py +0 -0
  69. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/activity.py +0 -0
  70. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/agent.py +0 -0
  71. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/base.py +0 -0
  72. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/brain_location.py +0 -0
  73. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/brain_region.py +0 -0
  74. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/brain_region_hierarchy.py +0 -0
  75. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/circuit.py +0 -0
  76. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/classification.py +0 -0
  77. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/contribution.py +0 -0
  78. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/core.py +0 -0
  79. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/electrical_cell_recording.py +0 -0
  80. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/emodel.py +0 -0
  81. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/entity.py +0 -0
  82. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/etype.py +0 -0
  83. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/ion_channel_model.py +0 -0
  84. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/license.py +0 -0
  85. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/memodel.py +0 -0
  86. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/memodelcalibrationresult.py +0 -0
  87. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/morphology.py +0 -0
  88. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/mtype.py +0 -0
  89. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/response.py +0 -0
  90. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/scientific_artifact.py +0 -0
  91. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/simulation.py +0 -0
  92. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/simulation_campaign.py +0 -0
  93. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/simulation_execution.py +0 -0
  94. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/simulation_generation.py +0 -0
  95. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/simulation_result.py +0 -0
  96. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/single_neuron_simulation.py +0 -0
  97. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/single_neuron_synaptome_simulation.py +0 -0
  98. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/subject.py +0 -0
  99. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/synaptome.py +0 -0
  100. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/taxonomy.py +0 -0
  101. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/models/validation_result.py +0 -0
  102. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/result.py +0 -0
  103. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/route.py +0 -0
  104. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/schemas/__init__.py +0 -0
  105. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/schemas/base.py +0 -0
  106. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/schemas/memodel.py +0 -0
  107. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/serdes.py +0 -0
  108. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/token_manager.py +0 -0
  109. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/util.py +0 -0
  110. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/utils/__init__.py +0 -0
  111. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/utils/asset.py +0 -0
  112. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk/utils/filesystem.py +0 -0
  113. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk.egg-info/dependency_links.txt +0 -0
  114. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk.egg-info/requires.txt +0 -0
  115. {entitysdk-0.6.2 → entitysdk-0.7.1}/src/entitysdk.egg-info/top_level.txt +0 -0
  116. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/__init__.py +0 -0
  117. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/integration/__init__.py +0 -0
  118. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/integration/conftest.py +0 -0
  119. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/integration/test_searching.py +0 -0
  120. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/__init__.py +0 -0
  121. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/conftest.py +0 -0
  122. {entitysdk-0.6.2/tests/unit/models → entitysdk-0.7.1/tests/unit/dependencies}/__init__.py +0 -0
  123. /entitysdk-0.6.2/tests/unit/models/data/.gitignore → /entitysdk-0.7.1/tests/unit/models/__init__.py +0 -0
  124. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/data/circuit.json +0 -0
  125. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/data/memodel_calibration_result.json +0 -0
  126. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/data/simulation_campaign.json +0 -0
  127. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/data/validation_result.json +0 -0
  128. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/test_agent.py +0 -0
  129. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/test_brain_region.py +0 -0
  130. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/test_circuit.py +0 -0
  131. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/test_contribution.py +0 -0
  132. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/test_electrical_cell_recording.py +0 -0
  133. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/test_init.py +0 -0
  134. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/test_ion_channel_model.py +0 -0
  135. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/test_memodel_calibration_result.py +0 -0
  136. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/test_morphology.py +0 -0
  137. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/test_simulation_campaign.py +0 -0
  138. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/models/test_validation_result.py +0 -0
  139. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/test_base.py +0 -0
  140. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/test_common.py +0 -0
  141. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/test_config.py +0 -0
  142. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/test_result.py +0 -0
  143. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/test_route.py +0 -0
  144. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/test_serdes.py +0 -0
  145. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/test_token_manager.py +0 -0
  146. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/test_util.py +0 -0
  147. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/util.py +0 -0
  148. {entitysdk-0.6.2 → entitysdk-0.7.1}/tests/unit/utils/test_filesystem.py +0 -0
  149. {entitysdk-0.6.2 → entitysdk-0.7.1}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entitysdk
3
- Version: 0.6.2
3
+ Version: 0.7.1
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>
@@ -46,6 +46,7 @@ addopts = [
46
46
  [tool.ruff]
47
47
  line-length = 100
48
48
  target-version = "py311"
49
+ include = ["pyproject.toml", "src/**/*.py", "tests/**/*.py", "examples/**/*.py"]
49
50
 
50
51
  [tool.ruff.lint]
51
52
  select = [
@@ -1,9 +1,10 @@
1
1
  """Identifiable SDK client."""
2
2
 
3
+ import concurrent.futures
3
4
  import io
4
5
  import os
5
6
  from pathlib import Path
6
- from typing import Any, cast
7
+ from typing import Any, TypeVar, cast
7
8
 
8
9
  import httpx
9
10
 
@@ -14,7 +15,7 @@ from entitysdk.models.asset import Asset, DetailedFileList, LocalAssetMetadata
14
15
  from entitysdk.models.core import Identifiable
15
16
  from entitysdk.models.entity import Entity
16
17
  from entitysdk.result import IteratorResult
17
- from entitysdk.schemas.asset import DownloadedAsset
18
+ from entitysdk.schemas.asset import DownloadedAssetFile
18
19
  from entitysdk.token_manager import TokenFromValue, TokenManager
19
20
  from entitysdk.types import ID, DeploymentEnvironment, Token
20
21
  from entitysdk.util import (
@@ -24,6 +25,8 @@ from entitysdk.util import (
24
25
  )
25
26
  from entitysdk.utils.asset import filter_assets
26
27
 
28
+ TEntity = TypeVar("TEntity", bound=Entity)
29
+
27
30
 
28
31
  class Client:
29
32
  """Client for entitysdk."""
@@ -94,9 +97,9 @@ class Client:
94
97
  self,
95
98
  entity_id: ID,
96
99
  *,
97
- entity_type: type[Identifiable],
100
+ entity_type: type[TEntity],
98
101
  project_context: ProjectContext | None = None,
99
- ) -> Identifiable:
102
+ ) -> TEntity:
100
103
  """Get entity from resource id.
101
104
 
102
105
  Args:
@@ -214,7 +217,7 @@ class Client:
214
217
  file_content_type: str,
215
218
  file_name: str | None = None,
216
219
  file_metadata: dict | None = None,
217
- asset_label: str | None = None,
220
+ asset_label: str,
218
221
  project_context: ProjectContext | None = None,
219
222
  ) -> Asset:
220
223
  """Upload asset to an existing entity's endpoint from a file path."""
@@ -250,7 +253,7 @@ class Client:
250
253
  file_name: str,
251
254
  file_content_type: str,
252
255
  file_metadata: dict | None = None,
253
- asset_label: str | None = None,
256
+ asset_label: str,
254
257
  project_context: ProjectContext | None = None,
255
258
  ) -> Asset:
256
259
  """Upload asset to an existing entity's endpoint from a file-like object."""
@@ -284,7 +287,7 @@ class Client:
284
287
  name: str,
285
288
  paths: dict[os.PathLike, os.PathLike],
286
289
  metadata: dict | None = None,
287
- label: str | None = None,
290
+ label: str,
288
291
  project_context: ProjectContext | None = None,
289
292
  ) -> Asset:
290
293
  """Attach directory to an entity from with a group of paths."""
@@ -347,8 +350,9 @@ class Client:
347
350
  output_path: os.PathLike,
348
351
  project_context: ProjectContext | None = None,
349
352
  ignore_directory_name: bool = False,
353
+ max_concurrent: int = 1,
350
354
  ) -> list[Path]:
351
- """List directory existing entity's endpoint from a directory path."""
355
+ """Download directory of assets."""
352
356
  output_path = Path(output_path)
353
357
 
354
358
  if output_path.exists() and output_path.is_file():
@@ -357,34 +361,35 @@ class Client:
357
361
 
358
362
  context = self._optional_user_context(override_context=project_context)
359
363
 
360
- asset = None
364
+ asset = cast(Asset, asset_id) if isinstance(asset_id, Asset) else None
365
+
361
366
  if not ignore_directory_name:
362
- asset_endpoint = route.get_assets_endpoint(
363
- api_url=self.api_url,
364
- entity_type=entity_type,
365
- entity_id=entity_id,
366
- asset_id=asset_id,
367
- )
368
- asset = core.get_entity(
369
- asset_endpoint,
370
- entity_type=Asset,
371
- project_context=context,
372
- http_client=self._http_client,
373
- token=self._token_manager.get_token(),
374
- )
367
+ if asset is None:
368
+ asset_endpoint = route.get_assets_endpoint(
369
+ api_url=self.api_url,
370
+ entity_type=entity_type,
371
+ entity_id=cast(ID, entity_id),
372
+ asset_id=asset_id,
373
+ )
374
+ asset = core.get_entity(
375
+ asset_endpoint,
376
+ entity_type=Asset,
377
+ project_context=context,
378
+ http_client=self._http_client,
379
+ token=self._token_manager.get_token(),
380
+ )
375
381
 
376
382
  output_path /= asset.path
377
383
 
378
384
  contents = self.list_directory(
379
385
  entity_id=entity_id,
380
386
  entity_type=entity_type,
381
- asset_id=asset_id,
387
+ asset_id=asset_id if isinstance(asset_id, ID) else asset.id,
382
388
  project_context=project_context,
383
389
  )
384
390
 
385
- paths = []
386
- for path in contents.files:
387
- paths.append(
391
+ if max_concurrent == 1:
392
+ paths = [
388
393
  self.download_file(
389
394
  entity_id=entity_id,
390
395
  entity_type=entity_type,
@@ -393,7 +398,24 @@ class Client:
393
398
  asset_path=path,
394
399
  project_context=context,
395
400
  )
396
- )
401
+ for path in contents.files
402
+ ]
403
+ else:
404
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_concurrent) as executor:
405
+ futures = [
406
+ executor.submit(
407
+ self.download_file,
408
+ entity_id=entity_id,
409
+ entity_type=entity_type,
410
+ asset_id=asset if asset else asset_id,
411
+ output_path=output_path / path,
412
+ asset_path=path,
413
+ project_context=context,
414
+ )
415
+ for path in contents.files
416
+ ]
417
+ result = concurrent.futures.wait(futures)
418
+ paths = [res.result() for res in result.done]
397
419
 
398
420
  return paths
399
421
 
@@ -457,6 +479,7 @@ class Client:
457
479
  Output file path.
458
480
  """
459
481
  context = self._optional_user_context(override_context=project_context)
482
+
460
483
  asset_endpoint = route.get_assets_endpoint(
461
484
  api_url=self.api_url,
462
485
  entity_type=entity_type,
@@ -498,6 +521,11 @@ class Client:
498
521
  token=self._token_manager.get_token(),
499
522
  )
500
523
 
524
+ @staticmethod
525
+ def select_assets(entity: Entity, selection: dict) -> IteratorResult:
526
+ """Select assets from entity based on selection."""
527
+ return IteratorResult(filter_assets(entity.assets, selection))
528
+
501
529
  def download_assets(
502
530
  self,
503
531
  entity_or_id: Entity | tuple[ID, type[Entity]],
@@ -520,9 +548,9 @@ class Client:
520
548
  project_context=context,
521
549
  )
522
550
 
523
- return DownloadedAsset(
551
+ return DownloadedAssetFile(
524
552
  asset=asset,
525
- output_path=path,
553
+ path=path,
526
554
  )
527
555
 
528
556
  context = self._optional_user_context(override_context=project_context)
@@ -587,7 +615,7 @@ class Client:
587
615
 
588
616
  Note: This operation is not atomic. Deletion can succeed and upload can fail.
589
617
  """
590
- self.delete_asset(
618
+ deleted_asset = self.delete_asset(
591
619
  entity_id=entity_id,
592
620
  entity_type=entity_type,
593
621
  asset_id=asset_id,
@@ -601,4 +629,5 @@ class Client:
601
629
  file_name=file_name,
602
630
  file_metadata=file_metadata,
603
631
  project_context=project_context,
632
+ asset_label=deleted_asset.label,
604
633
  )
@@ -187,7 +187,7 @@ def upload_asset_directory(
187
187
  name: str,
188
188
  paths: dict[Path, Path],
189
189
  metadata: dict | None = None,
190
- label: str | None = None,
190
+ label: str,
191
191
  project_context: ProjectContext,
192
192
  token: str,
193
193
  http_client: httpx.Client | None = None,
@@ -0,0 +1 @@
1
+ """Dependencies."""
@@ -0,0 +1,18 @@
1
+ """Entity dependencies."""
2
+
3
+ from entitysdk.exception import DependencyError
4
+ from entitysdk.models.entity import Entity
5
+
6
+
7
+ def ensure_has_id(model: Entity) -> Entity:
8
+ """Ensure entity has id."""
9
+ if model.id is None:
10
+ raise DependencyError(f"Model has no id: {repr(model)}")
11
+ return model
12
+
13
+
14
+ def ensure_has_assets(model: Entity) -> Entity:
15
+ """Ensure entity has assets."""
16
+ if not model.assets:
17
+ raise DependencyError(f"Model has no assets: {repr(model)}")
18
+ return model
@@ -26,4 +26,4 @@ def download_hoc(
26
26
  output_path=output_dir,
27
27
  ).one()
28
28
 
29
- return asset.output_path
29
+ return asset.path
@@ -26,4 +26,4 @@ def download_ion_channel_mechanism(
26
26
  output_path=output_dir,
27
27
  ).one()
28
28
 
29
- return asset.output_path
29
+ return asset.path
@@ -33,4 +33,4 @@ def download_morphology(
33
33
  output_path=output_dir,
34
34
  ).one()
35
35
 
36
- return asset.output_path
36
+ return asset.path
@@ -0,0 +1,78 @@
1
+ """Downloading functions for Simulation."""
2
+
3
+ import json
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import cast
7
+
8
+ from entitysdk.client import Client
9
+ from entitysdk.dependencies.entity import ensure_has_assets, ensure_has_id
10
+ from entitysdk.models import Simulation
11
+ from entitysdk.types import ID
12
+
13
+ L = logging.getLogger(__name__)
14
+
15
+
16
+ def download_simulation_config_content(client: Client, *, model: Simulation) -> dict:
17
+ """Download the the simulation config json into a dictionary."""
18
+ ensure_has_id(model)
19
+ ensure_has_assets(model)
20
+
21
+ asset = client.select_assets(
22
+ model,
23
+ selection={"label": "sonata_simulation_config"},
24
+ ).one()
25
+
26
+ json_content: bytes = client.download_content(
27
+ entity_id=cast(ID, model.id),
28
+ entity_type=Simulation,
29
+ asset_id=asset.id,
30
+ )
31
+
32
+ return json.loads(json_content)
33
+
34
+
35
+ def download_node_sets_file(client: Client, *, model: Simulation, output_path: Path) -> Path:
36
+ """Download the node sets file from simulation's assets."""
37
+ ensure_has_id(model)
38
+ ensure_has_assets(model)
39
+
40
+ asset = client.select_assets(
41
+ model,
42
+ selection={"label": "custom_node_sets"},
43
+ ).one()
44
+
45
+ path = client.download_file(
46
+ entity_id=cast(ID, model.id),
47
+ entity_type=Simulation,
48
+ asset_id=asset,
49
+ output_path=output_path,
50
+ )
51
+
52
+ L.info("Node sets file downloaded at %s", path)
53
+
54
+ return path
55
+
56
+
57
+ def download_spike_replay_files(
58
+ client: Client, *, model: Simulation, output_dir: Path
59
+ ) -> list[Path]:
60
+ """Download the spike replay files from simualtion's assets."""
61
+ ensure_has_id(model)
62
+ ensure_has_assets(model)
63
+
64
+ assets = client.select_assets(model, selection={"label": "replay_spikes"}).all()
65
+
66
+ spike_files: list[Path] = [
67
+ client.download_file(
68
+ entity_id=cast(ID, model.id),
69
+ entity_type=Simulation,
70
+ asset_id=asset,
71
+ output_path=output_dir / asset.path,
72
+ )
73
+ for asset in assets
74
+ ]
75
+
76
+ L.info("Downloaded %d spike replay files: %s", len(spike_files), spike_files)
77
+
78
+ return spike_files
@@ -0,0 +1,61 @@
1
+ """Downloading functions for SimulationResult."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import cast
6
+
7
+ from entitysdk.client import Client
8
+ from entitysdk.dependencies.entity import ensure_has_assets, ensure_has_id
9
+ from entitysdk.models import SimulationResult
10
+ from entitysdk.types import ID
11
+
12
+ L = logging.getLogger(__name__)
13
+
14
+
15
+ def download_spike_report_file(
16
+ client: Client, *, model: SimulationResult, output_path: Path
17
+ ) -> Path:
18
+ """Download spike report file from SimulationResult entity."""
19
+ ensure_has_id(model)
20
+ ensure_has_assets(model)
21
+
22
+ asset = client.select_assets(
23
+ model,
24
+ selection={"label": "spike_report"},
25
+ ).one()
26
+
27
+ path = client.download_file(
28
+ entity_id=cast(ID, model.id),
29
+ entity_type=SimulationResult,
30
+ asset_id=asset,
31
+ output_path=output_path / asset.path if output_path.is_dir() else output_path,
32
+ )
33
+ L.info("Spike report file downloaded at %s", path)
34
+ return path
35
+
36
+
37
+ def download_voltage_report_files(
38
+ client: Client, *, model: SimulationResult, output_dir: Path
39
+ ) -> list[Path]:
40
+ """Download voltage report files from SimulationResult entity."""
41
+ ensure_has_id(model)
42
+ ensure_has_assets(model)
43
+
44
+ assets = client.select_assets(
45
+ model,
46
+ selection={"label": "voltage_report"},
47
+ ).all()
48
+
49
+ files: list[Path] = [
50
+ client.download_file(
51
+ entity_id=cast(ID, model.id),
52
+ entity_type=SimulationResult,
53
+ asset_id=asset,
54
+ output_path=output_dir / asset.path,
55
+ )
56
+ for asset in assets
57
+ ]
58
+
59
+ L.info("Downloaded voltage report files: %s", files)
60
+
61
+ return files
@@ -11,3 +11,11 @@ class RouteNotFoundError(EntitySDKError):
11
11
 
12
12
  class IteratorResultError(EntitySDKError):
13
13
  """Raised when the result of an iterator is not as expected."""
14
+
15
+
16
+ class DependencyError(EntitySDKError):
17
+ """Raised when a dependency check fails."""
18
+
19
+
20
+ class StagingError(EntitySDKError):
21
+ """Raised when a staging operation has failed."""
@@ -8,11 +8,14 @@ from pydantic import ConfigDict, Field
8
8
 
9
9
  from entitysdk.models.base import BaseModel
10
10
  from entitysdk.models.core import Identifiable
11
+ from entitysdk.types import ID
11
12
 
12
13
 
13
14
  class Asset(Identifiable):
14
15
  """Asset."""
15
16
 
17
+ id: ID
18
+
16
19
  path: Annotated[
17
20
  str,
18
21
  Field(
@@ -62,7 +65,7 @@ class Asset(Identifiable):
62
65
  dict,
63
66
  Field(description="Asset json metadata."),
64
67
  ] = {}
65
- label: Annotated[str | None, Field(description="Optional asset label.")] = None
68
+ label: Annotated[str, Field(description="Asset label.")]
66
69
 
67
70
 
68
71
  class LocalAssetMetadata(BaseModel):
@@ -93,7 +96,7 @@ class LocalAssetMetadata(BaseModel):
93
96
  description="The metadata of the asset.",
94
97
  ),
95
98
  ] = None
96
- label: Annotated[str | None, Field(description="Optional asset label.")] = None
99
+ label: Annotated[str, Field(description="Optional asset label.")]
97
100
 
98
101
 
99
102
  class DetailedFile(BaseModel):
@@ -0,0 +1,20 @@
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 DownloadedAssetFile(Schema):
10
+ """Downloaded asset file."""
11
+
12
+ asset: Asset
13
+ path: Path
14
+
15
+
16
+ class DownloadedAssetContent(Schema):
17
+ """Downloaded asset content."""
18
+
19
+ asset: Asset
20
+ content: bytes
@@ -0,0 +1,7 @@
1
+ """Staging functions."""
2
+
3
+ from entitysdk.staging.circuit import stage_circuit
4
+ from entitysdk.staging.simulation import stage_simulation
5
+ from entitysdk.staging.simulation_result import stage_simulation_result
6
+
7
+ __all__ = ["stage_circuit", "stage_simulation", "stage_simulation_result"]
@@ -0,0 +1,44 @@
1
+ """Staging functions for Circuit."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import cast
6
+
7
+ from entitysdk.client import Client
8
+ from entitysdk.dependencies.entity import ensure_has_assets, ensure_has_id
9
+ from entitysdk.models import Circuit
10
+ from entitysdk.types import ID
11
+
12
+ L = logging.getLogger(__name__)
13
+
14
+
15
+ def stage_circuit(client: Client, *, model: Circuit, output_dir: Path) -> Path:
16
+ """Stage a Circuit directory into output_dir."""
17
+ ensure_has_id(model)
18
+ ensure_has_assets(model)
19
+
20
+ asset = client.select_assets(
21
+ model,
22
+ selection={
23
+ "content_type": "application/vnd.directory",
24
+ "is_directory": True,
25
+ "label": "sonata_circuit",
26
+ },
27
+ ).one()
28
+
29
+ paths = client.download_directory(
30
+ entity_id=cast(ID, model.id),
31
+ entity_type=Circuit,
32
+ asset_id=asset,
33
+ output_path=output_dir,
34
+ ignore_directory_name=True,
35
+ )
36
+
37
+ L.debug("Downloaded circuit %s paths: %s", model.id, paths)
38
+
39
+ circuit_config_path = output_dir / "circuit_config.json"
40
+ assert circuit_config_path in paths
41
+
42
+ L.info("Circuit %s staged at %s", model.id, circuit_config_path)
43
+
44
+ return circuit_config_path
@@ -0,0 +1,142 @@
1
+ """Staging functions for Simulation."""
2
+
3
+ import logging
4
+ from copy import deepcopy
5
+ from pathlib import Path
6
+
7
+ from entitysdk.client import Client
8
+ from entitysdk.downloaders.simulation import (
9
+ download_node_sets_file,
10
+ download_simulation_config_content,
11
+ download_spike_replay_files,
12
+ )
13
+ from entitysdk.exception import StagingError
14
+ from entitysdk.models import Circuit, Simulation
15
+ from entitysdk.staging.circuit import stage_circuit
16
+ from entitysdk.types import StrOrPath
17
+ from entitysdk.utils.filesystem import create_dir
18
+ from entitysdk.utils.io import write_json
19
+
20
+ L = logging.getLogger(__name__)
21
+
22
+ DEFAULT_NODE_SETS_FILENAME = "node_sets.json"
23
+ DEFAULT_SIMULATION_CONFIG_FILENAME = "simulation_config.json"
24
+ DEFAULT_CIRCUIT_DIR = "circuit"
25
+
26
+
27
+ def stage_simulation(
28
+ client: Client,
29
+ *,
30
+ model: Simulation,
31
+ output_dir: StrOrPath,
32
+ circuit_config_path: Path | None = None,
33
+ override_results_dir: Path | None = None,
34
+ ) -> Path:
35
+ """Stage a simulation entity into output_dir.
36
+
37
+ Args:
38
+ client: The client to use to stage the simulation.
39
+ model: The simulation entity to stage.
40
+ output_dir: The directory to stage the simulation into.
41
+ circuit_config_path: The path to the circuit config file.
42
+ If not provided, the circuit will be staged from metadata.
43
+ override_results_dir: Directory to update the simulation config section to point to.
44
+
45
+ Returns:
46
+ The path to the staged simulation config file.
47
+ """
48
+ output_dir = create_dir(output_dir).resolve()
49
+
50
+ simulation_config: dict = download_simulation_config_content(client, model=model)
51
+ node_sets_file: Path = download_node_sets_file(
52
+ client,
53
+ model=model,
54
+ output_path=output_dir / DEFAULT_NODE_SETS_FILENAME,
55
+ )
56
+ spike_paths: list[Path] = download_spike_replay_files(
57
+ client,
58
+ model=model,
59
+ output_dir=output_dir,
60
+ )
61
+ if circuit_config_path is None:
62
+ L.info(
63
+ "Circuit config path was not provided. Circuit is going to be staged from metadata. "
64
+ "Circuit id to be staged: %s"
65
+ )
66
+ circuit_config_path = stage_circuit(
67
+ client,
68
+ model=client.get_entity(
69
+ entity_id=model.entity_id,
70
+ entity_type=Circuit,
71
+ ),
72
+ output_dir=create_dir(output_dir / DEFAULT_CIRCUIT_DIR),
73
+ )
74
+
75
+ transformed_simulation_config: dict = _transform_simulation_config(
76
+ simulation_config=simulation_config,
77
+ circuit_config_path=circuit_config_path,
78
+ node_sets_path=node_sets_file,
79
+ spike_paths=spike_paths,
80
+ output_dir=output_dir,
81
+ override_results_dir=override_results_dir,
82
+ )
83
+
84
+ output_simulation_config_file = output_dir / DEFAULT_SIMULATION_CONFIG_FILENAME
85
+
86
+ write_json(
87
+ data=transformed_simulation_config,
88
+ path=output_simulation_config_file,
89
+ )
90
+
91
+ L.info("Staged Simulation %s at %s", model.id, output_dir)
92
+
93
+ return output_simulation_config_file
94
+
95
+
96
+ def _transform_simulation_config(
97
+ simulation_config: dict,
98
+ circuit_config_path: Path,
99
+ node_sets_path: Path,
100
+ spike_paths: list[Path],
101
+ output_dir: Path,
102
+ override_results_dir: Path | None,
103
+ ) -> dict:
104
+ return simulation_config | {
105
+ "network": str(circuit_config_path),
106
+ "node_sets_file": str(node_sets_path.relative_to(output_dir)),
107
+ "inputs": _transform_inputs(simulation_config["inputs"], spike_paths),
108
+ "output": _transform_output(simulation_config["output"], override_results_dir),
109
+ }
110
+
111
+
112
+ def _transform_inputs(inputs: dict, spike_paths: list[Path]) -> dict:
113
+ expected_spike_filenames = {p.name for p in spike_paths}
114
+
115
+ transformed_inputs = deepcopy(inputs)
116
+ for values in transformed_inputs.values():
117
+ if values["input_type"] == "spikes":
118
+ path = Path(values["spike_file"]).name
119
+
120
+ if path not in expected_spike_filenames:
121
+ raise StagingError(
122
+ f"Spike file name in config is not present in spike asset file names.\n"
123
+ f"Config file name: {path}\n"
124
+ f"Asset file names: {expected_spike_filenames}"
125
+ )
126
+
127
+ values["spike_file"] = str(path)
128
+ L.debug("Spike file %s -> %s", values["spike_file"], path)
129
+
130
+ return transformed_inputs
131
+
132
+
133
+ def _transform_output(output: dict, override_results_dir: StrOrPath | None) -> dict:
134
+ if override_results_dir is None:
135
+ return output
136
+
137
+ path = Path(override_results_dir)
138
+
139
+ return {
140
+ "output_dir": str(path),
141
+ "spikes_file": str(path / "spikes.h5"),
142
+ }