entitysdk 0.2.4__tar.gz → 0.3.0__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 (87) hide show
  1. {entitysdk-0.2.4/src/entitysdk.egg-info → entitysdk-0.3.0}/PKG-INFO +4 -4
  2. {entitysdk-0.2.4 → entitysdk-0.3.0}/README.md +3 -3
  3. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/client.py +29 -14
  4. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/core.py +5 -2
  5. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/__init__.py +4 -0
  6. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/asset.py +2 -2
  7. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/entity.py +2 -2
  8. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/memodel.py +7 -0
  9. entitysdk-0.3.0/src/entitysdk/models/memodelcalibrationresult.py +42 -0
  10. entitysdk-0.3.0/src/entitysdk/models/validation_result.py +34 -0
  11. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/route.py +6 -4
  12. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/serdes.py +1 -1
  13. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/util.py +13 -0
  14. {entitysdk-0.2.4 → entitysdk-0.3.0/src/entitysdk.egg-info}/PKG-INFO +4 -4
  15. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk.egg-info/SOURCES.txt +7 -1
  16. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/integration/test_searching.py +4 -0
  17. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/models/data/electrical_cell_recording.json +6 -3
  18. entitysdk-0.3.0/tests/unit/models/data/memodel_calibration_result.json +8 -0
  19. entitysdk-0.3.0/tests/unit/models/data/validation_result.json +7 -0
  20. entitysdk-0.3.0/tests/unit/models/test_memodel_calibration_result.py +42 -0
  21. entitysdk-0.3.0/tests/unit/models/test_validation_result.py +42 -0
  22. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/test_client.py +142 -1
  23. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/test_util.py +16 -0
  24. {entitysdk-0.2.4 → entitysdk-0.3.0}/.github/workflows/sdist.yml +0 -0
  25. {entitysdk-0.2.4 → entitysdk-0.3.0}/.github/workflows/tox.yml +0 -0
  26. {entitysdk-0.2.4 → entitysdk-0.3.0}/.gitignore +0 -0
  27. {entitysdk-0.2.4 → entitysdk-0.3.0}/CHANGELOG.rst +0 -0
  28. {entitysdk-0.2.4 → entitysdk-0.3.0}/CONTRIBUTING.md +0 -0
  29. {entitysdk-0.2.4 → entitysdk-0.3.0}/LICENSE.txt +0 -0
  30. {entitysdk-0.2.4 → entitysdk-0.3.0}/examples/01_searching.ipynb +0 -0
  31. {entitysdk-0.2.4 → entitysdk-0.3.0}/examples/02_morphology.ipynb +0 -0
  32. {entitysdk-0.2.4 → entitysdk-0.3.0}/examples/03_contribution.ipynb +0 -0
  33. {entitysdk-0.2.4 → entitysdk-0.3.0}/pyproject.toml +0 -0
  34. {entitysdk-0.2.4 → entitysdk-0.3.0}/setup.cfg +0 -0
  35. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/__init__.py +0 -0
  36. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/common.py +0 -0
  37. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/config.py +0 -0
  38. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/exception.py +0 -0
  39. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/mixin.py +0 -0
  40. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/agent.py +0 -0
  41. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/base.py +0 -0
  42. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/brain_location.py +0 -0
  43. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/brain_region.py +0 -0
  44. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/contribution.py +0 -0
  45. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/core.py +0 -0
  46. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/electrical_cell_recording.py +0 -0
  47. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/emodel.py +0 -0
  48. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/etype.py +0 -0
  49. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/ion_channel_model.py +0 -0
  50. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/license.py +0 -0
  51. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/morphology.py +0 -0
  52. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/mtype.py +0 -0
  53. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/response.py +0 -0
  54. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/subject.py +0 -0
  55. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/models/taxonomy.py +0 -0
  56. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/result.py +0 -0
  57. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/token_manager.py +0 -0
  58. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk/types.py +0 -0
  59. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk.egg-info/dependency_links.txt +0 -0
  60. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk.egg-info/requires.txt +0 -0
  61. {entitysdk-0.2.4 → entitysdk-0.3.0}/src/entitysdk.egg-info/top_level.txt +0 -0
  62. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/__init__.py +0 -0
  63. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/integration/__init__.py +0 -0
  64. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/integration/conftest.py +0 -0
  65. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/__init__.py +0 -0
  66. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/conftest.py +0 -0
  67. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/models/__init__.py +0 -0
  68. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/models/data/.gitignore +0 -0
  69. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/models/data/ion_channel_model.json +0 -0
  70. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/models/data/reconstruction_morphology.json +0 -0
  71. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/models/test_agent.py +0 -0
  72. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/models/test_asset.py +0 -0
  73. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/models/test_brain_region.py +0 -0
  74. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/models/test_contribution.py +0 -0
  75. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/models/test_electrical_cell_recording.py +0 -0
  76. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/models/test_init.py +0 -0
  77. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/models/test_ion_channel_model.py +0 -0
  78. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/models/test_morphology.py +0 -0
  79. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/test_base.py +0 -0
  80. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/test_common.py +0 -0
  81. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/test_config.py +0 -0
  82. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/test_result.py +0 -0
  83. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/test_route.py +0 -0
  84. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/test_serdes.py +0 -0
  85. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/test_token_manager.py +0 -0
  86. {entitysdk-0.2.4 → entitysdk-0.3.0}/tests/unit/util.py +0 -0
  87. {entitysdk-0.2.4 → entitysdk-0.3.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entitysdk
3
- Version: 0.2.4
3
+ Version: 0.3.0
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>
@@ -51,7 +51,7 @@ pip install obi-auth
51
51
  ```python
52
52
  from obi_auth import get_token
53
53
 
54
- access_token = get_token(environment="staging")
54
+ token = get_token(environment="staging")
55
55
  ```
56
56
 
57
57
  ## Quick Start
@@ -67,12 +67,12 @@ client = Client(
67
67
  project_context=ProjectContext(
68
68
  project_id=UUID("your-project-id"),
69
69
  virtual_lab_id=UUID("your-lab-id")
70
- )
70
+ ),
71
71
  environment="staging"
72
72
  )
73
73
 
74
74
  # Search for morphologies
75
- iterator = client.search(
75
+ iterator = client.search_entity(
76
76
  entity_type=ReconstructionMorphology,
77
77
  query={"mtype__pref_label": "L5_TPC:A"},
78
78
  token=token,
@@ -30,7 +30,7 @@ pip install obi-auth
30
30
  ```python
31
31
  from obi_auth import get_token
32
32
 
33
- access_token = get_token(environment="staging")
33
+ token = get_token(environment="staging")
34
34
  ```
35
35
 
36
36
  ## Quick Start
@@ -46,12 +46,12 @@ client = Client(
46
46
  project_context=ProjectContext(
47
47
  project_id=UUID("your-project-id"),
48
48
  virtual_lab_id=UUID("your-lab-id")
49
- )
49
+ ),
50
50
  environment="staging"
51
51
  )
52
52
 
53
53
  # Search for morphologies
54
- iterator = client.search(
54
+ iterator = client.search_entity(
55
55
  entity_type=ReconstructionMorphology,
56
56
  query={"mtype__pref_label": "L5_TPC:A"},
57
57
  token=token,
@@ -14,7 +14,11 @@ from entitysdk.models.core import Identifiable
14
14
  from entitysdk.result import IteratorResult
15
15
  from entitysdk.token_manager import TokenManager
16
16
  from entitysdk.types import ID, DeploymentEnvironment
17
- from entitysdk.util import build_api_url
17
+ from entitysdk.util import (
18
+ build_api_url,
19
+ create_intermediate_directories,
20
+ validate_filename_extension_consistency,
21
+ )
18
22
 
19
23
 
20
24
  class Client:
@@ -314,7 +318,7 @@ class Client:
314
318
  *,
315
319
  entity_id: ID,
316
320
  entity_type: type[Identifiable],
317
- asset_id: str,
321
+ asset_id: ID,
318
322
  project_context: ProjectContext | None = None,
319
323
  token: str | None = None,
320
324
  ) -> bytes:
@@ -353,7 +357,7 @@ class Client:
353
357
  *,
354
358
  entity_id: ID,
355
359
  entity_type: type[Identifiable],
356
- asset_id: str,
360
+ asset_id: ID,
357
361
  output_path: os.PathLike,
358
362
  project_context: ProjectContext | None = None,
359
363
  token: str | None = None,
@@ -364,26 +368,37 @@ class Client:
364
368
  entity_id: Id of the entity.
365
369
  entity_type: Type of the entity.
366
370
  asset_id: Id of the asset.
367
- output_path: Path to save the file to.
371
+ output_path: Either be a file path to write the file to or an output directory.
368
372
  project_context: Optional project context.
369
373
  token: Authorization access token.
370
374
  """
371
- url = (
372
- route.get_assets_endpoint(
373
- api_url=self.api_url,
374
- entity_type=entity_type,
375
- entity_id=entity_id,
376
- asset_id=asset_id,
377
- )
378
- + "/download"
375
+ asset_endpoint = route.get_assets_endpoint(
376
+ api_url=self.api_url,
377
+ entity_type=entity_type,
378
+ entity_id=entity_id,
379
+ asset_id=asset_id,
379
380
  )
380
381
  token = self._get_token(override_token=token)
381
382
  context = self._optional_user_context(override_context=project_context)
383
+ asset = core.get_entity(
384
+ asset_endpoint,
385
+ entity_type=Asset,
386
+ token=token,
387
+ project_context=context,
388
+ http_client=self._http_client,
389
+ )
390
+ path: Path = Path(output_path)
391
+ path = (
392
+ path / asset.path
393
+ if path.is_dir()
394
+ else validate_filename_extension_consistency(path, Path(asset.path).suffix)
395
+ )
396
+ create_intermediate_directories(path)
382
397
  return core.download_asset_file(
383
- url=url,
398
+ url=f"{asset_endpoint}/download",
384
399
  token=token,
385
400
  project_context=context,
386
- output_path=Path(output_path),
401
+ output_path=path,
387
402
  http_client=self._http_client,
388
403
  )
389
404
 
@@ -3,6 +3,7 @@
3
3
  import io
4
4
  from collections.abc import Iterator
5
5
  from pathlib import Path
6
+ from typing import TypeVar
6
7
 
7
8
  import httpx
8
9
 
@@ -13,6 +14,8 @@ from entitysdk.models.core import Identifiable
13
14
  from entitysdk.result import IteratorResult
14
15
  from entitysdk.util import make_db_api_request, stream_paginated_request
15
16
 
17
+ TIdentifiable = TypeVar("TIdentifiable", bound=Identifiable)
18
+
16
19
 
17
20
  def search_entities(
18
21
  url: str,
@@ -55,11 +58,11 @@ def search_entities(
55
58
  def get_entity(
56
59
  url: str,
57
60
  *,
58
- entity_type: type[Identifiable],
61
+ entity_type: type[TIdentifiable],
59
62
  project_context: ProjectContext | None = None,
60
63
  token: str,
61
64
  http_client: httpx.Client | None = None,
62
- ) -> Identifiable:
65
+ ) -> TIdentifiable:
63
66
  """Instantiate entity with model ``entity_type`` from resource id."""
64
67
  response = make_db_api_request(
65
68
  url=url,
@@ -10,9 +10,11 @@ from entitysdk.models.emodel import EModel
10
10
  from entitysdk.models.ion_channel_model import IonChannelModel, NeuronBlock, UseIon
11
11
  from entitysdk.models.license import License
12
12
  from entitysdk.models.memodel import MEModel
13
+ from entitysdk.models.memodelcalibrationresult import MEModelCalibrationResult
13
14
  from entitysdk.models.morphology import ReconstructionMorphology
14
15
  from entitysdk.models.mtype import MTypeClass
15
16
  from entitysdk.models.taxonomy import Species, Strain, Taxonomy
17
+ from entitysdk.models.validation_result import ValidationResult
16
18
 
17
19
  __all__ = [
18
20
  "Asset",
@@ -24,6 +26,7 @@ __all__ = [
24
26
  "IonChannelModel",
25
27
  "License",
26
28
  "MEModel",
29
+ "MEModelCalibrationResult",
27
30
  "MTypeClass",
28
31
  "NeuronBlock",
29
32
  "Organization",
@@ -34,4 +37,5 @@ __all__ = [
34
37
  "Strain",
35
38
  "Taxonomy",
36
39
  "UseIon",
40
+ "ValidationResult",
37
41
  ]
@@ -13,13 +13,13 @@ class Asset(Identifiable):
13
13
  path: Annotated[
14
14
  str,
15
15
  Field(
16
- description="The path of the asset.",
16
+ description="The relative path of the asset.",
17
17
  ),
18
18
  ]
19
19
  full_path: Annotated[
20
20
  str,
21
21
  Field(
22
- description="The full path of the asset.",
22
+ description="The full s3 path of the asset.",
23
23
  ),
24
24
  ]
25
25
  is_directory: Annotated[
@@ -46,12 +46,12 @@ class Entity(Identifiable, HasAssets):
46
46
  ),
47
47
  ] = None
48
48
  authorized_public: Annotated[
49
- bool | None,
49
+ bool,
50
50
  Field(
51
51
  examples=[True, False],
52
52
  description="Whether the resource is authorized to be public.",
53
53
  ),
54
- ] = None
54
+ ] = False
55
55
  authorized_project_id: Annotated[
56
56
  ID | None,
57
57
  Field(
@@ -8,6 +8,7 @@ from entitysdk.models.contribution import Contribution
8
8
  from entitysdk.models.emodel import EModel
9
9
  from entitysdk.models.entity import Entity
10
10
  from entitysdk.models.etype import ETypeClass
11
+ from entitysdk.models.memodelcalibrationresult import MEModelCalibrationResult
11
12
  from entitysdk.models.morphology import (
12
13
  BrainRegion,
13
14
  License,
@@ -93,4 +94,10 @@ class MEModel(Entity):
93
94
  description="The mtype classes of the memodel.",
94
95
  ),
95
96
  ] = None
97
+ calibration_result: Annotated[
98
+ MEModelCalibrationResult | None,
99
+ Field(
100
+ description="The calibration result of the memodel.",
101
+ ),
102
+ ] = None
96
103
  legacy_id: list[str] | None = None
@@ -0,0 +1,42 @@
1
+ """ME-Model Calibration Result."""
2
+
3
+ from typing import Annotated
4
+
5
+ from pydantic import Field
6
+
7
+ from entitysdk.models.entity import Entity
8
+ from entitysdk.types import ID
9
+
10
+
11
+ class MEModelCalibrationResult(Entity):
12
+ """ME-Model calibration result."""
13
+
14
+ holding_current: Annotated[
15
+ float,
16
+ Field(
17
+ description="The holding current to apply to the simulatable neuron, in nA.",
18
+ example=-0.016,
19
+ ),
20
+ ]
21
+ threshold_current: Annotated[
22
+ float,
23
+ Field(
24
+ description="The minimal amount of current needed to make "
25
+ "the simulatable neuron spike, in nA.",
26
+ example=0.1,
27
+ ),
28
+ ]
29
+ rin: Annotated[
30
+ float | None,
31
+ Field(
32
+ description="The input resistance of the simulatable neuron, in MOhm.",
33
+ example=0.1,
34
+ ),
35
+ ] = None
36
+ calibrated_entity_id: Annotated[
37
+ ID,
38
+ Field(
39
+ description="ID of the calibrated entity.",
40
+ example="85663316-a7ff-4107-9eb9-236de8868c5c",
41
+ ),
42
+ ]
@@ -0,0 +1,34 @@
1
+ """Validation result."""
2
+
3
+ from typing import Annotated
4
+
5
+ from pydantic import Field
6
+
7
+ from entitysdk.models.entity import Entity
8
+ from entitysdk.types import ID
9
+
10
+
11
+ class ValidationResult(Entity):
12
+ """Validation result."""
13
+
14
+ passed: Annotated[
15
+ bool,
16
+ Field(
17
+ description="True if the validation passed, False otherwise.",
18
+ example=True,
19
+ ),
20
+ ]
21
+ name: Annotated[
22
+ str,
23
+ Field(
24
+ description="Name of the validation.",
25
+ example="Neuron spiking validation",
26
+ ),
27
+ ]
28
+ validated_entity_id: Annotated[
29
+ ID,
30
+ Field(
31
+ description="ID of the validated entity.",
32
+ example="85663316-a7ff-4107-9eb9-236de8868c5c",
33
+ ),
34
+ ]
@@ -11,8 +11,13 @@ _ROUTES = {
11
11
  "BrainRegion": "brain-region",
12
12
  "Contribution": "contribution",
13
13
  "ElectricalCellRecording": "electrical-cell-recording",
14
+ "EModel": "emodel",
14
15
  "Entity": "entity",
16
+ "Ion": "ion",
17
+ "IonChannelModel": "ion-channel-model",
15
18
  "License": "license",
19
+ "MEModel": "memodel",
20
+ "MEModelCalibrationResult": "memodel-calibration-result",
16
21
  "MTypeClass": "mtype",
17
22
  "Organization": "organization",
18
23
  "Person": "person",
@@ -21,10 +26,7 @@ _ROUTES = {
21
26
  "Species": "species",
22
27
  "Strain": "strain",
23
28
  "Taxonomy": "taxonomy",
24
- "IonChannelModel": "ion-channel-model",
25
- "Ion": "ion",
26
- "EModel": "emodel",
27
- "MEModel": "memodel",
29
+ "ValidationResult": "validation-result",
28
30
  }
29
31
 
30
32
 
@@ -22,7 +22,7 @@ def serialize_entity(entity: BaseModel) -> dict:
22
22
  data = entity.model_dump(
23
23
  mode="json",
24
24
  exclude=SERIALIZATION_EXCLUDE_KEYS,
25
- exclude_none=True,
25
+ exclude_none=False,
26
26
  )
27
27
  processed = _convert_identifiables_to_ids(data)
28
28
  return processed
@@ -3,6 +3,7 @@
3
3
  import sys
4
4
  from collections.abc import Iterator
5
5
  from json import dumps
6
+ from pathlib import Path
6
7
 
7
8
  import httpx
8
9
 
@@ -135,3 +136,15 @@ def build_api_url(environment: DeploymentEnvironment) -> str:
135
136
  DeploymentEnvironment.staging: settings.staging_api_url,
136
137
  DeploymentEnvironment.production: settings.production_api_url,
137
138
  }[environment]
139
+
140
+
141
+ def validate_filename_extension_consistency(path: Path, expected_extension: str) -> Path:
142
+ """Validate file path extension against expected extension."""
143
+ if path.suffix.lower() == expected_extension.lower():
144
+ return path
145
+ raise EntitySDKError(f"File path {path} does not have expected extension {expected_extension}.")
146
+
147
+
148
+ def create_intermediate_directories(path: Path) -> None:
149
+ """Create intermediate directories in a path."""
150
+ path.parent.mkdir(parents=True, exist_ok=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entitysdk
3
- Version: 0.2.4
3
+ Version: 0.3.0
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>
@@ -51,7 +51,7 @@ pip install obi-auth
51
51
  ```python
52
52
  from obi_auth import get_token
53
53
 
54
- access_token = get_token(environment="staging")
54
+ token = get_token(environment="staging")
55
55
  ```
56
56
 
57
57
  ## Quick Start
@@ -67,12 +67,12 @@ client = Client(
67
67
  project_context=ProjectContext(
68
68
  project_id=UUID("your-project-id"),
69
69
  virtual_lab_id=UUID("your-lab-id")
70
- )
70
+ ),
71
71
  environment="staging"
72
72
  )
73
73
 
74
74
  # Search for morphologies
75
- iterator = client.search(
75
+ iterator = client.search_entity(
76
76
  entity_type=ReconstructionMorphology,
77
77
  query={"mtype__pref_label": "L5_TPC:A"},
78
78
  token=token,
@@ -43,11 +43,13 @@ src/entitysdk/models/etype.py
43
43
  src/entitysdk/models/ion_channel_model.py
44
44
  src/entitysdk/models/license.py
45
45
  src/entitysdk/models/memodel.py
46
+ src/entitysdk/models/memodelcalibrationresult.py
46
47
  src/entitysdk/models/morphology.py
47
48
  src/entitysdk/models/mtype.py
48
49
  src/entitysdk/models/response.py
49
50
  src/entitysdk/models/subject.py
50
51
  src/entitysdk/models/taxonomy.py
52
+ src/entitysdk/models/validation_result.py
51
53
  tests/__init__.py
52
54
  tests/integration/__init__.py
53
55
  tests/integration/conftest.py
@@ -72,8 +74,12 @@ tests/unit/models/test_contribution.py
72
74
  tests/unit/models/test_electrical_cell_recording.py
73
75
  tests/unit/models/test_init.py
74
76
  tests/unit/models/test_ion_channel_model.py
77
+ tests/unit/models/test_memodel_calibration_result.py
75
78
  tests/unit/models/test_morphology.py
79
+ tests/unit/models/test_validation_result.py
76
80
  tests/unit/models/data/.gitignore
77
81
  tests/unit/models/data/electrical_cell_recording.json
78
82
  tests/unit/models/data/ion_channel_model.json
79
- tests/unit/models/data/reconstruction_morphology.json
83
+ tests/unit/models/data/memodel_calibration_result.json
84
+ tests/unit/models/data/reconstruction_morphology.json
85
+ tests/unit/models/data/validation_result.json
@@ -7,6 +7,7 @@ from entitysdk.models import (
7
7
  IonChannelModel,
8
8
  License,
9
9
  MEModel,
10
+ MEModelCalibrationResult,
10
11
  MTypeClass,
11
12
  Organization,
12
13
  Person,
@@ -14,6 +15,7 @@ from entitysdk.models import (
14
15
  Role,
15
16
  Species,
16
17
  Strain,
18
+ ValidationResult,
17
19
  )
18
20
 
19
21
 
@@ -33,6 +35,8 @@ from entitysdk.models import (
33
35
  EModel,
34
36
  MEModel,
35
37
  ElectricalCellRecording,
38
+ ValidationResult,
39
+ MEModelCalibrationResult,
36
40
  ],
37
41
  )
38
42
  def test_is_searchable(entity_type, client):
@@ -97,7 +97,8 @@
97
97
  "name": "GenericStep",
98
98
  "description": "",
99
99
  "injection_type": "current_clamp",
100
- "shape": "step"
100
+ "shape": "step",
101
+ "authorized_public": false
101
102
  },
102
103
  {
103
104
  "type": "electrical_recording_stimulus",
@@ -107,7 +108,8 @@
107
108
  "name": "GenericStep",
108
109
  "description": "",
109
110
  "injection_type": "current_clamp",
110
- "shape": "step"
111
+ "shape": "step",
112
+ "authorized_public": false
111
113
  },
112
114
  {
113
115
  "type": "electrical_recording_stimulus",
@@ -117,7 +119,8 @@
117
119
  "name": "GenericStep",
118
120
  "description": "",
119
121
  "injection_type": "current_clamp",
120
- "shape": "step"
122
+ "shape": "step",
123
+ "authorized_public": false
121
124
  }
122
125
  ]
123
126
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "authorized_public": false,
3
+ "id" : "cfecab84-b37c-4fa0-adaa-86457d660061",
4
+ "holding_current" : 0.0,
5
+ "threshold_current" : 0.06554153953301577,
6
+ "rin" : 100.03205800593804,
7
+ "calibrated_entity_id" : "54004e96-cef0-4f1c-8c89-d9cdf7b94e43"
8
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "authorized_public": false,
3
+ "id" : "6fa52c8b-e9df-4f10-bdee-07c9af59a5ba",
4
+ "passed" : true,
5
+ "name" : "Simulatable Neuron Spiking Validation",
6
+ "validated_entity_id" : "54004e96-cef0-4f1c-8c89-d9cdf7b94e43"
7
+ }
@@ -0,0 +1,42 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+
6
+ from entitysdk.models import MEModelCalibrationResult
7
+
8
+ from ..util import MOCK_UUID
9
+
10
+ DATA_DIR = Path(__file__).parent / "data"
11
+
12
+ Model = MEModelCalibrationResult
13
+
14
+
15
+ @pytest.fixture
16
+ def json_data():
17
+ return json.loads(Path(DATA_DIR / "memodel_calibration_result.json").read_bytes())
18
+
19
+
20
+ @pytest.fixture
21
+ def model(json_data):
22
+ return Model.model_validate(json_data)
23
+
24
+
25
+ def test_read(client, httpx_mock, auth_token, json_data):
26
+ httpx_mock.add_response(method="GET", json=json_data)
27
+ entity = client.get_entity(
28
+ entity_id=MOCK_UUID,
29
+ entity_type=Model,
30
+ token=auth_token,
31
+ with_assets=False,
32
+ )
33
+ assert entity.model_dump(mode="json", exclude_none=True) == json_data
34
+
35
+
36
+ def test_register(client, httpx_mock, auth_token, model, json_data):
37
+ httpx_mock.add_response(
38
+ method="POST", json=model.model_dump(mode="json") | {"id": str(MOCK_UUID)}
39
+ )
40
+ registered = client.register_entity(entity=model, token=auth_token)
41
+ expected_json = json_data | {"id": str(MOCK_UUID)}
42
+ assert registered.model_dump(mode="json", exclude_none=True) == expected_json
@@ -0,0 +1,42 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+
6
+ from entitysdk.models import ValidationResult
7
+
8
+ from ..util import MOCK_UUID
9
+
10
+ DATA_DIR = Path(__file__).parent / "data"
11
+
12
+ Model = ValidationResult
13
+
14
+
15
+ @pytest.fixture
16
+ def json_data():
17
+ return json.loads(Path(DATA_DIR / "validation_result.json").read_bytes())
18
+
19
+
20
+ @pytest.fixture
21
+ def model(json_data):
22
+ return Model.model_validate(json_data)
23
+
24
+
25
+ def test_read(client, httpx_mock, auth_token, json_data):
26
+ httpx_mock.add_response(method="GET", json=json_data)
27
+ entity = client.get_entity(
28
+ entity_id=MOCK_UUID,
29
+ entity_type=Model,
30
+ token=auth_token,
31
+ with_assets=False,
32
+ )
33
+ assert entity.model_dump(mode="json", exclude_none=True) == json_data
34
+
35
+
36
+ def test_register(client, httpx_mock, auth_token, model, json_data):
37
+ httpx_mock.add_response(
38
+ method="POST", json=model.model_dump(mode="json") | {"id": str(MOCK_UUID)}
39
+ )
40
+ registered = client.register_entity(entity=model, token=auth_token)
41
+ expected_json = json_data | {"id": str(MOCK_UUID)}
42
+ assert registered.model_dump(mode="json", exclude_none=True) == expected_json
@@ -1,6 +1,7 @@
1
1
  import io
2
2
  import re
3
3
  import uuid
4
+ from pathlib import Path
4
5
  from unittest.mock import patch
5
6
 
6
7
  import pytest
@@ -237,7 +238,7 @@ def test_client_download_content(
237
238
  assert res == b"foo"
238
239
 
239
240
 
240
- def test_client_download_file(
241
+ def test_client_download_file__output_file(
241
242
  tmp_path,
242
243
  client,
243
244
  httpx_mock,
@@ -249,6 +250,12 @@ def test_client_download_file(
249
250
  entity_id = uuid.uuid4()
250
251
  asset_id = uuid.uuid4()
251
252
 
253
+ httpx_mock.add_response(
254
+ method="GET",
255
+ url=f"{api_url}/entity/{entity_id}/assets/{asset_id}",
256
+ match_headers=request_headers,
257
+ json=_mock_asset_response(asset_id) | {"path": "foo.h5"},
258
+ )
252
259
  httpx_mock.add_response(
253
260
  method="GET",
254
261
  url=f"{api_url}/entity/{entity_id}/assets/{asset_id}/download",
@@ -268,6 +275,140 @@ def test_client_download_file(
268
275
  assert output_path.read_bytes() == b"foo"
269
276
 
270
277
 
278
+ def test_client_download_file__output_file__inconsistent_ext(
279
+ tmp_path,
280
+ client,
281
+ httpx_mock,
282
+ api_url,
283
+ project_context,
284
+ auth_token,
285
+ request_headers,
286
+ ):
287
+ """User must provide a path extension that is consitent with the asset path."""
288
+
289
+ entity_id = uuid.uuid4()
290
+ asset_id = uuid.uuid4()
291
+
292
+ httpx_mock.add_response(
293
+ method="GET",
294
+ url=f"{api_url}/entity/{entity_id}/assets/{asset_id}",
295
+ match_headers=request_headers,
296
+ json=_mock_asset_response(asset_id) | {"path": "foo.swc"},
297
+ )
298
+ output_path = tmp_path / "foo.h5"
299
+
300
+ with pytest.raises(
301
+ EntitySDKError, match=f"File path {output_path} does not have expected extension .swc."
302
+ ):
303
+ client.download_file(
304
+ entity_id=entity_id,
305
+ entity_type=Entity,
306
+ asset_id=asset_id,
307
+ output_path=output_path,
308
+ token=auth_token,
309
+ )
310
+
311
+
312
+ def test_client_download_file__output_file__user_subdirectory_path(
313
+ tmp_path,
314
+ client,
315
+ httpx_mock,
316
+ api_url,
317
+ project_context,
318
+ auth_token,
319
+ request_headers,
320
+ ):
321
+ """User provides a nested output path that overrides the asset path."""
322
+
323
+ entity_id = uuid.uuid4()
324
+ asset_id = uuid.uuid4()
325
+
326
+ httpx_mock.add_response(
327
+ method="GET",
328
+ url=f"{api_url}/entity/{entity_id}/assets/{asset_id}",
329
+ match_headers=request_headers,
330
+ json=_mock_asset_response(asset_id) | {"path": "foo.h5"},
331
+ )
332
+
333
+ httpx_mock.add_response(
334
+ method="GET",
335
+ url=f"{api_url}/entity/{entity_id}/assets/{asset_id}/download",
336
+ match_headers=request_headers,
337
+ content=b"foo",
338
+ )
339
+
340
+ output_path = tmp_path / "foo" / "bar" / "bar.h5"
341
+
342
+ client.download_file(
343
+ entity_id=entity_id,
344
+ entity_type=Entity,
345
+ asset_id=asset_id,
346
+ output_path=output_path,
347
+ token=auth_token,
348
+ )
349
+ assert output_path.read_bytes() == b"foo"
350
+
351
+
352
+ def test_client_download_file__asset_subdirectory_paths(
353
+ tmp_path,
354
+ client,
355
+ httpx_mock,
356
+ api_url,
357
+ project_context,
358
+ auth_token,
359
+ request_headers,
360
+ ):
361
+ """User provides directory, relative file paths from assets are written to it."""
362
+
363
+ entity_id = uuid.uuid4()
364
+ asset1_id = uuid.uuid4()
365
+ asset2_id = uuid.uuid4()
366
+
367
+ httpx_mock.add_response(
368
+ method="GET",
369
+ url=f"{api_url}/entity/{entity_id}/assets/{asset1_id}",
370
+ match_headers=request_headers,
371
+ json=_mock_asset_response(asset1_id) | {"path": "foo/bar/foo.h5"},
372
+ )
373
+ httpx_mock.add_response(
374
+ method="GET",
375
+ url=f"{api_url}/entity/{entity_id}/assets/{asset1_id}/download",
376
+ match_headers=request_headers,
377
+ content=b"foo",
378
+ )
379
+ httpx_mock.add_response(
380
+ method="GET",
381
+ url=f"{api_url}/entity/{entity_id}/assets/{asset2_id}",
382
+ match_headers=request_headers,
383
+ json=_mock_asset_response(asset2_id) | {"path": "foo/bar/bar.swc"},
384
+ )
385
+ httpx_mock.add_response(
386
+ method="GET",
387
+ url=f"{api_url}/entity/{entity_id}/assets/{asset2_id}/download",
388
+ match_headers=request_headers,
389
+ content=b"bar",
390
+ )
391
+ output_path = tmp_path
392
+
393
+ client.download_file(
394
+ entity_id=entity_id,
395
+ entity_type=Entity,
396
+ asset_id=asset1_id,
397
+ output_path=output_path,
398
+ token=auth_token,
399
+ )
400
+ client.download_file(
401
+ entity_id=entity_id,
402
+ entity_type=Entity,
403
+ asset_id=asset2_id,
404
+ output_path=output_path,
405
+ token=auth_token,
406
+ )
407
+
408
+ assert Path(output_path, "foo/bar/foo.h5").read_bytes() == b"foo"
409
+ assert Path(output_path, "foo/bar/bar.swc").read_bytes() == b"bar"
410
+
411
+
271
412
  @patch("entitysdk.route.get_route_name")
272
413
  def test_client_get(
273
414
  mock_route,
@@ -430,3 +430,19 @@ def test_stream_paginated_request_with_unexpected_page_size(
430
430
  match="Unexpected response: payload.pagination.page_size=2 but it should be 123",
431
431
  ):
432
432
  next(it)
433
+
434
+
435
+ def test_validate_filename_extension_consistency(tmp_path):
436
+ assert test_module.validate_filename_extension_consistency(tmp_path / "foo.txt", ".txt")
437
+ assert test_module.validate_filename_extension_consistency(tmp_path / "foo.txt", ".TXT")
438
+
439
+
440
+ def test_create_intermediate_directories(tmp_path):
441
+ path = tmp_path / "foo" / "bar" / "foo.txt"
442
+
443
+ assert not path.parent.is_dir()
444
+
445
+ test_module.create_intermediate_directories(path)
446
+
447
+ assert path.parent.is_dir()
448
+ assert path.parent.parent.is_dir()
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