entitysdk 0.1.1__tar.gz → 0.2.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 (70) hide show
  1. {entitysdk-0.1.1/src/entitysdk.egg-info → entitysdk-0.2.0}/PKG-INFO +1 -1
  2. {entitysdk-0.1.1 → entitysdk-0.2.0}/examples/morphology.ipynb +1 -1
  3. {entitysdk-0.1.1 → entitysdk-0.2.0}/examples/searching.ipynb +2 -1
  4. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/client.py +32 -3
  5. entitysdk-0.2.0/src/entitysdk/common.py +45 -0
  6. entitysdk-0.2.0/src/entitysdk/config.py +37 -0
  7. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/models/agent.py +3 -0
  8. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/models/contribution.py +2 -3
  9. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/models/entity.py +3 -3
  10. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/models/morphology.py +1 -7
  11. entitysdk-0.2.0/src/entitysdk/typedef.py +13 -0
  12. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/util.py +9 -0
  13. {entitysdk-0.1.1 → entitysdk-0.2.0/src/entitysdk.egg-info}/PKG-INFO +1 -1
  14. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk.egg-info/SOURCES.txt +4 -2
  15. entitysdk-0.2.0/tests/unit/models/data/reconstruction_morphology.json +174 -0
  16. entitysdk-0.2.0/tests/unit/models/test_agent.py +66 -0
  17. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/unit/models/test_contribution.py +18 -0
  18. entitysdk-0.2.0/tests/unit/models/test_morphology.py +62 -0
  19. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/unit/test_client.py +30 -0
  20. entitysdk-0.2.0/tests/unit/test_common.py +89 -0
  21. entitysdk-0.1.1/src/entitysdk/common.py +0 -12
  22. entitysdk-0.1.1/src/entitysdk/config.py +0 -21
  23. entitysdk-0.1.1/src/entitysdk/typedef.py +0 -5
  24. entitysdk-0.1.1/tests/unit/models/test_agent.py +0 -25
  25. entitysdk-0.1.1/tests/unit/models/test_morphology.py +0 -138
  26. {entitysdk-0.1.1 → entitysdk-0.2.0}/.github/workflows/sdist.yml +0 -0
  27. {entitysdk-0.1.1 → entitysdk-0.2.0}/.github/workflows/tox.yml +0 -0
  28. {entitysdk-0.1.1 → entitysdk-0.2.0}/.gitignore +0 -0
  29. {entitysdk-0.1.1 → entitysdk-0.2.0}/CHANGELOG.rst +0 -0
  30. {entitysdk-0.1.1 → entitysdk-0.2.0}/CONTRIBUTING.md +0 -0
  31. {entitysdk-0.1.1 → entitysdk-0.2.0}/LICENSE.txt +0 -0
  32. {entitysdk-0.1.1 → entitysdk-0.2.0}/README.md +0 -0
  33. {entitysdk-0.1.1 → entitysdk-0.2.0}/examples/contribution.ipynb +0 -0
  34. {entitysdk-0.1.1 → entitysdk-0.2.0}/pyproject.toml +0 -0
  35. {entitysdk-0.1.1 → entitysdk-0.2.0}/setup.cfg +0 -0
  36. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/__init__.py +0 -0
  37. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/core.py +0 -0
  38. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/exception.py +0 -0
  39. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/mixin.py +0 -0
  40. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/models/__init__.py +0 -0
  41. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/models/asset.py +0 -0
  42. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/models/base.py +0 -0
  43. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/models/core.py +0 -0
  44. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/models/mtype.py +0 -0
  45. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/models/response.py +0 -0
  46. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/result.py +0 -0
  47. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/route.py +0 -0
  48. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/serdes.py +0 -0
  49. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk/token_manager.py +0 -0
  50. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk.egg-info/dependency_links.txt +0 -0
  51. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk.egg-info/requires.txt +0 -0
  52. {entitysdk-0.1.1 → entitysdk-0.2.0}/src/entitysdk.egg-info/top_level.txt +0 -0
  53. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/__init__.py +0 -0
  54. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/integration/__init__.py +0 -0
  55. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/integration/conftest.py +0 -0
  56. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/integration/test_searching.py +0 -0
  57. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/unit/__init__.py +0 -0
  58. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/unit/conftest.py +0 -0
  59. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/unit/models/__init__.py +0 -0
  60. {entitysdk-0.1.1/tests/unit → entitysdk-0.2.0/tests/unit/models}/data/.gitignore +0 -0
  61. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/unit/models/test_asset.py +0 -0
  62. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/unit/test_base.py +0 -0
  63. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/unit/test_config.py +0 -0
  64. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/unit/test_result.py +0 -0
  65. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/unit/test_route.py +0 -0
  66. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/unit/test_serdes.py +0 -0
  67. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/unit/test_token_manager.py +0 -0
  68. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/unit/test_util.py +0 -0
  69. {entitysdk-0.1.1 → entitysdk-0.2.0}/tests/unit/util.py +0 -0
  70. {entitysdk-0.1.1 → entitysdk-0.2.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entitysdk
3
- Version: 0.1.1
3
+ Version: 0.2.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>
@@ -134,7 +134,7 @@
134
134
  " strain=strain,\n",
135
135
  " brain_region=brain_region,\n",
136
136
  " location=brain_location,\n",
137
- " legacy_id=\"temp\",\n",
137
+ " legacy_id=None,\n",
138
138
  ")"
139
139
  ]
140
140
  },
@@ -13,7 +13,8 @@
13
13
  "\n",
14
14
  "from entitysdk.client import Client\n",
15
15
  "from entitysdk.common import ProjectContext\n",
16
- "from entitysdk.models.contribution import Organization, Person, Role\n",
16
+ "from entitysdk.models.agent import Organization, Person\n",
17
+ "from entitysdk.models.contribution import Role\n",
17
18
  "from entitysdk.models.morphology import (\n",
18
19
  " ReconstructionMorphology,\n",
19
20
  " Species,\n",
@@ -13,7 +13,8 @@ from entitysdk.models.asset import Asset, LocalAssetMetadata
13
13
  from entitysdk.models.core import Identifiable
14
14
  from entitysdk.result import IteratorResult
15
15
  from entitysdk.token_manager import TokenManager
16
- from entitysdk.typedef import ID
16
+ from entitysdk.typedef import ID, DeploymentEnvironment
17
+ from entitysdk.util import build_api_url
17
18
 
18
19
 
19
20
  class Client:
@@ -21,10 +22,11 @@ class Client:
21
22
 
22
23
  def __init__(
23
24
  self,
24
- api_url: str,
25
+ api_url: str | None = None,
25
26
  project_context: ProjectContext | None = None,
26
27
  http_client: httpx.Client | None = None,
27
28
  token_manager: TokenManager | None = None,
29
+ environment: DeploymentEnvironment | str | None = None,
28
30
  ) -> None:
29
31
  """Initialize client.
30
32
 
@@ -33,12 +35,39 @@ class Client:
33
35
  project_context: Project context.
34
36
  http_client: Optional HTTP client to use.
35
37
  token_manager: Optional token manager to use.
38
+ environment: Deployment environent.
36
39
  """
37
- self.api_url = api_url.rstrip("/")
40
+ try:
41
+ environment = DeploymentEnvironment(environment) if environment else None
42
+ except ValueError:
43
+ raise EntitySDKError(
44
+ f"'{environment}' is not a valid DeploymentEnvironment. "
45
+ f"Choose one of: {[str(env) for env in DeploymentEnvironment]}"
46
+ ) from None
47
+
48
+ self.api_url = self._handle_api_url(
49
+ api_url=api_url,
50
+ environment=environment,
51
+ )
38
52
  self.project_context = project_context
39
53
  self._http_client = http_client or httpx.Client()
40
54
  self._token_manager = token_manager
41
55
 
56
+ @staticmethod
57
+ def _handle_api_url(api_url: str | None, environment: DeploymentEnvironment | None) -> str:
58
+ """Return or create api url."""
59
+ match (api_url, environment):
60
+ case (str(), None):
61
+ return api_url
62
+ case (None, DeploymentEnvironment()):
63
+ return build_api_url(environment=environment)
64
+ case (None, None):
65
+ raise EntitySDKError("Neither api_url nor environment have been defined.")
66
+ case (str(), DeploymentEnvironment()):
67
+ raise EntitySDKError("Either the api_url or environment must be defined, not both.")
68
+ case _:
69
+ raise EntitySDKError("Either api_url or environment is of the wrong type.")
70
+
42
71
  def _get_token(self, override_token: str | None = None) -> str:
43
72
  """Get a token either from an override or from the token manager.
44
73
 
@@ -0,0 +1,45 @@
1
+ """Common stuff."""
2
+
3
+ import re
4
+ from typing import Self
5
+ from uuid import UUID
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from entitysdk.exception import EntitySDKError
10
+ from entitysdk.typedef import DeploymentEnvironment
11
+
12
+ UUID_RE = "[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}"
13
+
14
+
15
+ VLAB_URL_PATTERN = re.compile(
16
+ r"^https:\/\/(?P<env>staging|www)\.openbraininstitute\.org"
17
+ rf"\/app\/virtual-lab\/lab\/(?P<vlab>{UUID_RE})"
18
+ rf"\/project\/(?P<proj>{UUID_RE})(?:\/.*)?$"
19
+ )
20
+
21
+
22
+ class ProjectContext(BaseModel):
23
+ """Project context."""
24
+
25
+ project_id: UUID
26
+ virtual_lab_id: UUID
27
+ environment: DeploymentEnvironment | None = None
28
+
29
+ @classmethod
30
+ def from_vlab_url(cls, url: str) -> Self:
31
+ """Construct a ProjectContext from a virtual lab url."""
32
+ result = VLAB_URL_PATTERN.match(url)
33
+
34
+ if not result:
35
+ raise EntitySDKError(f"Badly formed vlab url: {url}")
36
+
37
+ env = {
38
+ "www": DeploymentEnvironment.production,
39
+ "staging": DeploymentEnvironment.staging,
40
+ }[result.group("env")]
41
+
42
+ vlab_id = UUID(result.group("vlab"))
43
+ proj_id = UUID(result.group("proj"))
44
+
45
+ return cls(project_id=proj_id, virtual_lab_id=vlab_id, environment=env)
@@ -0,0 +1,37 @@
1
+ """Configuration for this library."""
2
+
3
+ from typing import Annotated
4
+
5
+ from pydantic import Field
6
+ from pydantic_settings import BaseSettings
7
+
8
+
9
+ class Settings(BaseSettings):
10
+ """Constants for this library."""
11
+
12
+ page_size: Annotated[
13
+ int | None,
14
+ Field(
15
+ alias="ENTITYSDK_PAGE_SIZE",
16
+ description="Default pagination page size, or None to use server default.",
17
+ ),
18
+ ] = None
19
+
20
+ staging_api_url: Annotated[
21
+ str,
22
+ Field(
23
+ alias="ENTITYSDK_STAGING_API_URL",
24
+ description="Default staging entitycore API url.",
25
+ ),
26
+ ] = "https://staging.openbraininstitute.org/api/entitycore"
27
+
28
+ production_api_url: Annotated[
29
+ str,
30
+ Field(
31
+ alias="ENTITYSDK_PRODUCTION_API_URL",
32
+ description="Default production entitycore API url.",
33
+ ),
34
+ ] = "https://www.openbraininstitute.org/api/entitycore"
35
+
36
+
37
+ settings = Settings()
@@ -66,3 +66,6 @@ class Organization(Agent):
66
66
  description="The alternative name of the organization.",
67
67
  ),
68
68
  ] = None
69
+
70
+
71
+ AgentUnion = Annotated[Person | Organization, Field(discriminator="type")]
@@ -4,7 +4,7 @@ from typing import Annotated
4
4
 
5
5
  from pydantic import Field
6
6
 
7
- from entitysdk.models.agent import Organization, Person
7
+ from entitysdk.models.agent import AgentUnion
8
8
  from entitysdk.models.core import Identifiable
9
9
  from entitysdk.models.entity import Entity
10
10
 
@@ -30,9 +30,8 @@ class Contribution(Identifiable):
30
30
  """Contribution model."""
31
31
 
32
32
  agent: Annotated[
33
- Person | Organization,
33
+ AgentUnion,
34
34
  Field(
35
- discriminator="type",
36
35
  description="The agent of the contribution.",
37
36
  ),
38
37
  ]
@@ -5,7 +5,7 @@ from uuid import UUID
5
5
 
6
6
  from pydantic import Field
7
7
 
8
- from entitysdk.models.agent import Agent
8
+ from entitysdk.models.agent import AgentUnion
9
9
  from entitysdk.models.core import Identifiable
10
10
 
11
11
 
@@ -13,11 +13,11 @@ class Entity(Identifiable):
13
13
  """Entity is a model with id and authorization."""
14
14
 
15
15
  createdBy: Annotated[
16
- Agent | None,
16
+ AgentUnion | None,
17
17
  Field(description="The agent that created this entity."),
18
18
  ] = None
19
19
  updatedBy: Annotated[
20
- Agent | None,
20
+ AgentUnion | None,
21
21
  Field(
22
22
  description="The agent that updated this entity.",
23
23
  ),
@@ -190,13 +190,6 @@ class ReconstructionMorphology(HasAssets, Entity):
190
190
  description="The description of the morphology.",
191
191
  ),
192
192
  ]
193
- pref_label: Annotated[
194
- str | None,
195
- Field(
196
- examples=["layer 5 Pyramidal Cell"],
197
- description="The preferred label of the morphology.",
198
- ),
199
- ] = None
200
193
  species: Annotated[
201
194
  Species,
202
195
  Field(
@@ -227,3 +220,4 @@ class ReconstructionMorphology(HasAssets, Entity):
227
220
  description="The mtype classes of the morphology.",
228
221
  ),
229
222
  ] = None
223
+ legacy_id: list[str] | None = None
@@ -0,0 +1,13 @@
1
+ """Type definitions."""
2
+
3
+ import uuid
4
+ from enum import StrEnum
5
+
6
+ ID = uuid.UUID
7
+
8
+
9
+ class DeploymentEnvironment(StrEnum):
10
+ """Deployment environment."""
11
+
12
+ staging = "staging"
13
+ production = "production"
@@ -10,6 +10,7 @@ from entitysdk.common import ProjectContext
10
10
  from entitysdk.config import settings
11
11
  from entitysdk.exception import EntitySDKError
12
12
  from entitysdk.models.response import ListResponse
13
+ from entitysdk.typedef import DeploymentEnvironment
13
14
 
14
15
 
15
16
  def make_db_api_request(
@@ -126,3 +127,11 @@ def stream_paginated_request(
126
127
  if number_of_items >= limit:
127
128
  return
128
129
  page += 1
130
+
131
+
132
+ def build_api_url(environment: DeploymentEnvironment) -> str:
133
+ """Return API url for the respective deployment environment."""
134
+ return {
135
+ DeploymentEnvironment.staging: settings.staging_api_url,
136
+ DeploymentEnvironment.production: settings.production_api_url,
137
+ }[environment]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entitysdk
3
- Version: 0.1.1
3
+ Version: 0.2.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>
@@ -46,6 +46,7 @@ tests/unit/__init__.py
46
46
  tests/unit/conftest.py
47
47
  tests/unit/test_base.py
48
48
  tests/unit/test_client.py
49
+ tests/unit/test_common.py
49
50
  tests/unit/test_config.py
50
51
  tests/unit/test_result.py
51
52
  tests/unit/test_route.py
@@ -53,9 +54,10 @@ tests/unit/test_serdes.py
53
54
  tests/unit/test_token_manager.py
54
55
  tests/unit/test_util.py
55
56
  tests/unit/util.py
56
- tests/unit/data/.gitignore
57
57
  tests/unit/models/__init__.py
58
58
  tests/unit/models/test_agent.py
59
59
  tests/unit/models/test_asset.py
60
60
  tests/unit/models/test_contribution.py
61
- tests/unit/models/test_morphology.py
61
+ tests/unit/models/test_morphology.py
62
+ tests/unit/models/data/.gitignore
63
+ tests/unit/models/data/reconstruction_morphology.json
@@ -0,0 +1,174 @@
1
+ {
2
+ "id": "9720aa4a-0cd1-4f10-b6ad-04deb8f997b6",
3
+ "update_date": "2024-10-29T13:36:30.827776Z",
4
+ "creation_date": "2024-09-10T14:25:03.707956Z",
5
+ "createdBy": null,
6
+ "updatedBy": null,
7
+ "authorized_public": true,
8
+ "authorized_project_id": "0dbced5f-cc3d-488a-8c7f-cfb8ea039dc6",
9
+ "assets": [
10
+ {
11
+ "id": "a90bab5c-8529-4829-99fc-e90a270e8b30",
12
+ "update_date": null,
13
+ "creation_date": null,
14
+ "path": "mpg150211_A_idA.swc",
15
+ "full_path": "public/a98b7abc-fc46-4700-9e3d-37137812c730/0dbced5f-cc3d-488a-8c7f-cfb8ea039dc6/assets/reconstruction_morphology/9720aa4a-0cd1-4f10-b6ad-04deb8f997b6/mpg150211_A_idA.swc",
16
+ "bucket_name": "entitycore-data-dev",
17
+ "is_directory": false,
18
+ "content_type": "application/swc",
19
+ "size": 561221,
20
+ "sha256_digest": "8b850eface3e7b881db62c361dc5da62be008ca81501c406a59646bf7075c95b",
21
+ "status": "created",
22
+ "meta": {
23
+ "legacy": {
24
+ "@id": "https://bbp.epfl.ch/data/public/morphologies/42315b31-081f-4d40-8b94-b72dbd16c99e",
25
+ "name": "mpg150211_A_idA.swc",
26
+ "@type": "DataDownload",
27
+ "digest": {
28
+ "value": "8b850eface3e7b881db62c361dc5da62be008ca81501c406a59646bf7075c95b",
29
+ "algorithm": "SHA-256"
30
+ },
31
+ "atLocation": {
32
+ "@type": "Location",
33
+ "store": {
34
+ "@id": "nxv:diskStorageDefault",
35
+ "_rev": 1,
36
+ "@type": "DiskStorage"
37
+ }
38
+ },
39
+ "contentUrl": "https://bbp.epfl.ch/nexus/v1/files/public/morphologies/https:%2F%2Fbbp.epfl.ch%2Fdata%2Fpublic%2Fmorphologies%2F42315b31-081f-4d40-8b94-b72dbd16c99e",
40
+ "contentSize": {
41
+ "value": 561221,
42
+ "unitCode": "bytes"
43
+ },
44
+ "encodingFormat": "application/swc"
45
+ }
46
+ }
47
+ },
48
+ {
49
+ "id": "56394210-d2fb-4755-990c-8083a97a46fd",
50
+ "update_date": null,
51
+ "creation_date": null,
52
+ "path": "mpg150211_A_idA.ASC",
53
+ "full_path": "public/a98b7abc-fc46-4700-9e3d-37137812c730/0dbced5f-cc3d-488a-8c7f-cfb8ea039dc6/assets/reconstruction_morphology/9720aa4a-0cd1-4f10-b6ad-04deb8f997b6/mpg150211_A_idA.ASC",
54
+ "bucket_name": "entitycore-data-dev",
55
+ "is_directory": false,
56
+ "content_type": "application/asc",
57
+ "size": 525332,
58
+ "sha256_digest": "bf00878444539ead825f2d451cdf6f87072526f9a82c87de35fca5563ae5596b",
59
+ "status": "created",
60
+ "meta": {
61
+ "legacy": {
62
+ "@id": "https://bbp.epfl.ch/data/public/morphologies/7f422a37-48f0-4ea4-8760-f1481403d51f",
63
+ "name": "mpg150211_A_idA.ASC",
64
+ "@type": "DataDownload",
65
+ "digest": {
66
+ "value": "bf00878444539ead825f2d451cdf6f87072526f9a82c87de35fca5563ae5596b",
67
+ "algorithm": "SHA-256"
68
+ },
69
+ "atLocation": {
70
+ "@type": "Location",
71
+ "store": {
72
+ "@id": "nxv:diskStorageDefault",
73
+ "_rev": 1,
74
+ "@type": "DiskStorage"
75
+ }
76
+ },
77
+ "contentUrl": "https://bbp.epfl.ch/nexus/v1/files/public/morphologies/https:%2F%2Fbbp.epfl.ch%2Fdata%2Fpublic%2Fmorphologies%2F7f422a37-48f0-4ea4-8760-f1481403d51f",
78
+ "contentSize": {
79
+ "value": 525332,
80
+ "unitCode": "bytes"
81
+ },
82
+ "encodingFormat": "application/asc"
83
+ }
84
+ }
85
+ }
86
+ ],
87
+ "name": "mpg150211_A_idA",
88
+ "location": null,
89
+ "brain_region": {
90
+ "id": 407,
91
+ "update_date": "2025-03-29T16:48:20.636283Z",
92
+ "creation_date": "2025-03-29T16:48:20.636283Z",
93
+ "createdBy": null,
94
+ "updatedBy": null,
95
+ "authorized_public": null,
96
+ "authorized_project_id": null,
97
+ "name": "Field CA1, pyramidal layer",
98
+ "acronym": "CA1sp",
99
+ "children": []
100
+ },
101
+ "description": "Neuronal morphology of excitatory neuron cell 'mpg150211_A_idA' in Field CA1, pyramidal layer of cell type SR_PC. A neuron that releases excitatory neurotransmitters. This neuron doesn't have axon, has a basal dendrite and apical dendrite.",
102
+ "species": {
103
+ "id": "0895fa61-fa6d-4674-9014-7300f9edf8da",
104
+ "update_date": "2025-03-29T16:48:36.175090Z",
105
+ "creation_date": "2025-03-29T16:48:36.175090Z",
106
+ "createdBy": null,
107
+ "updatedBy": null,
108
+ "authorized_public": null,
109
+ "authorized_project_id": null,
110
+ "name": "Mus musculus",
111
+ "taxonomy_id": "NCBITaxon:10090"
112
+ },
113
+ "strain": null,
114
+ "license": null,
115
+ "contributions": [
116
+ {
117
+ "id": "79987156-9f97-4095-b69e-6b4e5ca213bb",
118
+ "update_date": "2025-03-29T16:49:12.446844Z",
119
+ "creation_date": "2025-03-29T16:49:12.446844Z",
120
+ "agent": {
121
+ "id": "eda92bb4-560b-4ad2-9c4b-4649bb254e83",
122
+ "update_date": "2024-04-22T14:40:13.122739Z",
123
+ "creation_date": "2021-03-17T16:59:51.555000Z",
124
+ "type": "person",
125
+ "pref_label": "Maurizio Pezzoli Gonzalez",
126
+ "givenName": "Maurizio",
127
+ "familyName": "Pezzoli Gonzalez"
128
+ },
129
+ "role": {
130
+ "id": "8b06e557-277b-4b0c-a439-bfb02d41927c",
131
+ "update_date": "2025-03-29T16:48:36.192831Z",
132
+ "creation_date": "2025-03-29T16:48:36.192831Z",
133
+ "name": "unspecified",
134
+ "role_id": "unspecified"
135
+ },
136
+ "entity": null
137
+ },
138
+ {
139
+ "id": "7f43ce20-e63d-48c1-bc73-dabb38ee181b",
140
+ "update_date": "2025-03-29T16:49:12.449364Z",
141
+ "creation_date": "2025-03-29T16:49:12.449364Z",
142
+ "agent": {
143
+ "id": "f27cfa11-d1bc-4a14-b651-174bb53674ff",
144
+ "update_date": "2021-04-14T09:51:14.062000Z",
145
+ "creation_date": "2021-04-14T09:51:14.062000Z",
146
+ "type": "organization",
147
+ "pref_label": "École Polytechnique Fédérale de Lausanne",
148
+ "alternative_name": ""
149
+ },
150
+ "role": {
151
+ "id": "8b06e557-277b-4b0c-a439-bfb02d41927c",
152
+ "update_date": "2025-03-29T16:48:36.192831Z",
153
+ "creation_date": "2025-03-29T16:48:36.192831Z",
154
+ "name": "unspecified",
155
+ "role_id": "unspecified"
156
+ },
157
+ "entity": null
158
+ }
159
+ ],
160
+ "mtypes": [
161
+ {
162
+ "id": "b2221a58-008a-45a6-8464-7159c324c985",
163
+ "update_date": "2021-09-06T15:47:05.039000Z",
164
+ "creation_date": "2021-09-06T15:47:04.890000Z",
165
+ "createdBy": null,
166
+ "updatedBy": null,
167
+ "authorized_public": null,
168
+ "authorized_project_id": null,
169
+ "pref_label": "SR_PC",
170
+ "definition": "",
171
+ "alt_label": ""
172
+ }
173
+ ]
174
+ }
@@ -0,0 +1,66 @@
1
+ from entitysdk.models import agent as test_module
2
+ from entitysdk.models.core import Identifiable
3
+
4
+ from ..util import MOCK_UUID
5
+
6
+
7
+ def test_person_entity():
8
+ agent = test_module.Person(
9
+ givenName="foo",
10
+ familyName="bar",
11
+ pref_label="test",
12
+ type="person",
13
+ )
14
+ assert agent.givenName == "foo"
15
+ assert agent.familyName == "bar"
16
+ assert agent.pref_label == "test"
17
+ assert agent.type == "person"
18
+
19
+
20
+ def test_organization_entity():
21
+ organization = test_module.Organization(
22
+ pref_label="foo",
23
+ alternative_name="bar",
24
+ type="organization",
25
+ )
26
+ assert organization.pref_label == "foo"
27
+ assert organization.alternative_name == "bar"
28
+ assert organization.type == "organization"
29
+
30
+
31
+ def test_agent_discriminated_union():
32
+ class A(Identifiable):
33
+ agent: test_module.AgentUnion | None = None
34
+
35
+ res = A.model_validate(
36
+ {
37
+ "id": MOCK_UUID,
38
+ }
39
+ )
40
+ assert res.id == MOCK_UUID
41
+
42
+ res = A.model_validate(
43
+ {
44
+ "id": MOCK_UUID,
45
+ "agent": {
46
+ "type": "organization",
47
+ "pref_label": "foo",
48
+ },
49
+ }
50
+ )
51
+ assert res.id == MOCK_UUID
52
+ assert isinstance(res.agent, test_module.Organization)
53
+
54
+ res = A.model_validate(
55
+ {
56
+ "id": MOCK_UUID,
57
+ "agent": {
58
+ "type": "person",
59
+ "pref_label": "foo",
60
+ "givenName": "John",
61
+ "familyName": "Smith",
62
+ },
63
+ }
64
+ )
65
+ assert res.id == MOCK_UUID
66
+ assert isinstance(res.agent, test_module.Person)
@@ -36,3 +36,21 @@ def test_contribution(role):
36
36
 
37
37
  assert res.agent == organization
38
38
  assert res.role == role
39
+
40
+ res = test_module.Contribution.model_validate(
41
+ {
42
+ "agent": person.model_dump(mode="json"),
43
+ "role": role.model_dump(mode="json"),
44
+ }
45
+ )
46
+ assert res.agent == person
47
+ assert res.role == role
48
+
49
+ res = test_module.Contribution.model_validate(
50
+ {
51
+ "agent": organization.model_dump(mode="json"),
52
+ "role": role.model_dump(mode="json"),
53
+ }
54
+ )
55
+ assert res.agent == organization
56
+ assert res.role == role
@@ -0,0 +1,62 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+
6
+ from entitysdk.models.morphology import (
7
+ ReconstructionMorphology,
8
+ )
9
+
10
+ from ..util import MOCK_UUID
11
+
12
+ DATA_DIR = Path(__file__).parent / "data"
13
+
14
+
15
+ @pytest.fixture
16
+ def json_morphology_expanded():
17
+ return json.loads(Path(DATA_DIR / "reconstruction_morphology.json").read_bytes())
18
+
19
+
20
+ @pytest.fixture
21
+ def morphology(json_morphology_expanded):
22
+ return ReconstructionMorphology.model_validate(json_morphology_expanded)
23
+
24
+
25
+ def test_read_reconstruction_morphology(client, httpx_mock, auth_token, json_morphology_expanded):
26
+ httpx_mock.add_response(method="GET", json=json_morphology_expanded)
27
+ entity = client.get_entity(
28
+ entity_id=MOCK_UUID,
29
+ entity_type=ReconstructionMorphology,
30
+ token=auth_token,
31
+ with_assets=False,
32
+ )
33
+ assert entity.model_dump(mode="json") == json_morphology_expanded | {"legacy_id": None}
34
+
35
+
36
+ def test_register_reconstruction_morphology(
37
+ client, httpx_mock, auth_token, morphology, json_morphology_expanded
38
+ ):
39
+ httpx_mock.add_response(
40
+ method="POST", json=morphology.model_dump(mode="json") | {"id": str(MOCK_UUID)}
41
+ )
42
+ registered = client.register_entity(entity=morphology, token=auth_token)
43
+ expected_json = json_morphology_expanded.copy() | {"id": str(MOCK_UUID)}
44
+ assert registered.model_dump(mode="json") == expected_json | {"legacy_id": None}
45
+
46
+
47
+ def test_update_reconstruction_morphology(
48
+ client, httpx_mock, auth_token, morphology, json_morphology_expanded
49
+ ):
50
+ httpx_mock.add_response(
51
+ method="PATCH",
52
+ json=morphology.model_dump(mode="json") | {"name": "foo"},
53
+ )
54
+ updated = client.update_entity(
55
+ entity_id=morphology.id,
56
+ entity_type=ReconstructionMorphology,
57
+ attrs_or_entity={"name": "foo"},
58
+ token=auth_token,
59
+ )
60
+
61
+ expected_json = json_morphology_expanded.copy() | {"name": "foo"}
62
+ assert updated.model_dump(mode="json") == expected_json | {"legacy_id": None}
@@ -1,13 +1,43 @@
1
1
  import io
2
+ import re
2
3
  import uuid
3
4
  from unittest.mock import patch
4
5
 
5
6
  import pytest
6
7
 
7
8
  from entitysdk.client import Client
9
+ from entitysdk.config import settings
8
10
  from entitysdk.exception import EntitySDKError
9
11
  from entitysdk.mixin import HasAssets
10
12
  from entitysdk.models.entity import Entity
13
+ from entitysdk.typedef import DeploymentEnvironment
14
+
15
+
16
+ def test_client_api_url():
17
+ client = Client(api_url="foo")
18
+ assert client.api_url == "foo"
19
+
20
+ client = Client(api_url=None, environment="staging")
21
+ assert client.api_url == settings.staging_api_url
22
+
23
+ client = Client(api_url=None, environment="production")
24
+ assert client.api_url == settings.production_api_url
25
+
26
+ with pytest.raises(
27
+ EntitySDKError, match="Either the api_url or environment must be defined, not both."
28
+ ):
29
+ Client(api_url="foo", environment="staging")
30
+
31
+ with pytest.raises(EntitySDKError, match="Neither api_url nor environment have been defined."):
32
+ Client()
33
+
34
+ with pytest.raises(EntitySDKError, match="Either api_url or environment is of the wrong type."):
35
+ Client(api_url=int)
36
+
37
+ str_envs = [str(env) for env in DeploymentEnvironment]
38
+ expected = f"'foo' is not a valid DeploymentEnvironment. Choose one of: {str_envs}"
39
+ with pytest.raises(EntitySDKError, match=re.escape(expected)):
40
+ Client(environment="foo")
11
41
 
12
42
 
13
43
  def test_client_get_token():
@@ -0,0 +1,89 @@
1
+ import pytest
2
+
3
+ from entitysdk import common as test_module
4
+ from entitysdk.exception import EntitySDKError
5
+ from entitysdk.typedef import DeploymentEnvironment
6
+
7
+ VLAB_ID = "ff888f05-f314-4702-8a92-b86f754270bb"
8
+ PROJ_ID = "f373e771-3a2f-4f45-ab59-0955efd7b1f4"
9
+
10
+
11
+ @pytest.mark.parametrize(
12
+ ("url", "expected"),
13
+ [
14
+ (
15
+ f"https://staging.openbraininstitute.org/app/virtual-lab/lab/{VLAB_ID}/project/{PROJ_ID}/home",
16
+ test_module.ProjectContext(
17
+ virtual_lab_id=VLAB_ID,
18
+ project_id=PROJ_ID,
19
+ environment=DeploymentEnvironment.staging,
20
+ ),
21
+ ),
22
+ (
23
+ f"https://staging.openbraininstitute.org/app/virtual-lab/lab/{VLAB_ID}/project/{PROJ_ID}",
24
+ test_module.ProjectContext(
25
+ virtual_lab_id=VLAB_ID,
26
+ project_id=PROJ_ID,
27
+ environment=DeploymentEnvironment.staging,
28
+ ),
29
+ ),
30
+ (
31
+ f"https://www.openbraininstitute.org/app/virtual-lab/lab/{VLAB_ID}/project/{PROJ_ID}/home",
32
+ test_module.ProjectContext(
33
+ virtual_lab_id=VLAB_ID,
34
+ project_id=PROJ_ID,
35
+ environment=DeploymentEnvironment.production,
36
+ ),
37
+ ),
38
+ (
39
+ f"https://www.openbraininstitute.org/app/virtual-lab/lab/{VLAB_ID}/project/{PROJ_ID}",
40
+ test_module.ProjectContext(
41
+ virtual_lab_id=VLAB_ID,
42
+ project_id=PROJ_ID,
43
+ environment=DeploymentEnvironment.production,
44
+ ),
45
+ ),
46
+ ],
47
+ )
48
+ def test_project_context__from_vlab_url(url, expected):
49
+ res = test_module.ProjectContext.from_vlab_url(url)
50
+ assert res == expected
51
+
52
+
53
+ @pytest.mark.parametrize(
54
+ ("url", "expected_error", "expected_msg"),
55
+ [
56
+ ("asdf", EntitySDKError, "Badly formed vlab url"),
57
+ ("https://", EntitySDKError, "Badly formed vlab url"),
58
+ ("https://openbraininstitute.org", EntitySDKError, "Badly formed vlab url"),
59
+ ("https://staging.openbraininstitute.org", EntitySDKError, "Badly formed vlab url"),
60
+ (
61
+ "https://staging.openbraininstitute.org/app/virtual-lab/lab/foo",
62
+ EntitySDKError,
63
+ "Badly formed vlab url",
64
+ ),
65
+ (
66
+ "https://staging.openbraininstitute.org/app/virtual-lab/lab/foo/project/bar",
67
+ EntitySDKError,
68
+ "Badly formed vlab url",
69
+ ),
70
+ (
71
+ f"https://dev.openbraininstitute.org/app/virtual-lab/lab/{VLAB_ID}/project/{PROJ_ID}",
72
+ EntitySDKError,
73
+ "Badly formed vlab url",
74
+ ),
75
+ (
76
+ f"https://dev.openbraininstitute.org/app/virtual-lab/lab/foo/project/{PROJ_ID}",
77
+ EntitySDKError,
78
+ "Badly formed vlab url",
79
+ ),
80
+ (
81
+ f"https://dev.openbraininstitute.org/app/virtual-lab/lab/{VLAB_ID}/project/bar",
82
+ EntitySDKError,
83
+ "Badly formed vlab url",
84
+ ),
85
+ ],
86
+ )
87
+ def test_project_context__from_vlab_url__raises(url, expected_error, expected_msg):
88
+ with pytest.raises(expected_error, match=expected_msg):
89
+ test_module.ProjectContext.from_vlab_url(url)
@@ -1,12 +0,0 @@
1
- """Common stuff."""
2
-
3
- from dataclasses import dataclass
4
- from uuid import UUID
5
-
6
-
7
- @dataclass
8
- class ProjectContext:
9
- """Project context."""
10
-
11
- project_id: UUID
12
- virtual_lab_id: UUID
@@ -1,21 +0,0 @@
1
- """Configuration for this library."""
2
-
3
- from typing import Annotated
4
-
5
- from pydantic import Field
6
- from pydantic_settings import BaseSettings
7
-
8
-
9
- class Settings(BaseSettings):
10
- """Constants for this library."""
11
-
12
- page_size: Annotated[
13
- int | None,
14
- Field(
15
- alias="ENTITYSDK_PAGE_SIZE",
16
- description="Default pagination page size, or None to use server default.",
17
- ),
18
- ] = None
19
-
20
-
21
- settings = Settings()
@@ -1,5 +0,0 @@
1
- """Type definitions."""
2
-
3
- import uuid
4
-
5
- ID = uuid.UUID
@@ -1,25 +0,0 @@
1
- from entitysdk.models import agent as test_module
2
-
3
-
4
- def test_person_entity():
5
- agent = test_module.Person(
6
- givenName="foo",
7
- familyName="bar",
8
- pref_label="test",
9
- type="person",
10
- )
11
- assert agent.givenName == "foo"
12
- assert agent.familyName == "bar"
13
- assert agent.pref_label == "test"
14
- assert agent.type == "person"
15
-
16
-
17
- def test_organization_entity():
18
- organization = test_module.Organization(
19
- pref_label="foo",
20
- alternative_name="bar",
21
- type="organization",
22
- )
23
- assert organization.pref_label == "foo"
24
- assert organization.alternative_name == "bar"
25
- assert organization.type == "organization"
@@ -1,138 +0,0 @@
1
- import pytest
2
-
3
- from entitysdk.models.morphology import (
4
- BrainLocation,
5
- BrainRegion,
6
- ReconstructionMorphology,
7
- Species,
8
- Strain,
9
- )
10
-
11
- from ..util import MOCK_UUID, PROJECT_ID
12
-
13
-
14
- @pytest.fixture
15
- def species(random_uuid):
16
- return Species(id=random_uuid, name="Mus musculus", taxonomy_id="NCBITaxon:10090")
17
-
18
-
19
- @pytest.fixture
20
- def strain(species, random_uuid):
21
- return Strain(
22
- name="Cux2-CreERT2",
23
- taxonomy_id="http://bbp.epfl.ch/neurosciencegraph/ontologies/speciestaxonomy/RBS4I6tyfUBSDt1i0jXLpgN",
24
- species_id=random_uuid,
25
- )
26
-
27
-
28
- @pytest.fixture
29
- def brain_location(random_uuid):
30
- return BrainLocation(
31
- id=random_uuid,
32
- x=4101.52490234375,
33
- y=1173.8499755859375,
34
- z=4744.60009765625,
35
- )
36
-
37
-
38
- @pytest.fixture
39
- def brain_region():
40
- return BrainRegion(
41
- id=68,
42
- name="Frontal pole, layer 1",
43
- acronym="FRP1",
44
- children=[],
45
- )
46
-
47
-
48
- @pytest.fixture
49
- def morphology(species, strain, brain_region):
50
- return ReconstructionMorphology(
51
- name="my-morph",
52
- description="my-description",
53
- species=species,
54
- strain=strain,
55
- brain_region=brain_region,
56
- )
57
-
58
-
59
- @pytest.fixture
60
- def json_morphology_expanded():
61
- return {
62
- "authorized_project_id": str(PROJECT_ID),
63
- "authorized_public": False,
64
- "license": {
65
- "id": str(MOCK_UUID),
66
- "creation_date": "2025-02-20T13:42:46.532333Z",
67
- "update_date": "2025-02-20T13:42:46.532333Z",
68
- "name": "https://creativecommons.org/licenses/by-nc-sa/4.0/",
69
- "description": "Foo",
70
- "label": "CC BY-NC-SA 4.0 Deed",
71
- },
72
- "id": str(MOCK_UUID),
73
- "creation_date": "2025-02-20T13:44:50.111791Z",
74
- "update_date": "2025-02-20T13:44:50.111791Z",
75
- "name": "04446-04462-X10187-Y13578_final",
76
- "description": "Bar",
77
- "location": None,
78
- "species": {
79
- "id": str(MOCK_UUID),
80
- "creation_date": "2025-02-20T13:42:56.228818Z",
81
- "update_date": "2025-02-20T13:42:56.228818Z",
82
- "name": "Mus musculus",
83
- "taxonomy_id": "NCBITaxon:10090",
84
- },
85
- "strain": None,
86
- "brain_region": {
87
- "id": 1,
88
- "creation_date": "2025-02-20T13:36:51.010167Z",
89
- "update_date": "2025-02-20T13:36:51.010167Z",
90
- "name": "Reticular nucleus of the thalamus",
91
- "acronym": "RT",
92
- "children": [],
93
- },
94
- }
95
-
96
-
97
- def test_read_reconstruction_morphology(client, httpx_mock, auth_token, json_morphology_expanded):
98
- httpx_mock.add_response(method="GET", json=json_morphology_expanded)
99
-
100
- entity = client.get_entity(
101
- entity_id=MOCK_UUID,
102
- entity_type=ReconstructionMorphology,
103
- token=auth_token,
104
- with_assets=False,
105
- )
106
-
107
- assert entity.id == MOCK_UUID
108
-
109
-
110
- def test_register_reconstruction_morphology(client, httpx_mock, auth_token, morphology):
111
- httpx_mock.add_response(
112
- method="POST", json=morphology.model_dump(mode="json") | {"id": str(MOCK_UUID)}
113
- )
114
-
115
- registered = client.register_entity(entity=morphology, token=auth_token)
116
-
117
- assert registered.id == MOCK_UUID
118
- assert registered.name == morphology.name
119
-
120
-
121
- def test_update_reconstruction_morphology(client, httpx_mock, auth_token, morphology):
122
- morphology = morphology.evolve(id=1)
123
- httpx_mock.add_response(
124
- method="PATCH",
125
- json=morphology.model_dump(mode="json") | {"id": str(MOCK_UUID), "name": "foo"},
126
- )
127
-
128
- updated = client.update_entity(
129
- entity_id=MOCK_UUID,
130
- entity_type=ReconstructionMorphology,
131
- attrs_or_entity={
132
- "name": "foo",
133
- },
134
- token=auth_token,
135
- )
136
-
137
- assert updated.id == MOCK_UUID
138
- assert updated.name == "foo"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes