From f238dec26cf184b489c4222346357cd2e21a3c24 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 30 Sep 2025 11:09:16 -0700 Subject: [PATCH 01/41] ran pre-commit --- synapseclient/models/__init__.py | 4 + .../protocols/schema_organization_protocol.py | 292 +++++++ synapseclient/models/schema_organization.py | 731 ++++++++++++++++++ .../synchronous/test_schema_organization.py | 247 ++++++ .../synchronous/test_schema_organization.py | 137 ++++ 5 files changed, 1411 insertions(+) create mode 100644 synapseclient/models/protocols/schema_organization_protocol.py create mode 100644 synapseclient/models/schema_organization.py create mode 100644 tests/integration/synapseclient/models/synchronous/test_schema_organization.py create mode 100644 tests/unit/synapseclient/models/synchronous/test_schema_organization.py diff --git a/synapseclient/models/__init__.py b/synapseclient/models/__init__.py index cd7534b3e..2b16f2d3a 100644 --- a/synapseclient/models/__init__.py +++ b/synapseclient/models/__init__.py @@ -14,6 +14,7 @@ from synapseclient.models.materializedview import MaterializedView from synapseclient.models.mixins.table_components import QueryMixin from synapseclient.models.project import Project +from synapseclient.models.schema_organization import JSONSchema, SchemaOrganization from synapseclient.models.services import FailureStrategy from synapseclient.models.submissionview import SubmissionView from synapseclient.models.table import Table @@ -112,6 +113,9 @@ "DatasetCollection", # Submission models "SubmissionView", + # JSON Schema models + "SchemaOrganization", + "JSONSchema", ] # Static methods to expose as functions diff --git a/synapseclient/models/protocols/schema_organization_protocol.py b/synapseclient/models/protocols/schema_organization_protocol.py new file mode 100644 index 000000000..08618f399 --- /dev/null +++ b/synapseclient/models/protocols/schema_organization_protocol.py @@ -0,0 +1,292 @@ +"""Protocols for the specific methods of this class that have synchronous counterparts +generated at runtime.""" + +from typing import TYPE_CHECKING, Any, Mapping, Optional, Protocol, Sequence + +if TYPE_CHECKING: + from synapseclient import Synapse + from synapseclient.models.mixins.json_schema import JSONSchemaVersionInfo + from synapseclient.models.schema_organization import JSONSchema + + +class SchemaOrganizationProtocol(Protocol): + """ + The protocol for methods that are asynchronous but also + have a synchronous counterpart that may also be called. + """ + + def get(self, synapse_client: Optional["Synapse"] = None) -> None: + """ + Gets the metadata from Synapse for this organization + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Example: + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + org.get() + + """ + return None + + def create(self, synapse_client: Optional["Synapse"] = None) -> None: + """ + Creates this organization in Synapse + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Example: + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + org.create() + + """ + return None + + def delete(self, synapse_client: Optional["Synapse"] = None) -> None: + """ + Deletes this organization in Synapse + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Example: + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + org.delete() + + """ + return None + + def get_json_schema_list( + self, synapse_client: Optional["Synapse"] = None + ) -> list["JSONSchema"]: + """ + Gets the list of JSON Schemas that are part of this organization + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Returns: A list of JSONSchema objects + + Example: + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + org.get_json_schema_list() + + """ + return [] + + def get_acl(self, synapse_client: Optional["Synapse"] = None) -> dict[str, Any]: + """ + Gets the ACL for this organization + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Returns: + A dictionary in the form of this response: + https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html + + Example: + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + org.get_acl() + """ + return {} + + def update_acl( + self, + resource_access: Sequence[Mapping[str, Sequence[str]]], + etag: str, + synapse_client: Optional["Synapse"] = None, + ) -> None: + """ + Updates the ACL for this organization + + Arguments: + resource_access: List of ResourceAccess objects, each containing: + - principalId: The user or team ID + - accessType: List of permission types (e.g., ["READ", "CREATE", "DELETE"]) + see: + https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/ResourceAccess.html + etag: The etag from get_organization_acl() for concurrency control + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Example: + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + resource_access = [{ + 'principalId': 1, + 'accessType': ['DELETE', 'READ', 'UPDATE', 'CREATE', 'CHANGE_PERMISSIONS'] + }] + org.update_acl_async(resource_access=resource_access) + + """ + return None + + +class JSONSchemaProtocol(Protocol): + """ + The protocol for methods that are asynchronous but also + have a synchronous counterpart that may also be called. + """ + + def get(self, synapse_client: Optional["Synapse"] = None) -> None: + """ + Gets this JSON Schemas metadata + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Raises: + ValueError: This JSONSchema doesn't exist in its organization + + Example: + from synapseclient.models import JSONSchema + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + js = JSONSchema("my.schema.name", "my.org.name") + js.get() + """ + return None + + def create( + self, + body: dict[str, Any], + version: Optional[str] = None, + dry_run: bool = False, + synapse_client: Optional["Synapse"] = None, + ) -> None: + return None + + def delete(self) -> None: + """ + Deletes this JSON Schema + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Example: + from synapseclient.models import JSONSchema + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + js = JSONSchema("my.schema.name", "my.org.name") + js.delete() + """ + return None + + def get_versions( + self, synapse_client: Optional["Synapse"] = None + ) -> list["JSONSchemaVersionInfo"]: + """ + Gets a list of all versions of this JSONSchema + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Returns: + A JSONSchemaVersionInfo for each version of this schema + + Example: + from synapseclient.models import JSONSchema + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + js = JSONSchema("my.schema.name", "my.org.name") + versions = get_versions() + """ + return [] + + def get_body( + self, version: Optional[str] = None, synapse_client: Optional["Synapse"] = None + ) -> dict[str, Any]: + """ + Gets the JSON body for the schema. + + Arguments: + version: Defaults to None. + - If a version is supplied, that versions body will be returned. + - If no version is supplied the most recent version will be returned. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Returns: + The JSON Schema body + + Example: + from synapseclient.models import JSONSchema + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + js = JSONSchema("my.schema.name", "my.org.name") + # Get latest version + latest = js.get_body() + # Get specific version + first = ajs.get_body("0.0.1") + """ + return {} diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py new file mode 100644 index 000000000..a1a4a5df5 --- /dev/null +++ b/synapseclient/models/schema_organization.py @@ -0,0 +1,731 @@ +""" +This file contains the SchemaOrganization and JSONSchema classes. +These are used to manage Organization and JSON Schema entities in Synapse. +""" + +import re +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence + +from synapseclient.api import ( + create_organization, + delete_json_schema, + delete_organization, + get_json_schema_body, + get_organization, + get_organization_acl, + list_json_schema_versions, + list_json_schemas, + list_organizations_sync, + update_organization_acl, +) +from synapseclient.core.async_utils import async_to_sync +from synapseclient.models.mixins.json_schema import JSONSchemaVersionInfo +from synapseclient.models.protocols.schema_organization_protocol import ( + JSONSchemaProtocol, + SchemaOrganizationProtocol, +) + +if TYPE_CHECKING: + from synapseclient import Synapse + + +@dataclass() +@async_to_sync +class SchemaOrganization(SchemaOrganizationProtocol): + """ + Represents an [Organization](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/Organization.html). + + Attributes: + id: The ID of the organization + name: The name of the organization + created_on: The date this organization was created + created_by: The ID of the user that created this organization + + Example: + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + # Create a new org + org = SchemaOrganization("my.new.org.name") + org.create() + + # Get the metadata and JSON Schemas for an existing org + org = SchemaOrganization("my.org.name") + org.get_async() + print(org) + schemas = org.get_json_schema_list() + print(schemas) + """ + + name: str + """The name of the organization""" + + id: Optional[str] = None + """The ID of the organization""" + + created_on: Optional[str] = None + """The date this organization was created""" + + created_by: Optional[str] = None + """The ID of the user that created this organization""" + + def __post_init__(self) -> None: + self._check_name(self.name) + + def __repr__(self): + return f"SchemaOrganization(name={self.name!r})" + + async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: + """ + Gets the metadata from Synapse for this organization + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Example: + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + import asyncio + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + asyncio.run(org.get_async()) + + """ + if self.id: + return + response = await get_organization(self.name, synapse_client=synapse_client) + self._fill_from_dict(response) + + # Should this be named 'store_async'? + async def create_async(self, synapse_client: Optional["Synapse"] = None) -> None: + """ + Creates this organization in Synapse + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Example: + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + import asyncio + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + asyncio.run(org.create()) + + """ + if self.id: + await self.get_async(synapse_client=synapse_client) + response = await create_organization(self.name, synapse_client=synapse_client) + self._fill_from_dict(response) + + async def delete_async(self, synapse_client: Optional["Synapse"] = None) -> None: + """ + Deletes this organization in Synapse + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Example: + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + import asyncio + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + asyncio.run(org.delete()) + + """ + if not self.id: + await self.get_async(synapse_client=synapse_client) + await delete_organization(self.id, synapse_client=synapse_client) + + async def get_json_schema_list_async( + self, synapse_client: Optional["Synapse"] = None + ) -> list["JSONSchema"]: + """ + Gets the list of JSON Schemas that are part of this organization + + Returns: A list of JSONSchema objects + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Example: + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + import asyncio + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + asyncio.run(org.get_json_schema_list_async()) + + """ + response = list_json_schemas(self.name, synapse_client=synapse_client) + schemas = [] + async for item in response: + schemas.append(JSONSchema.from_response(item)) + return schemas + + async def get_acl_async( + self, synapse_client: Optional["Synapse"] = None + ) -> dict[str, Any]: + """ + Gets the ACL for this organization + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Returns: + A dictionary in the form of this response: + https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html + + Example: + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + import asyncio + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + asyncio.run(org.get_acl_async()) + """ + if not self.id: + await self.get_async() + response = await get_organization_acl(self.id, synapse_client=synapse_client) + return response + + async def update_acl_async( + self, + resource_access: Sequence[Mapping[str, Sequence[str]]], + etag: str, + synapse_client: Optional["Synapse"] = None, + ) -> None: + """ + Updates the ACL for this organization + + Arguments: + resource_access: List of ResourceAccess objects, each containing: + - principalId: The user or team ID + - accessType: List of permission types (e.g., ["READ", "CREATE", "DELETE"]) + see: + https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/ResourceAccess.html + etag: The etag from get_organization_acl() for concurrency control + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Example: + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + import asyncio + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + resource_access = [{ + 'principalId': 1, + 'accessType': ['DELETE', 'READ', 'UPDATE', 'CREATE', 'CHANGE_PERMISSIONS'] + }] + asyncio.run(org.update_acl_async(resource_access=resource_access)) + + """ + if not self.id: + await self.get_async() + await update_organization_acl( + organization_id=self.id, + resource_access=resource_access, + etag=etag, + synapse_client=synapse_client, + ) + + def _check_name(self, name) -> None: + """ + Checks that the input name is a valid Synapse organization name + - Must start with a letter + - Must contains only letters, numbers and periods. + + Arguments: + name: The name of the organization to be checked + + Raises: + ValueError: When the name isn't valid + """ + if not re.match("^([A-Za-z])([A-Za-z]|\d|\.)*$", name): + raise ValueError( + "Organization name must start with a letter and contain " + f"only letters numbers, and periods: {name}" + ) + + def _fill_from_dict(self, response: dict[str, Any]) -> None: + """ + Fills in this classes attributes using a Synapse API response + + Args: + response: A response from this endpoint: + https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/Organization.html + """ + self.name = response.get("name") + self.id = response.get("id") + self.created_on = response.get("createdOn") + self.created_by = response.get("createdBy") + + @classmethod + def from_response(cls, response: dict[str, Any]) -> "SchemaOrganization": + """ + Creates an SchemaOrganization object using a Synapse API response + + Arguments: + response: A response from this endpoint: + https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/Organization.html + + Returns: + A SchemaOrganization object using the input response + + Example: + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + import asyncio + from synapseclient.api import get_organization + + syn = Synapse() + syn.login() + + response = asyncio.run(get_organization("my.org.name")) + org = SchemaOrganization.from_response(response) + + """ + org = SchemaOrganization(response.get("name")) + org._fill_from_dict(response) + return org + + +@dataclass() +@async_to_sync +class JSONSchema(JSONSchemaProtocol): + """ + Represents a: + [JSON Schema](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchemaInfo.html) + + Attributes: + name: The name of the schema + organization_name: The name of the organization the schema belongs to + organization_id: the id of the organization the schema belongs to + id: The ID of the schema + created_on: The date this schema was created + created_by: The ID of the user that created this schema + uri: The uri of this schema + + Example: + from synapseclient.models import JSON Schema + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + """ + + name: str + """The name of the schema""" + + organization_name: str + """The name of the organization the schema belongs to""" + + organization_id: Optional[int] = None + """The id of the organization the schema belongs to""" + + id: Optional[str] = None + """The ID of the schema""" + + created_on: Optional[str] = None + """The date this schema was created""" + + created_by: Optional[str] = None + """The ID of the user that created this schema""" + + uri: str = field(init=False) + """The uri of this schema""" + + def __post_init__(self) -> None: + self.uri = f"{self.organization_name}-{self.name}" + self._check_name(self.name) + + def __repr__(self): + return ( + f"JSONSchema(name={self.name!r}, organization={self.organization_name!r})" + ) + + async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: + """ + Gets this JSON Schemas metadata + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Raises: + ValueError: This JSONSchema doesn't exist in its organization + + Example: + from synapseclient.models import JSONSchema + from synapseclient import Synapse + import asyncio + + syn = Synapse() + syn.login() + + js = JSONSchema("my.schema.name", "my.org.name") + asyncio.run(js.get_async()) + """ + if self.id: + return + + # Check that the org exists, + # if it doesn't list_json_schemas will unhelpfully return an empty generator. + org = SchemaOrganization(self.organization_name) + await org.get_async() + + org_schemas = list_json_schemas( + self.organization_name, synapse_client=synapse_client + ) + async for schema in org_schemas: + if schema["schemaName"] == self.name: + self._fill_from_dict(schema) + return + raise ValueError( + ( + f"Organization: '{self.organization_name}' does not contain a schema with " + f"name: '{self.name}'" + ) + ) + + # Should this ba named store? + # TODO: crate api function, and async version of method, write docstring + def create( + self, + body: dict[str, Any], + version: Optional[str] = None, + dry_run: bool = False, + synapse_client: Optional["Synapse"] = None, + ) -> None: + uri = self.uri + if version: + self._check_semantic_version(version) + uri = f"{uri}-{version}" + body["$id"] = uri + + request_body = { + "concreteType": "org.sagebionetworks.repo.model.schema.CreateSchemaRequest", + "schema": body, + "dryRun": dry_run, + } + if not synapse_client: + from synapseclient import Synapse + + synapse_client = Synapse() + synapse_client.login() + + response = synapse_client._waitForAsync( + "/schema/type/create/async", request_body + ) + self._fill_from_dict(response["newVersionInfo"]) + + async def delete_async(self, synapse_client: Optional["Synapse"] = None) -> None: + """ + Deletes this JSONSchema + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Example: + from synapseclient.models import JSONSchema + from synapseclient import Synapse + import asyncio + + syn = Synapse() + syn.login() + + js = JSONSchema("my.schema.name", "my.org.name") + asyncio.run(js.delete_async()) + """ + await delete_json_schema(self.uri, synapse_client=synapse_client) + + async def get_versions_async( + self, synapse_client: Optional["Synapse"] = None + ) -> list[JSONSchemaVersionInfo]: + """ + Gets a list of all versions of this JSONSchema + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Returns: + A JSONSchemaVersionInfo for each version of this schema + + Example: + from synapseclient.models import JSONSchema + from synapseclient import Synapse + import asyncio + + syn = Synapse() + syn.login() + + js = JSONSchema("my.schema.name", "my.org.name") + versions = asyncio.run(get_versions_async()) + """ + all_schemas = list_json_schema_versions( + self.organization_name, self.name, synapse_client=synapse_client + ) + versions = [] + async for schema in all_schemas: + # Schemas created without a semantic version will be returned from the API call. + # Those won't be returned here since they aren't really versions. + # JSONSchemaVersionInfo.semantic_version could also be changed to optional. + if "semanticVersion" in schema: + versions.append(self._create_json_schema_version_from_response(schema)) + return versions + + async def get_body_async( + self, version: Optional[str] = None, synapse_client: Optional["Synapse"] = None + ) -> dict[str, Any]: + """ + Gets the JSON body for the schema. + + Arguments: + version: Defaults to None. + - If a version is supplied, that versions body will be returned. + - If no version is supplied the most recent version will be returned. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Returns: + The JSON Schema body + + Example: + from synapseclient.models import JSONSchema + from synapseclient import Synapse + import asyncio + + syn = Synapse() + syn.login() + + js = JSONSchema("my.schema.name", "my.org.name") + # Get latest version + latest = asyncio.run(js.get_body_async()) + # Get specific version + first = asyncio.run(js.get_body_async("0.0.1")) + """ + uri = self.uri + if version: + self._check_semantic_version(version) + uri = f"{uri}-{version}" + response = await get_json_schema_body(uri, synapse_client=synapse_client) + return response + + @classmethod + def from_uri(cls, uri: str) -> "JSONSchema": + """ + Creates a JSONSchema from a URI. + The URI can either be a semantic version or not + - non-semantic: ORGANIZATION.NAME-SCHEMA.NAME + - semantic: ORGANIZATION.NAME-SCHEMA.NAME-MAJOR.MINOR.PATCH + + Arguments: + uri: The URI to the JSON Schema in Synapse + + Raises: + ValueError: If the URI isn't in the correct form. + + Returns: + A JSONSchema object + + Example: + from synapseclient.models import JSONSchema + + # Non-semantic URI + js1 = JSONSchema.from_uri("my.org-my.schema") + + # Semantic URI + js2 = JSONSchema.from_uri("my.org-my.schema-0.0.1") + + """ + uri_parts = uri.split("-") + if len(uri_parts) > 3 or len(uri_parts) < 2: + msg = ( + "The URI must be in the form of " + "'-' or '--': " + f"{uri}" + ) + raise ValueError(msg) + return JSONSchema(name=uri_parts[1], organization_name=uri_parts[0]) + + @classmethod + def from_response(cls, response: dict[str, Any]) -> "JSONSchema": + """ + Creates a JSONSchema object using a Synapse API response + + Arguments: + response: A response from this endpoint: + [JSON Schema](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchemaInfo.html) + + Returns: + A JSONSchema object from the API response + + Example: + from synapseclient.models import JSONSchema + from synapseclient import Synapse + from synapseclient.api import list_json_schemas + import asyncio + + syn = Synapse() + syn.login() + + async def get_first_response(): + async_gen = list_json_schemas("my.org.name") + responses = [] + async for item in async_gen: + responses.append(item) + return responses[1] + + response = asyncio.run(get_first_response()) + JSONSchema.from_response(response) + + """ + js = JSONSchema(response.get("schemaName"), response.get("organizationName")) + js._fill_from_dict(response) + return js + + @staticmethod + def _create_json_schema_version_from_response( + response: dict[str, Any] + ) -> JSONSchemaVersionInfo: + """ + Creates a JSONSchemaVersionInfo object from a Synapse API response + + Arguments: + response: This Synapse API object: + [JsonSchemaVersionInfo]https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchemaVersionInfo.html + + Returns: + A JSONSchemaVersionInfo object + """ + return JSONSchemaVersionInfo( + organization_id=response.get("organizationId"), + organization_name=response.get("organizationName"), + schema_id=response.get("schemaId"), + id=response.get("$id"), + schema_name=response.get("schemaName"), + version_id=response.get("versionId"), + semantic_version=response.get("semanticVersion"), + json_sha256_hex=response.get("jsonSHA256Hex"), + created_on=response.get("createdOn"), + created_by=response.get("createdBy"), + ) + + def _fill_from_dict(self, response: dict[str, Any]) -> None: + """ + Fills in this classes attributes using a Synapse API response + + Arguments: + response: This Synapse API object: + [JsonSchema]https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchema.html + """ + self.organization_id = response.get("organizationId") + self.organization_name = response.get("organizationName") + self.id = response.get("schemaId") + self.name = response.get("schemaName") + self.created_on = response.get("createdOn") + self.created_by = response.get("createdBy") + self.uri = f"{self.organization_name}-{self.name}" + + def _check_semantic_version(self, version: str) -> None: + """ + Checks that the semantic version is correctly formatted + + Args: + version: A semantic version(ie. `1.0.0`) to be checked + + Raises: + ValueError: If the string is not a correct semantic version + """ + if not re.match("^(\d+)\.(\d+)\.([1-9]\d*)$", version): + raise ValueError( + ( + "Schema version must be a semantic version with no letters " + "and a major, minor and patch version, such as 0.0.1: " + f"{version}" + ) + ) + + def _check_name(self, name) -> None: + """ + Checks that the input name is a valid Synapse JSONSchema name + - Must start with a letter + - Must contains only letters, numbers and periods. + + Arguments: + name: The name of the organization to be checked + + Raises: + ValueError: When the name isn't valid + """ + if not re.match("^([A-Za-z])([A-Za-z]|\d|\.)*$", name): + raise ValueError( + ( + "Schema name must start with a letter and contain " + f"only letters numbers and periods: {name}" + ) + ) + + +# TODO: Move to a utils module +def list_json_schema_organizations( + synapse_client: Optional["Synapse"] = None, +) -> list[SchemaOrganization]: + """ + Lists all the Schema Organizations currently in Synapse + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Returns: + A list of SchemaOrganizations + """ + all_orgs = [ + SchemaOrganization.from_response(org) + for org in list_organizations_sync(synapse_client=synapse_client) + ] + return all_orgs diff --git a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py new file mode 100644 index 000000000..276a61610 --- /dev/null +++ b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py @@ -0,0 +1,247 @@ +"""Integration tests for SchemaOrganization and JSONSchema classes""" +import uuid +from typing import Any, Optional + +import pytest + +from synapseclient import Synapse +from synapseclient.core.exceptions import SynapseHTTPError +from synapseclient.models import JSONSchema, SchemaOrganization +from synapseclient.models.schema_organization import list_json_schema_organizations + + +def org_exists(name: str, synapse_client: Optional[Synapse] = None) -> bool: + """ + Checks if any organizations exists with the given name + + Args: + name: the name to check + syn: Synapse client + + Returns: + bool: True if an organization match the given name + """ + matching_orgs = [ + org + for org in list_json_schema_organizations(synapse_client=synapse_client) + if org.name == name + ] + return len(matching_orgs) == 1 + + +@pytest.fixture(name="module_organization", scope="module") +def fixture_module_organization(request) -> SchemaOrganization: + """ + Returns a created organization at the module scope. Used to hold JSON Schemas created by tests. + """ + + name = "".join(i for i in str(uuid.uuid4()) if i.isalpha()) + org = SchemaOrganization(name) + org.create() + + def delete_org(): + for schema in org.get_json_schema_list(): + schema.delete() + org.delete() + + request.addfinalizer(delete_org) + + return org + + +@pytest.fixture(name="json_schema", scope="function") +def fixture_json_schema(module_organization: SchemaOrganization) -> JSONSchema: + """ + Returns a JSON Schema + """ + + name = "".join(i for i in str(uuid.uuid4()) if i.isalpha()) + js = JSONSchema(name, module_organization.name) + return js + + +@pytest.fixture(name="organization", scope="function") +def fixture_organization(syn: Synapse, request) -> SchemaOrganization: + """ + Returns a Synapse organization. + """ + + name = "".join(i for i in str(uuid.uuid4()) if i.isalpha()) + org = SchemaOrganization(name) + + def delete_org(): + if org_exists(name, syn): + org.delete() + + request.addfinalizer(delete_org) + + return org + + +@pytest.fixture(name="organization_with_schema", scope="function") +def fixture_organization_with_schema(request) -> SchemaOrganization: + """ + Returns a Synapse organization. + As Cleanup it checks for JSON Schemas and deletes them""" + + name = "".join(i for i in str(uuid.uuid4()) if i.isalpha()) + org = SchemaOrganization(name) + org.create() + js1 = JSONSchema("schema1", name) + js2 = JSONSchema("schema2", name) + js3 = JSONSchema("schema3", name) + js1.create({}) + js2.create({}) + js3.create({}) + + def delete_org(): + for schema in org.get_json_schema_list(): + schema.delete() + org.delete() + + request.addfinalizer(delete_org) + + return org + + +class TestSchemaOrganization: + """Synchronous integration tests for SchemaOrganization.""" + + @pytest.fixture(autouse=True, scope="function") + def init(self, syn: Synapse) -> None: + self.syn = syn + + def test_create_and_get(self, organization: SchemaOrganization) -> None: + # GIVEN an initialized organization object that hasn't been created in Synapse + # THEN it shouldn't have any metadata besides it's name + assert organization.name is not None + assert organization.id is None + assert organization.created_by is None + assert organization.created_on is None + # AND it shouldn't exists in Synapse + assert not org_exists(organization.name, synapse_client=self.syn) + # WHEN I create the organization the metadata will be saved + organization.create(synapse_client=self.syn) + assert organization.name is not None + assert organization.id is not None + assert organization.created_by is not None + assert organization.created_on is not None + # AND it should exist in Synapse + assert org_exists(organization.name, synapse_client=self.syn) + # AND it should be getable by future instances with the same name + org2 = SchemaOrganization(organization.name) + org2.get(synapse_client=self.syn) + assert organization.name is not None + assert organization.id is not None + assert organization.created_by is not None + assert organization.created_on is not None + + def test_get_json_schema_list( + self, + organization: SchemaOrganization, + organization_with_schema: SchemaOrganization, + ) -> None: + # GIVEN an organization with no schemas and one with 3 schemas + organization.create(synapse_client=self.syn) + # THEN get_json_schema_list should return the correct list of schemas + assert not organization.get_json_schema_list(synapse_client=self.syn) + assert ( + len(organization_with_schema.get_json_schema_list(synapse_client=self.syn)) + == 3 + ) + + def test_get_acl_and_update_acl(self, organization: SchemaOrganization) -> None: + # GIVEN an organization that has been initialized, but not created + # THEN get_acl should raise an error + with pytest.raises( + SynapseHTTPError, match="404 Client Error: Organization with name" + ): + organization.get_acl(synapse_client=self.syn) + # GIVEN an organization that has been created + organization.create(synapse_client=self.syn) + acl = organization.get_acl(synapse_client=self.syn) + resource_access: list[dict[str, Any]] = acl["resourceAccess"] + # THEN the resource access should be have one principal + assert len(resource_access) == 1 + # WHEN adding another principal to the resource access + resource_access.append({"principalId": 1, "accessType": ["READ"]}) + etag = acl["etag"] + # AND updating the acl + organization.update_acl(resource_access, etag, synapse_client=self.syn) + # THEN the resource access should be have two principals + acl = organization.get_acl(synapse_client=self.syn) + assert len(acl["resourceAccess"]) == 2 + + +class TestJSONSchema: + """Synchronous integration tests for JSONSchema.""" + + @pytest.fixture(autouse=True, scope="function") + def init(self, syn: Synapse) -> None: + self.syn = syn + + def test_create_and_get(self, json_schema: JSONSchema) -> None: + # GIVEN an initialized schema object that hasn't been created in Synapse + # THEN it shouldn't have any metadata besides it's name and organization name, and uri + assert json_schema.name + assert json_schema.organization_name + assert json_schema.uri + assert not json_schema.organization_id + assert not json_schema.id + assert not json_schema.created_by + assert not json_schema.created_on + # WHEN the object is created + json_schema.create({}, synapse_client=self.syn) + assert json_schema.name + assert json_schema.organization_name + assert json_schema.uri + assert json_schema.organization_id + assert json_schema.id + assert json_schema.created_by + assert json_schema.created_on + # AND it should be getable by future instances with the same name + js2 = JSONSchema(json_schema.name, json_schema.organization_name) + js2.get(synapse_client=self.syn) + assert js2.name + assert js2.organization_name + assert js2.uri + assert js2.organization_id + assert js2.id + assert js2.created_by + assert js2.created_on + + def test_get_versions(self, json_schema: JSONSchema) -> None: + # GIVEN an schema that hasn't been created + # THEN get_versions should return an empty list + assert not json_schema.get_versions(synapse_client=self.syn) + # WHEN creating a schema with no version + json_schema.create(body={}, synapse_client=self.syn) + # THEN get_versions should return an empty list + assert json_schema.get_versions(synapse_client=self.syn) == [] + # WHEN creating a schema with a version + json_schema.create(body={}, version="0.0.1", synapse_client=self.syn) + # THEN get_versions should return that version + schemas = json_schema.get_versions(synapse_client=self.syn) + assert len(schemas) == 1 + assert schemas[0].semantic_version == "0.0.1" + + def test_get_body(self, json_schema: JSONSchema) -> None: + # GIVEN an schema that hasn't been created + # WHEN creating a schema with 2 version + first_body = {} + latest_body = {"description": ""} + json_schema.create(body=first_body, version="0.0.1", synapse_client=self.syn) + json_schema.create(body=latest_body, version="0.0.2", synapse_client=self.syn) + # WHEN get_body has no version argument + body0 = json_schema.get_body(synapse_client=self.syn) + # THEN the body should be the latest version + del body0["$id"] + assert body0 == {"description": ""} + # WHEN get_body has a version argument + body1 = json_schema.get_body(version="0.0.1", synapse_client=self.syn) + body2 = json_schema.get_body(version="0.0.2", synapse_client=self.syn) + # THEN the appropriate body should be returned + del body1["$id"] + del body2["$id"] + assert body1 == {} + assert body2 == {"description": ""} diff --git a/tests/unit/synapseclient/models/synchronous/test_schema_organization.py b/tests/unit/synapseclient/models/synchronous/test_schema_organization.py new file mode 100644 index 000000000..6e2d85dd2 --- /dev/null +++ b/tests/unit/synapseclient/models/synchronous/test_schema_organization.py @@ -0,0 +1,137 @@ +"""Unit tests for SchemaOrganization and JSONSchema classes""" +from typing import Any + +import pytest + +from synapseclient.models import JSONSchema, SchemaOrganization + + +class TestSchemaOrganization: + """Synchronous unit tests for SchemaOrganization.""" + + @pytest.mark.parametrize( + "name", + ["AAAAAAA", "A12345", "A....."], + ids=["Just letters", "Numbers", "Periods"], + ) + def test_init(self, name: str) -> None: + "Tests that legal names don't raise a ValueError on init" + assert SchemaOrganization(name) + + @pytest.mark.parametrize( + "name", + ["1AAAAAA", ".AAAAAA", "AAAAAA!"], + ids=["Starts with a number", "Starts with a period", "Special character"], + ) + def test_init_name_exceptions(self, name: str) -> None: + "Tests that illegal names raise a ValueError on init" + with pytest.raises(ValueError, match="Organization name must start with"): + SchemaOrganization(name) + + @pytest.mark.parametrize( + "response", + [ + { + "name": "AAAAAAA", + "id": "abc", + "createdOn": "9-30-25", + "createdBy": "123", + } + ], + ) + def test_from_response(self, response: dict[str, Any]): + "Tests that legal Synapse API responses result in created objects." + assert SchemaOrganization.from_response(response) + + @pytest.mark.parametrize( + "response", + [ + { + "name": None, + "id": None, + "createdOn": None, + "createdBy": None, + } + ], + ) + def test_from_response_with_exception(self, response: dict[str, Any]): + "Tests that illegal Synapse API responses cause exceptions" + with pytest.raises(TypeError): + SchemaOrganization.from_response(response) + + +class TestJSONSchema: + """Synchronous unit tests for JSONSchema.""" + + @pytest.mark.parametrize( + "name", + ["AAAAAAA", "A12345", "A....."], + ids=["Just letters", "Numbers", "Periods"], + ) + def test_init(self, name: str) -> None: + "Tests that legal names don't raise a ValueError on init" + assert JSONSchema(name, "org.name") + + @pytest.mark.parametrize( + "name", + ["1AAAAAA", ".AAAAAA", "AAAAAA!"], + ids=["Starts with a number", "Starts with a period", "Special character"], + ) + def test_init_name_exceptions(self, name: str) -> None: + "Tests that illegal names raise a ValueError on init" + with pytest.raises(ValueError, match="Schema name must start with"): + JSONSchema(name, "org.name") + + @pytest.mark.parametrize( + "uri", + ["ORG.NAME-SCHEMA.NAME", "ORG.NAME-SCHEMA.NAME-0.0.1"], + ids=["Non-semantic URI", "Semantic URI"], + ) + def test_from_uri(self, uri: str): + "Tests that legal schema URIs result in created objects." + assert JSONSchema.from_uri(uri) + + @pytest.mark.parametrize( + "uri", + ["ORG.NAME", "ORG.NAME-SCHEMA.NAME-0.0.1-extra.part"], + ids=["No dashes", "Too many dashes"], + ) + def test_from_uri_with_exceptions(self, uri: str): + "Tests that illegal schema URIs result in an exception." + with pytest.raises(ValueError, match="The URI must be in the form of"): + JSONSchema.from_uri(uri) + + @pytest.mark.parametrize( + "response", + [ + { + "createdOn": "9-30-25", + "createdBy": "123", + "organizationId": "123", + "organizationName": "org.name", + "schemaId": "123", + "schemaName": "schema.name", + } + ], + ) + def test_from_response(self, response: dict[str, Any]): + "Tests that legal Synapse API responses result in created objects." + assert JSONSchema.from_response(response) + + @pytest.mark.parametrize( + "response", + [ + { + "createdOn": None, + "createdBy": None, + "organizationId": None, + "organizationName": None, + "schemaId": None, + "schemaName": None, + } + ], + ) + def test_from_response_with_exception(self, response: dict[str, Any]): + "Tests that illegal Synapse API responses cause exceptions" + with pytest.raises(TypeError): + JSONSchema.from_response(response) From 2735b69de8f1a90e6512a86f4fff15dae58f0b6c Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 30 Sep 2025 12:21:27 -0700 Subject: [PATCH 02/41] fix example in get_acl methods --- .../models/protocols/schema_organization_protocol.py | 10 +++++----- synapseclient/models/schema_organization.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/synapseclient/models/protocols/schema_organization_protocol.py b/synapseclient/models/protocols/schema_organization_protocol.py index 08618f399..56fda235f 100644 --- a/synapseclient/models/protocols/schema_organization_protocol.py +++ b/synapseclient/models/protocols/schema_organization_protocol.py @@ -163,11 +163,11 @@ def update_acl( syn.login() org = SchemaOrganization("my.org.name") - resource_access = [{ - 'principalId': 1, - 'accessType': ['DELETE', 'READ', 'UPDATE', 'CREATE', 'CHANGE_PERMISSIONS'] - }] - org.update_acl_async(resource_access=resource_access) + current acl = org.get_acl() + resource_access = current_acl["resourceAccess"] + resource_access.append({"principalId": 1, "accessType": ["READ"]}) + etag = current_acl["etag"] + org.update_acl(resource_access, etag) """ return None diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index a1a4a5df5..72f3356f8 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -248,11 +248,11 @@ async def update_acl_async( syn.login() org = SchemaOrganization("my.org.name") - resource_access = [{ - 'principalId': 1, - 'accessType': ['DELETE', 'READ', 'UPDATE', 'CREATE', 'CHANGE_PERMISSIONS'] - }] - asyncio.run(org.update_acl_async(resource_access=resource_access)) + current acl = asyncio.run(org.get_acl_async()) + resource_access = current_acl["resourceAccess"] + resource_access.append({"principalId": 1, "accessType": ["READ"]}) + etag = current_acl["etag"] + asyncio.run(org.update_acl_async(resource_access, etag)) """ if not self.id: From b8cca36e1e70da30a6005646b67c1757e2ef91a8 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 30 Sep 2025 12:30:56 -0700 Subject: [PATCH 03/41] ran pre-commit --- .../synchronous/test_schema_organization.py | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py index 276a61610..b60604250 100644 --- a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py +++ b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py @@ -35,7 +35,7 @@ def fixture_module_organization(request) -> SchemaOrganization: Returns a created organization at the module scope. Used to hold JSON Schemas created by tests. """ - name = "".join(i for i in str(uuid.uuid4()) if i.isalpha()) + name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" org = SchemaOrganization(name) org.create() @@ -55,7 +55,7 @@ def fixture_json_schema(module_organization: SchemaOrganization) -> JSONSchema: Returns a JSON Schema """ - name = "".join(i for i in str(uuid.uuid4()) if i.isalpha()) + name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" js = JSONSchema(name, module_organization.name) return js @@ -66,7 +66,7 @@ def fixture_organization(syn: Synapse, request) -> SchemaOrganization: Returns a Synapse organization. """ - name = "".join(i for i in str(uuid.uuid4()) if i.isalpha()) + name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" org = SchemaOrganization(name) def delete_org(): @@ -84,7 +84,7 @@ def fixture_organization_with_schema(request) -> SchemaOrganization: Returns a Synapse organization. As Cleanup it checks for JSON Schemas and deletes them""" - name = "".join(i for i in str(uuid.uuid4()) if i.isalpha()) + name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" org = SchemaOrganization(name) org.create() js1 = JSONSchema("schema1", name) @@ -235,13 +235,18 @@ def test_get_body(self, json_schema: JSONSchema) -> None: # WHEN get_body has no version argument body0 = json_schema.get_body(synapse_client=self.syn) # THEN the body should be the latest version - del body0["$id"] - assert body0 == {"description": ""} + assert body0 == { + "description": "", + "$id": f"https://repo-prod.prod.sagebase.org/repo/v1/schema/type/registered/{json_schema.organization_name}-{json_schema.name}", + } # WHEN get_body has a version argument body1 = json_schema.get_body(version="0.0.1", synapse_client=self.syn) body2 = json_schema.get_body(version="0.0.2", synapse_client=self.syn) # THEN the appropriate body should be returned - del body1["$id"] - del body2["$id"] - assert body1 == {} - assert body2 == {"description": ""} + assert body1 == { + "$id": f"https://repo-prod.prod.sagebase.org/repo/v1/schema/type/registered/{json_schema.organization_name}-{json_schema.name}-0.0.1", + } + assert body2 == { + "description": "", + "$id": f"https://repo-prod.prod.sagebase.org/repo/v1/schema/type/registered/{json_schema.organization_name}-{json_schema.name}-0.0.2", + } From b07b15fed463bc5c1f8e66c35f173e9bdd4ad0a4 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 30 Sep 2025 13:16:56 -0700 Subject: [PATCH 04/41] ran pre-commit --- .../models/async/test_schema_organization.py | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 tests/integration/synapseclient/models/async/test_schema_organization.py diff --git a/tests/integration/synapseclient/models/async/test_schema_organization.py b/tests/integration/synapseclient/models/async/test_schema_organization.py new file mode 100644 index 000000000..21473e815 --- /dev/null +++ b/tests/integration/synapseclient/models/async/test_schema_organization.py @@ -0,0 +1,186 @@ +"""Integration tests for SchemaOrganization and JSONSchema classes""" +import uuid +from typing import Any, Optional + +import pytest + +from synapseclient import Synapse +from synapseclient.core.exceptions import SynapseHTTPError +from synapseclient.models import JSONSchema, SchemaOrganization +from synapseclient.models.schema_organization import list_json_schema_organizations + + +def org_exists(name: str, synapse_client: Optional[Synapse] = None) -> bool: + """ + Checks if any organizations exists with the given name + + Args: + name: the name to check + syn: Synapse client + + Returns: + bool: True if an organization match the given name + """ + matching_orgs = [ + org + for org in list_json_schema_organizations(synapse_client=synapse_client) + if org.name == name + ] + return len(matching_orgs) == 1 + + +@pytest.fixture(name="module_organization", scope="module") +async def fixture_module_organization(request) -> SchemaOrganization: + """ + Returns a created organization at the module scope. Used to hold JSON Schemas created by tests. + """ + + name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" + org = SchemaOrganization(name) + org.create() + + async def delete_org(): + for schema in org.get_json_schema_list(): + await schema.delete_async() + org.delete() + + request.addfinalizer(delete_org) + + return org + + +@pytest.fixture(name="json_schema", scope="function") +def fixture_json_schema(module_organization: SchemaOrganization) -> JSONSchema: + """ + Returns a JSON Schema + """ + + name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" + js = JSONSchema(name, module_organization.name) + return js + + +@pytest.fixture(name="organization", scope="function") +async def fixture_organization(syn: Synapse, request) -> SchemaOrganization: + """ + Returns a Synapse organization. + """ + name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" + org = SchemaOrganization(name) + + async def delete_org(): + if org_exists(name, syn): + org.delete_async() + + request.addfinalizer(delete_org) + + return org + + +@pytest.fixture(name="organization_with_schema", scope="function") +async def fixture_organization_with_schema(request) -> SchemaOrganization: + """ + Returns a Synapse organization. + As Cleanup it checks for JSON Schemas and deletes them""" + + name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" + org = SchemaOrganization(name) + await org.create_async() + js1 = JSONSchema("schema1", name) + js2 = JSONSchema("schema2", name) + js3 = JSONSchema("schema3", name) + # TODO: Change to create_async when method is working + js1.create({}) + js2.create({}) + js3.create({}) + + async def delete_org(): + for schema in org.get_json_schema_list_async(): + await schema.delete_async() + org.delete() + + request.addfinalizer(delete_org) + + return org + + +class TestSchemaOrganization: + """Synchronous integration tests for SchemaOrganization.""" + + @pytest.fixture(autouse=True, scope="function") + def init(self, syn: Synapse) -> None: + self.syn = syn + + @pytest.mark.asyncio + async def test_create_and_get(self, organization: SchemaOrganization) -> None: + # GIVEN an initialized organization object that hasn't been created in Synapse + # THEN it shouldn't have any metadata besides it's name + assert organization.name is not None + assert organization.id is None + assert organization.created_by is None + assert organization.created_on is None + # AND it shouldn't exists in Synapse + assert not org_exists(organization.name, synapse_client=self.syn) + # WHEN I create the organization the metadata will be saved + await organization.create_async(synapse_client=self.syn) + assert organization.name is not None + assert organization.id is not None + assert organization.created_by is not None + assert organization.created_on is not None + # AND it should exist in Synapse + assert org_exists(organization.name, synapse_client=self.syn) + # AND it should be getable by future instances with the same name + org2 = SchemaOrganization(organization.name) + await org2.get_async(synapse_client=self.syn) + assert organization.name is not None + assert organization.id is not None + assert organization.created_by is not None + assert organization.created_on is not None + + @pytest.mark.asyncio + async def test_get_json_schema_list( + self, + organization: SchemaOrganization, + organization_with_schema: SchemaOrganization, + ) -> None: + # GIVEN an organization with no schemas and one with 3 schemas + await organization.create_async(synapse_client=self.syn) + # THEN get_json_schema_list should return the correct list of schemas + schema_list = await organization.get_json_schema_list_async( + synapse_client=self.syn + ) + assert not schema_list + schema_list2 = await organization_with_schema.get_json_schema_list_async( + synapse_client=self.syn + ) + assert len(schema_list2) == 3 + + @pytest.mark.asyncio + async def test_get_acl_and_update_acl( + self, organization: SchemaOrganization + ) -> None: + # GIVEN an organization that has been initialized, but not created + # THEN get_acl should raise an error + with pytest.raises( + SynapseHTTPError, match="404 Client Error: Organization with name" + ): + await organization.get_acl_async(synapse_client=self.syn) + # GIVEN an organization that has been created + await organization.create_async(synapse_client=self.syn) + acl = await organization.get_acl_async(synapse_client=self.syn) + resource_access: list[dict[str, Any]] = acl["resourceAccess"] + # THEN the resource access should be have one principal + assert len(resource_access) == 1 + # WHEN adding another principal to the resource access + resource_access.append({"principalId": 1, "accessType": ["READ"]}) + etag = acl["etag"] + # AND updating the acl + await organization.update_acl_async( + resource_access, etag, synapse_client=self.syn + ) + # THEN the resource access should be have two principals + acl = await organization.get_acl_async(synapse_client=self.syn) + assert len(acl["resourceAccess"]) == 2 + + +# TODO: Add JSONSchema async tests once create_async is working From 3d96e2e4844086c05cc485cad10341b81cf80585 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 1 Oct 2025 07:26:48 -0700 Subject: [PATCH 05/41] get rid of repr methods --- synapseclient/models/schema_organization.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 72f3356f8..52923f01d 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -76,9 +76,6 @@ class SchemaOrganization(SchemaOrganizationProtocol): def __post_init__(self) -> None: self._check_name(self.name) - def __repr__(self): - return f"SchemaOrganization(name={self.name!r})" - async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: """ Gets the metadata from Synapse for this organization @@ -375,11 +372,6 @@ def __post_init__(self) -> None: self.uri = f"{self.organization_name}-{self.name}" self._check_name(self.name) - def __repr__(self): - return ( - f"JSONSchema(name={self.name!r}, organization={self.organization_name!r})" - ) - async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: """ Gets this JSON Schemas metadata From 6a92565b7f03e47e835df6541876489b381b266e Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 1 Oct 2025 07:31:18 -0700 Subject: [PATCH 06/41] ran pre-commit --- .../models/protocols/schema_organization_protocol.py | 8 ++++---- synapseclient/models/schema_organization.py | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/synapseclient/models/protocols/schema_organization_protocol.py b/synapseclient/models/protocols/schema_organization_protocol.py index 56fda235f..fbc2e324d 100644 --- a/synapseclient/models/protocols/schema_organization_protocol.py +++ b/synapseclient/models/protocols/schema_organization_protocol.py @@ -35,7 +35,7 @@ def get(self, synapse_client: Optional["Synapse"] = None) -> None: org.get() """ - return None + return self def create(self, synapse_client: Optional["Synapse"] = None) -> None: """ @@ -57,7 +57,7 @@ def create(self, synapse_client: Optional["Synapse"] = None) -> None: org.create() """ - return None + return self def delete(self, synapse_client: Optional["Synapse"] = None) -> None: """ @@ -201,7 +201,7 @@ def get(self, synapse_client: Optional["Synapse"] = None) -> None: js = JSONSchema("my.schema.name", "my.org.name") js.get() """ - return None + return self def create( self, @@ -210,7 +210,7 @@ def create( dry_run: bool = False, synapse_client: Optional["Synapse"] = None, ) -> None: - return None + return self def delete(self) -> None: """ diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 52923f01d..e2ab862a0 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -101,6 +101,7 @@ async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: return response = await get_organization(self.name, synapse_client=synapse_client) self._fill_from_dict(response) + return self # Should this be named 'store_async'? async def create_async(self, synapse_client: Optional["Synapse"] = None) -> None: @@ -128,6 +129,7 @@ async def create_async(self, synapse_client: Optional["Synapse"] = None) -> None await self.get_async(synapse_client=synapse_client) response = await create_organization(self.name, synapse_client=synapse_client) self._fill_from_dict(response) + return self async def delete_async(self, synapse_client: Optional["Synapse"] = None) -> None: """ @@ -409,7 +411,7 @@ async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: async for schema in org_schemas: if schema["schemaName"] == self.name: self._fill_from_dict(schema) - return + return self raise ValueError( ( f"Organization: '{self.organization_name}' does not contain a schema with " @@ -447,6 +449,7 @@ def create( "/schema/type/create/async", request_body ) self._fill_from_dict(response["newVersionInfo"]) + return self async def delete_async(self, synapse_client: Optional["Synapse"] = None) -> None: """ From 14b31e1fc7661e6b18792d36d0e6088a4a92b1f5 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 1 Oct 2025 07:43:26 -0700 Subject: [PATCH 07/41] ran pre-commit --- .../protocols/schema_organization_protocol.py | 8 +++--- synapseclient/models/schema_organization.py | 11 ++++---- .../models/async/test_schema_organization.py | 20 +++++++------- .../synchronous/test_schema_organization.py | 26 +++++++++---------- 4 files changed, 32 insertions(+), 33 deletions(-) diff --git a/synapseclient/models/protocols/schema_organization_protocol.py b/synapseclient/models/protocols/schema_organization_protocol.py index fbc2e324d..34546a4bf 100644 --- a/synapseclient/models/protocols/schema_organization_protocol.py +++ b/synapseclient/models/protocols/schema_organization_protocol.py @@ -37,9 +37,9 @@ def get(self, synapse_client: Optional["Synapse"] = None) -> None: """ return self - def create(self, synapse_client: Optional["Synapse"] = None) -> None: + def store(self, synapse_client: Optional["Synapse"] = None) -> None: """ - Creates this organization in Synapse + Stores this organization in Synapse Arguments: synapse_client: If not passed in and caching was not disabled by @@ -54,7 +54,7 @@ def create(self, synapse_client: Optional["Synapse"] = None) -> None: syn.login() org = SchemaOrganization("my.org.name") - org.create() + org.store() """ return self @@ -203,7 +203,7 @@ def get(self, synapse_client: Optional["Synapse"] = None) -> None: """ return self - def create( + def store( self, body: dict[str, Any], version: Optional[str] = None, diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index e2ab862a0..4c9971619 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -104,9 +104,9 @@ async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: return self # Should this be named 'store_async'? - async def create_async(self, synapse_client: Optional["Synapse"] = None) -> None: + async def store_async(self, synapse_client: Optional["Synapse"] = None) -> None: """ - Creates this organization in Synapse + Stores this organization in Synapse Arguments: synapse_client: If not passed in and caching was not disabled by @@ -122,7 +122,7 @@ async def create_async(self, synapse_client: Optional["Synapse"] = None) -> None syn.login() org = SchemaOrganization("my.org.name") - asyncio.run(org.create()) + asyncio.run(org.store_async()) """ if self.id: @@ -419,9 +419,8 @@ async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: ) ) - # Should this ba named store? - # TODO: crate api function, and async version of method, write docstring - def create( + # TODO: create api function, and async version of method, write docstring + def store( self, body: dict[str, Any], version: Optional[str] = None, diff --git a/tests/integration/synapseclient/models/async/test_schema_organization.py b/tests/integration/synapseclient/models/async/test_schema_organization.py index 21473e815..8481e4e8a 100644 --- a/tests/integration/synapseclient/models/async/test_schema_organization.py +++ b/tests/integration/synapseclient/models/async/test_schema_organization.py @@ -37,12 +37,12 @@ async def fixture_module_organization(request) -> SchemaOrganization: name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" org = SchemaOrganization(name) - org.create() + await org.store_async() async def delete_org(): for schema in org.get_json_schema_list(): await schema.delete_async() - org.delete() + await org.delete_async() request.addfinalizer(delete_org) @@ -85,19 +85,19 @@ async def fixture_organization_with_schema(request) -> SchemaOrganization: name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" org = SchemaOrganization(name) - await org.create_async() + await org.store_async() js1 = JSONSchema("schema1", name) js2 = JSONSchema("schema2", name) js3 = JSONSchema("schema3", name) # TODO: Change to create_async when method is working - js1.create({}) - js2.create({}) - js3.create({}) + js1.store({}) + js2.store({}) + js3.store({}) async def delete_org(): for schema in org.get_json_schema_list_async(): await schema.delete_async() - org.delete() + await org.delete_async() request.addfinalizer(delete_org) @@ -122,7 +122,7 @@ async def test_create_and_get(self, organization: SchemaOrganization) -> None: # AND it shouldn't exists in Synapse assert not org_exists(organization.name, synapse_client=self.syn) # WHEN I create the organization the metadata will be saved - await organization.create_async(synapse_client=self.syn) + await organization.store_async(synapse_client=self.syn) assert organization.name is not None assert organization.id is not None assert organization.created_by is not None @@ -144,7 +144,7 @@ async def test_get_json_schema_list( organization_with_schema: SchemaOrganization, ) -> None: # GIVEN an organization with no schemas and one with 3 schemas - await organization.create_async(synapse_client=self.syn) + await organization.store_async(synapse_client=self.syn) # THEN get_json_schema_list should return the correct list of schemas schema_list = await organization.get_json_schema_list_async( synapse_client=self.syn @@ -166,7 +166,7 @@ async def test_get_acl_and_update_acl( ): await organization.get_acl_async(synapse_client=self.syn) # GIVEN an organization that has been created - await organization.create_async(synapse_client=self.syn) + await organization.store_async(synapse_client=self.syn) acl = await organization.get_acl_async(synapse_client=self.syn) resource_access: list[dict[str, Any]] = acl["resourceAccess"] # THEN the resource access should be have one principal diff --git a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py index b60604250..803053d7d 100644 --- a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py +++ b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py @@ -37,7 +37,7 @@ def fixture_module_organization(request) -> SchemaOrganization: name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" org = SchemaOrganization(name) - org.create() + org.store() def delete_org(): for schema in org.get_json_schema_list(): @@ -86,13 +86,13 @@ def fixture_organization_with_schema(request) -> SchemaOrganization: name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" org = SchemaOrganization(name) - org.create() + org.store() js1 = JSONSchema("schema1", name) js2 = JSONSchema("schema2", name) js3 = JSONSchema("schema3", name) - js1.create({}) - js2.create({}) - js3.create({}) + js1.store({}) + js2.store({}) + js3.store({}) def delete_org(): for schema in org.get_json_schema_list(): @@ -121,7 +121,7 @@ def test_create_and_get(self, organization: SchemaOrganization) -> None: # AND it shouldn't exists in Synapse assert not org_exists(organization.name, synapse_client=self.syn) # WHEN I create the organization the metadata will be saved - organization.create(synapse_client=self.syn) + organization.store(synapse_client=self.syn) assert organization.name is not None assert organization.id is not None assert organization.created_by is not None @@ -142,7 +142,7 @@ def test_get_json_schema_list( organization_with_schema: SchemaOrganization, ) -> None: # GIVEN an organization with no schemas and one with 3 schemas - organization.create(synapse_client=self.syn) + organization.store(synapse_client=self.syn) # THEN get_json_schema_list should return the correct list of schemas assert not organization.get_json_schema_list(synapse_client=self.syn) assert ( @@ -158,7 +158,7 @@ def test_get_acl_and_update_acl(self, organization: SchemaOrganization) -> None: ): organization.get_acl(synapse_client=self.syn) # GIVEN an organization that has been created - organization.create(synapse_client=self.syn) + organization.store(synapse_client=self.syn) acl = organization.get_acl(synapse_client=self.syn) resource_access: list[dict[str, Any]] = acl["resourceAccess"] # THEN the resource access should be have one principal @@ -191,7 +191,7 @@ def test_create_and_get(self, json_schema: JSONSchema) -> None: assert not json_schema.created_by assert not json_schema.created_on # WHEN the object is created - json_schema.create({}, synapse_client=self.syn) + json_schema.store({}, synapse_client=self.syn) assert json_schema.name assert json_schema.organization_name assert json_schema.uri @@ -215,11 +215,11 @@ def test_get_versions(self, json_schema: JSONSchema) -> None: # THEN get_versions should return an empty list assert not json_schema.get_versions(synapse_client=self.syn) # WHEN creating a schema with no version - json_schema.create(body={}, synapse_client=self.syn) + json_schema.store(body={}, synapse_client=self.syn) # THEN get_versions should return an empty list assert json_schema.get_versions(synapse_client=self.syn) == [] # WHEN creating a schema with a version - json_schema.create(body={}, version="0.0.1", synapse_client=self.syn) + json_schema.store(body={}, version="0.0.1", synapse_client=self.syn) # THEN get_versions should return that version schemas = json_schema.get_versions(synapse_client=self.syn) assert len(schemas) == 1 @@ -230,8 +230,8 @@ def test_get_body(self, json_schema: JSONSchema) -> None: # WHEN creating a schema with 2 version first_body = {} latest_body = {"description": ""} - json_schema.create(body=first_body, version="0.0.1", synapse_client=self.syn) - json_schema.create(body=latest_body, version="0.0.2", synapse_client=self.syn) + json_schema.store(body=first_body, version="0.0.1", synapse_client=self.syn) + json_schema.store(body=latest_body, version="0.0.2", synapse_client=self.syn) # WHEN get_body has no version argument body0 = json_schema.get_body(synapse_client=self.syn) # THEN the body should be the latest version From 982eb52fad2727fa26c0c3080d3db3989cefc9ee Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 1 Oct 2025 07:57:21 -0700 Subject: [PATCH 08/41] fix docstring --- .../models/async/test_schema_organization.py | 20 +++++++++++----- .../synchronous/test_schema_organization.py | 24 +++++++++++-------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/tests/integration/synapseclient/models/async/test_schema_organization.py b/tests/integration/synapseclient/models/async/test_schema_organization.py index 8481e4e8a..fc76cd979 100644 --- a/tests/integration/synapseclient/models/async/test_schema_organization.py +++ b/tests/integration/synapseclient/models/async/test_schema_organization.py @@ -10,6 +10,16 @@ from synapseclient.models.schema_organization import list_json_schema_organizations +def create_test_entity_name(): + """Creates a random string for naming orgs and schemas in Synapse for testing + + Returns: + A legal Synapse org/schema name + """ + random_string = "".join(i for i in str(uuid.uuid4()) if i.isalpha()) + return f"SYNPY.TEST.{random_string}" + + def org_exists(name: str, synapse_client: Optional[Synapse] = None) -> bool: """ Checks if any organizations exists with the given name @@ -34,8 +44,7 @@ async def fixture_module_organization(request) -> SchemaOrganization: """ Returns a created organization at the module scope. Used to hold JSON Schemas created by tests. """ - - name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" + name = create_test_entity_name() org = SchemaOrganization(name) await org.store_async() @@ -54,8 +63,7 @@ def fixture_json_schema(module_organization: SchemaOrganization) -> JSONSchema: """ Returns a JSON Schema """ - - name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" + name = create_test_entity_name() js = JSONSchema(name, module_organization.name) return js @@ -65,7 +73,7 @@ async def fixture_organization(syn: Synapse, request) -> SchemaOrganization: """ Returns a Synapse organization. """ - name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" + name = create_test_entity_name() org = SchemaOrganization(name) async def delete_org(): @@ -83,7 +91,7 @@ async def fixture_organization_with_schema(request) -> SchemaOrganization: Returns a Synapse organization. As Cleanup it checks for JSON Schemas and deletes them""" - name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" + name = create_test_entity_name() org = SchemaOrganization(name) await org.store_async() js1 = JSONSchema("schema1", name) diff --git a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py index 803053d7d..cc37ee6dd 100644 --- a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py +++ b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py @@ -10,6 +10,16 @@ from synapseclient.models.schema_organization import list_json_schema_organizations +def create_test_entity_name(): + """Creates a random string for naming orgs and schemas in Synapse for testing + + Returns: + A legal Synapse org/schema name + """ + random_string = "".join(i for i in str(uuid.uuid4()) if i.isalpha()) + return f"SYNPY.TEST.{random_string}" + + def org_exists(name: str, synapse_client: Optional[Synapse] = None) -> bool: """ Checks if any organizations exists with the given name @@ -34,9 +44,7 @@ def fixture_module_organization(request) -> SchemaOrganization: """ Returns a created organization at the module scope. Used to hold JSON Schemas created by tests. """ - - name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" - org = SchemaOrganization(name) + org = SchemaOrganization(create_test_entity_name()) org.store() def delete_org(): @@ -54,9 +62,7 @@ def fixture_json_schema(module_organization: SchemaOrganization) -> JSONSchema: """ Returns a JSON Schema """ - - name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" - js = JSONSchema(name, module_organization.name) + js = JSONSchema(create_test_entity_name(), module_organization.name) return js @@ -65,8 +71,7 @@ def fixture_organization(syn: Synapse, request) -> SchemaOrganization: """ Returns a Synapse organization. """ - - name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" + name = create_test_entity_name() org = SchemaOrganization(name) def delete_org(): @@ -83,8 +88,7 @@ def fixture_organization_with_schema(request) -> SchemaOrganization: """ Returns a Synapse organization. As Cleanup it checks for JSON Schemas and deletes them""" - - name = f"SYNPY.TEST.{" ".join(i for i in str(uuid.uuid4()) if i.isalpha())}" + name = create_test_entity_name() org = SchemaOrganization(name) org.store() js1 = JSONSchema("schema1", name) From fc7b8f752949831c32f27d8141499243d3deb101 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 1 Oct 2025 08:31:12 -0700 Subject: [PATCH 09/41] remove self.get call from store method --- synapseclient/models/schema_organization.py | 3 --- .../models/async/test_schema_organization.py | 10 +++++++--- .../models/synchronous/test_schema_organization.py | 10 +++++++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 4c9971619..f52e1718c 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -103,7 +103,6 @@ async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: self._fill_from_dict(response) return self - # Should this be named 'store_async'? async def store_async(self, synapse_client: Optional["Synapse"] = None) -> None: """ Stores this organization in Synapse @@ -125,8 +124,6 @@ async def store_async(self, synapse_client: Optional["Synapse"] = None) -> None: asyncio.run(org.store_async()) """ - if self.id: - await self.get_async(synapse_client=synapse_client) response = await create_organization(self.name, synapse_client=synapse_client) self._fill_from_dict(response) return self diff --git a/tests/integration/synapseclient/models/async/test_schema_organization.py b/tests/integration/synapseclient/models/async/test_schema_organization.py index fc76cd979..93db3d59e 100644 --- a/tests/integration/synapseclient/models/async/test_schema_organization.py +++ b/tests/integration/synapseclient/models/async/test_schema_organization.py @@ -121,15 +121,15 @@ def init(self, syn: Synapse) -> None: @pytest.mark.asyncio async def test_create_and_get(self, organization: SchemaOrganization) -> None: - # GIVEN an initialized organization object that hasn't been created in Synapse + # GIVEN an initialized organization object that hasn't been stored in Synapse # THEN it shouldn't have any metadata besides it's name assert organization.name is not None assert organization.id is None assert organization.created_by is None assert organization.created_on is None - # AND it shouldn't exists in Synapse + # AND it shouldn't exist in Synapse assert not org_exists(organization.name, synapse_client=self.syn) - # WHEN I create the organization the metadata will be saved + # WHEN I store the organization the metadata will be saved await organization.store_async(synapse_client=self.syn) assert organization.name is not None assert organization.id is not None @@ -144,6 +144,10 @@ async def test_create_and_get(self, organization: SchemaOrganization) -> None: assert organization.id is not None assert organization.created_by is not None assert organization.created_on is not None + # WHEN I try to store an organization that exists in Synapse + # THEN I should get an exception + with pytest.raises(SynapseHTTPError): + org2.store() @pytest.mark.asyncio async def test_get_json_schema_list( diff --git a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py index cc37ee6dd..dc0ab2f7b 100644 --- a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py +++ b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py @@ -116,15 +116,15 @@ def init(self, syn: Synapse) -> None: self.syn = syn def test_create_and_get(self, organization: SchemaOrganization) -> None: - # GIVEN an initialized organization object that hasn't been created in Synapse + # GIVEN an initialized organization object that hasn't been stored in Synapse # THEN it shouldn't have any metadata besides it's name assert organization.name is not None assert organization.id is None assert organization.created_by is None assert organization.created_on is None - # AND it shouldn't exists in Synapse + # AND it shouldn't exist in Synapse assert not org_exists(organization.name, synapse_client=self.syn) - # WHEN I create the organization the metadata will be saved + # WHEN I store the organization the metadata will be saved organization.store(synapse_client=self.syn) assert organization.name is not None assert organization.id is not None @@ -139,6 +139,10 @@ def test_create_and_get(self, organization: SchemaOrganization) -> None: assert organization.id is not None assert organization.created_by is not None assert organization.created_on is not None + # WHEN I try to store an organization that exists in Synapse + # THEN I should get an exception + with pytest.raises(SynapseHTTPError): + org2.store() def test_get_json_schema_list( self, From 9bf1a66d4d7fe1e7d996301c3ab99628ec8a715f Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 1 Oct 2025 08:34:44 -0700 Subject: [PATCH 10/41] remove todo --- synapseclient/models/schema_organization.py | 1 - 1 file changed, 1 deletion(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index f52e1718c..fdee835cb 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -700,7 +700,6 @@ def _check_name(self, name) -> None: ) -# TODO: Move to a utils module def list_json_schema_organizations( synapse_client: Optional["Synapse"] = None, ) -> list[SchemaOrganization]: From d37434da16aecc4fbedf6b623bf0dbd2a362421f Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 2 Oct 2025 13:04:45 -0700 Subject: [PATCH 11/41] added async store method --- .../protocols/schema_organization_protocol.py | 2 +- synapseclient/models/schema_organization.py | 183 ++++++++++++-- .../models/async/test_schema_organization.py | 234 ++++++++++++++++-- .../synchronous/test_schema_organization.py | 16 +- ...on.py => unit_test_schema_organization.py} | 73 ++++++ 5 files changed, 459 insertions(+), 49 deletions(-) rename tests/unit/synapseclient/models/synchronous/{test_schema_organization.py => unit_test_schema_organization.py} (62%) diff --git a/synapseclient/models/protocols/schema_organization_protocol.py b/synapseclient/models/protocols/schema_organization_protocol.py index 34546a4bf..890ec394d 100644 --- a/synapseclient/models/protocols/schema_organization_protocol.py +++ b/synapseclient/models/protocols/schema_organization_protocol.py @@ -205,7 +205,7 @@ def get(self, synapse_client: Optional["Synapse"] = None) -> None: def store( self, - body: dict[str, Any], + schema_body: dict[str, Any], version: Optional[str] = None, dry_run: bool = False, synapse_client: Optional["Synapse"] = None, diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index fdee835cb..2483519b1 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -20,6 +20,8 @@ update_organization_acl, ) from synapseclient.core.async_utils import async_to_sync +from synapseclient.core.constants.concrete_types import CREATE_SCHEMA_REQUEST +from synapseclient.models.mixins.asynchronous_job import AsynchronousCommunicator from synapseclient.models.mixins.json_schema import JSONSchemaVersionInfo from synapseclient.models.protocols.schema_organization_protocol import ( JSONSchemaProtocol, @@ -29,6 +31,10 @@ if TYPE_CHECKING: from synapseclient import Synapse +SYNAPSE_SCHEMA_URL = ( + "https://repo-prod.prod.sagebase.org/repo/v1/schema/type/registered/" +) + @dataclass() @async_to_sync @@ -416,35 +422,28 @@ async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: ) ) - # TODO: create api function, and async version of method, write docstring - def store( + async def store_async( self, - body: dict[str, Any], + schema_body: dict[str, Any], version: Optional[str] = None, dry_run: bool = False, synapse_client: Optional["Synapse"] = None, ) -> None: - uri = self.uri - if version: - self._check_semantic_version(version) - uri = f"{uri}-{version}" - body["$id"] = uri - - request_body = { - "concreteType": "org.sagebionetworks.repo.model.schema.CreateSchemaRequest", - "schema": body, - "dryRun": dry_run, - } - if not synapse_client: - from synapseclient import Synapse - - synapse_client = Synapse() - synapse_client.login() - - response = synapse_client._waitForAsync( - "/schema/type/create/async", request_body + request = CreateSchemaRequest( + schema=schema_body, + name=self.name, + organization_name=self.organization_name, + version=version, + dry_run=dry_run, + ) + completed_request: CreateSchemaRequest = await request.send_job_and_wait_async( + synapse_client=synapse_client ) - self._fill_from_dict(response["newVersionInfo"]) + new_version_info = completed_request.new_version_info + self.organization_id = new_version_info.organization_id + self.id = new_version_info.id + self.created_by = new_version_info.created_by + self.created_on = new_version_info.created_on return self async def delete_async(self, synapse_client: Optional["Synapse"] = None) -> None: @@ -700,6 +699,144 @@ def _check_name(self, name) -> None: ) +@dataclass +class CreateSchemaRequest(AsynchronousCommunicator): + """ + This result is modeled from: + """ + + schema: dict[str, Any] + """The JSON Schema to be stored""" + + name: str + """The name of the schema being stored""" + + organization_name: str + """The name of the organization the schema to store the schema in""" + + version: Optional[str] = None + """The version to store the schema as if given""" + + dry_run: bool = False + """Whether or not to do the request as a dry-run""" + + concrete_type: str = field(init=False) + """The concrete type of the request""" + + uri: str = field(init=False) + """The URI of this schema""" + + id: str = field(init=False) + """The ID/URL of this schema""" + + new_version_info: JSONSchemaVersionInfo = None + """Info from the API response""" + + def __post_init__(self) -> None: + self.concrete_type = CREATE_SCHEMA_REQUEST + self._check_name(self.name) + self._check_name(self.organization_name) + uri = f"{self.organization_name}-{self.name}" + if self.version: + self._check_semantic_version(self.version) + uri = f"{uri}-{self.version}" + self.uri = uri + self.id = f"{SYNAPSE_SCHEMA_URL}{uri}" + self.schema["$id"] = self.id + + def to_synapse_request(self) -> dict[str, Any]: + """ + Create a CreateSchemaRequest from attributes + https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/CreateSchemaRequest.html + """ + + result = { + "concreteType": self.concrete_type, + "schema": self.schema, + "dryRun": self.dry_run, + } + + return result + + def fill_from_dict(self, synapse_response: dict[str, Any]) -> "CreateSchemaRequest": + """ + Set attributes from CreateSchemaResponse + https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/CreateSchemaResponse.html + """ + self.new_version_info = self._create_json_schema_version_from_response( + synapse_response.get("newVersionInfo") + ) + self.schema = synapse_response.get("validationSchema") + + return self + + def _check_semantic_version(self, version: str) -> None: + """ + Checks that the semantic version is correctly formatted + + Args: + version: A semantic version(ie. `1.0.0`) to be checked + + Raises: + ValueError: If the string is not a correct semantic version + """ + if not re.match("^(\d+)\.(\d+)\.(\d*)$", version) or version == "0.0.0": + raise ValueError( + ( + "Schema version must be a semantic version starting at 0.0.1 with no letters " + "and a major, minor and patch version " + f"{version}" + ) + ) + + def _check_name(self, name: str) -> None: + """ + Checks that the input name is a valid Synapse JSONSchema or Organization name + - Must start with a letter + - Must contains only letters, numbers and periods. + + Arguments: + name: The name of the organization/schema to be checked + + Raises: + ValueError: When the name isn't valid + """ + if not re.match("^([A-Za-z])([A-Za-z]|\d|\.)*$", name): + raise ValueError( + ( + "Schema name must start with a letter and contain " + f"only letters numbers and periods: {name}" + ) + ) + + @staticmethod + def _create_json_schema_version_from_response( + response: dict[str, Any] + ) -> JSONSchemaVersionInfo: + """ + Creates a JSONSchemaVersionInfo object from a Synapse API response + + Arguments: + response: This Synapse API object: + [JsonSchemaVersionInfo]https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchemaVersionInfo.html + + Returns: + A JSONSchemaVersionInfo object + """ + return JSONSchemaVersionInfo( + organization_id=response.get("organizationId"), + organization_name=response.get("organizationName"), + schema_id=response.get("schemaId"), + id=response.get("$id"), + schema_name=response.get("schemaName"), + version_id=response.get("versionId"), + semantic_version=response.get("semanticVersion"), + json_sha256_hex=response.get("jsonSHA256Hex"), + created_on=response.get("createdOn"), + created_by=response.get("createdBy"), + ) + + def list_json_schema_organizations( synapse_client: Optional["Synapse"] = None, ) -> list[SchemaOrganization]: diff --git a/tests/integration/synapseclient/models/async/test_schema_organization.py b/tests/integration/synapseclient/models/async/test_schema_organization.py index 93db3d59e..bbc990730 100644 --- a/tests/integration/synapseclient/models/async/test_schema_organization.py +++ b/tests/integration/synapseclient/models/async/test_schema_organization.py @@ -3,11 +3,17 @@ from typing import Any, Optional import pytest +import pytest_asyncio from synapseclient import Synapse +from synapseclient.core.constants.concrete_types import CREATE_SCHEMA_REQUEST from synapseclient.core.exceptions import SynapseHTTPError from synapseclient.models import JSONSchema, SchemaOrganization -from synapseclient.models.schema_organization import list_json_schema_organizations +from synapseclient.models.schema_organization import ( + SYNAPSE_SCHEMA_URL, + CreateSchemaRequest, + list_json_schema_organizations, +) def create_test_entity_name(): @@ -40,18 +46,18 @@ def org_exists(name: str, synapse_client: Optional[Synapse] = None) -> bool: @pytest.fixture(name="module_organization", scope="module") -async def fixture_module_organization(request) -> SchemaOrganization: +def fixture_module_organization(syn: Synapse, request) -> SchemaOrganization: """ Returns a created organization at the module scope. Used to hold JSON Schemas created by tests. """ name = create_test_entity_name() org = SchemaOrganization(name) - await org.store_async() + org.store(synapse_client=syn) - async def delete_org(): - for schema in org.get_json_schema_list(): - await schema.delete_async() - await org.delete_async() + def delete_org(): + for schema in org.get_json_schema_list(synapse_client=syn): + schema.delete() + org.delete(synapse_client=syn) request.addfinalizer(delete_org) @@ -69,16 +75,16 @@ def fixture_json_schema(module_organization: SchemaOrganization) -> JSONSchema: @pytest.fixture(name="organization", scope="function") -async def fixture_organization(syn: Synapse, request) -> SchemaOrganization: +def fixture_organization(syn: Synapse, request) -> SchemaOrganization: """ Returns a Synapse organization. """ name = create_test_entity_name() org = SchemaOrganization(name) - async def delete_org(): + def delete_org(): if org_exists(name, syn): - org.delete_async() + org.delete() request.addfinalizer(delete_org) @@ -86,26 +92,25 @@ async def delete_org(): @pytest.fixture(name="organization_with_schema", scope="function") -async def fixture_organization_with_schema(request) -> SchemaOrganization: +def fixture_organization_with_schema(request) -> SchemaOrganization: """ Returns a Synapse organization. As Cleanup it checks for JSON Schemas and deletes them""" name = create_test_entity_name() org = SchemaOrganization(name) - await org.store_async() + org.store() js1 = JSONSchema("schema1", name) js2 = JSONSchema("schema2", name) js3 = JSONSchema("schema3", name) - # TODO: Change to create_async when method is working js1.store({}) js2.store({}) js3.store({}) - async def delete_org(): - for schema in org.get_json_schema_list_async(): - await schema.delete_async() - await org.delete_async() + def delete_org(): + for schema in org.get_json_schema_list(): + schema.delete() + org.delete() request.addfinalizer(delete_org) @@ -113,7 +118,7 @@ async def delete_org(): class TestSchemaOrganization: - """Synchronous integration tests for SchemaOrganization.""" + """Asynchronous integration tests for SchemaOrganization.""" @pytest.fixture(autouse=True, scope="function") def init(self, syn: Synapse) -> None: @@ -195,4 +200,195 @@ async def test_get_acl_and_update_acl( assert len(acl["resourceAccess"]) == 2 -# TODO: Add JSONSchema async tests once create_async is working +class TestJSONSchema: + """Asynchronous integration tests for JSONSchema.""" + + @pytest.fixture(autouse=True, scope="function") + def init(self, syn: Synapse) -> None: + self.syn = syn + + async def test_store_and_get(self, json_schema: JSONSchema) -> None: + # GIVEN an initialized schema object that hasn't been created in Synapse + # THEN it shouldn't have any metadata besides it's name and organization name, and uri + assert json_schema.name + assert json_schema.organization_name + assert json_schema.uri + assert not json_schema.organization_id + assert not json_schema.id + assert not json_schema.created_by + assert not json_schema.created_on + # WHEN the object is stored + # THEN the Synapse metadata is filled out + await json_schema.store_async({}, synapse_client=self.syn) + assert json_schema.name + assert json_schema.organization_name + assert json_schema.uri + assert json_schema.organization_id + assert json_schema.id + assert json_schema.created_by + assert json_schema.created_on + # AND it should be getable by future instances using the same name + js2 = JSONSchema(json_schema.name, json_schema.organization_name) + await js2.get_async(synapse_client=self.syn) + assert js2.name + assert js2.organization_name + assert js2.uri + assert js2.organization_id + assert js2.id + assert js2.created_by + assert js2.created_on + + async def test_get_versions(self, json_schema: JSONSchema) -> None: + # GIVEN an schema that hasn't been created + # THEN get_versions should return an empty list + versions = await json_schema.get_versions_async(synapse_client=self.syn) + assert len(versions) == 0 + # WHEN creating a schema with no version + await json_schema.store_async(schema_body={}, synapse_client=self.syn) + # THEN get_versions should return an empty list + versions = await json_schema.get_versions_async(synapse_client=self.syn) + assert len(versions) == 0 + # WHEN creating a schema with a version + await json_schema.store_async( + schema_body={}, version="0.0.1", synapse_client=self.syn + ) + # THEN get_versions should return that version + versions = await json_schema.get_versions_async(synapse_client=self.syn) + assert len(versions) == 1 + assert versions[0].semantic_version == "0.0.1" + + async def test_get_body(self, json_schema: JSONSchema) -> None: + # GIVEN a schema + # WHEN storing 2 versions of the schema + first_body = {} + latest_body = {"description": ""} + await json_schema.store_async( + schema_body=first_body, version="0.0.1", synapse_client=self.syn + ) + await json_schema.store_async( + schema_body=latest_body, version="0.0.2", synapse_client=self.syn + ) + # WHEN get_body has no version argument + body0 = await json_schema.get_body_async(synapse_client=self.syn) + # THEN the body should be the latest version + assert body0 == { + "description": "", + "$id": f"https://repo-prod.prod.sagebase.org/repo/v1/schema/type/registered/{json_schema.organization_name}-{json_schema.name}", + } + # WHEN get_body has a version argument + body1 = await json_schema.get_body_async( + version="0.0.1", synapse_client=self.syn + ) + body2 = await json_schema.get_body_async( + version="0.0.2", synapse_client=self.syn + ) + # THEN the appropriate body should be returned + assert body1 == { + "$id": f"https://repo-prod.prod.sagebase.org/repo/v1/schema/type/registered/{json_schema.organization_name}-{json_schema.name}-0.0.1", + } + assert body2 == { + "description": "", + "$id": f"https://repo-prod.prod.sagebase.org/repo/v1/schema/type/registered/{json_schema.organization_name}-{json_schema.name}-0.0.2", + } + + +class TestCreateSchemaRequest: + @pytest.fixture(autouse=True, scope="function") + def init(self, syn: Synapse) -> None: + self.syn = syn + + @pytest.mark.asyncio + async def test_create_schema_request_no_version( + self, module_organization: SchemaOrganization + ) -> None: + # GIVEN an organization + # WHEN creating a CreateSchemaRequest with no version given + schema_name = create_test_entity_name() + request = CreateSchemaRequest( + schema={}, name=schema_name, organization_name=module_organization.name + ) + # THEN id in schema will not include version + assert request.schema == { + "$id": f"{SYNAPSE_SCHEMA_URL}{module_organization.name}-{schema_name}" + } + assert request.name == schema_name + assert request.organization_name == module_organization.name + # AND version will be None + assert request.version is None + assert request.dry_run is False + assert request.concrete_type == CREATE_SCHEMA_REQUEST + # AND URI and id will not include a version + assert request.uri == f"{module_organization.name}-{schema_name}" + assert ( + request.id + == f"{SYNAPSE_SCHEMA_URL}{module_organization.name}-{schema_name}" + ) + assert not request.new_version_info + # THEN the Schema should not be part of the organization yet + assert request.uri not in [ + schema.uri for schema in module_organization.get_json_schema_list() + ] + + # WHEN sending the CreateSchemaRequest + completed_request = await request.send_job_and_wait_async( + synapse_client=self.syn + ) + assert completed_request.new_version_info + # THEN the Schema should be part of the organization + assert completed_request.uri in [ + schema.uri for schema in module_organization.get_json_schema_list() + ] + + @pytest.mark.asyncio + async def test_create_schema_request_with_version( + self, module_organization: SchemaOrganization + ) -> None: + # GIVEN an organization + # WHEN creating a CreateSchemaRequest with no version given + schema_name = create_test_entity_name() + version = "0.0.1" + request = CreateSchemaRequest( + schema={}, + name=schema_name, + organization_name=module_organization.name, + version=version, + ) + # THEN id in schema will include version + assert request.schema == { + "$id": f"{SYNAPSE_SCHEMA_URL}{module_organization.name}-{schema_name}-{version}" + } + assert request.name == schema_name + assert request.organization_name == module_organization.name + # AND version will be set + assert request.version == version + assert request.dry_run is False + assert request.concrete_type == CREATE_SCHEMA_REQUEST + # AND URI and id will include a version + assert request.uri == f"{module_organization.name}-{schema_name}-{version}" + assert ( + request.id + == f"{SYNAPSE_SCHEMA_URL}{module_organization.name}-{schema_name}-{version}" + ) + assert not request.new_version_info + # THEN the Schema should not be part of the organization yet + assert f"{module_organization.name}-{schema_name}" not in [ + schema.uri for schema in module_organization.get_json_schema_list() + ] + + # WHEN sending the CreateSchemaRequest + completed_request = await request.send_job_and_wait_async( + synapse_client=self.syn + ) + assert completed_request.new_version_info + # THEN the Schema (minus version) should be part of the organization yet + schemas = [ + schema + for schema in module_organization.get_json_schema_list() + if schema.uri == f"{module_organization.name}-{schema_name}" + ] + assert len(schemas) == 1 + schema = schemas[0] + # AND schema version should have matching full uri + assert completed_request.uri in [ + version.id for version in schema.get_versions() + ] diff --git a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py index dc0ab2f7b..aad338101 100644 --- a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py +++ b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py @@ -188,8 +188,8 @@ class TestJSONSchema: def init(self, syn: Synapse) -> None: self.syn = syn - def test_create_and_get(self, json_schema: JSONSchema) -> None: - # GIVEN an initialized schema object that hasn't been created in Synapse + def test_store_and_get(self, json_schema: JSONSchema) -> None: + # GIVEN an initialized schema object that hasn't been stored in Synapse # THEN it shouldn't have any metadata besides it's name and organization name, and uri assert json_schema.name assert json_schema.organization_name @@ -223,11 +223,11 @@ def test_get_versions(self, json_schema: JSONSchema) -> None: # THEN get_versions should return an empty list assert not json_schema.get_versions(synapse_client=self.syn) # WHEN creating a schema with no version - json_schema.store(body={}, synapse_client=self.syn) + json_schema.store(schema_body={}, synapse_client=self.syn) # THEN get_versions should return an empty list assert json_schema.get_versions(synapse_client=self.syn) == [] # WHEN creating a schema with a version - json_schema.store(body={}, version="0.0.1", synapse_client=self.syn) + json_schema.store(schema_body={}, version="0.0.1", synapse_client=self.syn) # THEN get_versions should return that version schemas = json_schema.get_versions(synapse_client=self.syn) assert len(schemas) == 1 @@ -238,8 +238,12 @@ def test_get_body(self, json_schema: JSONSchema) -> None: # WHEN creating a schema with 2 version first_body = {} latest_body = {"description": ""} - json_schema.store(body=first_body, version="0.0.1", synapse_client=self.syn) - json_schema.store(body=latest_body, version="0.0.2", synapse_client=self.syn) + json_schema.store( + schema_body=first_body, version="0.0.1", synapse_client=self.syn + ) + json_schema.store( + schema_body=latest_body, version="0.0.2", synapse_client=self.syn + ) # WHEN get_body has no version argument body0 = json_schema.get_body(synapse_client=self.syn) # THEN the body should be the latest version diff --git a/tests/unit/synapseclient/models/synchronous/test_schema_organization.py b/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py similarity index 62% rename from tests/unit/synapseclient/models/synchronous/test_schema_organization.py rename to tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py index 6e2d85dd2..47fbe84d8 100644 --- a/tests/unit/synapseclient/models/synchronous/test_schema_organization.py +++ b/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py @@ -4,6 +4,10 @@ import pytest from synapseclient.models import JSONSchema, SchemaOrganization +from synapseclient.models.schema_organization import ( + CreateSchemaRequest, + list_json_schema_organizations, +) class TestSchemaOrganization: @@ -135,3 +139,72 @@ def test_from_response_with_exception(self, response: dict[str, Any]): "Tests that illegal Synapse API responses cause exceptions" with pytest.raises(TypeError): JSONSchema.from_response(response) + + +class TestCreateSchemaRequest: + @pytest.mark.parametrize( + "name", + ["AAAAAAA", "A12345", "A....."], + ids=["Just letters", "Numbers", "Periods"], + ) + def test_init_name(self, name: str) -> None: + "Tests that legal names don't raise a ValueError on init" + assert CreateSchemaRequest(schema={}, name=name, organization_name="org.name") + + @pytest.mark.parametrize( + "name", + ["1AAAAAA", ".AAAAAA", "AAAAAA!"], + ids=["Starts with a number", "Starts with a period", "Special character"], + ) + def test_init_name_exceptions(self, name: str) -> None: + "Tests that illegal names raise a ValueError on init" + with pytest.raises(ValueError, match="Schema name must start with"): + CreateSchemaRequest(schema={}, name=name, organization_name="org.name") + + @pytest.mark.parametrize( + "name", + ["AAAAAAA", "A12345", "A....."], + ids=["Just letters", "Numbers", "Periods"], + ) + def test_init_org_name(self, name: str) -> None: + "Tests that legal org names don't raise a ValueError on init" + assert CreateSchemaRequest( + schema={}, name="schema.name", organization_name=name + ) + + @pytest.mark.parametrize( + "name", + ["1AAAAAA", ".AAAAAA", "AAAAAA!"], + ids=["Starts with a number", "Starts with a period", "Special character"], + ) + def test_init_org_name_exceptions(self, name: str) -> None: + "Tests that illegal org names raise a ValueError on init" + with pytest.raises(ValueError, match="Schema name must start with"): + CreateSchemaRequest(schema={}, name="schema.name", organization_name=name) + + @pytest.mark.parametrize( + "version", + ["0.0.1", "1.0.0"], + ) + def test_init_version(self, version: str) -> None: + "Tests that legal versions don't raise a ValueError on init" + assert CreateSchemaRequest( + schema={}, name="schema.name", organization_name="org.name", version=version + ) + + @pytest.mark.parametrize( + "version", + ["1", "1.0", "0.0.0.1", "0.0.0"], + ) + def test_init_version_exceptions(self, version: str) -> None: + "Tests that illegal versions raise a ValueError on init" + with pytest.raises( + ValueError, + match="Schema version must be a semantic version starting at 0.0.1", + ): + CreateSchemaRequest( + schema={}, + name="schema.name", + organization_name="org.name", + version=version, + ) From 6e195f8c23e1b657189eaf64e71e745a8ebacc5d Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 2 Oct 2025 15:08:44 -0700 Subject: [PATCH 12/41] fix docstring example format --- .../protocols/schema_organization_protocol.py | 86 ++++++++-- synapseclient/models/schema_organization.py | 160 ++++++++++++------ 2 files changed, 177 insertions(+), 69 deletions(-) diff --git a/synapseclient/models/protocols/schema_organization_protocol.py b/synapseclient/models/protocols/schema_organization_protocol.py index 890ec394d..a985a51cd 100644 --- a/synapseclient/models/protocols/schema_organization_protocol.py +++ b/synapseclient/models/protocols/schema_organization_protocol.py @@ -24,7 +24,10 @@ def get(self, synapse_client: Optional["Synapse"] = None) -> None: `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor - Example: + Example: Get an existing SchemaOrganization +   + + ```python from synapseclient.models import SchemaOrganization from synapseclient import Synapse @@ -33,6 +36,7 @@ def get(self, synapse_client: Optional["Synapse"] = None) -> None: org = SchemaOrganization("my.org.name") org.get() + ``` """ return self @@ -46,7 +50,10 @@ def store(self, synapse_client: Optional["Synapse"] = None) -> None: `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor - Example: + Example: Store the SchemaOrganization in Synapse +   + + ```python from synapseclient.models import SchemaOrganization from synapseclient import Synapse @@ -55,6 +62,7 @@ def store(self, synapse_client: Optional["Synapse"] = None) -> None: org = SchemaOrganization("my.org.name") org.store() + ``` """ return self @@ -68,7 +76,10 @@ def delete(self, synapse_client: Optional["Synapse"] = None) -> None: `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor - Example: + Example: Delete the SchemaOrganization from Synapse +   + + ```python from synapseclient.models import SchemaOrganization from synapseclient import Synapse @@ -77,7 +88,7 @@ def delete(self, synapse_client: Optional["Synapse"] = None) -> None: org = SchemaOrganization("my.org.name") org.delete() - + ``` """ return None @@ -94,7 +105,10 @@ def get_json_schema_list( Returns: A list of JSONSchema objects - Example: + Example: Get the JSONSchemas that are part of this SchemaOrganization +   + + ```python from synapseclient.models import SchemaOrganization from synapseclient import Synapse @@ -103,7 +117,7 @@ def get_json_schema_list( org = SchemaOrganization("my.org.name") org.get_json_schema_list() - + ``` """ return [] @@ -123,7 +137,10 @@ def get_acl(self, synapse_client: Optional["Synapse"] = None) -> dict[str, Any]: A dictionary in the form of this response: https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html - Example: + Example: Get the ACL for the SchemaOrganization +   + + ```python from synapseclient.models import SchemaOrganization from synapseclient import Synapse @@ -132,6 +149,7 @@ def get_acl(self, synapse_client: Optional["Synapse"] = None) -> dict[str, Any]: org = SchemaOrganization("my.org.name") org.get_acl() + ``` """ return {} @@ -155,7 +173,10 @@ def update_acl( `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor - Example: + Example: Update the ACL for the SchemaOrganization +   + + ```python from synapseclient.models import SchemaOrganization from synapseclient import Synapse @@ -168,7 +189,7 @@ def update_acl( resource_access.append({"principalId": 1, "accessType": ["READ"]}) etag = current_acl["etag"] org.update_acl(resource_access, etag) - + ``` """ return None @@ -191,7 +212,10 @@ def get(self, synapse_client: Optional["Synapse"] = None) -> None: Raises: ValueError: This JSONSchema doesn't exist in its organization - Example: + Example: Get an Existing JSONSchema +   + + ```python from synapseclient.models import JSONSchema from synapseclient import Synapse @@ -200,6 +224,7 @@ def get(self, synapse_client: Optional["Synapse"] = None) -> None: js = JSONSchema("my.schema.name", "my.org.name") js.get() + ``` """ return self @@ -210,6 +235,28 @@ def store( dry_run: bool = False, synapse_client: Optional["Synapse"] = None, ) -> None: + """ + Stores this organization in Synapse + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Example: Store a new SchemaOrganization +   + + ```python + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + org.store() + ``` + """ return self def delete(self) -> None: @@ -221,7 +268,10 @@ def delete(self) -> None: `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor - Example: + Example: Delete this JSONSchema from Synapse +   + + ```python from synapseclient.models import JSONSchema from synapseclient import Synapse @@ -230,6 +280,7 @@ def delete(self) -> None: js = JSONSchema("my.schema.name", "my.org.name") js.delete() + ``` """ return None @@ -247,7 +298,10 @@ def get_versions( Returns: A JSONSchemaVersionInfo for each version of this schema - Example: + Example: Get all versions of the JSONSchema +   + + ```python from synapseclient.models import JSONSchema from synapseclient import Synapse @@ -276,7 +330,10 @@ def get_body( Returns: The JSON Schema body - Example: + Example: Get the JSONSchema body from Synapse +   + + ```python from synapseclient.models import JSONSchema from synapseclient import Synapse @@ -287,6 +344,7 @@ def get_body( # Get latest version latest = js.get_body() # Get specific version - first = ajs.get_body("0.0.1") + first = js.get_body("0.0.1") + ``` """ return {} diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 2483519b1..4d44bddad 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -41,30 +41,6 @@ class SchemaOrganization(SchemaOrganizationProtocol): """ Represents an [Organization](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/Organization.html). - - Attributes: - id: The ID of the organization - name: The name of the organization - created_on: The date this organization was created - created_by: The ID of the user that created this organization - - Example: - from synapseclient.models import SchemaOrganization - from synapseclient import Synapse - - syn = Synapse() - syn.login() - - # Create a new org - org = SchemaOrganization("my.new.org.name") - org.create() - - # Get the metadata and JSON Schemas for an existing org - org = SchemaOrganization("my.org.name") - org.get_async() - print(org) - schemas = org.get_json_schema_list() - print(schemas) """ name: str @@ -91,7 +67,10 @@ async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor - Example: + Example: Get an existing SchemaOrganization +   + + ```python from synapseclient.models import SchemaOrganization from synapseclient import Synapse import asyncio @@ -99,9 +78,9 @@ async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: syn = Synapse() syn.login() - org = SchemaOrganization("my.org.name") + org = SchemaOrganization("dpetest") asyncio.run(org.get_async()) - + ``` """ if self.id: return @@ -118,7 +97,10 @@ async def store_async(self, synapse_client: Optional["Synapse"] = None) -> None: `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor - Example: + Example: Store a new SchemaOrganization +   + + ```python from synapseclient.models import SchemaOrganization from synapseclient import Synapse import asyncio @@ -128,7 +110,7 @@ async def store_async(self, synapse_client: Optional["Synapse"] = None) -> None: org = SchemaOrganization("my.org.name") asyncio.run(org.store_async()) - + ``` """ response = await create_organization(self.name, synapse_client=synapse_client) self._fill_from_dict(response) @@ -143,7 +125,10 @@ async def delete_async(self, synapse_client: Optional["Synapse"] = None) -> None `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor - Example: + Example: Delete a SchemaOrganization +   + + ```python from synapseclient.models import SchemaOrganization from synapseclient import Synapse import asyncio @@ -152,8 +137,8 @@ async def delete_async(self, synapse_client: Optional["Synapse"] = None) -> None syn.login() org = SchemaOrganization("my.org.name") - asyncio.run(org.delete()) - + asyncio.run(org.delete_async()) + ``` """ if not self.id: await self.get_async(synapse_client=synapse_client) @@ -172,7 +157,10 @@ async def get_json_schema_list_async( `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor - Example: + Example: Get a list of JSONSchemas that belong to the SchemaOrganization +   + + ```python from synapseclient.models import SchemaOrganization from synapseclient import Synapse import asyncio @@ -180,8 +168,9 @@ async def get_json_schema_list_async( syn = Synapse() syn.login() - org = SchemaOrganization("my.org.name") - asyncio.run(org.get_json_schema_list_async()) + org = SchemaOrganization("dpetest") + schemas = asyncio.run(org.get_json_schema_list_async()) + ``` """ response = list_json_schemas(self.name, synapse_client=synapse_client) @@ -205,7 +194,10 @@ async def get_acl_async( A dictionary in the form of this response: https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html - Example: + Example: Get the ACL for a SchemaOrganization +   + + ```python from synapseclient.models import SchemaOrganization from synapseclient import Synapse import asyncio @@ -213,8 +205,9 @@ async def get_acl_async( syn = Synapse() syn.login() - org = SchemaOrganization("my.org.name") - asyncio.run(org.get_acl_async()) + org = SchemaOrganization("dpetest") + acl = asyncio.run(org.get_acl_async()) + ``` """ if not self.id: await self.get_async() @@ -241,7 +234,10 @@ async def update_acl_async( `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor - Example: + Example: Update the ACL or a SchemaOrganization +   + + ```python from synapseclient.models import SchemaOrganization from synapseclient import Synapse import asyncio @@ -249,12 +245,19 @@ async def update_acl_async( syn = Synapse() syn.login() + # Store a new org org = SchemaOrganization("my.org.name") + asyncio.run(org.store_async()) + + # Get and modify the ACL current acl = asyncio.run(org.get_acl_async()) resource_access = current_acl["resourceAccess"] resource_access.append({"principalId": 1, "accessType": ["READ"]}) etag = current_acl["etag"] + + # Update the ACL asyncio.run(org.update_acl_async(resource_access, etag)) + ``` """ if not self.id: @@ -309,7 +312,10 @@ def from_response(cls, response: dict[str, Any]) -> "SchemaOrganization": Returns: A SchemaOrganization object using the input response - Example: + Example: Create a SchemaOrganization form an API response +   + + ```python from synapseclient.models import SchemaOrganization from synapseclient import Synapse import asyncio @@ -320,6 +326,7 @@ def from_response(cls, response: dict[str, Any]) -> "SchemaOrganization": response = asyncio.run(get_organization("my.org.name")) org = SchemaOrganization.from_response(response) + ``` """ org = SchemaOrganization(response.get("name")) @@ -342,14 +349,6 @@ class JSONSchema(JSONSchemaProtocol): created_on: The date this schema was created created_by: The ID of the user that created this schema uri: The uri of this schema - - Example: - from synapseclient.models import JSON Schema - from synapseclient import Synapse - - syn = Synapse() - syn.login() - """ name: str @@ -389,7 +388,10 @@ async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: Raises: ValueError: This JSONSchema doesn't exist in its organization - Example: + Example: Get an existing JSONSchema +   + + ```python from synapseclient.models import JSONSchema from synapseclient import Synapse import asyncio @@ -399,6 +401,7 @@ async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: js = JSONSchema("my.schema.name", "my.org.name") asyncio.run(js.get_async()) + ``` """ if self.id: return @@ -429,6 +432,33 @@ async def store_async( dry_run: bool = False, synapse_client: Optional["Synapse"] = None, ) -> None: + """ + Stores this JSONSchema in Synapse + + Arguments: + schema_body: _description_ + version: _description_. Defaults to None. + dry_run: _description_. Defaults to False. + synapse_client: _description_. Defaults to None. + + Returns: + _description_ + + Example: Store a JSON Schema in Synapse +   + + ```python + from synapseclient.models import JSONSchema + from synapseclient import Synapse + import asyncio + + syn = Synapse() + syn.login() + + js = JSONSchema("my.schema.name", "my.org.name") + asyncio.run(js.store_async()) + ``` + """ request = CreateSchemaRequest( schema=schema_body, name=self.name, @@ -455,7 +485,10 @@ async def delete_async(self, synapse_client: Optional["Synapse"] = None) -> None `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor - Example: + Example: Delete an existing JSONSchema +   + + ```python from synapseclient.models import JSONSchema from synapseclient import Synapse import asyncio @@ -465,6 +498,7 @@ async def delete_async(self, synapse_client: Optional["Synapse"] = None) -> None js = JSONSchema("my.schema.name", "my.org.name") asyncio.run(js.delete_async()) + ``` """ await delete_json_schema(self.uri, synapse_client=synapse_client) @@ -482,7 +516,10 @@ async def get_versions_async( Returns: A JSONSchemaVersionInfo for each version of this schema - Example: + Example: Get all the versions of the JSONSchema +   + + ```python from synapseclient.models import JSONSchema from synapseclient import Synapse import asyncio @@ -492,6 +529,7 @@ async def get_versions_async( js = JSONSchema("my.schema.name", "my.org.name") versions = asyncio.run(get_versions_async()) + ``` """ all_schemas = list_json_schema_versions( self.organization_name, self.name, synapse_client=synapse_client @@ -522,7 +560,10 @@ async def get_body_async( Returns: The JSON Schema body - Example: + Example: Get the body of the JSONSchema +   + + ```python from synapseclient.models import JSONSchema from synapseclient import Synapse import asyncio @@ -535,6 +576,7 @@ async def get_body_async( latest = asyncio.run(js.get_body_async()) # Get specific version first = asyncio.run(js.get_body_async("0.0.1")) + ``` """ uri = self.uri if version: @@ -560,7 +602,10 @@ def from_uri(cls, uri: str) -> "JSONSchema": Returns: A JSONSchema object - Example: + Example: Create a JSONSchema from a URI +   + + ```python from synapseclient.models import JSONSchema # Non-semantic URI @@ -568,6 +613,7 @@ def from_uri(cls, uri: str) -> "JSONSchema": # Semantic URI js2 = JSONSchema.from_uri("my.org-my.schema-0.0.1") + ``` """ uri_parts = uri.split("-") @@ -592,7 +638,10 @@ def from_response(cls, response: dict[str, Any]) -> "JSONSchema": Returns: A JSONSchema object from the API response - Example: + Example: Create a JSONSchema from an API response +   + + ```python from synapseclient.models import JSONSchema from synapseclient import Synapse from synapseclient.api import list_json_schemas @@ -610,6 +659,7 @@ async def get_first_response(): response = asyncio.run(get_first_response()) JSONSchema.from_response(response) + ``` """ js = JSONSchema(response.get("schemaName"), response.get("organizationName")) @@ -663,7 +713,7 @@ def _check_semantic_version(self, version: str) -> None: """ Checks that the semantic version is correctly formatted - Args: + Arguments: version: A semantic version(ie. `1.0.0`) to be checked Raises: From 7aaa540fada9f3e7aee7396536a3d478143c306d Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 2 Oct 2025 15:49:17 -0700 Subject: [PATCH 13/41] fix return types for get and store methods --- .../protocols/schema_organization_protocol.py | 29 ++++++++++---- synapseclient/models/schema_organization.py | 39 +++++++++++++------ 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/synapseclient/models/protocols/schema_organization_protocol.py b/synapseclient/models/protocols/schema_organization_protocol.py index a985a51cd..aa2afc00e 100644 --- a/synapseclient/models/protocols/schema_organization_protocol.py +++ b/synapseclient/models/protocols/schema_organization_protocol.py @@ -6,7 +6,7 @@ if TYPE_CHECKING: from synapseclient import Synapse from synapseclient.models.mixins.json_schema import JSONSchemaVersionInfo - from synapseclient.models.schema_organization import JSONSchema + from synapseclient.models.schema_organization import JSONSchema, SchemaOrganization class SchemaOrganizationProtocol(Protocol): @@ -15,7 +15,7 @@ class SchemaOrganizationProtocol(Protocol): have a synchronous counterpart that may also be called. """ - def get(self, synapse_client: Optional["Synapse"] = None) -> None: + def get(self, synapse_client: Optional["Synapse"] = None) -> "SchemaOrganization": """ Gets the metadata from Synapse for this organization @@ -24,6 +24,9 @@ def get(self, synapse_client: Optional["Synapse"] = None) -> None: `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor + Returns: + Itself + Example: Get an existing SchemaOrganization   @@ -41,7 +44,7 @@ def get(self, synapse_client: Optional["Synapse"] = None) -> None: """ return self - def store(self, synapse_client: Optional["Synapse"] = None) -> None: + def store(self, synapse_client: Optional["Synapse"] = None) -> "SchemaOrganization": """ Stores this organization in Synapse @@ -50,6 +53,9 @@ def store(self, synapse_client: Optional["Synapse"] = None) -> None: `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor + Returns: + Itself + Example: Store the SchemaOrganization in Synapse   @@ -200,7 +206,7 @@ class JSONSchemaProtocol(Protocol): have a synchronous counterpart that may also be called. """ - def get(self, synapse_client: Optional["Synapse"] = None) -> None: + def get(self, synapse_client: Optional["Synapse"] = None) -> "JSONSchema": """ Gets this JSON Schemas metadata @@ -209,6 +215,9 @@ def get(self, synapse_client: Optional["Synapse"] = None) -> None: `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor + Returns: + Itself + Raises: ValueError: This JSONSchema doesn't exist in its organization @@ -234,16 +243,22 @@ def store( version: Optional[str] = None, dry_run: bool = False, synapse_client: Optional["Synapse"] = None, - ) -> None: + ) -> "JSONSchema": """ - Stores this organization in Synapse + Stores this JSONSchema in Synapse Arguments: + schema_body: The body of the JSONSchema to store + version: The version of the JSONSchema body to store + dry_run: Whether or not to do a dry-run synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor - Example: Store a new SchemaOrganization + Returns: + Itself + + Example: Store a JSON Schema in Synapse   ```python diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 4d44bddad..89b158fd9 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -58,7 +58,9 @@ class SchemaOrganization(SchemaOrganizationProtocol): def __post_init__(self) -> None: self._check_name(self.name) - async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: + async def get_async( + self, synapse_client: Optional["Synapse"] = None + ) -> "SchemaOrganization": """ Gets the metadata from Synapse for this organization @@ -67,6 +69,9 @@ async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor + Returns: + Itself + Example: Get an existing SchemaOrganization   @@ -83,12 +88,14 @@ async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: ``` """ if self.id: - return + return self response = await get_organization(self.name, synapse_client=synapse_client) self._fill_from_dict(response) return self - async def store_async(self, synapse_client: Optional["Synapse"] = None) -> None: + async def store_async( + self, synapse_client: Optional["Synapse"] = None + ) -> "SchemaOrganization": """ Stores this organization in Synapse @@ -97,6 +104,9 @@ async def store_async(self, synapse_client: Optional["Synapse"] = None) -> None: `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor + Returns: + Itself + Example: Store a new SchemaOrganization   @@ -376,7 +386,9 @@ def __post_init__(self) -> None: self.uri = f"{self.organization_name}-{self.name}" self._check_name(self.name) - async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: + async def get_async( + self, synapse_client: Optional["Synapse"] = None + ) -> "JSONSchema": """ Gets this JSON Schemas metadata @@ -388,6 +400,9 @@ async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: Raises: ValueError: This JSONSchema doesn't exist in its organization + Returns: + Itself + Example: Get an existing JSONSchema   @@ -404,7 +419,7 @@ async def get_async(self, synapse_client: Optional["Synapse"] = None) -> None: ``` """ if self.id: - return + return self # Check that the org exists, # if it doesn't list_json_schemas will unhelpfully return an empty generator. @@ -431,18 +446,20 @@ async def store_async( version: Optional[str] = None, dry_run: bool = False, synapse_client: Optional["Synapse"] = None, - ) -> None: + ) -> "JSONSchema": """ Stores this JSONSchema in Synapse Arguments: - schema_body: _description_ - version: _description_. Defaults to None. - dry_run: _description_. Defaults to False. - synapse_client: _description_. Defaults to None. + schema_body: The body of the JSONSchema to store + version: The version of the JSONSchema body to store + dry_run: Whether or not to do a dry-run + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor Returns: - _description_ + Itself Example: Store a JSON Schema in Synapse   From 69c865d4b5476ad19c53644b5130242610aafcbd Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 2 Oct 2025 16:10:15 -0700 Subject: [PATCH 14/41] fix links in docstrings --- synapseclient/models/schema_organization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 89b158fd9..2eb568981 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -692,7 +692,7 @@ def _create_json_schema_version_from_response( Arguments: response: This Synapse API object: - [JsonSchemaVersionInfo]https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchemaVersionInfo.html + [JsonSchemaVersionInfo](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchemaVersionInfo.html) Returns: A JSONSchemaVersionInfo object @@ -716,7 +716,7 @@ def _fill_from_dict(self, response: dict[str, Any]) -> None: Arguments: response: This Synapse API object: - [JsonSchema]https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchema.html + [JsonSchema](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchema.html) """ self.organization_id = response.get("organizationId") self.organization_name = response.get("organizationName") From 43b367a14e06c4dbaef5a795d0909a8c03241b05 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 2 Oct 2025 16:16:38 -0700 Subject: [PATCH 15/41] expand test --- .../models/synchronous/unit_test_schema_organization.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py b/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py index 47fbe84d8..5a3209f15 100644 --- a/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py +++ b/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py @@ -120,7 +120,13 @@ def test_from_uri_with_exceptions(self, uri: str): ) def test_from_response(self, response: dict[str, Any]): "Tests that legal Synapse API responses result in created objects." - assert JSONSchema.from_response(response) + js = JSONSchema.from_response(response) + assert js.created_on == "9-30-25" + assert js.created_by == "123" + assert js.organization_id == "123" + assert js.organization_name == "org.name" + assert js.id == "123" + assert js.name == "schema.name" @pytest.mark.parametrize( "response", From b3f51581fa5eea7b4a35b876564483152bfee345 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 2 Oct 2025 17:04:46 -0700 Subject: [PATCH 16/41] fix docstring --- synapseclient/models/schema_organization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 2eb568981..d2c0cce05 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -260,7 +260,7 @@ async def update_acl_async( asyncio.run(org.store_async()) # Get and modify the ACL - current acl = asyncio.run(org.get_acl_async()) + current_acl = asyncio.run(org.get_acl_async()) resource_access = current_acl["resourceAccess"] resource_access.append({"principalId": 1, "accessType": ["READ"]}) etag = current_acl["etag"] From 21307aa6ac4acd8dc01ec573b17c33376874265e Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Mon, 6 Oct 2025 15:19:29 -0700 Subject: [PATCH 17/41] removed from response method --- synapseclient/models/schema_organization.py | 84 ++++++++----------- .../unit_test_schema_organization.py | 36 +------- 2 files changed, 34 insertions(+), 86 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index d2c0cce05..c973d5918 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -43,7 +43,7 @@ class SchemaOrganization(SchemaOrganizationProtocol): Represents an [Organization](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/Organization.html). """ - name: str + name: Optional[str] = None """The name of the organization""" id: Optional[str] = None @@ -56,7 +56,8 @@ class SchemaOrganization(SchemaOrganizationProtocol): """The ID of the user that created this organization""" def __post_init__(self) -> None: - self._check_name(self.name) + if self.name: + self._check_name(self.name) async def get_async( self, synapse_client: Optional["Synapse"] = None @@ -90,7 +91,7 @@ async def get_async( if self.id: return self response = await get_organization(self.name, synapse_client=synapse_client) - self._fill_from_dict(response) + self.fill_from_dict(response) return self async def store_async( @@ -107,6 +108,9 @@ async def store_async( Returns: Itself + Raises: + ValueError: If the name has not been set + Example: Store a new SchemaOrganization   @@ -122,8 +126,10 @@ async def store_async( asyncio.run(org.store_async()) ``` """ + if not self.name: + raise ValueError("SchemaOrganization must have a name") response = await create_organization(self.name, synapse_client=synapse_client) - self._fill_from_dict(response) + self.fill_from_dict(response) return self async def delete_async(self, synapse_client: Optional["Synapse"] = None) -> None: @@ -162,6 +168,9 @@ async def get_json_schema_list_async( Returns: A list of JSONSchema objects + Raises: + ValueError: If the name has not been set + Arguments: synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created @@ -183,6 +192,8 @@ async def get_json_schema_list_async( ``` """ + if not self.name: + raise ValueError("SchemaOrganization must have a name") response = list_json_schemas(self.name, synapse_client=synapse_client) schemas = [] async for item in response: @@ -279,69 +290,40 @@ async def update_acl_async( synapse_client=synapse_client, ) - def _check_name(self, name) -> None: - """ - Checks that the input name is a valid Synapse organization name - - Must start with a letter - - Must contains only letters, numbers and periods. - - Arguments: - name: The name of the organization to be checked - - Raises: - ValueError: When the name isn't valid - """ - if not re.match("^([A-Za-z])([A-Za-z]|\d|\.)*$", name): - raise ValueError( - "Organization name must start with a letter and contain " - f"only letters numbers, and periods: {name}" - ) - - def _fill_from_dict(self, response: dict[str, Any]) -> None: + def fill_from_dict(self, response: dict[str, Any]) -> "SchemaOrganization": """ Fills in this classes attributes using a Synapse API response Args: response: A response from this endpoint: https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/Organization.html + + Returns: + Itself """ self.name = response.get("name") self.id = response.get("id") self.created_on = response.get("createdOn") self.created_by = response.get("createdBy") + return self - @classmethod - def from_response(cls, response: dict[str, Any]) -> "SchemaOrganization": + def _check_name(self, name) -> None: """ - Creates an SchemaOrganization object using a Synapse API response + Checks that the input name is a valid Synapse organization name + - Must start with a letter + - Must contains only letters, numbers and periods. Arguments: - response: A response from this endpoint: - https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/Organization.html - - Returns: - A SchemaOrganization object using the input response - - Example: Create a SchemaOrganization form an API response -   - - ```python - from synapseclient.models import SchemaOrganization - from synapseclient import Synapse - import asyncio - from synapseclient.api import get_organization - - syn = Synapse() - syn.login() - - response = asyncio.run(get_organization("my.org.name")) - org = SchemaOrganization.from_response(response) - ``` + name: The name of the organization to be checked + Raises: + ValueError: When the name isn't valid """ - org = SchemaOrganization(response.get("name")) - org._fill_from_dict(response) - return org + if not re.match("^([A-Za-z])([A-Za-z]|\d|\.)*$", name): + raise ValueError( + "Organization name must start with a letter and contain " + f"only letters numbers, and periods: {name}" + ) @dataclass() @@ -919,7 +901,7 @@ def list_json_schema_organizations( A list of SchemaOrganizations """ all_orgs = [ - SchemaOrganization.from_response(org) + SchemaOrganization().fill_from_dict(org) for org in list_organizations_sync(synapse_client=synapse_client) ] return all_orgs diff --git a/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py b/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py index 5a3209f15..b22d88b6b 100644 --- a/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py +++ b/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py @@ -4,10 +4,7 @@ import pytest from synapseclient.models import JSONSchema, SchemaOrganization -from synapseclient.models.schema_organization import ( - CreateSchemaRequest, - list_json_schema_organizations, -) +from synapseclient.models.schema_organization import CreateSchemaRequest class TestSchemaOrganization: @@ -32,37 +29,6 @@ def test_init_name_exceptions(self, name: str) -> None: with pytest.raises(ValueError, match="Organization name must start with"): SchemaOrganization(name) - @pytest.mark.parametrize( - "response", - [ - { - "name": "AAAAAAA", - "id": "abc", - "createdOn": "9-30-25", - "createdBy": "123", - } - ], - ) - def test_from_response(self, response: dict[str, Any]): - "Tests that legal Synapse API responses result in created objects." - assert SchemaOrganization.from_response(response) - - @pytest.mark.parametrize( - "response", - [ - { - "name": None, - "id": None, - "createdOn": None, - "createdBy": None, - } - ], - ) - def test_from_response_with_exception(self, response: dict[str, Any]): - "Tests that illegal Synapse API responses cause exceptions" - with pytest.raises(TypeError): - SchemaOrganization.from_response(response) - class TestJSONSchema: """Synchronous unit tests for JSONSchema.""" From 37e9a9b385eb9596edb6d5c630092aa54627dcbb Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 15 Oct 2025 11:01:17 -0700 Subject: [PATCH 18/41] change classes to have all attributes be optional --- synapseclient/models/schema_organization.py | 94 ++++++------------- .../unit_test_schema_organization.py | 83 ++++++++-------- 2 files changed, 76 insertions(+), 101 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index c973d5918..fbf6a2555 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -197,7 +197,7 @@ async def get_json_schema_list_async( response = list_json_schemas(self.name, synapse_client=synapse_client) schemas = [] async for item in response: - schemas.append(JSONSchema.from_response(item)) + schemas.append(JSONSchema().fill_from_dict(item)) return schemas async def get_acl_async( @@ -343,10 +343,10 @@ class JSONSchema(JSONSchemaProtocol): uri: The uri of this schema """ - name: str + name: Optional[str] = None """The name of the schema""" - organization_name: str + organization_name: Optional[str] = None """The name of the organization the schema belongs to""" organization_id: Optional[int] = None @@ -361,12 +361,16 @@ class JSONSchema(JSONSchemaProtocol): created_by: Optional[str] = None """The ID of the user that created this schema""" - uri: str = field(init=False) + uri: Optional[str] = field(init=False) """The uri of this schema""" def __post_init__(self) -> None: - self.uri = f"{self.organization_name}-{self.name}" - self._check_name(self.name) + if self.name: + self._check_name(self.name) + if self.organization_name: + self._check_name(self.organization_name) + if self.name and self.organization_name: + self.uri = f"{self.organization_name}-{self.name}" async def get_async( self, synapse_client: Optional["Synapse"] = None @@ -413,7 +417,7 @@ async def get_async( ) async for schema in org_schemas: if schema["schemaName"] == self.name: - self._fill_from_dict(schema) + self.fill_from_dict(schema) return self raise ValueError( ( @@ -584,6 +588,26 @@ async def get_body_async( response = await get_json_schema_body(uri, synapse_client=synapse_client) return response + def fill_from_dict(self, response: dict[str, Any]) -> "JSONSchema": + """ + Fills in this classes attributes using a Synapse API response + + Arguments: + response: This Synapse API object: + [JsonSchema](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchema.html) + + Returns: + Itself + """ + self.organization_id = response.get("organizationId") + self.organization_name = response.get("organizationName") + self.id = response.get("schemaId") + self.name = response.get("schemaName") + self.created_on = response.get("createdOn") + self.created_by = response.get("createdBy") + self.uri = f"{self.organization_name}-{self.name}" + return self + @classmethod def from_uri(cls, uri: str) -> "JSONSchema": """ @@ -625,46 +649,6 @@ def from_uri(cls, uri: str) -> "JSONSchema": raise ValueError(msg) return JSONSchema(name=uri_parts[1], organization_name=uri_parts[0]) - @classmethod - def from_response(cls, response: dict[str, Any]) -> "JSONSchema": - """ - Creates a JSONSchema object using a Synapse API response - - Arguments: - response: A response from this endpoint: - [JSON Schema](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchemaInfo.html) - - Returns: - A JSONSchema object from the API response - - Example: Create a JSONSchema from an API response -   - - ```python - from synapseclient.models import JSONSchema - from synapseclient import Synapse - from synapseclient.api import list_json_schemas - import asyncio - - syn = Synapse() - syn.login() - - async def get_first_response(): - async_gen = list_json_schemas("my.org.name") - responses = [] - async for item in async_gen: - responses.append(item) - return responses[1] - - response = asyncio.run(get_first_response()) - JSONSchema.from_response(response) - ``` - - """ - js = JSONSchema(response.get("schemaName"), response.get("organizationName")) - js._fill_from_dict(response) - return js - @staticmethod def _create_json_schema_version_from_response( response: dict[str, Any] @@ -692,22 +676,6 @@ def _create_json_schema_version_from_response( created_by=response.get("createdBy"), ) - def _fill_from_dict(self, response: dict[str, Any]) -> None: - """ - Fills in this classes attributes using a Synapse API response - - Arguments: - response: This Synapse API object: - [JsonSchema](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchema.html) - """ - self.organization_id = response.get("organizationId") - self.organization_name = response.get("organizationName") - self.id = response.get("schemaId") - self.name = response.get("schemaName") - self.created_on = response.get("createdOn") - self.created_by = response.get("createdBy") - self.uri = f"{self.organization_name}-{self.name}" - def _check_semantic_version(self, version: str) -> None: """ Checks that the semantic version is correctly formatted diff --git a/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py b/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py index b22d88b6b..a60ddea46 100644 --- a/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py +++ b/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py @@ -29,6 +29,26 @@ def test_init_name_exceptions(self, name: str) -> None: with pytest.raises(ValueError, match="Organization name must start with"): SchemaOrganization(name) + def test_fill_from_dict(self) -> None: + "Tests that fill_from_dict fills in all fields" + organization = SchemaOrganization() + assert organization.name is None + assert organization.id is None + assert organization.created_by is None + assert organization.created_on is None + organization.fill_from_dict( + { + "name": "org.name", + "id": "org.id", + "createdOn": "1", + "createdBy": "2", + } + ) + assert organization.name == "org.name" + assert organization.id == "org.id" + assert organization.created_by == "1" + assert organization.created_on == "2" + class TestJSONSchema: """Synchronous unit tests for JSONSchema.""" @@ -57,7 +77,7 @@ def test_init_name_exceptions(self, name: str) -> None: ["ORG.NAME-SCHEMA.NAME", "ORG.NAME-SCHEMA.NAME-0.0.1"], ids=["Non-semantic URI", "Semantic URI"], ) - def test_from_uri(self, uri: str): + def test_from_uri(self, uri: str) -> None: "Tests that legal schema URIs result in created objects." assert JSONSchema.from_uri(uri) @@ -66,51 +86,38 @@ def test_from_uri(self, uri: str): ["ORG.NAME", "ORG.NAME-SCHEMA.NAME-0.0.1-extra.part"], ids=["No dashes", "Too many dashes"], ) - def test_from_uri_with_exceptions(self, uri: str): + def test_from_uri_with_exceptions(self, uri: str) -> None: "Tests that illegal schema URIs result in an exception." with pytest.raises(ValueError, match="The URI must be in the form of"): JSONSchema.from_uri(uri) - @pytest.mark.parametrize( - "response", - [ + def test_fill_from_dict(self) -> None: + "Tests that fill_from_dict fills in all fields" + js = JSONSchema() + assert js.name is None + assert js.organization_name is None + assert js.id is None + assert js.organization_id is None + assert js.created_on is None + assert js.created_by is None + assert js.uri is None + js.fill_from_dict( { - "createdOn": "9-30-25", - "createdBy": "123", - "organizationId": "123", + "organizationId": "org.id", "organizationName": "org.name", - "schemaId": "123", - "schemaName": "schema.name", + "schemaId": "id", + "schemaName": "name", + "createdOn": "1", + "createdBy": "2", } - ], - ) - def test_from_response(self, response: dict[str, Any]): - "Tests that legal Synapse API responses result in created objects." - js = JSONSchema.from_response(response) - assert js.created_on == "9-30-25" - assert js.created_by == "123" - assert js.organization_id == "123" + ) + assert js.name == "name" assert js.organization_name == "org.name" - assert js.id == "123" - assert js.name == "schema.name" - - @pytest.mark.parametrize( - "response", - [ - { - "createdOn": None, - "createdBy": None, - "organizationId": None, - "organizationName": None, - "schemaId": None, - "schemaName": None, - } - ], - ) - def test_from_response_with_exception(self, response: dict[str, Any]): - "Tests that illegal Synapse API responses cause exceptions" - with pytest.raises(TypeError): - JSONSchema.from_response(response) + assert js.id == "id" + assert js.organization_id == "org.id" + assert js.created_on == "1" + assert js.created_by == "2" + assert js.uri == "org.name-name" class TestCreateSchemaRequest: From 5f0c255398778306190c6d2aec49ada045ddcbfe Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 16 Oct 2025 09:02:48 -0700 Subject: [PATCH 19/41] fixed method calls --- synapseclient/models/schema_organization.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index fbf6a2555..c0c0ec865 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -88,8 +88,8 @@ async def get_async( asyncio.run(org.get_async()) ``` """ - if self.id: - return self + if not self.name: + raise ValueError("SchemaOrganization must have a name") response = await get_organization(self.name, synapse_client=synapse_client) self.fill_from_dict(response) return self @@ -231,7 +231,7 @@ async def get_acl_async( ``` """ if not self.id: - await self.get_async() + await self.get_async(synapse_client=synapse_client) response = await get_organization_acl(self.id, synapse_client=synapse_client) return response @@ -282,7 +282,7 @@ async def update_acl_async( """ if not self.id: - await self.get_async() + await self.get_async(synapse_client=synapse_client) await update_organization_acl( organization_id=self.id, resource_access=resource_access, From 7b9382449c99084d324b7eebe7c175b941d594ed Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 16 Oct 2025 09:25:48 -0700 Subject: [PATCH 20/41] fixed tests --- synapseclient/models/schema_organization.py | 25 ++++++++++++++++--- .../unit_test_schema_organization.py | 4 +-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index c0c0ec865..cd4e72d68 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -371,6 +371,8 @@ def __post_init__(self) -> None: self._check_name(self.organization_name) if self.name and self.organization_name: self.uri = f"{self.organization_name}-{self.name}" + else: + self.uri = None async def get_async( self, synapse_client: Optional["Synapse"] = None @@ -404,13 +406,15 @@ async def get_async( asyncio.run(js.get_async()) ``` """ - if self.id: - return self + if not self.name: + raise ValueError("JSONSchema must have a name") + if not self.organization_name: + raise ValueError("JSONSchema must have a organization_name") # Check that the org exists, # if it doesn't list_json_schemas will unhelpfully return an empty generator. org = SchemaOrganization(self.organization_name) - await org.get_async() + await org.get_async(synapse_client=synapse_client) org_schemas = list_json_schemas( self.organization_name, synapse_client=synapse_client @@ -462,6 +466,11 @@ async def store_async( asyncio.run(js.store_async()) ``` """ + if not self.name: + raise ValueError("JSONSchema must have a name") + if not self.organization_name: + raise ValueError("JSONSchema must have a organization_name") + request = CreateSchemaRequest( schema=schema_body, name=self.name, @@ -503,6 +512,11 @@ async def delete_async(self, synapse_client: Optional["Synapse"] = None) -> None asyncio.run(js.delete_async()) ``` """ + if not self.name: + raise ValueError("JSONSchema must have a name") + if not self.organization_name: + raise ValueError("JSONSchema must have a organization_name") + await delete_json_schema(self.uri, synapse_client=synapse_client) async def get_versions_async( @@ -581,6 +595,11 @@ async def get_body_async( first = asyncio.run(js.get_body_async("0.0.1")) ``` """ + if not self.name: + raise ValueError("JSONSchema must have a name") + if not self.organization_name: + raise ValueError("JSONSchema must have a organization_name") + uri = self.uri if version: self._check_semantic_version(version) diff --git a/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py b/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py index a60ddea46..71bb11114 100644 --- a/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py +++ b/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py @@ -46,8 +46,8 @@ def test_fill_from_dict(self) -> None: ) assert organization.name == "org.name" assert organization.id == "org.id" - assert organization.created_by == "1" - assert organization.created_on == "2" + assert organization.created_on == "1" + assert organization.created_by == "2" class TestJSONSchema: From a86582503493b051d881d079302c1baed403c7be Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 16 Oct 2025 09:34:28 -0700 Subject: [PATCH 21/41] moved _check_name method to helper function --- synapseclient/models/schema_organization.py | 89 ++++++------------- .../unit_test_schema_organization.py | 20 ++++- 2 files changed, 42 insertions(+), 67 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index cd4e72d68..2ad40fff7 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -57,7 +57,7 @@ class SchemaOrganization(SchemaOrganizationProtocol): def __post_init__(self) -> None: if self.name: - self._check_name(self.name) + _check_name(self.name) async def get_async( self, synapse_client: Optional["Synapse"] = None @@ -307,24 +307,6 @@ def fill_from_dict(self, response: dict[str, Any]) -> "SchemaOrganization": self.created_by = response.get("createdBy") return self - def _check_name(self, name) -> None: - """ - Checks that the input name is a valid Synapse organization name - - Must start with a letter - - Must contains only letters, numbers and periods. - - Arguments: - name: The name of the organization to be checked - - Raises: - ValueError: When the name isn't valid - """ - if not re.match("^([A-Za-z])([A-Za-z]|\d|\.)*$", name): - raise ValueError( - "Organization name must start with a letter and contain " - f"only letters numbers, and periods: {name}" - ) - @dataclass() @async_to_sync @@ -366,9 +348,9 @@ class JSONSchema(JSONSchemaProtocol): def __post_init__(self) -> None: if self.name: - self._check_name(self.name) + _check_name(self.name) if self.organization_name: - self._check_name(self.organization_name) + _check_name(self.organization_name) if self.name and self.organization_name: self.uri = f"{self.organization_name}-{self.name}" else: @@ -714,26 +696,6 @@ def _check_semantic_version(self, version: str) -> None: ) ) - def _check_name(self, name) -> None: - """ - Checks that the input name is a valid Synapse JSONSchema name - - Must start with a letter - - Must contains only letters, numbers and periods. - - Arguments: - name: The name of the organization to be checked - - Raises: - ValueError: When the name isn't valid - """ - if not re.match("^([A-Za-z])([A-Za-z]|\d|\.)*$", name): - raise ValueError( - ( - "Schema name must start with a letter and contain " - f"only letters numbers and periods: {name}" - ) - ) - @dataclass class CreateSchemaRequest(AsynchronousCommunicator): @@ -770,8 +732,8 @@ class CreateSchemaRequest(AsynchronousCommunicator): def __post_init__(self) -> None: self.concrete_type = CREATE_SCHEMA_REQUEST - self._check_name(self.name) - self._check_name(self.organization_name) + _check_name(self.name) + _check_name(self.organization_name) uri = f"{self.organization_name}-{self.name}" if self.version: self._check_semantic_version(self.version) @@ -825,26 +787,6 @@ def _check_semantic_version(self, version: str) -> None: ) ) - def _check_name(self, name: str) -> None: - """ - Checks that the input name is a valid Synapse JSONSchema or Organization name - - Must start with a letter - - Must contains only letters, numbers and periods. - - Arguments: - name: The name of the organization/schema to be checked - - Raises: - ValueError: When the name isn't valid - """ - if not re.match("^([A-Za-z])([A-Za-z]|\d|\.)*$", name): - raise ValueError( - ( - "Schema name must start with a letter and contain " - f"only letters numbers and periods: {name}" - ) - ) - @staticmethod def _create_json_schema_version_from_response( response: dict[str, Any] @@ -892,3 +834,24 @@ def list_json_schema_organizations( for org in list_organizations_sync(synapse_client=synapse_client) ] return all_orgs + + +def _check_name(name) -> None: + """ + Checks that the input name is a valid Synapse Organization or JSONSchema name + - Must start with a letter + - Must contains only letters, numbers and periods. + + Arguments: + name: The name of the organization to be checked + + Raises: + ValueError: When the name isn't valid + """ + if not re.match("^([A-Za-z])([A-Za-z]|\d|\.)*$", name): + raise ValueError( + ( + "Name must start with a letter and contain " + f"only letters numbers and periods: {name}" + ) + ) diff --git a/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py b/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py index 71bb11114..10fcdb166 100644 --- a/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py +++ b/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py @@ -26,7 +26,10 @@ def test_init(self, name: str) -> None: ) def test_init_name_exceptions(self, name: str) -> None: "Tests that illegal names raise a ValueError on init" - with pytest.raises(ValueError, match="Organization name must start with"): + with pytest.raises( + ValueError, + match="Name must start with a letter and contain only letters numbers and periods", + ): SchemaOrganization(name) def test_fill_from_dict(self) -> None: @@ -69,7 +72,10 @@ def test_init(self, name: str) -> None: ) def test_init_name_exceptions(self, name: str) -> None: "Tests that illegal names raise a ValueError on init" - with pytest.raises(ValueError, match="Schema name must start with"): + with pytest.raises( + ValueError, + match="Name must start with a letter and contain only letters numbers and periods", + ): JSONSchema(name, "org.name") @pytest.mark.parametrize( @@ -137,7 +143,10 @@ def test_init_name(self, name: str) -> None: ) def test_init_name_exceptions(self, name: str) -> None: "Tests that illegal names raise a ValueError on init" - with pytest.raises(ValueError, match="Schema name must start with"): + with pytest.raises( + ValueError, + match="Name must start with a letter and contain only letters numbers and periods", + ): CreateSchemaRequest(schema={}, name=name, organization_name="org.name") @pytest.mark.parametrize( @@ -158,7 +167,10 @@ def test_init_org_name(self, name: str) -> None: ) def test_init_org_name_exceptions(self, name: str) -> None: "Tests that illegal org names raise a ValueError on init" - with pytest.raises(ValueError, match="Schema name must start with"): + with pytest.raises( + ValueError, + match="Name must start with a letter and contain only letters numbers and periods", + ): CreateSchemaRequest(schema={}, name="schema.name", organization_name=name) @pytest.mark.parametrize( From 90c5447a69ae53942573d1f3d764045d9c9fa746 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 16 Oct 2025 09:38:58 -0700 Subject: [PATCH 22/41] fix CreateSchemaRequest docstring links --- synapseclient/models/schema_organization.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 2ad40fff7..502707a29 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -700,7 +700,7 @@ def _check_semantic_version(self, version: str) -> None: @dataclass class CreateSchemaRequest(AsynchronousCommunicator): """ - This result is modeled from: + This class is for creating a [CreateSchemaRequest]https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/CreateSchemaRequest.html """ schema: dict[str, Any] @@ -745,7 +745,7 @@ def __post_init__(self) -> None: def to_synapse_request(self) -> dict[str, Any]: """ Create a CreateSchemaRequest from attributes - https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/CreateSchemaRequest.html + [CreateSchemaRequest]https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/CreateSchemaRequest.html """ result = { @@ -758,8 +758,8 @@ def to_synapse_request(self) -> dict[str, Any]: def fill_from_dict(self, synapse_response: dict[str, Any]) -> "CreateSchemaRequest": """ - Set attributes from CreateSchemaResponse - https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/CreateSchemaResponse.html + Set attributes from + [CreateSchemaResponse]https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/CreateSchemaResponse.html """ self.new_version_info = self._create_json_schema_version_from_response( synapse_response.get("newVersionInfo") From 745c615aa884937b62014922d6a5e5b6090f69c4 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 16 Oct 2025 10:21:07 -0700 Subject: [PATCH 23/41] change update_acl to be more user friendly --- synapseclient/models/schema_organization.py | 51 +++++++------------ .../models/async/test_schema_organization.py | 6 +-- .../synchronous/test_schema_organization.py | 4 +- 3 files changed, 21 insertions(+), 40 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 502707a29..f82b5f400 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -5,7 +5,7 @@ import re from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence +from typing import TYPE_CHECKING, Any, Optional from synapseclient.api import ( create_organization, @@ -237,52 +237,39 @@ async def get_acl_async( async def update_acl_async( self, - resource_access: Sequence[Mapping[str, Sequence[str]]], - etag: str, + principal_id: int, + access_type: list[str], synapse_client: Optional["Synapse"] = None, ) -> None: """ - Updates the ACL for this organization + Updates the ACL for a principal for this organization Arguments: - resource_access: List of ResourceAccess objects, each containing: - - principalId: The user or team ID - - accessType: List of permission types (e.g., ["READ", "CREATE", "DELETE"]) + principal_id: the id of the principal whose permissions are to be updates + access_type: List of permission types (e.g., ["READ", "CREATE", "DELETE"]) see: https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/ResourceAccess.html - etag: The etag from get_organization_acl() for concurrency control synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor - Example: Update the ACL or a SchemaOrganization -   - - ```python - from synapseclient.models import SchemaOrganization - from synapseclient import Synapse - import asyncio - - syn = Synapse() - syn.login() + """ + acl = await self.get_acl_async(synapse_client=synapse_client) - # Store a new org - org = SchemaOrganization("my.org.name") - asyncio.run(org.store_async()) + resource_access: list[dict[str, Any]] = acl["resourceAccess"] + etag = acl["etag"] - # Get and modify the ACL - current_acl = asyncio.run(org.get_acl_async()) - resource_access = current_acl["resourceAccess"] - resource_access.append({"principalId": 1, "accessType": ["READ"]}) - etag = current_acl["etag"] + principal_id_match = False + for permissions in resource_access: + if permissions["principalId"] == principal_id: + permissions["accessType"] = access_type + principal_id_match = True - # Update the ACL - asyncio.run(org.update_acl_async(resource_access, etag)) - ``` + if not principal_id_match: + resource_access.append( + {"principalId": principal_id, "accessType": access_type} + ) - """ - if not self.id: - await self.get_async(synapse_client=synapse_client) await update_organization_acl( organization_id=self.id, resource_access=resource_access, diff --git a/tests/integration/synapseclient/models/async/test_schema_organization.py b/tests/integration/synapseclient/models/async/test_schema_organization.py index bbc990730..5a190f3fa 100644 --- a/tests/integration/synapseclient/models/async/test_schema_organization.py +++ b/tests/integration/synapseclient/models/async/test_schema_organization.py @@ -189,12 +189,8 @@ async def test_get_acl_and_update_acl( # THEN the resource access should be have one principal assert len(resource_access) == 1 # WHEN adding another principal to the resource access - resource_access.append({"principalId": 1, "accessType": ["READ"]}) - etag = acl["etag"] # AND updating the acl - await organization.update_acl_async( - resource_access, etag, synapse_client=self.syn - ) + await organization.update_acl_async(1, ["READ"], synapse_client=self.syn) # THEN the resource access should be have two principals acl = await organization.get_acl_async(synapse_client=self.syn) assert len(acl["resourceAccess"]) == 2 diff --git a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py index aad338101..750231d58 100644 --- a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py +++ b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py @@ -172,10 +172,8 @@ def test_get_acl_and_update_acl(self, organization: SchemaOrganization) -> None: # THEN the resource access should be have one principal assert len(resource_access) == 1 # WHEN adding another principal to the resource access - resource_access.append({"principalId": 1, "accessType": ["READ"]}) - etag = acl["etag"] # AND updating the acl - organization.update_acl(resource_access, etag, synapse_client=self.syn) + organization.update_acl(1, ["READ"], synapse_client=self.syn) # THEN the resource access should be have two principals acl = organization.get_acl(synapse_client=self.syn) assert len(acl["resourceAccess"]) == 2 From 0420bda7ba077f5339ade08bc2a84d036f2439a7 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 16 Oct 2025 12:58:44 -0700 Subject: [PATCH 24/41] improve examples --- synapseclient/models/schema_organization.py | 169 +++++++++++++++----- 1 file changed, 133 insertions(+), 36 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index f82b5f400..bda3e1302 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -73,19 +73,30 @@ async def get_async( Returns: Itself + Raises: + ValueError: If the name has not been set + Example: Get an existing SchemaOrganization   ```python + from synapseclient.models import SchemaOrganization from synapseclient import Synapse import asyncio - syn = Synapse() - syn.login() + async def get_org(): - org = SchemaOrganization("dpetest") - asyncio.run(org.get_async()) + syn = Synapse() + syn.login() + + org = SchemaOrganization("dpetest") + await org.get_async() + return org + + org = asyncio.run(get_org()) + print(org.name) + print(org.id) ``` """ if not self.name: @@ -119,11 +130,18 @@ async def store_async( from synapseclient import Synapse import asyncio - syn = Synapse() - syn.login() + async def store_org(): + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.new.org") + await org.store_async() + return org - org = SchemaOrganization("my.org.name") - asyncio.run(org.store_async()) + org = asyncio.run(store_org()) + print(org.name) + print(org.id) ``` """ if not self.name: @@ -149,11 +167,15 @@ async def delete_async(self, synapse_client: Optional["Synapse"] = None) -> None from synapseclient import Synapse import asyncio - syn = Synapse() - syn.login() + async def delete_org(): + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org") + await org.delete_async() - org = SchemaOrganization("my.org.name") - asyncio.run(org.delete_async()) + asyncio.run(delete_org()) ``` """ if not self.id: @@ -223,11 +245,24 @@ async def get_acl_async( from synapseclient import Synapse import asyncio - syn = Synapse() - syn.login() + async def get_acl(): - org = SchemaOrganization("dpetest") - acl = asyncio.run(org.get_acl_async()) + syn = Synapse() + syn.login() + + org = SchemaOrganization("dpetest") + acl = await org.get_acl_async() + return acl + + acl = asyncio.run(get_acl()) + etag = acl["etag"] + print(etag) + resource_access = acl["resourceAccess"] + for item in resource_access: + principal_id = item["principalId"] + print((principal_id)) + access_types = item["accessType"] + print(access_types) ``` """ if not self.id: @@ -253,6 +288,27 @@ async def update_acl_async( `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor + Example: Update the ACL for a SchemaOrganization +   + + ```python + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + import asyncio + + async def update_acl() -> None: + + syn = Synapse() + syn.login() + + org = SchemaOrganization("dpetest") + await org.update_acl_async( + principal_id=1, + access_type=["READ"] + ) + + asyncio.run(update_acl()) + """ acl = await self.get_acl_async(synapse_client=synapse_client) @@ -355,6 +411,8 @@ async def get_async( instance from the Synapse class constructor Raises: + ValueError: This JSONSchema doesn't have a name + ValueError: This JSONSchema doesn't have an organization name ValueError: This JSONSchema doesn't exist in its organization Returns: @@ -368,11 +426,17 @@ async def get_async( from synapseclient import Synapse import asyncio - syn = Synapse() - syn.login() + async def get_schema(): - js = JSONSchema("my.schema.name", "my.org.name") - asyncio.run(js.get_async()) + syn = Synapse() + syn.login() + + schema = JSONSchema(organization_name="dpetest", name="test.schematic.Biospecimen") + await schema.get_async() + return schema + + schema = asyncio.run(get_schema()) + print(schema.uri) ``` """ if not self.name: @@ -428,11 +492,15 @@ async def store_async( from synapseclient import Synapse import asyncio - syn = Synapse() - syn.login() + async def store_schema(): - js = JSONSchema("my.schema.name", "my.org.name") - asyncio.run(js.store_async()) + syn = Synapse() + syn.login() + + schema = JSONSchema(organization_name="my.org", name="test.schema") + await schema.store_async(schema_body = {}) + + asyncio.run(store_schema()) ``` """ if not self.name: @@ -474,11 +542,15 @@ async def delete_async(self, synapse_client: Optional["Synapse"] = None) -> None from synapseclient import Synapse import asyncio - syn = Synapse() - syn.login() + async def delete_schema(): - js = JSONSchema("my.schema.name", "my.org.name") - asyncio.run(js.delete_async()) + syn = Synapse() + syn.login() + + schema = JSONSchema(organization_name="my.org", name="test.schema") + await schema.delete_async(schema_body = {}) + + asyncio.run(delete_schema()) ``` """ if not self.name: @@ -547,22 +619,47 @@ async def get_body_async( The JSON Schema body Example: Get the body of the JSONSchema -   + + Get latest version ```python from synapseclient.models import JSONSchema from synapseclient import Synapse import asyncio - syn = Synapse() - syn.login() + async def get_body(): - js = JSONSchema("my.schema.name", "my.org.name") - # Get latest version - latest = asyncio.run(js.get_body_async()) - # Get specific version - first = asyncio.run(js.get_body_async("0.0.1")) + syn = Synapse() + syn.login() + + schema = JSONSchema(organization_name="dpetest", name="test.schematic.Biospecimen") + body = await schema.get_body_async() + return body + + body = asyncio.run(get_body()) + print(body) ``` + + Get specific version + + ```python + from synapseclient.models import JSONSchema + from synapseclient import Synapse + import asyncio + + async def get_body(): + + syn = Synapse() + syn.login() + + schema = JSONSchema(organization_name="dpetest", name="test.schematic.Biospecimen") + body = await schema.get_body_async(version="0.0.1") + return body + + body = asyncio.run(get_body()) + print(body) + ``` + """ if not self.name: raise ValueError("JSONSchema must have a name") From 7cb5be07f4a920b92782afd3a4197ffea921abe8 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 16 Oct 2025 13:03:48 -0700 Subject: [PATCH 25/41] moved protocols to model file --- .../protocols/schema_organization_protocol.py | 365 ------------------ synapseclient/models/schema_organization.py | 360 ++++++++++++++++- 2 files changed, 355 insertions(+), 370 deletions(-) delete mode 100644 synapseclient/models/protocols/schema_organization_protocol.py diff --git a/synapseclient/models/protocols/schema_organization_protocol.py b/synapseclient/models/protocols/schema_organization_protocol.py deleted file mode 100644 index aa2afc00e..000000000 --- a/synapseclient/models/protocols/schema_organization_protocol.py +++ /dev/null @@ -1,365 +0,0 @@ -"""Protocols for the specific methods of this class that have synchronous counterparts -generated at runtime.""" - -from typing import TYPE_CHECKING, Any, Mapping, Optional, Protocol, Sequence - -if TYPE_CHECKING: - from synapseclient import Synapse - from synapseclient.models.mixins.json_schema import JSONSchemaVersionInfo - from synapseclient.models.schema_organization import JSONSchema, SchemaOrganization - - -class SchemaOrganizationProtocol(Protocol): - """ - The protocol for methods that are asynchronous but also - have a synchronous counterpart that may also be called. - """ - - def get(self, synapse_client: Optional["Synapse"] = None) -> "SchemaOrganization": - """ - Gets the metadata from Synapse for this organization - - Arguments: - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor - - Returns: - Itself - - Example: Get an existing SchemaOrganization -   - - ```python - from synapseclient.models import SchemaOrganization - from synapseclient import Synapse - - syn = Synapse() - syn.login() - - org = SchemaOrganization("my.org.name") - org.get() - ``` - - """ - return self - - def store(self, synapse_client: Optional["Synapse"] = None) -> "SchemaOrganization": - """ - Stores this organization in Synapse - - Arguments: - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor - - Returns: - Itself - - Example: Store the SchemaOrganization in Synapse -   - - ```python - from synapseclient.models import SchemaOrganization - from synapseclient import Synapse - - syn = Synapse() - syn.login() - - org = SchemaOrganization("my.org.name") - org.store() - ``` - - """ - return self - - def delete(self, synapse_client: Optional["Synapse"] = None) -> None: - """ - Deletes this organization in Synapse - - Arguments: - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor - - Example: Delete the SchemaOrganization from Synapse -   - - ```python - from synapseclient.models import SchemaOrganization - from synapseclient import Synapse - - syn = Synapse() - syn.login() - - org = SchemaOrganization("my.org.name") - org.delete() - ``` - """ - return None - - def get_json_schema_list( - self, synapse_client: Optional["Synapse"] = None - ) -> list["JSONSchema"]: - """ - Gets the list of JSON Schemas that are part of this organization - - Arguments: - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor - - Returns: A list of JSONSchema objects - - Example: Get the JSONSchemas that are part of this SchemaOrganization -   - - ```python - from synapseclient.models import SchemaOrganization - from synapseclient import Synapse - - syn = Synapse() - syn.login() - - org = SchemaOrganization("my.org.name") - org.get_json_schema_list() - ``` - """ - return [] - - def get_acl(self, synapse_client: Optional["Synapse"] = None) -> dict[str, Any]: - """ - Gets the ACL for this organization - - Arguments: - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor - - Returns: - A dictionary in the form of this response: - https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html - - Example: Get the ACL for the SchemaOrganization -   - - ```python - from synapseclient.models import SchemaOrganization - from synapseclient import Synapse - - syn = Synapse() - syn.login() - - org = SchemaOrganization("my.org.name") - org.get_acl() - ``` - """ - return {} - - def update_acl( - self, - resource_access: Sequence[Mapping[str, Sequence[str]]], - etag: str, - synapse_client: Optional["Synapse"] = None, - ) -> None: - """ - Updates the ACL for this organization - - Arguments: - resource_access: List of ResourceAccess objects, each containing: - - principalId: The user or team ID - - accessType: List of permission types (e.g., ["READ", "CREATE", "DELETE"]) - see: - https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/ResourceAccess.html - etag: The etag from get_organization_acl() for concurrency control - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor - - Example: Update the ACL for the SchemaOrganization -   - - ```python - from synapseclient.models import SchemaOrganization - from synapseclient import Synapse - - syn = Synapse() - syn.login() - - org = SchemaOrganization("my.org.name") - current acl = org.get_acl() - resource_access = current_acl["resourceAccess"] - resource_access.append({"principalId": 1, "accessType": ["READ"]}) - etag = current_acl["etag"] - org.update_acl(resource_access, etag) - ``` - """ - return None - - -class JSONSchemaProtocol(Protocol): - """ - The protocol for methods that are asynchronous but also - have a synchronous counterpart that may also be called. - """ - - def get(self, synapse_client: Optional["Synapse"] = None) -> "JSONSchema": - """ - Gets this JSON Schemas metadata - - Arguments: - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor - - Returns: - Itself - - Raises: - ValueError: This JSONSchema doesn't exist in its organization - - Example: Get an Existing JSONSchema -   - - ```python - from synapseclient.models import JSONSchema - from synapseclient import Synapse - - syn = Synapse() - syn.login() - - js = JSONSchema("my.schema.name", "my.org.name") - js.get() - ``` - """ - return self - - def store( - self, - schema_body: dict[str, Any], - version: Optional[str] = None, - dry_run: bool = False, - synapse_client: Optional["Synapse"] = None, - ) -> "JSONSchema": - """ - Stores this JSONSchema in Synapse - - Arguments: - schema_body: The body of the JSONSchema to store - version: The version of the JSONSchema body to store - dry_run: Whether or not to do a dry-run - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor - - Returns: - Itself - - Example: Store a JSON Schema in Synapse -   - - ```python - from synapseclient.models import SchemaOrganization - from synapseclient import Synapse - - syn = Synapse() - syn.login() - - org = SchemaOrganization("my.org.name") - org.store() - ``` - """ - return self - - def delete(self) -> None: - """ - Deletes this JSON Schema - - Arguments: - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor - - Example: Delete this JSONSchema from Synapse -   - - ```python - from synapseclient.models import JSONSchema - from synapseclient import Synapse - - syn = Synapse() - syn.login() - - js = JSONSchema("my.schema.name", "my.org.name") - js.delete() - ``` - """ - return None - - def get_versions( - self, synapse_client: Optional["Synapse"] = None - ) -> list["JSONSchemaVersionInfo"]: - """ - Gets a list of all versions of this JSONSchema - - Arguments: - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor - - Returns: - A JSONSchemaVersionInfo for each version of this schema - - Example: Get all versions of the JSONSchema -   - - ```python - from synapseclient.models import JSONSchema - from synapseclient import Synapse - - syn = Synapse() - syn.login() - - js = JSONSchema("my.schema.name", "my.org.name") - versions = get_versions() - """ - return [] - - def get_body( - self, version: Optional[str] = None, synapse_client: Optional["Synapse"] = None - ) -> dict[str, Any]: - """ - Gets the JSON body for the schema. - - Arguments: - version: Defaults to None. - - If a version is supplied, that versions body will be returned. - - If no version is supplied the most recent version will be returned. - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor - - Returns: - The JSON Schema body - - Example: Get the JSONSchema body from Synapse -   - - ```python - from synapseclient.models import JSONSchema - from synapseclient import Synapse - - syn = Synapse() - syn.login() - - js = JSONSchema("my.schema.name", "my.org.name") - # Get latest version - latest = js.get_body() - # Get specific version - first = js.get_body("0.0.1") - ``` - """ - return {} diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index bda3e1302..462c85604 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -5,7 +5,7 @@ import re from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Optional, Protocol from synapseclient.api import ( create_organization, @@ -23,10 +23,6 @@ from synapseclient.core.constants.concrete_types import CREATE_SCHEMA_REQUEST from synapseclient.models.mixins.asynchronous_job import AsynchronousCommunicator from synapseclient.models.mixins.json_schema import JSONSchemaVersionInfo -from synapseclient.models.protocols.schema_organization_protocol import ( - JSONSchemaProtocol, - SchemaOrganizationProtocol, -) if TYPE_CHECKING: from synapseclient import Synapse @@ -36,6 +32,195 @@ ) +class SchemaOrganizationProtocol(Protocol): + """ + The protocol for methods that are asynchronous but also + have a synchronous counterpart that may also be called. + """ + + def get(self, synapse_client: Optional["Synapse"] = None) -> "SchemaOrganization": + """ + Gets the metadata from Synapse for this organization + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Returns: + Itself + + Example: Get an existing SchemaOrganization +   + + ```python + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + org.get() + ``` + + """ + return self + + def store(self, synapse_client: Optional["Synapse"] = None) -> "SchemaOrganization": + """ + Stores this organization in Synapse + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Returns: + Itself + + Example: Store the SchemaOrganization in Synapse +   + + ```python + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + org.store() + ``` + + """ + return self + + def delete(self, synapse_client: Optional["Synapse"] = None) -> None: + """ + Deletes this organization in Synapse + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Example: Delete the SchemaOrganization from Synapse +   + + ```python + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + org.delete() + ``` + """ + return None + + def get_json_schema_list( + self, synapse_client: Optional["Synapse"] = None + ) -> list["JSONSchema"]: + """ + Gets the list of JSON Schemas that are part of this organization + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Returns: A list of JSONSchema objects + + Example: Get the JSONSchemas that are part of this SchemaOrganization +   + + ```python + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + org.get_json_schema_list() + ``` + """ + return [] + + def get_acl(self, synapse_client: Optional["Synapse"] = None) -> dict[str, Any]: + """ + Gets the ACL for this organization + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Returns: + A dictionary in the form of this response: + https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html + + Example: Get the ACL for the SchemaOrganization +   + + ```python + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + org.get_acl() + ``` + """ + return {} + + def update_acl( + self, + principal_id: int, + access_type: list[str], + synapse_client: Optional["Synapse"] = None, + ) -> None: + """ + Updates the ACL for a principal for this organization + + Arguments: + principal_id: the id of the principal whose permissions are to be updates + access_type: List of permission types (e.g., ["READ", "CREATE", "DELETE"]) + see: + https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/ResourceAccess.html + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Example: Update the ACL for the SchemaOrganization +   + + ```python + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + current acl = org.get_acl() + resource_access = current_acl["resourceAccess"] + resource_access.append({"principalId": 1, "accessType": ["READ"]}) + etag = current_acl["etag"] + org.update_acl(resource_access, etag) + ``` + """ + return None + + @dataclass() @async_to_sync class SchemaOrganization(SchemaOrganizationProtocol): @@ -351,6 +536,171 @@ def fill_from_dict(self, response: dict[str, Any]) -> "SchemaOrganization": return self +class JSONSchemaProtocol(Protocol): + """ + The protocol for methods that are asynchronous but also + have a synchronous counterpart that may also be called. + """ + + def get(self, synapse_client: Optional["Synapse"] = None) -> "JSONSchema": + """ + Gets this JSON Schemas metadata + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Returns: + Itself + + Raises: + ValueError: This JSONSchema doesn't exist in its organization + + Example: Get an Existing JSONSchema +   + + ```python + from synapseclient.models import JSONSchema + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + js = JSONSchema("my.schema.name", "my.org.name") + js.get() + ``` + """ + return self + + def store( + self, + schema_body: dict[str, Any], + version: Optional[str] = None, + dry_run: bool = False, + synapse_client: Optional["Synapse"] = None, + ) -> "JSONSchema": + """ + Stores this JSONSchema in Synapse + + Arguments: + schema_body: The body of the JSONSchema to store + version: The version of the JSONSchema body to store + dry_run: Whether or not to do a dry-run + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Returns: + Itself + + Example: Store a JSON Schema in Synapse +   + + ```python + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + org = SchemaOrganization("my.org.name") + org.store() + ``` + """ + return self + + def delete(self) -> None: + """ + Deletes this JSON Schema + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Example: Delete this JSONSchema from Synapse +   + + ```python + from synapseclient.models import JSONSchema + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + js = JSONSchema("my.schema.name", "my.org.name") + js.delete() + ``` + """ + return None + + def get_versions( + self, synapse_client: Optional["Synapse"] = None + ) -> list["JSONSchemaVersionInfo"]: + """ + Gets a list of all versions of this JSONSchema + + Arguments: + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Returns: + A JSONSchemaVersionInfo for each version of this schema + + Example: Get all versions of the JSONSchema +   + + ```python + from synapseclient.models import JSONSchema + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + js = JSONSchema("my.schema.name", "my.org.name") + versions = get_versions() + """ + return [] + + def get_body( + self, version: Optional[str] = None, synapse_client: Optional["Synapse"] = None + ) -> dict[str, Any]: + """ + Gets the JSON body for the schema. + + Arguments: + version: Defaults to None. + - If a version is supplied, that versions body will be returned. + - If no version is supplied the most recent version will be returned. + synapse_client: If not passed in and caching was not disabled by + `Synapse.allow_client_caching(False)` this will use the last created + instance from the Synapse class constructor + + Returns: + The JSON Schema body + + Example: Get the JSONSchema body from Synapse +   + + ```python + from synapseclient.models import JSONSchema + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + js = JSONSchema("my.schema.name", "my.org.name") + # Get latest version + latest = js.get_body() + # Get specific version + first = js.get_body("0.0.1") + ``` + """ + return {} + + @dataclass() @async_to_sync class JSONSchema(JSONSchemaProtocol): From 29b76c4d8e65a23fa70209d242db14db3d8dd8c6 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 16 Oct 2025 13:12:29 -0700 Subject: [PATCH 26/41] improve examples --- synapseclient/models/schema_organization.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 462c85604..a354cf5ce 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -60,8 +60,9 @@ def get(self, synapse_client: Optional["Synapse"] = None) -> "SchemaOrganization syn = Synapse() syn.login() - org = SchemaOrganization("my.org.name") + org = SchemaOrganization("dpetest") org.get() + print(org) ``` """ @@ -91,6 +92,7 @@ def store(self, synapse_client: Optional["Synapse"] = None) -> "SchemaOrganizati org = SchemaOrganization("my.org.name") org.store() + print(org) ``` """ @@ -211,11 +213,10 @@ def update_acl( syn.login() org = SchemaOrganization("my.org.name") - current acl = org.get_acl() - resource_access = current_acl["resourceAccess"] - resource_access.append({"principalId": 1, "accessType": ["READ"]}) - etag = current_acl["etag"] - org.update_acl(resource_access, etag) + org.update_acl_async( + principal_id=1, + access_type=["READ"] + ) ``` """ return None @@ -569,6 +570,7 @@ def get(self, synapse_client: Optional["Synapse"] = None) -> "JSONSchema": js = JSONSchema("my.schema.name", "my.org.name") js.get() + print(js) ``` """ return self @@ -606,6 +608,7 @@ def store( org = SchemaOrganization("my.org.name") org.store() + print(org) ``` """ return self @@ -694,8 +697,10 @@ def get_body( js = JSONSchema("my.schema.name", "my.org.name") # Get latest version latest = js.get_body() + print(latest) # Get specific version first = js.get_body("0.0.1") + print(first) ``` """ return {} From 8890e9d20f277f1b4fffedc08fb02ccd4eed8a70 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 16 Oct 2025 13:29:29 -0700 Subject: [PATCH 27/41] add delete_async example using id --- synapseclient/models/schema_organization.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index a354cf5ce..5f7e8d321 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -346,7 +346,8 @@ async def delete_async(self, synapse_client: Optional["Synapse"] = None) -> None instance from the Synapse class constructor Example: Delete a SchemaOrganization -   + + Delete using a name ```python from synapseclient.models import SchemaOrganization @@ -363,6 +364,23 @@ async def delete_org(): asyncio.run(delete_org()) ``` + + Delete using an id + + ```python + from synapseclient.models import SchemaOrganization + from synapseclient import Synapse + import asyncio + + async def delete_org(): + + syn = Synapse() + syn.login() + + org = SchemaOrganization(id=1075) + await org.delete_async() + + asyncio.run(delete_org()) """ if not self.id: await self.get_async(synapse_client=synapse_client) From f8486222f67afb86312cf885194a4fd92e8d45b2 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Thu, 16 Oct 2025 14:32:05 -0700 Subject: [PATCH 28/41] use repoEndpoint for Synapse URL --- synapseclient/models/schema_organization.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 5f7e8d321..9b69eaa82 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -5,8 +5,9 @@ import re from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Optional, Protocol +from typing import Any, Optional, Protocol +from synapseclient import Synapse from synapseclient.api import ( create_organization, delete_json_schema, @@ -24,12 +25,7 @@ from synapseclient.models.mixins.asynchronous_job import AsynchronousCommunicator from synapseclient.models.mixins.json_schema import JSONSchemaVersionInfo -if TYPE_CHECKING: - from synapseclient import Synapse - -SYNAPSE_SCHEMA_URL = ( - "https://repo-prod.prod.sagebase.org/repo/v1/schema/type/registered/" -) +SYNAPSE_SCHEMA_URL = f"{Synapse().repoEndpoint}/schema/type/registered/" class SchemaOrganizationProtocol(Protocol): From 780105dcccd375c1b398eedd5686e919d800eb1c Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 17 Oct 2025 10:54:04 -0700 Subject: [PATCH 29/41] chnage list methods to return generators --- synapseclient/models/__init__.py | 7 +- synapseclient/models/schema_organization.py | 100 ++++++++++++------ .../models/async/test_schema_organization.py | 45 ++++---- .../synchronous/test_schema_organization.py | 16 +-- 4 files changed, 103 insertions(+), 65 deletions(-) diff --git a/synapseclient/models/__init__.py b/synapseclient/models/__init__.py index 933e9d017..f0486d6b4 100644 --- a/synapseclient/models/__init__.py +++ b/synapseclient/models/__init__.py @@ -21,11 +21,8 @@ from synapseclient.models.materializedview import MaterializedView from synapseclient.models.mixins.table_components import QueryMixin from synapseclient.models.project import Project -from synapseclient.models.schema_organization import ( - JSONSchema, - RecordSet, - SchemaOrganization, -) +from synapseclient.models.recordset import RecordSet +from synapseclient.models.schema_organization import JSONSchema, SchemaOrganization from synapseclient.models.services import FailureStrategy from synapseclient.models.submissionview import SubmissionView from synapseclient.models.table import Table diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 9b69eaa82..c6931bb58 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -5,7 +5,7 @@ import re from dataclasses import dataclass, field -from typing import Any, Optional, Protocol +from typing import Any, AsyncGenerator, Generator, Optional, Protocol from synapseclient import Synapse from synapseclient.api import ( @@ -20,7 +20,11 @@ list_organizations_sync, update_organization_acl, ) -from synapseclient.core.async_utils import async_to_sync +from synapseclient.core.async_utils import ( + async_to_sync, + skip_async_to_sync, + wrap_async_generator_to_sync_generator, +) from synapseclient.core.constants.concrete_types import CREATE_SCHEMA_REQUEST from synapseclient.models.mixins.asynchronous_job import AsynchronousCommunicator from synapseclient.models.mixins.json_schema import JSONSchemaVersionInfo @@ -119,9 +123,9 @@ def delete(self, synapse_client: Optional["Synapse"] = None) -> None: """ return None - def get_json_schema_list( + def get_json_schemas( self, synapse_client: Optional["Synapse"] = None - ) -> list["JSONSchema"]: + ) -> Generator["JSONSchema", None, None]: """ Gets the list of JSON Schemas that are part of this organization @@ -142,11 +146,16 @@ def get_json_schema_list( syn = Synapse() syn.login() - org = SchemaOrganization("my.org.name") - org.get_json_schema_list() + org = SchemaOrganization("dpetest") + js_generator = org.get_json_schemas() + for item in js_generator: + print(item) ``` """ - return [] + yield from wrap_async_generator_to_sync_generator( + async_gen_func=self.get_json_schemas_async, + synapse_client=synapse_client, + ) def get_acl(self, synapse_client: Optional["Synapse"] = None) -> dict[str, Any]: """ @@ -382,13 +391,14 @@ async def delete_org(): await self.get_async(synapse_client=synapse_client) await delete_organization(self.id, synapse_client=synapse_client) - async def get_json_schema_list_async( + @skip_async_to_sync + async def get_json_schemas_async( self, synapse_client: Optional["Synapse"] = None - ) -> list["JSONSchema"]: + ) -> AsyncGenerator["JSONSchema", None]: """ - Gets the list of JSON Schemas that are part of this organization + Gets the JSON Schemas that are part of this organization - Returns: A list of JSONSchema objects + Returns: An AsyncGenerator of JSONSchema objects Raises: ValueError: If the name has not been set @@ -406,21 +416,29 @@ async def get_json_schema_list_async( from synapseclient import Synapse import asyncio - syn = Synapse() - syn.login() + async def get_schemas(): - org = SchemaOrganization("dpetest") - schemas = asyncio.run(org.get_json_schema_list_async()) + syn = Synapse() + syn.login() + + org = SchemaOrganization("dpetest") + js_generator = org.get_json_schemas_async() + js_list = [] + async for item in js_generator: + js_list.append(item) + return js_list + + js_list = asyncio.run(get_schemas()) + for item in js_list: + print(item) ``` """ if not self.name: raise ValueError("SchemaOrganization must have a name") response = list_json_schemas(self.name, synapse_client=synapse_client) - schemas = [] async for item in response: - schemas.append(JSONSchema().fill_from_dict(item)) - return schemas + yield JSONSchema().fill_from_dict(item) async def get_acl_async( self, synapse_client: Optional["Synapse"] = None @@ -654,9 +672,9 @@ def delete(self) -> None: def get_versions( self, synapse_client: Optional["Synapse"] = None - ) -> list["JSONSchemaVersionInfo"]: + ) -> Generator["JSONSchemaVersionInfo", None, None]: """ - Gets a list of all versions of this JSONSchema + Gets all versions of this JSONSchema Arguments: synapse_client: If not passed in and caching was not disabled by @@ -664,7 +682,7 @@ def get_versions( instance from the Synapse class constructor Returns: - A JSONSchemaVersionInfo for each version of this schema + A Generator containing the JSONSchemaVersionInfo for each version of this schema Example: Get all versions of the JSONSchema   @@ -676,10 +694,15 @@ def get_versions( syn = Synapse() syn.login() - js = JSONSchema("my.schema.name", "my.org.name") - versions = get_versions() + schema = JSONSchema(organization_name="dpetest", name="test.schematic.Biospecimen") + version_generator = schema.get_versions() + for item in version_generator: + print(item) """ - return [] + yield from wrap_async_generator_to_sync_generator( + async_gen_func=self.get_versions_async, + synapse_client=synapse_client, + ) def get_body( self, version: Optional[str] = None, synapse_client: Optional["Synapse"] = None @@ -929,11 +952,12 @@ async def delete_schema(): await delete_json_schema(self.uri, synapse_client=synapse_client) + @skip_async_to_sync async def get_versions_async( self, synapse_client: Optional["Synapse"] = None - ) -> list[JSONSchemaVersionInfo]: + ) -> AsyncGenerator[JSONSchemaVersionInfo, None]: """ - Gets a list of all versions of this JSONSchema + Gets all versions of this JSONSchema Arguments: synapse_client: If not passed in and caching was not disabled by @@ -941,7 +965,7 @@ async def get_versions_async( instance from the Synapse class constructor Returns: - A JSONSchemaVersionInfo for each version of this schema + A generator containing each version of this schema Example: Get all the versions of the JSONSchema   @@ -951,24 +975,32 @@ async def get_versions_async( from synapseclient import Synapse import asyncio - syn = Synapse() - syn.login() + async def get_versions(): - js = JSONSchema("my.schema.name", "my.org.name") - versions = asyncio.run(get_versions_async()) + syn = Synapse() + syn.login() + + schema = JSONSchema(organization_name="dpetest", name="test.schematic.Biospecimen") + version_generator = schema.get_versions_async() + version_list = [] + async for item in version_generator: + version_list.append(item) + return version_list + + version_list = asyncio.run(get_versions()) + for item in version_list: + print(item) ``` """ all_schemas = list_json_schema_versions( self.organization_name, self.name, synapse_client=synapse_client ) - versions = [] async for schema in all_schemas: # Schemas created without a semantic version will be returned from the API call. # Those won't be returned here since they aren't really versions. # JSONSchemaVersionInfo.semantic_version could also be changed to optional. if "semanticVersion" in schema: - versions.append(self._create_json_schema_version_from_response(schema)) - return versions + yield self._create_json_schema_version_from_response(schema) async def get_body_async( self, version: Optional[str] = None, synapse_client: Optional["Synapse"] = None diff --git a/tests/integration/synapseclient/models/async/test_schema_organization.py b/tests/integration/synapseclient/models/async/test_schema_organization.py index 5a190f3fa..f3bee2c52 100644 --- a/tests/integration/synapseclient/models/async/test_schema_organization.py +++ b/tests/integration/synapseclient/models/async/test_schema_organization.py @@ -3,7 +3,6 @@ from typing import Any, Optional import pytest -import pytest_asyncio from synapseclient import Synapse from synapseclient.core.constants.concrete_types import CREATE_SCHEMA_REQUEST @@ -55,7 +54,7 @@ def fixture_module_organization(syn: Synapse, request) -> SchemaOrganization: org.store(synapse_client=syn) def delete_org(): - for schema in org.get_json_schema_list(synapse_client=syn): + for schema in org.get_json_schemas(synapse_client=syn): schema.delete() org.delete(synapse_client=syn) @@ -108,7 +107,7 @@ def fixture_organization_with_schema(request) -> SchemaOrganization: js3.store({}) def delete_org(): - for schema in org.get_json_schema_list(): + for schema in org.get_json_schemas(): schema.delete() org.delete() @@ -155,7 +154,7 @@ async def test_create_and_get(self, organization: SchemaOrganization) -> None: org2.store() @pytest.mark.asyncio - async def test_get_json_schema_list( + async def test_get_json_schemas_async( self, organization: SchemaOrganization, organization_with_schema: SchemaOrganization, @@ -163,13 +162,15 @@ async def test_get_json_schema_list( # GIVEN an organization with no schemas and one with 3 schemas await organization.store_async(synapse_client=self.syn) # THEN get_json_schema_list should return the correct list of schemas - schema_list = await organization.get_json_schema_list_async( + schema_list = [] + async for item in organization.get_json_schemas_async(synapse_client=self.syn): + schema_list.append(item) + assert len(schema_list) == 0 + schema_list2 = [] + async for item in organization_with_schema.get_json_schemas_async( synapse_client=self.syn - ) - assert not schema_list - schema_list2 = await organization_with_schema.get_json_schema_list_async( - synapse_client=self.syn - ) + ): + schema_list2.append(item) assert len(schema_list2) == 3 @pytest.mark.asyncio @@ -237,19 +238,25 @@ async def test_store_and_get(self, json_schema: JSONSchema) -> None: async def test_get_versions(self, json_schema: JSONSchema) -> None: # GIVEN an schema that hasn't been created # THEN get_versions should return an empty list - versions = await json_schema.get_versions_async(synapse_client=self.syn) + versions = [] + async for item in json_schema.get_versions_async(synapse_client=self.syn): + versions.append(item) assert len(versions) == 0 # WHEN creating a schema with no version await json_schema.store_async(schema_body={}, synapse_client=self.syn) # THEN get_versions should return an empty list - versions = await json_schema.get_versions_async(synapse_client=self.syn) + versions = [] + async for item in json_schema.get_versions_async(synapse_client=self.syn): + versions.append(item) assert len(versions) == 0 # WHEN creating a schema with a version await json_schema.store_async( schema_body={}, version="0.0.1", synapse_client=self.syn ) # THEN get_versions should return that version - versions = await json_schema.get_versions_async(synapse_client=self.syn) + versions = [] + async for item in json_schema.get_versions_async(synapse_client=self.syn): + versions.append(item) assert len(versions) == 1 assert versions[0].semantic_version == "0.0.1" @@ -322,7 +329,7 @@ async def test_create_schema_request_no_version( assert not request.new_version_info # THEN the Schema should not be part of the organization yet assert request.uri not in [ - schema.uri for schema in module_organization.get_json_schema_list() + schema.uri for schema in module_organization.get_json_schemas() ] # WHEN sending the CreateSchemaRequest @@ -331,9 +338,9 @@ async def test_create_schema_request_no_version( ) assert completed_request.new_version_info # THEN the Schema should be part of the organization - assert completed_request.uri in [ - schema.uri for schema in module_organization.get_json_schema_list() - ] + # assert completed_request.uri in [ + # schema.uri for schema in module_organization.get_json_schema_list() + # ] @pytest.mark.asyncio async def test_create_schema_request_with_version( @@ -368,7 +375,7 @@ async def test_create_schema_request_with_version( assert not request.new_version_info # THEN the Schema should not be part of the organization yet assert f"{module_organization.name}-{schema_name}" not in [ - schema.uri for schema in module_organization.get_json_schema_list() + schema.uri for schema in module_organization.get_json_schemas() ] # WHEN sending the CreateSchemaRequest @@ -379,7 +386,7 @@ async def test_create_schema_request_with_version( # THEN the Schema (minus version) should be part of the organization yet schemas = [ schema - for schema in module_organization.get_json_schema_list() + for schema in module_organization.get_json_schemas() if schema.uri == f"{module_organization.name}-{schema_name}" ] assert len(schemas) == 1 diff --git a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py index 750231d58..1c15f994c 100644 --- a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py +++ b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py @@ -48,7 +48,7 @@ def fixture_module_organization(request) -> SchemaOrganization: org.store() def delete_org(): - for schema in org.get_json_schema_list(): + for schema in org.get_json_schemas(): schema.delete() org.delete() @@ -99,7 +99,7 @@ def fixture_organization_with_schema(request) -> SchemaOrganization: js3.store({}) def delete_org(): - for schema in org.get_json_schema_list(): + for schema in org.get_json_schemas(): schema.delete() org.delete() @@ -152,9 +152,11 @@ def test_get_json_schema_list( # GIVEN an organization with no schemas and one with 3 schemas organization.store(synapse_client=self.syn) # THEN get_json_schema_list should return the correct list of schemas - assert not organization.get_json_schema_list(synapse_client=self.syn) + assert len(list(organization.get_json_schemas(synapse_client=self.syn))) == 0 assert ( - len(organization_with_schema.get_json_schema_list(synapse_client=self.syn)) + len( + list(organization_with_schema.get_json_schemas(synapse_client=self.syn)) + ) == 3 ) @@ -219,15 +221,15 @@ def test_store_and_get(self, json_schema: JSONSchema) -> None: def test_get_versions(self, json_schema: JSONSchema) -> None: # GIVEN an schema that hasn't been created # THEN get_versions should return an empty list - assert not json_schema.get_versions(synapse_client=self.syn) + assert len(list(json_schema.get_versions(synapse_client=self.syn))) == 0 # WHEN creating a schema with no version json_schema.store(schema_body={}, synapse_client=self.syn) # THEN get_versions should return an empty list - assert json_schema.get_versions(synapse_client=self.syn) == [] + assert len(list(json_schema.get_versions(synapse_client=self.syn))) == 0 # WHEN creating a schema with a version json_schema.store(schema_body={}, version="0.0.1", synapse_client=self.syn) # THEN get_versions should return that version - schemas = json_schema.get_versions(synapse_client=self.syn) + schemas = list(json_schema.get_versions(synapse_client=self.syn)) assert len(schemas) == 1 assert schemas[0].semantic_version == "0.0.1" From a5931399c7e1231f972938818871467d3770e6a8 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 17 Oct 2025 10:58:57 -0700 Subject: [PATCH 30/41] clear up comment --- synapseclient/models/schema_organization.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index c6931bb58..cfc29bcab 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -996,9 +996,9 @@ async def get_versions(): self.organization_name, self.name, synapse_client=synapse_client ) async for schema in all_schemas: - # Schemas created without a semantic version will be returned from the API call. - # Those won't be returned here since they aren't really versions. - # JSONSchemaVersionInfo.semantic_version could also be changed to optional. + # Schema "versions" without a semantic version will be returned from the API call, + # but will be filtered out by this method. + # Only those with a semantic version will be returned. if "semanticVersion" in schema: yield self._create_json_schema_version_from_response(schema) From 98d1092248260e94f9c1afc7c97118acb2b8c3c3 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 17 Oct 2025 11:04:09 -0700 Subject: [PATCH 31/41] added example to docstring --- synapseclient/models/schema_organization.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index cfc29bcab..a349b8b23 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -1313,6 +1313,17 @@ def list_json_schema_organizations( Returns: A list of SchemaOrganizations + + Example: + from synapseclient.models.schema_organization import list_json_schema_organizations + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + all_orgs = list_json_schema_organizations() + for item in all_orgs: + print(item) """ all_orgs = [ SchemaOrganization().fill_from_dict(org) From bcb5b33501b03746205237b748a0167a74b65c5c Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 17 Oct 2025 11:21:33 -0700 Subject: [PATCH 32/41] improve docstrings --- synapseclient/models/schema_organization.py | 35 ++++++++++----------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index a349b8b23..76d980da2 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -127,14 +127,14 @@ def get_json_schemas( self, synapse_client: Optional["Synapse"] = None ) -> Generator["JSONSchema", None, None]: """ - Gets the list of JSON Schemas that are part of this organization + Gets the JSON Schemas that are part of this organization Arguments: synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor - Returns: A list of JSONSchema objects + Returns: A Generator containing the JSONSchemas that belong to this organization Example: Get the JSONSchemas that are part of this SchemaOrganization   @@ -165,13 +165,10 @@ def get_acl(self, synapse_client: Optional["Synapse"] = None) -> dict[str, Any]: synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor - synapse_client: If not passed in and caching was not disabled by - `Synapse.allow_client_caching(False)` this will use the last created - instance from the Synapse class constructor Returns: A dictionary in the form of this response: - https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html + [AccessControlList]https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html Example: Get the ACL for the SchemaOrganization   @@ -199,10 +196,10 @@ def update_acl( Updates the ACL for a principal for this organization Arguments: - principal_id: the id of the principal whose permissions are to be updates + principal_id: the id of the principal whose permissions are to be updated access_type: List of permission types (e.g., ["READ", "CREATE", "DELETE"]) see: - https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/ResourceAccess.html + [ACCESS_TYPE]https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/ACCESS_TYPE.html synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor @@ -396,9 +393,9 @@ async def get_json_schemas_async( self, synapse_client: Optional["Synapse"] = None ) -> AsyncGenerator["JSONSchema", None]: """ - Gets the JSON Schemas that are part of this organization + Gets the JSONSchemas that are part of this organization - Returns: An AsyncGenerator of JSONSchema objects + Returns: An AsyncGenerator of JSONSchemas Raises: ValueError: If the name has not been set @@ -453,7 +450,7 @@ async def get_acl_async( Returns: A dictionary in the form of this response: - https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html + [AccessControlList]https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html Example: Get the ACL for a SchemaOrganization   @@ -501,7 +498,7 @@ async def update_acl_async( principal_id: the id of the principal whose permissions are to be updates access_type: List of permission types (e.g., ["READ", "CREATE", "DELETE"]) see: - https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/ResourceAccess.html + [ACCESS_TYPE]https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/ACCESS_TYPE.html synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor @@ -557,7 +554,7 @@ def fill_from_dict(self, response: dict[str, Any]) -> "SchemaOrganization": Args: response: A response from this endpoint: - https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/Organization.html + [Organization]https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/Organization.html Returns: Itself @@ -577,7 +574,7 @@ class JSONSchemaProtocol(Protocol): def get(self, synapse_client: Optional["Synapse"] = None) -> "JSONSchema": """ - Gets this JSON Schemas metadata + Gets this JSONSchemas metadata Arguments: synapse_client: If not passed in and caching was not disabled by @@ -647,7 +644,7 @@ def store( def delete(self) -> None: """ - Deletes this JSON Schema + Deletes this JSONSchema Arguments: synapse_client: If not passed in and caching was not disabled by @@ -708,7 +705,7 @@ def get_body( self, version: Optional[str] = None, synapse_client: Optional["Synapse"] = None ) -> dict[str, Any]: """ - Gets the JSON body for the schema. + Gets the body of this JSONSchema. Arguments: version: Defaults to None. @@ -795,7 +792,7 @@ async def get_async( self, synapse_client: Optional["Synapse"] = None ) -> "JSONSchema": """ - Gets this JSON Schemas metadata + Gets the metadata for this JSONSchema from Synapse Arguments: synapse_client: If not passed in and caching was not disabled by @@ -919,7 +916,7 @@ async def store_schema(): async def delete_async(self, synapse_client: Optional["Synapse"] = None) -> None: """ - Deletes this JSONSchema + Deletes this JSONSchema from Synapse Arguments: synapse_client: If not passed in and caching was not disabled by @@ -1006,7 +1003,7 @@ async def get_body_async( self, version: Optional[str] = None, synapse_client: Optional["Synapse"] = None ) -> dict[str, Any]: """ - Gets the JSON body for the schema. + Gets the body of this JSONSchema Arguments: version: Defaults to None. From 4922e7cfb7ffb7909cd1085d64aca54d325e1f8d Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 17 Oct 2025 11:45:46 -0700 Subject: [PATCH 33/41] remove pytest marks --- .../synapseclient/models/async/test_schema_organization.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/integration/synapseclient/models/async/test_schema_organization.py b/tests/integration/synapseclient/models/async/test_schema_organization.py index f3bee2c52..8855f4219 100644 --- a/tests/integration/synapseclient/models/async/test_schema_organization.py +++ b/tests/integration/synapseclient/models/async/test_schema_organization.py @@ -123,7 +123,6 @@ class TestSchemaOrganization: def init(self, syn: Synapse) -> None: self.syn = syn - @pytest.mark.asyncio async def test_create_and_get(self, organization: SchemaOrganization) -> None: # GIVEN an initialized organization object that hasn't been stored in Synapse # THEN it shouldn't have any metadata besides it's name @@ -153,7 +152,6 @@ async def test_create_and_get(self, organization: SchemaOrganization) -> None: with pytest.raises(SynapseHTTPError): org2.store() - @pytest.mark.asyncio async def test_get_json_schemas_async( self, organization: SchemaOrganization, @@ -173,7 +171,6 @@ async def test_get_json_schemas_async( schema_list2.append(item) assert len(schema_list2) == 3 - @pytest.mark.asyncio async def test_get_acl_and_update_acl( self, organization: SchemaOrganization ) -> None: @@ -300,7 +297,6 @@ class TestCreateSchemaRequest: def init(self, syn: Synapse) -> None: self.syn = syn - @pytest.mark.asyncio async def test_create_schema_request_no_version( self, module_organization: SchemaOrganization ) -> None: @@ -342,7 +338,6 @@ async def test_create_schema_request_no_version( # schema.uri for schema in module_organization.get_json_schema_list() # ] - @pytest.mark.asyncio async def test_create_schema_request_with_version( self, module_organization: SchemaOrganization ) -> None: From d4a4508dfbbc3d01dca059822de3b483e7930a22 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 17 Oct 2025 11:49:33 -0700 Subject: [PATCH 34/41] redid some examples --- synapseclient/models/schema_organization.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 76d980da2..1fce3cf52 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -420,14 +420,10 @@ async def get_schemas(): org = SchemaOrganization("dpetest") js_generator = org.get_json_schemas_async() - js_list = [] async for item in js_generator: - js_list.append(item) - return js_list + print(item) - js_list = asyncio.run(get_schemas()) - for item in js_list: - print(item) + asyncio.run(get_schemas()) ``` """ @@ -979,14 +975,10 @@ async def get_versions(): schema = JSONSchema(organization_name="dpetest", name="test.schematic.Biospecimen") version_generator = schema.get_versions_async() - version_list = [] async for item in version_generator: - version_list.append(item) - return version_list + print(item) - version_list = asyncio.run(get_versions()) - for item in version_list: - print(item) + asyncio.run(get_versions()) ``` """ all_schemas = list_json_schema_versions( From 3f94b72a73c2b319fb8ed55cb02dc5431b2a8f26 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 17 Oct 2025 12:02:52 -0700 Subject: [PATCH 35/41] changed some fixtures to async --- .../models/async/test_schema_organization.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/tests/integration/synapseclient/models/async/test_schema_organization.py b/tests/integration/synapseclient/models/async/test_schema_organization.py index 8855f4219..514a54c2e 100644 --- a/tests/integration/synapseclient/models/async/test_schema_organization.py +++ b/tests/integration/synapseclient/models/async/test_schema_organization.py @@ -3,6 +3,7 @@ from typing import Any, Optional import pytest +import pytest_asyncio from synapseclient import Synapse from synapseclient.core.constants.concrete_types import CREATE_SCHEMA_REQUEST @@ -25,7 +26,7 @@ def create_test_entity_name(): return f"SYNPY.TEST.{random_string}" -def org_exists(name: str, synapse_client: Optional[Synapse] = None) -> bool: +async def org_exists(name: str, synapse_client: Optional[Synapse] = None) -> bool: """ Checks if any organizations exists with the given name @@ -73,16 +74,17 @@ def fixture_json_schema(module_organization: SchemaOrganization) -> JSONSchema: return js -@pytest.fixture(name="organization", scope="function") -def fixture_organization(syn: Synapse, request) -> SchemaOrganization: +@pytest_asyncio.fixture(name="organization", loop_scope="function", scope="function") +async def fixture_organization(syn: Synapse, request) -> SchemaOrganization: """ Returns a Synapse organization. """ name = create_test_entity_name() org = SchemaOrganization(name) - def delete_org(): - if org_exists(name, syn): + async def delete_org(): + exists = await org_exists(name, syn) + if exists: org.delete() request.addfinalizer(delete_org) @@ -90,8 +92,10 @@ def delete_org(): return org -@pytest.fixture(name="organization_with_schema", scope="function") -def fixture_organization_with_schema(request) -> SchemaOrganization: +@pytest_asyncio.fixture( + name="organization_with_schema", loop_scope="function", scope="function" +) +async def fixture_organization_with_schema(request) -> SchemaOrganization: """ Returns a Synapse organization. As Cleanup it checks for JSON Schemas and deletes them""" @@ -131,7 +135,8 @@ async def test_create_and_get(self, organization: SchemaOrganization) -> None: assert organization.created_by is None assert organization.created_on is None # AND it shouldn't exist in Synapse - assert not org_exists(organization.name, synapse_client=self.syn) + exists = await org_exists(organization.name, synapse_client=self.syn) + assert not exists # WHEN I store the organization the metadata will be saved await organization.store_async(synapse_client=self.syn) assert organization.name is not None @@ -139,7 +144,8 @@ async def test_create_and_get(self, organization: SchemaOrganization) -> None: assert organization.created_by is not None assert organization.created_on is not None # AND it should exist in Synapse - assert org_exists(organization.name, synapse_client=self.syn) + exists = await org_exists(organization.name, synapse_client=self.syn) + assert exists # AND it should be getable by future instances with the same name org2 = SchemaOrganization(organization.name) await org2.get_async(synapse_client=self.syn) From b941b287ca4905aac39df749775ca0b2b67851c5 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 17 Oct 2025 12:23:26 -0700 Subject: [PATCH 36/41] convert tests to async --- .../synchronous/test_schema_organization.py | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py index 1c15f994c..9f124ebfc 100644 --- a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py +++ b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py @@ -3,6 +3,7 @@ from typing import Any, Optional import pytest +import pytest_asyncio from synapseclient import Synapse from synapseclient.core.exceptions import SynapseHTTPError @@ -20,7 +21,7 @@ def create_test_entity_name(): return f"SYNPY.TEST.{random_string}" -def org_exists(name: str, synapse_client: Optional[Synapse] = None) -> bool: +async def org_exists(name: str, synapse_client: Optional[Synapse] = None) -> bool: """ Checks if any organizations exists with the given name @@ -66,16 +67,17 @@ def fixture_json_schema(module_organization: SchemaOrganization) -> JSONSchema: return js -@pytest.fixture(name="organization", scope="function") -def fixture_organization(syn: Synapse, request) -> SchemaOrganization: +@pytest_asyncio.fixture(name="organization", loop_scope="function", scope="function") +async def fixture_organization(syn: Synapse, request) -> SchemaOrganization: """ Returns a Synapse organization. """ name = create_test_entity_name() org = SchemaOrganization(name) - def delete_org(): - if org_exists(name, syn): + async def delete_org(): + exists = await org_exists(name, syn) + if exists: org.delete() request.addfinalizer(delete_org) @@ -115,7 +117,7 @@ class TestSchemaOrganization: def init(self, syn: Synapse) -> None: self.syn = syn - def test_create_and_get(self, organization: SchemaOrganization) -> None: + async def test_create_and_get(self, organization: SchemaOrganization) -> None: # GIVEN an initialized organization object that hasn't been stored in Synapse # THEN it shouldn't have any metadata besides it's name assert organization.name is not None @@ -123,7 +125,8 @@ def test_create_and_get(self, organization: SchemaOrganization) -> None: assert organization.created_by is None assert organization.created_on is None # AND it shouldn't exist in Synapse - assert not org_exists(organization.name, synapse_client=self.syn) + exists = await org_exists(organization.name, synapse_client=self.syn) + assert not exists # WHEN I store the organization the metadata will be saved organization.store(synapse_client=self.syn) assert organization.name is not None @@ -131,7 +134,8 @@ def test_create_and_get(self, organization: SchemaOrganization) -> None: assert organization.created_by is not None assert organization.created_on is not None # AND it should exist in Synapse - assert org_exists(organization.name, synapse_client=self.syn) + exists = await org_exists(organization.name, synapse_client=self.syn) + assert exists # AND it should be getable by future instances with the same name org2 = SchemaOrganization(organization.name) org2.get(synapse_client=self.syn) @@ -144,7 +148,7 @@ def test_create_and_get(self, organization: SchemaOrganization) -> None: with pytest.raises(SynapseHTTPError): org2.store() - def test_get_json_schema_list( + async def test_get_json_schemas( self, organization: SchemaOrganization, organization_with_schema: SchemaOrganization, @@ -160,7 +164,9 @@ def test_get_json_schema_list( == 3 ) - def test_get_acl_and_update_acl(self, organization: SchemaOrganization) -> None: + async def test_get_acl_and_update_acl( + self, organization: SchemaOrganization + ) -> None: # GIVEN an organization that has been initialized, but not created # THEN get_acl should raise an error with pytest.raises( @@ -188,7 +194,7 @@ class TestJSONSchema: def init(self, syn: Synapse) -> None: self.syn = syn - def test_store_and_get(self, json_schema: JSONSchema) -> None: + async def test_store_and_get(self, json_schema: JSONSchema) -> None: # GIVEN an initialized schema object that hasn't been stored in Synapse # THEN it shouldn't have any metadata besides it's name and organization name, and uri assert json_schema.name @@ -218,7 +224,7 @@ def test_store_and_get(self, json_schema: JSONSchema) -> None: assert js2.created_by assert js2.created_on - def test_get_versions(self, json_schema: JSONSchema) -> None: + async def test_get_versions(self, json_schema: JSONSchema) -> None: # GIVEN an schema that hasn't been created # THEN get_versions should return an empty list assert len(list(json_schema.get_versions(synapse_client=self.syn))) == 0 @@ -233,7 +239,7 @@ def test_get_versions(self, json_schema: JSONSchema) -> None: assert len(schemas) == 1 assert schemas[0].semantic_version == "0.0.1" - def test_get_body(self, json_schema: JSONSchema) -> None: + async def test_get_body(self, json_schema: JSONSchema) -> None: # GIVEN an schema that hasn't been created # WHEN creating a schema with 2 version first_body = {} From 259e0cb7e93540a7139ee360c3bcd7e8cce2735a Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 17 Oct 2025 12:46:23 -0700 Subject: [PATCH 37/41] fix regex bug --- synapseclient/models/schema_organization.py | 4 +++- .../unit_test_schema_organization.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 1fce3cf52..6901336ab 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -1161,7 +1161,9 @@ def _check_semantic_version(self, version: str) -> None: Raises: ValueError: If the string is not a correct semantic version """ - if not re.match("^(\d+)\.(\d+)\.([1-9]\d*)$", version): + if version == "0.0.0": + raise ValueError("Schema version must start at '0.0.1' or higher") + if not re.match("^(\d+)\.(\d+)\.(\d+)$", version): raise ValueError( ( "Schema version must be a semantic version with no letters " diff --git a/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py b/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py index 10fcdb166..312e88e6d 100644 --- a/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py +++ b/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py @@ -125,6 +125,25 @@ def test_fill_from_dict(self) -> None: assert js.created_by == "2" assert js.uri == "org.name-name" + @pytest.mark.parametrize("version", ["1.0.0", "0.0.1", "0.1.0"]) + def test_check_semantic_version(self, version: str) -> None: + "Tests that only correct versions are allowed" + js = JSONSchema() + js._check_semantic_version(version) + + def test_check_semantic_version_with_exceptions(self) -> None: + "Tests that only correct versions are allowed" + js = JSONSchema() + with pytest.raises( + ValueError, match="Schema version must start at '0.0.1' or higher" + ): + js._check_semantic_version("0.0.0") + with pytest.raises( + ValueError, + match="Schema version must be a semantic version with no letters and a major, minor and patch version", + ): + js._check_semantic_version("0.0.1.rc") + class TestCreateSchemaRequest: @pytest.mark.parametrize( From 1c7d86b5fbfdce2b53082b8f4f7e33ed33c44ac1 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 21 Oct 2025 09:27:55 -0700 Subject: [PATCH 38/41] add version to delete methods --- synapseclient/models/schema_organization.py | 84 ++++++++++++++++--- .../models/async/test_schema_organization.py | 45 ++++++++++ .../synchronous/test_schema_organization.py | 34 ++++++++ 3 files changed, 152 insertions(+), 11 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 6901336ab..99d50a805 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -638,17 +638,23 @@ def store( """ return self - def delete(self) -> None: + def delete( + self, version: Optional[str] = None, synapse_client: Optional["Synapse"] = None + ) -> None: """ Deletes this JSONSchema Arguments: + version: Defaults to None. + - If a version is supplied, that version will be deleted. + - If no version is supplied the whole schema will be deleted. synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor - Example: Delete this JSONSchema from Synapse -   + Example: Delete from Synapse + + Delete the entire schema ```python from synapseclient.models import JSONSchema @@ -660,6 +666,19 @@ def delete(self) -> None: js = JSONSchema("my.schema.name", "my.org.name") js.delete() ``` + + Delete a specific version from Synapse + + ```python + from synapseclient.models import JSONSchema + from synapseclient import Synapse + + syn = Synapse() + syn.login() + + js = JSONSchema("my.schema.name", "my.org.name") + js.delete(version="0.0.1") + ``` """ return None @@ -750,7 +769,7 @@ class JSONSchema(JSONSchemaProtocol): id: The ID of the schema created_on: The date this schema was created created_by: The ID of the user that created this schema - uri: The uri of this schema + uri: The schema identifier in format: - """ name: Optional[str] = None @@ -772,7 +791,7 @@ class JSONSchema(JSONSchemaProtocol): """The ID of the user that created this schema""" uri: Optional[str] = field(init=False) - """The uri of this schema""" + """The schema identifier in format: -""" def __post_init__(self) -> None: if self.name: @@ -883,7 +902,20 @@ async def store_schema(): syn.login() schema = JSONSchema(organization_name="my.org", name="test.schema") - await schema.store_async(schema_body = {}) + schema_body = { + { + "properties": { + "Component": { + "description": "TBD", + "not": { + "type": "null" + }, + "title": "Component" + } + } + } + } + await schema.store_async(schema_body = schema_body) asyncio.run(store_schema()) ``` @@ -910,17 +942,24 @@ async def store_schema(): self.created_on = new_version_info.created_on return self - async def delete_async(self, synapse_client: Optional["Synapse"] = None) -> None: + async def delete_async( + self, version: Optional[str] = None, synapse_client: Optional["Synapse"] = None + ) -> None: """ - Deletes this JSONSchema from Synapse + If a version is supplied the specific version is deleted from Synapse. + Otherwise the entire schema is deleted from Synapse Arguments: + version: Defaults to None. + - If a version is supplied, that version will be deleted. + - If no version is supplied the whole schema will be deleted. synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created instance from the Synapse class constructor Example: Delete an existing JSONSchema -   + + Delete the whole schema ```python from synapseclient.models import JSONSchema @@ -933,7 +972,25 @@ async def delete_schema(): syn.login() schema = JSONSchema(organization_name="my.org", name="test.schema") - await schema.delete_async(schema_body = {}) + await schema.delete_async() + + asyncio.run(delete_schema()) + ``` + + Delete a specific version of the schema + + ```python + from synapseclient.models import JSONSchema + from synapseclient import Synapse + import asyncio + + async def delete_schema(): + + syn = Synapse() + syn.login() + + schema = JSONSchema(organization_name="my.org", name="test.schema") + await schema.delete_async(version = "0.0.1") asyncio.run(delete_schema()) ``` @@ -943,7 +1000,12 @@ async def delete_schema(): if not self.organization_name: raise ValueError("JSONSchema must have a organization_name") - await delete_json_schema(self.uri, synapse_client=synapse_client) + uri = self.uri + if version: + self._check_semantic_version(version) + uri = f"{uri}-{version}" + + await delete_json_schema(uri, synapse_client=synapse_client) @skip_async_to_sync async def get_versions_async( diff --git a/tests/integration/synapseclient/models/async/test_schema_organization.py b/tests/integration/synapseclient/models/async/test_schema_organization.py index 514a54c2e..bb674dd6e 100644 --- a/tests/integration/synapseclient/models/async/test_schema_organization.py +++ b/tests/integration/synapseclient/models/async/test_schema_organization.py @@ -12,6 +12,7 @@ from synapseclient.models.schema_organization import ( SYNAPSE_SCHEMA_URL, CreateSchemaRequest, + JSONSchemaVersionInfo, list_json_schema_organizations, ) @@ -238,6 +239,50 @@ async def test_store_and_get(self, json_schema: JSONSchema) -> None: assert js2.created_by assert js2.created_on + async def test_delete(self, organization_with_schema: SchemaOrganization) -> None: + # GIVEN an organization with 3 schema + schemas: list[JSONSchema] = [] + async for item in organization_with_schema.get_json_schemas_async(): + schemas.append(item) + assert len(schemas) == 3 + # WHEN deleting one of those schemas + schema = schemas[0] + await schema.delete_async() + # THEN there should be only two left + schemas2: list[JSONSchema] = [] + async for item in organization_with_schema.get_json_schemas_async(): + schemas2.append(item) + assert len(schemas2) == 2 + + async def test_delete_version(self, json_schema: JSONSchema) -> None: + # GIVEN an organization and a JSONSchema + await json_schema.store_async(schema_body={}, version="0.0.1") + # THEN that schema should have one version + js_versions: list[JSONSchemaVersionInfo] = [] + async for item in json_schema.get_versions_async(): + js_versions.append(item) + assert len(js_versions) == 1 + # WHEN storing a second version + await json_schema.store_async(schema_body={}, version="0.0.2") + # THEN that schema should have two versions + js_versions = [] + async for item in json_schema.get_versions_async(): + js_versions.append(item) + assert len(js_versions) == 2 + # AND they should be the ones stored + versions = [js_version.semantic_version for js_version in js_versions] + assert versions == ["0.0.1", "0.0.2"] + # WHEN deleting the first schema version + await json_schema.delete_async(version="0.0.1") + # THEN there should only be one version left + js_versions = [] + async for item in json_schema.get_versions_async(): + js_versions.append(item) + assert len(js_versions) == 1 + # AND it should be the second version + versions = [js_version.semantic_version for js_version in js_versions] + assert versions == ["0.0.2"] + async def test_get_versions(self, json_schema: JSONSchema) -> None: # GIVEN an schema that hasn't been created # THEN get_versions should return an empty list diff --git a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py index 9f124ebfc..74e09c934 100644 --- a/tests/integration/synapseclient/models/synchronous/test_schema_organization.py +++ b/tests/integration/synapseclient/models/synchronous/test_schema_organization.py @@ -224,6 +224,40 @@ async def test_store_and_get(self, json_schema: JSONSchema) -> None: assert js2.created_by assert js2.created_on + async def test_delete(self, organization_with_schema: SchemaOrganization) -> None: + # GIVEN an organization with 3 schema + schemas = list(organization_with_schema.get_json_schemas()) + assert len(schemas) == 3 + # WHEN deleting one of those schemas + schema = schemas[0] + schema.delete() + # THEN there should be only two left + schemas = list(organization_with_schema.get_json_schemas()) + assert len(schemas) == 2 + + async def test_delete_version(self, json_schema: JSONSchema) -> None: + # GIVEN an organization and a JSONSchema + json_schema.store(schema_body={}, version="0.0.1") + # THEN that schema should have one version + js_versions = list(json_schema.get_versions()) + assert len(js_versions) == 1 + # WHEN storing a second version + json_schema.store(schema_body={}, version="0.0.2") + # THEN that schema should have two versions + js_versions = list(json_schema.get_versions()) + assert len(js_versions) == 2 + # AND they should be the ones stored + versions = [js_version.semantic_version for js_version in js_versions] + assert versions == ["0.0.1", "0.0.2"] + # WHEN deleting the first schema version + json_schema.delete(version="0.0.1") + # THEN there should only be one version left + js_versions = list(json_schema.get_versions()) + assert len(js_versions) == 1 + # AND it should be the second version + versions = [js_version.semantic_version for js_version in js_versions] + assert versions == ["0.0.2"] + async def test_get_versions(self, json_schema: JSONSchema) -> None: # GIVEN an schema that hasn't been created # THEN get_versions should return an empty list From 6e96fbafd3c5d367fe62e32201c231e55a16896e Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 21 Oct 2025 09:31:41 -0700 Subject: [PATCH 39/41] fix docstrings --- synapseclient/models/schema_organization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 99d50a805..28494dfa3 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -215,7 +215,7 @@ def update_acl( syn.login() org = SchemaOrganization("my.org.name") - org.update_acl_async( + org.update_acl( principal_id=1, access_type=["READ"] ) @@ -491,7 +491,7 @@ async def update_acl_async( Updates the ACL for a principal for this organization Arguments: - principal_id: the id of the principal whose permissions are to be updates + principal_id: the id of the principal whose permissions are to be updated access_type: List of permission types (e.g., ["READ", "CREATE", "DELETE"]) see: [ACCESS_TYPE]https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/ACCESS_TYPE.html From 4e311bbacbeefb32ba2af3f4192351980c97866a Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 21 Oct 2025 10:25:58 -0700 Subject: [PATCH 40/41] added additonal checks for org and schema names --- synapseclient/models/schema_organization.py | 28 +++- .../unit_test_schema_organization.py | 155 +++++++----------- 2 files changed, 82 insertions(+), 101 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index 28494dfa3..f673bb5fd 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -1385,11 +1385,14 @@ def list_json_schema_organizations( return all_orgs -def _check_name(name) -> None: +def _check_name(name: str) -> None: """ Checks that the input name is a valid Synapse Organization or JSONSchema name - - Must start with a letter - - Must contains only letters, numbers and periods. + - Length requirement of 6 ≤ x ≤ 250 + - Names do not contain the string sagebionetworks (case insensitive) + - May contain periods (each part is separated by periods) + - Each part must start with a letter + - Each part contains only letters and numbers Arguments: name: The name of the organization to be checked @@ -1397,10 +1400,17 @@ def _check_name(name) -> None: Raises: ValueError: When the name isn't valid """ - if not re.match("^([A-Za-z])([A-Za-z]|\d|\.)*$", name): - raise ValueError( - ( - "Name must start with a letter and contain " - f"only letters numbers and periods: {name}" + if not 6 <= len(name) <= 250: + raise ValueError(f"The name must be of length 6 to 250 characters: {name}") + if re.search("sagebionetworks", name.lower()): + raise ValueError(f"The name must not contain 'sagebionetworks' : {name}") + parts = name.split(".") + for part in parts: + if not re.match("^([A-Za-z])([A-Za-z]|\d|)*$", part): + raise ValueError( + ( + "Name may be separated by periods, " + "but each part must start with a letter and contain " + f"only letters and numbers: {name}" + ) ) - ) diff --git a/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py b/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py index 312e88e6d..745ac5aac 100644 --- a/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py +++ b/tests/unit/synapseclient/models/synchronous/unit_test_schema_organization.py @@ -1,37 +1,13 @@ """Unit tests for SchemaOrganization and JSONSchema classes""" -from typing import Any - import pytest from synapseclient.models import JSONSchema, SchemaOrganization -from synapseclient.models.schema_organization import CreateSchemaRequest +from synapseclient.models.schema_organization import CreateSchemaRequest, _check_name class TestSchemaOrganization: """Synchronous unit tests for SchemaOrganization.""" - @pytest.mark.parametrize( - "name", - ["AAAAAAA", "A12345", "A....."], - ids=["Just letters", "Numbers", "Periods"], - ) - def test_init(self, name: str) -> None: - "Tests that legal names don't raise a ValueError on init" - assert SchemaOrganization(name) - - @pytest.mark.parametrize( - "name", - ["1AAAAAA", ".AAAAAA", "AAAAAA!"], - ids=["Starts with a number", "Starts with a period", "Special character"], - ) - def test_init_name_exceptions(self, name: str) -> None: - "Tests that illegal names raise a ValueError on init" - with pytest.raises( - ValueError, - match="Name must start with a letter and contain only letters numbers and periods", - ): - SchemaOrganization(name) - def test_fill_from_dict(self) -> None: "Tests that fill_from_dict fills in all fields" organization = SchemaOrganization() @@ -56,28 +32,6 @@ def test_fill_from_dict(self) -> None: class TestJSONSchema: """Synchronous unit tests for JSONSchema.""" - @pytest.mark.parametrize( - "name", - ["AAAAAAA", "A12345", "A....."], - ids=["Just letters", "Numbers", "Periods"], - ) - def test_init(self, name: str) -> None: - "Tests that legal names don't raise a ValueError on init" - assert JSONSchema(name, "org.name") - - @pytest.mark.parametrize( - "name", - ["1AAAAAA", ".AAAAAA", "AAAAAA!"], - ids=["Starts with a number", "Starts with a period", "Special character"], - ) - def test_init_name_exceptions(self, name: str) -> None: - "Tests that illegal names raise a ValueError on init" - with pytest.raises( - ValueError, - match="Name must start with a letter and contain only letters numbers and periods", - ): - JSONSchema(name, "org.name") - @pytest.mark.parametrize( "uri", ["ORG.NAME-SCHEMA.NAME", "ORG.NAME-SCHEMA.NAME-0.0.1"], @@ -147,74 +101,91 @@ def test_check_semantic_version_with_exceptions(self) -> None: class TestCreateSchemaRequest: @pytest.mark.parametrize( - "name", - ["AAAAAAA", "A12345", "A....."], - ids=["Just letters", "Numbers", "Periods"], + "version", + ["0.0.1", "1.0.0"], ) - def test_init_name(self, name: str) -> None: - "Tests that legal names don't raise a ValueError on init" - assert CreateSchemaRequest(schema={}, name=name, organization_name="org.name") + def test_init_version(self, version: str) -> None: + "Tests that legal versions don't raise a ValueError on init" + assert CreateSchemaRequest( + schema={}, name="schema.name", organization_name="org.name", version=version + ) @pytest.mark.parametrize( - "name", - ["1AAAAAA", ".AAAAAA", "AAAAAA!"], - ids=["Starts with a number", "Starts with a period", "Special character"], + "version", + ["1", "1.0", "0.0.0.1", "0.0.0"], ) - def test_init_name_exceptions(self, name: str) -> None: - "Tests that illegal names raise a ValueError on init" + def test_init_version_exceptions(self, version: str) -> None: + "Tests that illegal versions raise a ValueError on init" with pytest.raises( ValueError, - match="Name must start with a letter and contain only letters numbers and periods", + match="Schema version must be a semantic version starting at 0.0.1", ): - CreateSchemaRequest(schema={}, name=name, organization_name="org.name") + CreateSchemaRequest( + schema={}, + name="schema.name", + organization_name="org.name", + version=version, + ) + + +class TestCheckName: + """Tests for check name helper function""" @pytest.mark.parametrize( "name", - ["AAAAAAA", "A12345", "A....."], - ids=["Just letters", "Numbers", "Periods"], + ["aaaaaaa", "aaaaaa1", "aa.aa.aa", "a1.a1.a1"], ) - def test_init_org_name(self, name: str) -> None: - "Tests that legal org names don't raise a ValueError on init" - assert CreateSchemaRequest( - schema={}, name="schema.name", organization_name=name - ) + def test_check_name(self, name: str): + """Checks that legal names don't raise an exception""" + _check_name(name) @pytest.mark.parametrize( "name", - ["1AAAAAA", ".AAAAAA", "AAAAAA!"], - ids=["Starts with a number", "Starts with a period", "Special character"], + [ + "a", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ], ) - def test_init_org_name_exceptions(self, name: str) -> None: - "Tests that illegal org names raise a ValueError on init" + def test_check_length_exception(self, name: str): + """Checks that names that are too short or long raise an exception""" with pytest.raises( - ValueError, - match="Name must start with a letter and contain only letters numbers and periods", + ValueError, match="The name must be of length 6 to 250 characters" ): - CreateSchemaRequest(schema={}, name="schema.name", organization_name=name) + _check_name(name) @pytest.mark.parametrize( - "version", - ["0.0.1", "1.0.0"], + "name", + [ + "sagebionetworks", + "asagebionetworks", + "sagebionetworksa", + "aaa.sagebionetworks.aaa", + "SAGEBIONETWORKS", + "SageBionetworks", + ], ) - def test_init_version(self, version: str) -> None: - "Tests that legal versions don't raise a ValueError on init" - assert CreateSchemaRequest( - schema={}, name="schema.name", organization_name="org.name", version=version - ) + def test_check_sage_exception(self, name: str): + """Checks that names that contain 'sagebionetworks' raise an exception""" + with pytest.raises( + ValueError, match="The name must not contain 'sagebionetworks'" + ): + _check_name(name) @pytest.mark.parametrize( - "version", - ["1", "1.0", "0.0.0.1", "0.0.0"], + "name", + ["1AAAAA", "AAA.1AAA", "AAA.AAA.1AAA", ".AAAAAAA", "AAAAAAAA!"], + ids=[ + "Starts with number", + "Part2 starts with number", + "Part3 starts with number", + "Starts with period", + "Contains special characters", + ], ) - def test_init_version_exceptions(self, version: str) -> None: - "Tests that illegal versions raise a ValueError on init" + def test_check_content_exception(self, name: str): + """Checks that names that contain special characters(besides periods) or have parts that start with numbers raise an exception""" with pytest.raises( ValueError, - match="Schema version must be a semantic version starting at 0.0.1", + match="Name may be separated by periods, but each part must start with a letter and contain only letters and numbers", ): - CreateSchemaRequest( - schema={}, - name="schema.name", - organization_name="org.name", - version=version, - ) + _check_name(name) From 8f0b30cccd63e1a4d92927c6390cf9a160231beb Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Wed, 22 Oct 2025 08:08:27 -0700 Subject: [PATCH 41/41] fix docstring --- synapseclient/models/schema_organization.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/synapseclient/models/schema_organization.py b/synapseclient/models/schema_organization.py index f673bb5fd..71c0ab304 100644 --- a/synapseclient/models/schema_organization.py +++ b/synapseclient/models/schema_organization.py @@ -625,15 +625,27 @@ def store(   ```python - from synapseclient.models import SchemaOrganization + from synapseclient.models import JSONSchema from synapseclient import Synapse syn = Synapse() syn.login() - org = SchemaOrganization("my.org.name") - org.store() - print(org) + schema = JSONSchema(organization_name="my.org", name="test.schema") + schema_body = { + { + "properties": { + "Component": { + "description": "TBD", + "not": { + "type": "null" + }, + "title": "Component" + } + } + } + } + schema.store(schema_body = schema_body) ``` """ return self