synapseclient
Advanced tools
| """This module is responsible for exposing the services defined at: | ||
| <https://rest-docs.synapse.org#org.sagebionetworks.repo.web.controller.CurationTaskController> | ||
| """ | ||
| import json | ||
| from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, Optional, Union | ||
| from synapseclient.api.api_client import rest_post_paginated_async | ||
| if TYPE_CHECKING: | ||
| from synapseclient import Synapse | ||
| async def create_curation_task( | ||
| curation_task: Dict[str, Union[str, int, Dict[str, Any]]], | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Union[str, int]]: | ||
| """ | ||
| Create a CurationTask associated with a project. | ||
| https://rest-docs.synapse.org/rest/POST/curation/task.html | ||
| Arguments: | ||
| curation_task: The complete CurationTask object to create. | ||
| 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 created CurationTask. | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| return await client.rest_post_async( | ||
| uri="/curation/task", body=json.dumps(curation_task) | ||
| ) | ||
| async def get_curation_task( | ||
| task_id: int, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Union[str, int]]: | ||
| """ | ||
| Get a CurationTask by its ID. | ||
| https://rest-docs.synapse.org/rest/GET/curation/task/taskId.html | ||
| Arguments: | ||
| task_id: The unique identifier of the task. | ||
| 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 CurationTask. | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| return await client.rest_get_async(uri=f"/curation/task/{task_id}") | ||
| async def update_curation_task( | ||
| task_id: int, | ||
| curation_task: Dict[str, Union[str, int, Dict[str, Any]]], | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Union[str, int]]: | ||
| """ | ||
| Update a CurationTask. | ||
| https://rest-docs.synapse.org/rest/PUT/curation/task/taskId.html | ||
| Arguments: | ||
| task_id: The unique identifier of the task. | ||
| curation_task: The complete CurationTask object to update. | ||
| 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 updated CurationTask. | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| return await client.rest_put_async( | ||
| uri=f"/curation/task/{task_id}", body=json.dumps(curation_task) | ||
| ) | ||
| async def delete_curation_task( | ||
| task_id: int, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> None: | ||
| """ | ||
| Delete a CurationTask. | ||
| https://rest-docs.synapse.org/rest/DELETE/curation/task/taskId.html | ||
| Arguments: | ||
| task_id: The unique identifier of the task. | ||
| 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: | ||
| None | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| await client.rest_delete_async(uri=f"/curation/task/{task_id}") | ||
| async def list_curation_tasks( | ||
| project_id: str, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> AsyncGenerator[Dict[str, Any], None]: | ||
| """ | ||
| Generator to get a list of CurationTasks for a project. | ||
| https://rest-docs.synapse.org/rest/POST/curation/task/list.html | ||
| Arguments: | ||
| project_id: The synId of the project. | ||
| 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. | ||
| Yields: | ||
| Individual CurationTask objects from each page of the response. | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| request_body = {"projectId": project_id} | ||
| async for item in rest_post_paginated_async( | ||
| "/curation/task/list", body=request_body, synapse_client=client | ||
| ): | ||
| yield item | ||
| async def list_grid_sessions( | ||
| source_id: Optional[str] = None, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> AsyncGenerator[Dict[str, Any], None]: | ||
| """ | ||
| Generator to get a list of active grid sessions for the user. | ||
| https://rest-docs.synapse.org/rest/POST/grid/session/list.html | ||
| Arguments: | ||
| source_id: Optional. When provided, only sessions with this synId 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. | ||
| Yields: | ||
| Individual GridSession objects from each page of the response. | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| request_body = {} | ||
| if source_id is not None: | ||
| request_body["sourceId"] = source_id | ||
| async for item in rest_post_paginated_async( | ||
| "/grid/session/list", body=request_body, synapse_client=client | ||
| ): | ||
| yield item | ||
| async def delete_grid_session( | ||
| session_id: str, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> None: | ||
| """ | ||
| Delete a grid session. | ||
| https://rest-docs.synapse.org/rest/DELETE/grid/session/sessionId.html | ||
| Note: Only the user that created a grid session may delete it. | ||
| Arguments: | ||
| session_id: The unique identifier of the grid session to delete. | ||
| 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: | ||
| None | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| await client.rest_delete_async(uri=f"/grid/session/{session_id}") |
Sorry, the diff of this file is too big to display
| """Link dataclass model for Synapse entities.""" | ||
| import dataclasses | ||
| from copy import deepcopy | ||
| from dataclasses import dataclass, field, replace | ||
| from datetime import date, datetime | ||
| from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol, Union | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import get_from_entity_factory | ||
| from synapseclient.core.async_utils import async_to_sync, otel_trace_method | ||
| from synapseclient.core.constants.concrete_types import LINK_ENTITY | ||
| from synapseclient.core.utils import delete_none_keys, merge_dataclass_entities | ||
| from synapseclient.models import Activity, Annotations | ||
| from synapseclient.models.services.search import get_id | ||
| from synapseclient.models.services.storable_entity import store_entity | ||
| from synapseclient.models.services.storable_entity_components import ( | ||
| store_entity_components, | ||
| ) | ||
| if TYPE_CHECKING: | ||
| from synapseclient.models import ( | ||
| Dataset, | ||
| DatasetCollection, | ||
| EntityView, | ||
| File, | ||
| Folder, | ||
| MaterializedView, | ||
| Project, | ||
| SubmissionView, | ||
| Table, | ||
| VirtualTable, | ||
| ) | ||
| from synapseclient.operations.factory_operations import FileOptions | ||
| class LinkSynchronousProtocol(Protocol): | ||
| """Protocol defining the synchronous interface for Link operations.""" | ||
| def get( | ||
| self, | ||
| parent: Optional[Union["Folder", "Project"]] = None, | ||
| follow_link: bool = True, | ||
| file_options: Optional["FileOptions"] = None, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> Union[ | ||
| "Dataset", | ||
| "DatasetCollection", | ||
| "EntityView", | ||
| "File", | ||
| "Folder", | ||
| "MaterializedView", | ||
| "Project", | ||
| "SubmissionView", | ||
| "Table", | ||
| "VirtualTable", | ||
| "Link", | ||
| ]: | ||
| """Get the link metadata from Synapse. You are able to find a link by | ||
| either the id or the name and parent_id. | ||
| Arguments: | ||
| parent: The parent folder or project this link exists under. | ||
| follow_link: If True then the entity this link points to will be fetched | ||
| and returned instead of the Link entity itself. | ||
| file_options: Options that modify file retrieval. Only used if `follow_link` | ||
| is True and the link points to a File entity. | ||
| 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 link object. | ||
| Raises: | ||
| ValueError: If the link does not have an id or a | ||
| (name and (`parent_id` or parent with an id)) set. | ||
| Example: Using this function | ||
| Retrieve a link and follow it to get the target entity: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Link | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Get the target entity that the link points to | ||
| target_entity = Link(id="syn123").get() | ||
| ``` | ||
| Retrieve only the link metadata without following the link: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Link | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Get just the link entity itself | ||
| link_entity = Link(id="syn123").get(follow_link=False) | ||
| ``` | ||
| When the link points to a File, you can specify file download options: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Link, FileOptions | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Follow link to file with custom download options | ||
| file_entity = Link(id="syn123").get( | ||
| file_options=FileOptions( | ||
| download_file=True, | ||
| download_location="/path/to/downloads/", | ||
| if_collision="overwrite.local" | ||
| ) | ||
| ) | ||
| ``` | ||
| """ | ||
| return self | ||
| def store( | ||
| self, | ||
| parent: Optional[Union["Folder", "Project"]] = None, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> "Link": | ||
| """Store the link in Synapse. | ||
| Arguments: | ||
| parent: The parent folder or project to store the link in. May also be | ||
| specified in the Link object. If both are provided the parent passed | ||
| into `store` will take precedence. | ||
| 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 link object. | ||
| Raises: | ||
| ValueError: If the link does not have a name and parent_id, or target_id. | ||
| Example: Using this function | ||
| Link with the name `my_link` referencing entity `syn123` and parent folder `syn456`: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Link | ||
| syn = Synapse() | ||
| syn.login() | ||
| link_instance = Link( | ||
| name="my_link", | ||
| parent_id="syn456", | ||
| target_id="syn123" | ||
| ).store() | ||
| ``` | ||
| """ | ||
| return self | ||
| @dataclass() | ||
| @async_to_sync | ||
| class Link(LinkSynchronousProtocol): | ||
| """A Link entity within Synapse that references another entity. | ||
| Represents a [Synapse Link](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/Link.html). | ||
| Attributes: | ||
| name: The name of this entity. Must be 256 characters or less. Names may only | ||
| contain: letters, numbers, spaces, underscores, hyphens, periods, plus signs, | ||
| apostrophes, and parentheses | ||
| description: The description of this entity. Must be 1000 characters or less. | ||
| id: The unique immutable ID for this entity. A new ID will be generated for new | ||
| Entities. Once issued, this ID is guaranteed to never change or be re-issued | ||
| etag: Synapse employs an Optimistic Concurrency Control (OCC) scheme to handle | ||
| concurrent updates. Since the E-Tag changes every time an entity is updated | ||
| it is used to detect when a client's current representation of an entity is | ||
| out-of-date. | ||
| created_on: (Read Only) The date this entity was created. | ||
| modified_on: (Read Only) The date this entity was last modified. | ||
| created_by: (Read Only) The ID of the user that created this entity. | ||
| modified_by: (Read Only) The ID of the user that last modified this entity. | ||
| parent_id: The ID of the Entity that is the parent of this Entity. | ||
| concrete_type: Indicates which implementation of Entity this object represents. | ||
| The value is the fully qualified class name, e.g. org.sagebionetworks.repo.model.FileEntity. | ||
| target_id: The ID of the entity to which this link refers | ||
| target_version_number: The version number of the entity to which this link refers | ||
| links_to_class_name: The synapse Entity's class name that this link points to. | ||
| activity: The Activity model represents the main record of Provenance in | ||
| Synapse. It is analogous to the Activity defined in the | ||
| W3C Specification on Provenance. Activity cannot be removed during a store | ||
| operation by setting it to None. You must use Activity.delete_async or | ||
| Activity.disassociate_from_entity_async. | ||
| annotations: Additional metadata associated with the link. The key is the name | ||
| of your desired annotations. The value is an object containing a list of | ||
| values (use empty list to represent no values for key) and the value type | ||
| associated with all values in the list. To remove all annotations set this | ||
| to an empty dict {} or None and store the entity. | ||
| """ | ||
| name: Optional[str] = None | ||
| """The name of this entity. Must be 256 characters or less. Names may only contain: | ||
| letters, numbers, spaces, underscores, hyphens, periods, plus signs, apostrophes, | ||
| and parentheses""" | ||
| description: Optional[str] = None | ||
| """The description of this entity. Must be 1000 characters or less.""" | ||
| id: Optional[str] = None | ||
| """The unique immutable ID for this entity. A new ID will be generated for new | ||
| Entities. Once issued, this ID is guaranteed to never change or be re-issued""" | ||
| etag: Optional[str] = None | ||
| """Synapse employs an Optimistic Concurrency Control (OCC) scheme to handle | ||
| concurrent updates. Since the E-Tag changes every time an entity is updated | ||
| it is used to detect when a client's current representation of an entity is | ||
| out-of-date.""" | ||
| created_on: Optional[str] = None | ||
| """(Read Only) The date this entity was created.""" | ||
| modified_on: Optional[str] = None | ||
| """(Read Only) The date this entity was last modified.""" | ||
| created_by: Optional[str] = None | ||
| """(Read Only) The ID of the user that created this entity.""" | ||
| modified_by: Optional[str] = None | ||
| """(Read Only) The ID of the user that last modified this entity.""" | ||
| parent_id: Optional[str] = None | ||
| """The ID of the Entity that is the parent of this Entity.""" | ||
| target_id: Optional[str] = None | ||
| """The ID of the entity to which this link refers""" | ||
| target_version_number: Optional[int] = None | ||
| """The version number of the entity to which this link refers""" | ||
| links_to_class_name: Optional[str] = None | ||
| """The synapse Entity's class name that this link points to.""" | ||
| activity: Optional[Activity] = field(default=None, compare=False) | ||
| """The Activity model represents the main record of Provenance in Synapse. It is | ||
| analogous to the Activity defined in the W3C Specification on Provenance. Activity | ||
| cannot be removed during a store operation by setting it to None. You must use | ||
| Activity.delete_async or Activity.disassociate_from_entity_async.""" | ||
| annotations: Optional[ | ||
| Dict[ | ||
| str, | ||
| Union[ | ||
| List[str], | ||
| List[bool], | ||
| List[float], | ||
| List[int], | ||
| List[date], | ||
| List[datetime], | ||
| ], | ||
| ] | ||
| ] = field(default_factory=dict, compare=False) | ||
| """Additional metadata associated with the link. The key is the name of your | ||
| desired annotations. The value is an object containing a list of values | ||
| (use empty list to represent no values for key) and the value type associated with | ||
| all values in the list. To remove all annotations set this to an empty dict {} or | ||
| None and store the entity.""" | ||
| _last_persistent_instance: Optional["Link"] = field( | ||
| default=None, repr=False, compare=False | ||
| ) | ||
| """The last persistent instance of this object. This is used to determine if the | ||
| object has been changed and needs to be updated in Synapse.""" | ||
| @property | ||
| def has_changed(self) -> bool: | ||
| """Determines if the object has been changed and needs to be updated in Synapse.""" | ||
| return ( | ||
| not self._last_persistent_instance or self._last_persistent_instance != self | ||
| ) | ||
| def _set_last_persistent_instance(self) -> None: | ||
| """Stash the last time this object interacted with Synapse. This is used to | ||
| determine if the object has been changed and needs to be updated in Synapse.""" | ||
| del self._last_persistent_instance | ||
| self._last_persistent_instance = replace(self) | ||
| self._last_persistent_instance.activity = ( | ||
| dataclasses.replace(self.activity) if self.activity else None | ||
| ) | ||
| self._last_persistent_instance.annotations = ( | ||
| deepcopy(self.annotations) if self.annotations else {} | ||
| ) | ||
| def fill_from_dict( | ||
| self, synapse_entity: Dict[str, Any], set_annotations: bool = True | ||
| ) -> "Link": | ||
| """ | ||
| Converts a response from the REST API into this dataclass. | ||
| Arguments: | ||
| synapse_entity: The response from the REST API. | ||
| set_annotations: Whether to set the annotations from the response. | ||
| Returns: | ||
| The Link object. | ||
| """ | ||
| self.name = synapse_entity.get("name", None) | ||
| self.description = synapse_entity.get("description", None) | ||
| self.id = synapse_entity.get("id", None) | ||
| self.etag = synapse_entity.get("etag", None) | ||
| self.created_on = synapse_entity.get("createdOn", None) | ||
| self.modified_on = synapse_entity.get("modifiedOn", None) | ||
| self.created_by = synapse_entity.get("createdBy", None) | ||
| self.modified_by = synapse_entity.get("modifiedBy", None) | ||
| self.parent_id = synapse_entity.get("parentId", None) | ||
| # Handle nested Reference object | ||
| links_to_data = synapse_entity.get("linksTo", None) | ||
| if links_to_data: | ||
| self.target_id = links_to_data.get("targetId", None) | ||
| self.target_version_number = links_to_data.get("targetVersionNumber", None) | ||
| else: | ||
| self.target_id = None | ||
| self.target_version_number = None | ||
| self.links_to_class_name = synapse_entity.get("linksToClassName", None) | ||
| if set_annotations: | ||
| self.annotations = Annotations.from_dict( | ||
| synapse_entity.get("annotations", {}) | ||
| ) | ||
| return self | ||
| def to_synapse_request(self) -> Dict[str, Any]: | ||
| """ | ||
| Converts this dataclass to a dictionary suitable for a Synapse REST API request. | ||
| Returns: | ||
| A dictionary representation of this object for API requests. | ||
| """ | ||
| request_dict = { | ||
| "name": self.name, | ||
| "description": self.description, | ||
| "id": self.id, | ||
| "etag": self.etag, | ||
| "createdOn": self.created_on, | ||
| "modifiedOn": self.modified_on, | ||
| "createdBy": self.created_by, | ||
| "modifiedBy": self.modified_by, | ||
| "parentId": self.parent_id, | ||
| "concreteType": LINK_ENTITY, | ||
| "linksTo": { | ||
| "targetId": self.target_id, | ||
| "targetVersionNumber": self.target_version_number, | ||
| } | ||
| if self.target_id | ||
| else None, | ||
| "linksToClassName": self.links_to_class_name, | ||
| } | ||
| if request_dict["linksTo"]: | ||
| delete_none_keys(request_dict["linksTo"]) | ||
| delete_none_keys(request_dict) | ||
| return request_dict | ||
| @otel_trace_method( | ||
| method_to_trace_name=lambda self, **kwargs: f"Link_Get: {self.id}" | ||
| ) | ||
| async def get_async( | ||
| self, | ||
| parent: Optional[Union["Folder", "Project"]] = None, | ||
| follow_link: bool = True, | ||
| file_options: Optional["FileOptions"] = None, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> Union[ | ||
| "Dataset", | ||
| "DatasetCollection", | ||
| "EntityView", | ||
| "File", | ||
| "Folder", | ||
| "MaterializedView", | ||
| "Project", | ||
| "SubmissionView", | ||
| "Table", | ||
| "VirtualTable", | ||
| "Link", | ||
| ]: | ||
| """Get the link metadata from Synapse. You are able to find a link by | ||
| either the id or the name and parent_id. | ||
| Arguments: | ||
| parent: The parent folder or project this link exists under. | ||
| follow_link: If True then the entity this link points to will be fetched | ||
| and returned instead of the Link entity itself. | ||
| file_options: Options that modify file retrieval. Only used if `follow_link` | ||
| is True and the link points to a File entity. | ||
| 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 link object. | ||
| Raises: | ||
| ValueError: If the link does not have an id or a | ||
| (name and (`parent_id` or parent with an id)) set. | ||
| Example: Using this function | ||
| Retrieve a link and follow it to get the target entity: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Link | ||
| async def get_link_target(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Get the target entity that the link points to | ||
| target_entity = await Link(id="syn123").get_async() | ||
| return target_entity | ||
| # Run the async function | ||
| target_entity = asyncio.run(get_link_target()) | ||
| ``` | ||
| Retrieve only the link metadata without following the link: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Link | ||
| async def get_link_metadata(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Get just the link entity itself | ||
| link_entity = await Link(id="syn123").get_async(follow_link=False) | ||
| return link_entity | ||
| # Run the async function | ||
| link_entity = asyncio.run(get_link_metadata()) | ||
| ``` | ||
| When the link points to a File, you can specify file download options: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Link, FileOptions | ||
| async def get_link_with_file_options(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Follow link to file with custom download options | ||
| file_entity = await Link(id="syn123").get_async( | ||
| file_options=FileOptions( | ||
| download_file=True, | ||
| download_location="/path/to/downloads/", | ||
| if_collision="overwrite.local" | ||
| ) | ||
| ) | ||
| return file_entity | ||
| # Run the async function | ||
| file_entity = asyncio.run(get_link_with_file_options()) | ||
| ``` | ||
| """ | ||
| parent_id = parent.id if parent else self.parent_id | ||
| if not (self.id or (self.name and parent_id)): | ||
| raise ValueError( | ||
| "The link must have an id or a " | ||
| "(name and (`parent_id` or parent with an id)) set." | ||
| ) | ||
| self.parent_id = parent_id | ||
| entity_id = await get_id(entity=self, synapse_client=synapse_client) | ||
| await get_from_entity_factory( | ||
| synapse_id_or_path=entity_id, | ||
| entity_to_update=self, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| self._set_last_persistent_instance() | ||
| if follow_link: | ||
| from synapseclient.operations.factory_operations import ( | ||
| get_async as factory_get_async, | ||
| ) | ||
| return await factory_get_async( | ||
| synapse_id=self.target_id, | ||
| version_number=self.target_version_number, | ||
| file_options=file_options, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| else: | ||
| return self | ||
| @otel_trace_method( | ||
| method_to_trace_name=lambda self, **kwargs: f"Link_Store: {self.name}" | ||
| ) | ||
| async def store_async( | ||
| self, | ||
| parent: Optional[Union["Folder", "Project"]] = None, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> "Link": | ||
| """Store the link in Synapse. | ||
| Arguments: | ||
| parent: The parent folder or project to store the link in. May also be | ||
| specified in the Link object. If both are provided the parent passed | ||
| into `store` will take precedence. | ||
| 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 link object. | ||
| Raises: | ||
| ValueError: If the link does not have a name and parent_id, or target_id. | ||
| Example: Using this function | ||
| Link with the name `my_link` referencing entity `syn123` and parent folder `syn456`: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Link | ||
| async def store_link(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| link_instance = await Link( | ||
| name="my_link", | ||
| parent_id="syn456", | ||
| target_id="syn123" | ||
| ).store_async() | ||
| return link_instance | ||
| # Run the async function | ||
| link_instance = asyncio.run(store_link()) | ||
| ``` | ||
| """ | ||
| if parent: | ||
| self.parent_id = parent.id | ||
| if not self.name and not self.id: | ||
| raise ValueError("The link must have a name.") | ||
| if not self.parent_id and not self.id: | ||
| raise ValueError("The link must have a parent_id.") | ||
| if not self.target_id and not self.id: | ||
| raise ValueError("The link must have a target_id.") | ||
| if existing_entity := await self._find_existing_entity( | ||
| synapse_client=synapse_client | ||
| ): | ||
| merge_dataclass_entities( | ||
| source=existing_entity, | ||
| destination=self, | ||
| ) | ||
| if self.has_changed: | ||
| entity = await store_entity( | ||
| resource=self, | ||
| entity=self.to_synapse_request(), | ||
| synapse_client=synapse_client, | ||
| ) | ||
| self.fill_from_dict(synapse_entity=entity, set_annotations=False) | ||
| re_read_required = await store_entity_components( | ||
| root_resource=self, synapse_client=synapse_client | ||
| ) | ||
| if re_read_required: | ||
| await self.get_async( | ||
| synapse_client=synapse_client, | ||
| ) | ||
| self._set_last_persistent_instance() | ||
| return self | ||
| async def _find_existing_entity( | ||
| self, *, synapse_client: Optional[Synapse] = None | ||
| ) -> Union["Link", None]: | ||
| """Determines if the entity already exists in Synapse. If it does it will return | ||
| the entity object, otherwise it will return None. This is used to determine if the | ||
| entity should be updated or created.""" | ||
| async def get_link(existing_id: str) -> "Link": | ||
| """Small wrapper to retrieve a link instance without raising an error if it | ||
| does not exist. | ||
| Arguments: | ||
| existing_id: The ID of the entity to retrieve. | ||
| Returns: | ||
| The entity object if it exists, otherwise None. | ||
| """ | ||
| link_copy = Link( | ||
| id=existing_id, | ||
| parent_id=self.parent_id, | ||
| ) | ||
| return await link_copy.get_async( | ||
| synapse_client=synapse_client, | ||
| follow_link=False, | ||
| ) | ||
| if ( | ||
| not self._last_persistent_instance | ||
| and ( | ||
| existing_entity_id := await get_id( | ||
| entity=self, | ||
| failure_strategy=None, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| ) | ||
| and (existing_entity := await get_link(existing_entity_id)) | ||
| ): | ||
| return existing_entity | ||
| return None |
| """Script to work with Synapse files.""" | ||
| import asyncio | ||
| import dataclasses | ||
| import os | ||
| from copy import deepcopy | ||
| from dataclasses import dataclass, field | ||
| from datetime import date, datetime | ||
| from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol, Union | ||
| from opentelemetry import trace | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import get_from_entity_factory | ||
| from synapseclient.core import utils | ||
| from synapseclient.core.async_utils import async_to_sync | ||
| from synapseclient.core.constants import concrete_types | ||
| from synapseclient.core.exceptions import SynapseFileNotFoundError | ||
| from synapseclient.core.utils import ( | ||
| delete_none_keys, | ||
| guess_file_name, | ||
| merge_dataclass_entities, | ||
| ) | ||
| from synapseclient.models import Activity, Annotations | ||
| from synapseclient.models.mixins import AccessControllable, BaseJSONSchema | ||
| from synapseclient.models.services.search import get_id | ||
| from synapseclient.models.services.storable_entity import store_entity | ||
| from synapseclient.models.services.storable_entity_components import ( | ||
| store_entity_components, | ||
| ) | ||
| if TYPE_CHECKING: | ||
| from synapseclient.models import CsvTableDescriptor, FileHandle, Folder, Project | ||
| @dataclass() | ||
| class ValidationSummary: | ||
| """Summary statistics for the JSON schema validation results for the children of | ||
| an Entity container (Project or Folder). | ||
| Attributes: | ||
| container_id: The ID of the container Entity. | ||
| total_number_of_children: The total number of children in the container. | ||
| number_of_valid_children: The total number of children that are valid according | ||
| to their bound JSON schema. | ||
| number_of_invalid_children: The total number of children that are invalid | ||
| according to their bound JSON schema. | ||
| number_of_unknown_children: The total number of children that do not have | ||
| validation results. This can occur when a child does not have a bound JSON | ||
| schema or when a child has not been validated yet. | ||
| generated_on: The date-time when the statistics were calculated. | ||
| """ | ||
| container_id: Optional[str] = None | ||
| """The ID of the container Entity.""" | ||
| total_number_of_children: Optional[int] = None | ||
| """The total number of children in the container.""" | ||
| number_of_valid_children: Optional[int] = None | ||
| """The total number of children that are valid according to their bound JSON schema.""" | ||
| number_of_invalid_children: Optional[int] = None | ||
| """The total number of children that are invalid according to their bound JSON schema.""" | ||
| number_of_unknown_children: Optional[int] = None | ||
| """The total number of children that do not have validation results. This can occur | ||
| when a child does not have a bound JSON schema or when a child has not been | ||
| validated yet. | ||
| """ | ||
| generated_on: Optional[str] = None | ||
| """The date-time when the statistics were calculated.""" | ||
| class RecordSetSynchronousProtocol(Protocol): | ||
| """ | ||
| The protocol for methods that are asynchronous but also | ||
| have a synchronous counterpart that may also be called. | ||
| """ | ||
| def store( | ||
| self, | ||
| parent: Optional[Union["Folder", "Project"]] = None, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> "RecordSet": | ||
| """ | ||
| Store the RecordSet in Synapse. | ||
| This method uploads or updates a RecordSet in Synapse. It can handle both | ||
| creating new RecordSets and updating existing ones based on the | ||
| `create_or_update` flag. The method supports file uploads, metadata updates, | ||
| and merging with existing entities when appropriate. | ||
| Arguments: | ||
| parent: The parent Folder or Project for this RecordSet. If provided, | ||
| this will override the `parent_id` attribute. | ||
| 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 RecordSet object with updated metadata from Synapse after the | ||
| store operation. | ||
| Raises: | ||
| ValueError: If the RecordSet does not have the required information | ||
| for storing. Must have either: (ID with path or data_file_handle_id), | ||
| or (path with parent_id), or (data_file_handle_id with parent_id). | ||
| Example: Storing a new RecordSet | ||
| Creating and storing a new RecordSet in Synapse: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import RecordSet | ||
| syn = Synapse() | ||
| syn.login() | ||
| record_set = RecordSet( | ||
| name="My RecordSet", | ||
| description="A dataset for analysis", | ||
| parent_id="syn123456", | ||
| path="/path/to/data.csv" | ||
| ) | ||
| stored_record_set = record_set.store() | ||
| print(f"Stored RecordSet with ID: {stored_record_set.id}") | ||
| ``` | ||
| Updating an existing RecordSet: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import RecordSet | ||
| syn = Synapse() | ||
| syn.login() | ||
| record_set = RecordSet(id="syn789012").get() | ||
| record_set.description = "Updated description" | ||
| updated_record_set = record_set.store() | ||
| ``` | ||
| """ | ||
| return self | ||
| def get( | ||
| self, | ||
| include_activity: bool = False, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> "RecordSet": | ||
| """ | ||
| Get the RecordSet from Synapse. | ||
| This method retrieves a RecordSet entity from Synapse. You may retrieve | ||
| a RecordSet by either its ID or path. If you specify both, the ID will | ||
| take precedence. | ||
| If you specify the path and the RecordSet is stored in multiple locations | ||
| in Synapse, only the first one found will be returned. The other matching | ||
| RecordSets will be printed to the console. | ||
| You may also specify a `version_number` to get a specific version of the | ||
| RecordSet. | ||
| Arguments: | ||
| include_activity: If True, the activity will be included in the RecordSet | ||
| if it exists. | ||
| 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 RecordSet object with data populated from Synapse. | ||
| Raises: | ||
| ValueError: If the RecordSet does not have an ID or path to retrieve. | ||
| Example: Retrieving a RecordSet by ID | ||
| Get an existing RecordSet from Synapse: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import RecordSet | ||
| syn = Synapse() | ||
| syn.login() | ||
| record_set = RecordSet(id="syn123").get() | ||
| print(f"RecordSet name: {record_set.name}") | ||
| ``` | ||
| Downloading a RecordSet to a specific directory: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import RecordSet | ||
| syn = Synapse() | ||
| syn.login() | ||
| record_set = RecordSet( | ||
| id="syn123", | ||
| path="/path/to/download/directory" | ||
| ).get() | ||
| ``` | ||
| Including activity information: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import RecordSet | ||
| syn = Synapse() | ||
| syn.login() | ||
| record_set = RecordSet(id="syn123").get(include_activity=True) | ||
| if record_set.activity: | ||
| print(f"Activity: {record_set.activity.name}") | ||
| ``` | ||
| """ | ||
| return self | ||
| def delete( | ||
| self, | ||
| version_only: Optional[bool] = False, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> None: | ||
| """ | ||
| Delete the RecordSet from Synapse using its ID. | ||
| This method removes a RecordSet entity from Synapse. You can choose to | ||
| delete either a specific version or the entire RecordSet including all | ||
| its versions. | ||
| Arguments: | ||
| version_only: If True, only the version specified in the `version_number` | ||
| attribute of the RecordSet will be deleted. If False, the entire | ||
| RecordSet including all versions 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. | ||
| Returns: | ||
| None | ||
| Raises: | ||
| ValueError: If the RecordSet does not have an ID to delete. | ||
| ValueError: If the RecordSet does not have a version number to delete a | ||
| specific version, and `version_only` is True. | ||
| Example: Deleting a RecordSet | ||
| Delete an entire RecordSet and all its versions: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import RecordSet | ||
| syn = Synapse() | ||
| syn.login() | ||
| RecordSet(id="syn123").delete() | ||
| ``` | ||
| Delete only a specific version: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import RecordSet | ||
| syn = Synapse() | ||
| syn.login() | ||
| record_set = RecordSet(id="syn123", version_number=2) | ||
| record_set.delete(version_only=True) | ||
| ``` | ||
| """ | ||
| return None | ||
| @dataclass() | ||
| @async_to_sync | ||
| class RecordSet(RecordSetSynchronousProtocol, AccessControllable, BaseJSONSchema): | ||
| """A RecordSet within Synapse. | ||
| Attributes: | ||
| id: The unique immutable ID for this file. A new ID will be generated for new | ||
| Files. Once issued, this ID is guaranteed to never change or be re-issued. | ||
| name: The name of this entity. Must be 256 characters or less. Names may only | ||
| contain: letters, numbers, spaces, underscores, hyphens, periods, plus | ||
| signs, apostrophes, and parentheses. If not specified, the name will be | ||
| derived from the file name. | ||
| path: The path to the file on disk. Using shorthand `~` will be expanded to | ||
| the user's home directory. | ||
| This is used during a `get` operation to specify where to download the | ||
| file to. It should be pointing to a directory. | ||
| This is also used during a `store` operation to specify the file to | ||
| upload. It should be pointing to a file. | ||
| description: The description of this file. Must be 1000 characters or less. | ||
| parent_id: The ID of the Entity that is the parent of this Entity. Setting | ||
| this to a new value and storing it will move this File under the new | ||
| parent. | ||
| version_label: The version label for this entity. Updates to the entity will | ||
| increment the version number. | ||
| version_comment: The version comment for this entity. | ||
| data_file_handle_id: ID of the file handle associated with this entity. You | ||
| may define an existing data_file_handle_id to use the existing | ||
| data_file_handle_id. The creator of the file must also be the owner of | ||
| the data_file_handle_id to have permission to store the file. | ||
| activity: The Activity model represents the main record of Provenance in | ||
| Synapse. It is analygous to the Activity defined in the | ||
| [W3C Specification](https://www.w3.org/TR/prov-n/) on Provenance. | ||
| Activity cannot be removed during a store operation by setting it to None. | ||
| You must use: [synapseclient.models.Activity.delete_async][] or | ||
| [synapseclient.models.Activity.disassociate_from_entity_async][]. | ||
| annotations: Additional metadata associated with the entity. The key is the | ||
| name of your desired annotations. The value is an object containing a list | ||
| of values (use empty list to represent no values for key) and the value | ||
| type associated with all values in the list. To remove all annotations | ||
| set this to an empty dict `{}`. | ||
| upsert_keys: One or more column names that define this upsert key for this | ||
| set. This key is used to determine if a new record should be treated as | ||
| an update or an insert. | ||
| csv_descriptor: The description of a CSV for upload or download. | ||
| validation_summary: Summary statistics for the JSON schema validation results | ||
| for the children of an Entity container (Project or Folder). | ||
| file_name_override: An optional replacement for the name of the uploaded | ||
| file. This is distinct from the entity name. If omitted the file will | ||
| retain its original name. | ||
| content_type: (New Upload Only) Used to manually specify Content-type header, | ||
| for example 'application/png' or 'application/json; charset=UTF-8'. If not | ||
| specified, the content type will be derived from the file extension. | ||
| This can be specified only during the initial store of this file. In order | ||
| to change this after the File has been created use | ||
| [synapseclient.models.File.change_metadata][]. | ||
| content_size: (New Upload Only) The size of the file in bytes. This can be | ||
| specified only during the initial creation of the File. This is also only | ||
| applicable to files not uploaded to Synapse. ie: `synapse_store` is False. | ||
| content_md5: (Store only) The MD5 of the file is known. If not supplied this | ||
| will be computed in the client is possible. If supplied for a file entity | ||
| already stored in Synapse it will be calculated again to check if a new | ||
| upload needs to occur. This will not be filled in during a read for data. | ||
| It is only used during a store operation. To retrieve the md5 of the file | ||
| after read from synapse use the `.file_handle.content_md5` attribute. | ||
| create_or_update: (Store only) Indicates whether the method should | ||
| automatically perform an update if the file conflicts with an existing | ||
| Synapse object. | ||
| force_version: (Store only) Indicates whether the method should increment the | ||
| version of the object if something within the entity has changed. For | ||
| example updating the description or name. You may set this to False and | ||
| an update to the entity will not increment the version. | ||
| Updating the `version_label` attribute will also cause a version update | ||
| regardless of this flag. | ||
| An update to the MD5 of the file will force a version update regardless | ||
| of this flag. | ||
| is_restricted: (Store only) If set to true, an email will be sent to the | ||
| Synapse access control team to start the process of adding terms-of-use | ||
| or review board approval for this entity. You will be contacted with | ||
| regards to the specific data being restricted and the requirements of | ||
| access. | ||
| This may be used only by an administrator of the specified file. | ||
| merge_existing_annotations: (Store only) Works in conjunction with | ||
| `create_or_update` in that this is only evaluated if `create_or_update` | ||
| is True. If this entity exists in Synapse that has annotations that are | ||
| not present in a store operation, these annotations will be added to the | ||
| entity. If this is False any annotations that are not present within a | ||
| store operation will be removed from this entity. This allows one to | ||
| complete a destructive update of annotations on an entity. | ||
| associate_activity_to_new_version: (Store only) Works in conjunction with | ||
| `create_or_update` in that this is only evaluated if `create_or_update` | ||
| is True. When true an activity already attached to the current version of | ||
| this entity will be associated the new version during a store operation | ||
| if the version was updated. This is useful if you are updating the entity | ||
| and want to ensure that the activity is persisted onto the new version | ||
| the entity. | ||
| When this is False the activity will not be associated to the new version | ||
| of the entity during a store operation. | ||
| Regardless of this setting, if you have an Activity object on the entity | ||
| it will be persisted onto the new version. This is only used when you | ||
| don't have an Activity object on the entity. | ||
| synapse_store: (Store only) Whether the File should be uploaded or if false: | ||
| only the path should be stored when [synapseclient.models.File.store][] | ||
| is called. | ||
| download_file: (Get only) If True the file will be downloaded. | ||
| if_collision: (Get only) Determines how to handle file collisions. Defaults | ||
| to "keep.both". May be: | ||
| - `overwrite.local` | ||
| - `keep.local` | ||
| - `keep.both` | ||
| synapse_container_limit: (Get only) A Synanpse ID used to limit the search in | ||
| Synapse if file is specified as a local file. That is, if the file is | ||
| stored in multiple locations in Synapse only the ones in the specified | ||
| folder/project will be returned. | ||
| etag: (Read Only) Synapse employs an Optimistic Concurrency Control (OCC) | ||
| scheme to handle concurrent updates. Since the E-Tag changes every time | ||
| an entity is updated it is used to detect when a client's current | ||
| representation of an entity is out-of-date. | ||
| created_on: (Read Only) The date this entity was created. | ||
| modified_on: (Read Only) The date this entity was last modified. | ||
| created_by: (Read Only) The ID of the user that created this entity. | ||
| modified_by: (Read Only) The ID of the user that last modified this entity. | ||
| version_number: (Read Only) The version number issued to this version on the | ||
| object. | ||
| is_latest_version: (Read Only) If this is the latest version of the object. | ||
| file_handle: (Read Only) The file handle associated with this entity. | ||
| """ | ||
| id: Optional[str] = None | ||
| """The unique immutable ID for this file. A new ID will be generated for new Files. | ||
| Once issued, this ID is guaranteed to never change or be re-issued.""" | ||
| name: Optional[str] = None | ||
| """ | ||
| The name of this entity. Must be 256 characters or less. | ||
| Names may only contain: letters, numbers, spaces, underscores, hyphens, periods, | ||
| plus signs, apostrophes, and parentheses. If not specified, the name will be | ||
| derived from the file name. | ||
| """ | ||
| path: Optional[str] = field(default=None, compare=False) | ||
| """The path to the file on disk. Using shorthand `~` will be expanded to the user's | ||
| home directory. | ||
| This is used during a `get` operation to specify where to download the file to. It | ||
| should be pointing to a directory. | ||
| This is also used during a `store` operation to specify the file to upload. It | ||
| should be pointing to a file.""" | ||
| description: Optional[str] = None | ||
| """The description of this file. Must be 1000 characters or less.""" | ||
| parent_id: Optional[str] = None | ||
| """The ID of the Entity that is the parent of this Entity. Setting this to a new | ||
| value and storing it will move this File under the new parent.""" | ||
| version_label: Optional[str] = None | ||
| """The version label for this entity. Updates to the entity will increment the | ||
| version number.""" | ||
| version_comment: Optional[str] = None | ||
| """The version comment for this entity.""" | ||
| data_file_handle_id: Optional[str] = None | ||
| """ | ||
| ID of the file handle associated with this entity. You may define an existing | ||
| data_file_handle_id to use the existing data_file_handle_id. The creator of the | ||
| file must also be the owner of the data_file_handle_id to have permission to | ||
| store the file. | ||
| """ | ||
| activity: Optional[Activity] = field(default=None, compare=False) | ||
| """The Activity model represents the main record of Provenance in Synapse. It is | ||
| analygous to the Activity defined in the | ||
| [W3C Specification](https://www.w3.org/TR/prov-n/) on Provenance. Activity cannot | ||
| be removed during a store operation by setting it to None. You must use: | ||
| [synapseclient.models.Activity.delete_async][] or | ||
| [synapseclient.models.Activity.disassociate_from_entity_async][]. | ||
| """ | ||
| annotations: Optional[ | ||
| Dict[ | ||
| str, | ||
| Union[ | ||
| List[str], | ||
| List[bool], | ||
| List[float], | ||
| List[int], | ||
| List[date], | ||
| List[datetime], | ||
| ], | ||
| ] | ||
| ] = field(default_factory=dict, compare=False) | ||
| """Additional metadata associated with the folder. The key is the name of your | ||
| desired annotations. The value is an object containing a list of values | ||
| (use empty list to represent no values for key) and the value type associated with | ||
| all values in the list. To remove all annotations set this to an empty dict `{}`.""" | ||
| upsert_keys: Optional[List[str]] = field(default_factory=list) | ||
| """One or more column names that define this upsert key for this set. This key is | ||
| used to determine if a new record should be treated as an update or an insert. | ||
| """ | ||
| csv_descriptor: Optional["CsvTableDescriptor"] = field(default=None) | ||
| """The description of a CSV for upload or download.""" | ||
| validation_summary: Optional[ValidationSummary] = field(default=None, compare=False) | ||
| """Summary statistics for the JSON schema validation results for the children of | ||
| an Entity container (Project or Folder)""" | ||
| file_name_override: Optional[str] = None | ||
| """An optional replacement for the name of the uploaded file. This is distinct from | ||
| the entity name. If omitted the file will retain its original name. | ||
| """ | ||
| content_type: Optional[str] = None | ||
| """ | ||
| (New Upload Only) | ||
| Used to manually specify Content-type header, for example 'application/png' | ||
| or 'application/json; charset=UTF-8'. If not specified, the content type will be | ||
| derived from the file extension. | ||
| This can be specified only during the initial store of this file. In order to change | ||
| this after the File has been created use | ||
| [synapseclient.models.File.change_metadata][]. | ||
| """ | ||
| content_size: Optional[int] = None | ||
| """ | ||
| (New Upload Only) | ||
| The size of the file in bytes. This can be specified only during the initial | ||
| creation of the File. This is also only applicable to files not uploaded to Synapse. | ||
| ie: `synapse_store` is False. | ||
| """ | ||
| content_md5: Optional[str] = field(default=None, compare=False) | ||
| """ | ||
| (Store only) | ||
| The MD5 of the file is known. If not supplied this will be computed in the client | ||
| is possible. If supplied for a file entity already stored in Synapse it will be | ||
| calculated again to check if a new upload needs to occur. This will not be filled | ||
| in during a read for data. It is only used during a store operation. To retrieve | ||
| the md5 of the file after read from synapse use the `.file_handle.content_md5` | ||
| attribute. | ||
| """ | ||
| create_or_update: bool = field(default=True, repr=False, compare=False) | ||
| """ | ||
| (Store only) | ||
| Indicates whether the method should automatically perform an update if the file | ||
| conflicts with an existing Synapse object. | ||
| """ | ||
| force_version: bool = field(default=True, repr=False, compare=False) | ||
| """ | ||
| (Store only) | ||
| Indicates whether the method should increment the version of the object if something | ||
| within the entity has changed. For example updating the description or name. | ||
| You may set this to False and an update to the entity will not increment the | ||
| version. | ||
| Updating the `version_label` attribute will also cause a version update regardless | ||
| of this flag. | ||
| An update to the MD5 of the file will force a version update regardless of this | ||
| flag. | ||
| """ | ||
| is_restricted: bool = field(default=False, repr=False) | ||
| """ | ||
| (Store only) | ||
| If set to true, an email will be sent to the Synapse access control team to start | ||
| the process of adding terms-of-use or review board approval for this entity. | ||
| You will be contacted with regards to the specific data being restricted and the | ||
| requirements of access. | ||
| This may be used only by an administrator of the specified file. | ||
| """ | ||
| merge_existing_annotations: bool = field(default=True, repr=False, compare=False) | ||
| """ | ||
| (Store only) | ||
| Works in conjunction with `create_or_update` in that this is only evaluated if | ||
| `create_or_update` is True. If this entity exists in Synapse that has annotations | ||
| that are not present in a store operation, these annotations will be added to the | ||
| entity. If this is False any annotations that are not present within a store | ||
| operation will be removed from this entity. This allows one to complete a | ||
| destructive update of annotations on an entity. | ||
| """ | ||
| associate_activity_to_new_version: bool = field( | ||
| default=False, repr=False, compare=False | ||
| ) | ||
| """ | ||
| (Store only) | ||
| Works in conjunction with `create_or_update` in that this is only evaluated if | ||
| `create_or_update` is True. When true an activity already attached to the current | ||
| version of this entity will be associated the new version during a store operation | ||
| if the version was updated. This is useful if you are updating the entity and want | ||
| to ensure that the activity is persisted onto the new version the entity. | ||
| When this is False the activity will not be associated to the new version of the | ||
| entity during a store operation. | ||
| Regardless of this setting, if you have an Activity object on the entity it will be | ||
| persisted onto the new version. This is only used when you don't have an Activity | ||
| object on the entity. | ||
| """ | ||
| synapse_store: bool = field(default=True, repr=False) | ||
| """ | ||
| (Store only) | ||
| Whether the File should be uploaded or if false: only the path should be stored when | ||
| [synapseclient.models.File.store][] is called. | ||
| """ | ||
| download_file: bool = field(default=True, repr=False, compare=False) | ||
| """ | ||
| (Get only) | ||
| If True the file will be downloaded.""" | ||
| if_collision: str = field(default="keep.both", repr=False, compare=False) | ||
| """ | ||
| (Get only) | ||
| Determines how to handle file collisions. Defaults to "keep.both". | ||
| May be | ||
| - `overwrite.local` | ||
| - `keep.local` | ||
| - `keep.both` | ||
| """ | ||
| synapse_container_limit: Optional[str] = field( | ||
| default=None, repr=False, compare=False | ||
| ) | ||
| """A Synanpse ID used to limit the search in Synapse if file is specified as a local | ||
| file. That is, if the file is stored in multiple locations in Synapse only the | ||
| ones in the specified folder/project will be returned.""" | ||
| etag: Optional[str] = field(default=None, compare=False) | ||
| """ | ||
| (Read Only) | ||
| Synapse employs an Optimistic Concurrency Control (OCC) scheme to handle | ||
| concurrent updates. Since the E-Tag changes every time an entity is updated it is | ||
| used to detect when a client's current representation of an entity is out-of-date. | ||
| """ | ||
| created_on: Optional[str] = field(default=None, compare=False) | ||
| """(Read Only) The date this entity was created.""" | ||
| modified_on: Optional[str] = field(default=None, compare=False) | ||
| """(Read Only) The date this entity was last modified.""" | ||
| created_by: Optional[str] = field(default=None, compare=False) | ||
| """(Read Only) The ID of the user that created this entity.""" | ||
| modified_by: Optional[str] = field(default=None, compare=False) | ||
| """(Read Only) The ID of the user that last modified this entity.""" | ||
| version_number: Optional[int] = field(default=None, compare=False) | ||
| """(Read Only) The version number issued to this version on the object.""" | ||
| is_latest_version: Optional[bool] = field(default=None, compare=False) | ||
| """(Read Only) If this is the latest version of the object.""" | ||
| file_handle: Optional["FileHandle"] = field(default=None, compare=False) | ||
| """(Read Only) The file handle associated with this entity.""" | ||
| _last_persistent_instance: Optional["RecordSet"] = field( | ||
| default=None, repr=False, compare=False | ||
| ) | ||
| """The last persistent instance of this object. This is used to determine if the | ||
| object has been changed and needs to be updated in Synapse.""" | ||
| @property | ||
| def has_changed(self) -> bool: | ||
| """ | ||
| Determines if the object has been changed and needs to be updated in Synapse.""" | ||
| return ( | ||
| not self._last_persistent_instance or self._last_persistent_instance != self | ||
| ) | ||
| def _set_last_persistent_instance(self) -> None: | ||
| """Stash the last time this object interacted with Synapse. This is used to | ||
| determine if the object has been changed and needs to be updated in Synapse.""" | ||
| del self._last_persistent_instance | ||
| self._last_persistent_instance = dataclasses.replace(self) | ||
| self._last_persistent_instance.activity = ( | ||
| dataclasses.replace(self.activity) if self.activity else None | ||
| ) | ||
| self._last_persistent_instance.annotations = ( | ||
| deepcopy(self.annotations) if self.annotations else {} | ||
| ) | ||
| def _fill_from_file_handle(self) -> None: | ||
| """Fill the file object from the file handle.""" | ||
| if self.file_handle: | ||
| self.data_file_handle_id = self.file_handle.id | ||
| self.content_type = self.file_handle.content_type | ||
| self.content_size = self.file_handle.content_size | ||
| def fill_from_dict( | ||
| self, | ||
| entity: Dict[str, Union[bool, str, int]], | ||
| set_annotations: bool = True, | ||
| ) -> "RecordSet": | ||
| """ | ||
| Converts a response from the REST API into this dataclass. | ||
| This method populates the RecordSet instance with data from a Synapse REST API | ||
| response or a dictionary containing RecordSet information. It handles the | ||
| conversion from Synapse API field names (camelCase) to Python attribute names | ||
| (snake_case) and processes nested objects appropriately. | ||
| Arguments: | ||
| synapse_file: The response from the REST API or a dictionary containing | ||
| RecordSet data. Can be either a Synapse_File object or a dictionary | ||
| with string keys and various value types. | ||
| set_annotations: Whether to set the annotations from the response. | ||
| If True, annotations will be populated from the API response. | ||
| Returns: | ||
| The RecordSet object with updated attributes from the API response. | ||
| """ | ||
| self.id = entity.get("id", None) | ||
| self.name = entity.get("name", None) | ||
| self.description = entity.get("description", None) | ||
| self.etag = entity.get("etag", None) | ||
| self.created_on = entity.get("createdOn", None) | ||
| self.modified_on = entity.get("modifiedOn", None) | ||
| self.created_by = entity.get("createdBy", None) | ||
| self.modified_by = entity.get("modifiedBy", None) | ||
| self.parent_id = entity.get("parentId", None) | ||
| self.version_number = entity.get("versionNumber", None) | ||
| self.version_label = entity.get("versionLabel", None) | ||
| self.version_comment = entity.get("versionComment", None) | ||
| self.is_latest_version = entity.get("isLatestVersion", False) | ||
| self.data_file_handle_id = entity.get("dataFileHandleId", None) | ||
| self.path = entity.get("path", self.path) | ||
| self.file_name_override = entity.get("fileNameOverride", None) | ||
| csv_descriptor = entity.get("csvDescriptor", None) | ||
| if csv_descriptor: | ||
| from synapseclient.models import CsvTableDescriptor | ||
| self.csv_descriptor = CsvTableDescriptor().fill_from_dict(csv_descriptor) | ||
| validation_summary = entity.get("validationSummary", None) | ||
| if validation_summary: | ||
| self.validation_summary = ValidationSummary( | ||
| container_id=validation_summary.get("containerId", None), | ||
| total_number_of_children=validation_summary.get( | ||
| "totalNumberOfChildren", None | ||
| ), | ||
| number_of_valid_children=validation_summary.get( | ||
| "numberOfValidChildren", None | ||
| ), | ||
| number_of_invalid_children=validation_summary.get( | ||
| "numberOfInvalidChildren", None | ||
| ), | ||
| number_of_unknown_children=validation_summary.get( | ||
| "numberOfUnknownChildren", None | ||
| ), | ||
| generated_on=validation_summary.get("generatedOn", None), | ||
| ) | ||
| self.upsert_keys = entity.get("upsertKey", []) | ||
| synapse_file_handle = entity.get("_file_handle", None) | ||
| if synapse_file_handle: | ||
| from synapseclient.models import FileHandle | ||
| file_handle = self.file_handle or FileHandle() | ||
| self.file_handle = file_handle.fill_from_dict( | ||
| synapse_instance=synapse_file_handle | ||
| ) | ||
| self._fill_from_file_handle() | ||
| if set_annotations: | ||
| self.annotations = Annotations.from_dict(entity.get("annotations", {})) | ||
| return self | ||
| def _cannot_store(self) -> bool: | ||
| """ | ||
| Determines based on guard conditions if a store operation can proceed. | ||
| """ | ||
| return ( | ||
| not ( | ||
| self.id is not None | ||
| and (self.path is not None or self.data_file_handle_id is not None) | ||
| ) | ||
| and not (self.path is not None and self.parent_id is not None) | ||
| and not ( | ||
| self.parent_id is not None and self.data_file_handle_id is not None | ||
| ) | ||
| ) | ||
| async def _load_local_md5(self) -> None: | ||
| """ | ||
| Load the MD5 hash of a local file if it exists and hasn't been loaded yet. | ||
| This method computes and sets the content_md5 attribute for local files | ||
| that exist on disk. It only performs the calculation if the content_md5 | ||
| is not already set and the path points to an existing file. | ||
| """ | ||
| if not self.content_md5 and self.path and os.path.isfile(self.path): | ||
| self.content_md5 = utils.md5_for_file_hex(filename=self.path) | ||
| async def _find_existing_entity( | ||
| self, *, synapse_client: Optional[Synapse] = None | ||
| ) -> Union["RecordSet", None]: | ||
| """ | ||
| Determines if the RecordSet already exists in Synapse. | ||
| This method searches for an existing RecordSet in Synapse that matches the | ||
| current instance. If found, it returns the existing RecordSet object, otherwise | ||
| it returns None. This is used to determine if the RecordSet should be updated | ||
| or created during a store operation. | ||
| Arguments: | ||
| synapse_client: If not passed in and caching was not disabled, this will | ||
| use the last created instance from the Synapse class constructor. | ||
| Returns: | ||
| The existing RecordSet object if it exists in Synapse, None otherwise. | ||
| """ | ||
| async def get_entity(existing_id: str) -> "RecordSet": | ||
| """Small wrapper to retrieve a file instance without raising an error if it | ||
| does not exist. | ||
| Arguments: | ||
| existing_id: The ID of the file to retrieve. | ||
| Returns: | ||
| The file object if it exists, otherwise None. | ||
| """ | ||
| try: | ||
| entity_copy = RecordSet( | ||
| id=existing_id, | ||
| download_file=False, | ||
| version_number=self.version_number, | ||
| synapse_container_limit=self.synapse_container_limit, | ||
| parent_id=self.parent_id, | ||
| ) | ||
| return await entity_copy.get_async( | ||
| synapse_client=synapse_client, | ||
| include_activity=self.activity is not None | ||
| or self.associate_activity_to_new_version, | ||
| ) | ||
| except SynapseFileNotFoundError: | ||
| return None | ||
| if ( | ||
| self.create_or_update | ||
| and not self._last_persistent_instance | ||
| and ( | ||
| existing_entity_id := await get_id( | ||
| entity=self, | ||
| failure_strategy=None, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| ) | ||
| and (existing_file := await get_entity(existing_entity_id)) | ||
| ): | ||
| return existing_file | ||
| return None | ||
| def _determine_fields_to_ignore_in_merge(self) -> List[str]: | ||
| """ | ||
| Determine which fields should not be merged when merging two entities. | ||
| This method returns a list of field names that should be ignored during | ||
| entity merging operations. This allows for fine-tuned destructive updates | ||
| of an entity based on the current configuration settings. | ||
| The method has special handling for manifest uploads where specific fields | ||
| are provided in the manifest and should take precedence over existing | ||
| entity values. | ||
| Returns: | ||
| A list of field names that should not be merged from the existing entity. | ||
| """ | ||
| fields_to_not_merge = [] | ||
| if not self.merge_existing_annotations: | ||
| fields_to_not_merge.append("annotations") | ||
| if not self.associate_activity_to_new_version: | ||
| fields_to_not_merge.append("activity") | ||
| return fields_to_not_merge | ||
| def to_synapse_request(self) -> Dict[str, Any]: | ||
| """ | ||
| Converts this dataclass to a dictionary suitable for a Synapse REST API request. | ||
| This method transforms the RecordSet object into a dictionary format that | ||
| matches the structure expected by the Synapse REST API. It handles the | ||
| conversion of Python snake_case attribute names to the camelCase format | ||
| used by the API, and ensures that nested objects are properly serialized. | ||
| Returns: | ||
| A dictionary representation of this object formatted for API requests. | ||
| None values are automatically removed from the dictionary. | ||
| Example: Converting a RecordSet for API submission | ||
| This method is used internally when storing or updating RecordSets: | ||
| ```python | ||
| from synapseclient.models import RecordSet | ||
| record_set = RecordSet( | ||
| name="My RecordSet", | ||
| description="A test record set", | ||
| parent_id="syn123456" | ||
| ) | ||
| api_dict = record_set.to_synapse_request() | ||
| # api_dict contains properly formatted data for the REST API | ||
| ``` | ||
| """ | ||
| entity = { | ||
| "concreteType": concrete_types.RECORD_SET_ENTITY, | ||
| "name": self.name, | ||
| "description": self.description, | ||
| "id": self.id, | ||
| "etag": self.etag, | ||
| "createdOn": self.created_on, | ||
| "modifiedOn": self.modified_on, | ||
| "createdBy": self.created_by, | ||
| "modifiedBy": self.modified_by, | ||
| "parentId": self.parent_id, | ||
| "versionNumber": self.version_number, | ||
| "versionLabel": self.version_label, | ||
| "versionComment": self.version_comment, | ||
| "isLatestVersion": self.is_latest_version, | ||
| "dataFileHandleId": self.data_file_handle_id, | ||
| "upsertKey": self.upsert_keys, | ||
| "csvDescriptor": self.csv_descriptor.to_synapse_request() | ||
| if self.csv_descriptor | ||
| else None, | ||
| "validationSummary": { | ||
| "containerId": self.validation_summary.container_id, | ||
| "totalNumberOfChildren": self.validation_summary.total_number_of_children, | ||
| "numberOfValidChildren": self.validation_summary.number_of_valid_children, | ||
| "numberOfInvalidChildren": self.validation_summary.number_of_invalid_children, | ||
| "numberOfUnknownChildren": self.validation_summary.number_of_unknown_children, | ||
| "generatedOn": self.validation_summary.generated_on, | ||
| } | ||
| if self.validation_summary | ||
| else None, | ||
| "fileNameOverride": self.file_name_override, | ||
| } | ||
| delete_none_keys(entity) | ||
| return entity | ||
| async def store_async( | ||
| self, | ||
| parent: Optional[Union["Folder", "Project"]] = None, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> "RecordSet": | ||
| """ | ||
| Store the RecordSet in Synapse. | ||
| This method uploads or updates a RecordSet in Synapse. It can handle both | ||
| creating new RecordSets and updating existing ones based on the | ||
| `create_or_update` flag. The method supports file uploads, metadata updates, | ||
| and merging with existing entities when appropriate. | ||
| Arguments: | ||
| parent: The parent Folder or Project for this RecordSet. If provided, | ||
| this will override the `parent_id` attribute. | ||
| 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 RecordSet object with updated metadata from Synapse after the | ||
| store operation. | ||
| Raises: | ||
| ValueError: If the RecordSet does not have the required information | ||
| for storing. Must have either: (ID with path or data_file_handle_id), | ||
| or (path with parent_id), or (data_file_handle_id with parent_id). | ||
| Example: Storing a new RecordSet | ||
| Creating and storing a new RecordSet in Synapse: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import RecordSet | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| record_set = RecordSet( | ||
| name="My RecordSet", | ||
| description="A dataset for analysis", | ||
| parent_id="syn123456", | ||
| path="/path/to/data.csv" | ||
| ) | ||
| stored_record_set = await record_set.store_async() | ||
| print(f"Stored RecordSet with ID: {stored_record_set.id}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| Updating an existing RecordSet: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import RecordSet | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| record_set = await RecordSet(id="syn789012").get_async() | ||
| record_set.description = "Updated description" | ||
| updated_record_set = await record_set.store_async() | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| self.parent_id = parent.id if parent else self.parent_id | ||
| if self._cannot_store(): | ||
| raise ValueError( | ||
| "The file must have an (ID with a (path or `data_file_handle_id`)), or a " | ||
| "(path with a (`parent_id` or parent with an id)), or a " | ||
| "(data_file_handle_id with a (`parent_id` or parent with an id)) to store." | ||
| ) | ||
| self.name = self.name or (guess_file_name(self.path) if self.path else None) | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| if existing_file := await self._find_existing_entity(synapse_client=client): | ||
| merge_dataclass_entities( | ||
| source=existing_file, | ||
| destination=self, | ||
| fields_to_ignore=self._determine_fields_to_ignore_in_merge(), | ||
| ) | ||
| if self.id: | ||
| trace.get_current_span().set_attributes( | ||
| { | ||
| "synapse.id": self.id, | ||
| } | ||
| ) | ||
| if self.path: | ||
| self.path = os.path.expanduser(self.path) | ||
| async with client._get_parallel_file_transfer_semaphore( | ||
| asyncio_event_loop=asyncio.get_running_loop() | ||
| ): | ||
| from synapseclient.models.file import _upload_file | ||
| await _upload_file(entity_to_upload=self, synapse_client=client) | ||
| elif self.data_file_handle_id: | ||
| self.path = client.cache.get(file_handle_id=self.data_file_handle_id) | ||
| if self.has_changed: | ||
| entity = await store_entity( | ||
| resource=self, entity=self.to_synapse_request(), synapse_client=client | ||
| ) | ||
| self.fill_from_dict(entity=entity, set_annotations=False) | ||
| re_read_required = await store_entity_components( | ||
| root_resource=self, synapse_client=client | ||
| ) | ||
| if re_read_required: | ||
| before_download_file = self.download_file | ||
| self.download_file = False | ||
| await self.get_async( | ||
| synapse_client=client, | ||
| ) | ||
| self.download_file = before_download_file | ||
| self._set_last_persistent_instance() | ||
| client.logger.debug(f"Stored File {self.name}, id: {self.id}: {self.path}") | ||
| # Clear the content_md5 so that it is recalculated if the file is updated | ||
| self.content_md5 = None | ||
| return self | ||
| async def get_async( | ||
| self, | ||
| include_activity: bool = False, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> "RecordSet": | ||
| """ | ||
| Get the RecordSet from Synapse. | ||
| This method retrieves a RecordSet entity from Synapse. You may retrieve | ||
| a RecordSet by either its ID or path. If you specify both, the ID will | ||
| take precedence. | ||
| If you specify the path and the RecordSet is stored in multiple locations | ||
| in Synapse, only the first one found will be returned. The other matching | ||
| RecordSets will be printed to the console. | ||
| You may also specify a `version_number` to get a specific version of the | ||
| RecordSet. | ||
| Arguments: | ||
| include_activity: If True, the activity will be included in the RecordSet | ||
| if it exists. | ||
| 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 RecordSet object with data populated from Synapse. | ||
| Raises: | ||
| ValueError: If the RecordSet does not have an ID or path to retrieve. | ||
| Example: Retrieving a RecordSet by ID | ||
| Get an existing RecordSet from Synapse: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import RecordSet | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| record_set = await RecordSet(id="syn123").get_async() | ||
| print(f"RecordSet name: {record_set.name}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| Downloading a RecordSet to a specific directory: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import RecordSet | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| record_set = await RecordSet( | ||
| id="syn123", | ||
| path="/path/to/download/directory" | ||
| ).get_async() | ||
| asyncio.run(main()) | ||
| ``` | ||
| Including activity information: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import RecordSet | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| record_set = await RecordSet(id="syn123").get_async(include_activity=True) | ||
| if record_set.activity: | ||
| print(f"Activity: {record_set.activity.name}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| if not self.id and not self.path: | ||
| raise ValueError("The file must have an ID or path to get.") | ||
| syn = Synapse.get_client(synapse_client=synapse_client) | ||
| await self._load_local_md5() | ||
| await get_from_entity_factory( | ||
| entity_to_update=self, | ||
| synapse_id_or_path=self.id or self.path, | ||
| version=self.version_number, | ||
| if_collision=self.if_collision, | ||
| limit_search=self.synapse_container_limit or self.parent_id, | ||
| download_file=self.download_file, | ||
| download_location=os.path.dirname(self.path) | ||
| if self.path and os.path.isfile(self.path) | ||
| else self.path, | ||
| md5=self.content_md5, | ||
| synapse_client=syn, | ||
| ) | ||
| if ( | ||
| self.data_file_handle_id | ||
| and (not self.path or (self.path and not os.path.isfile(self.path))) | ||
| and (cached_path := syn.cache.get(file_handle_id=self.data_file_handle_id)) | ||
| ): | ||
| self.path = cached_path | ||
| if include_activity: | ||
| self.activity = await Activity.from_parent_async( | ||
| parent=self, synapse_client=synapse_client | ||
| ) | ||
| self._set_last_persistent_instance() | ||
| Synapse.get_client(synapse_client=synapse_client).logger.debug( | ||
| f"Got file {self.name}, id: {self.id}, path: {self.path}" | ||
| ) | ||
| return self | ||
| async def delete_async( | ||
| self, | ||
| version_only: Optional[bool] = False, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> None: | ||
| """ | ||
| Delete the RecordSet from Synapse using its ID. | ||
| This method removes a RecordSet entity from Synapse. You can choose to | ||
| delete either a specific version or the entire RecordSet including all | ||
| its versions. | ||
| Arguments: | ||
| version_only: If True, only the version specified in the `version_number` | ||
| attribute of the RecordSet will be deleted. If False, the entire | ||
| RecordSet including all versions 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. | ||
| Returns: | ||
| None | ||
| Raises: | ||
| ValueError: If the RecordSet does not have an ID to delete. | ||
| ValueError: If the RecordSet does not have a version number to delete a | ||
| specific version, and `version_only` is True. | ||
| Example: Deleting a RecordSet | ||
| Delete an entire RecordSet and all its versions: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import RecordSet | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| await RecordSet(id="syn123").delete_async() | ||
| asyncio.run(main()) | ||
| ``` | ||
| Delete only a specific version: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import RecordSet | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| record_set = RecordSet(id="syn123", version_number=2) | ||
| await record_set.delete_async(version_only=True) | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| if not self.id: | ||
| raise ValueError("The file must have an ID to delete.") | ||
| if version_only and not self.version_number: | ||
| raise ValueError("The file must have a version number to delete a version.") | ||
| loop = asyncio.get_event_loop() | ||
| await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client(synapse_client=synapse_client).delete( | ||
| obj=self.id, | ||
| version=self.version_number if version_only else None, | ||
| ), | ||
| ) | ||
| Synapse.get_client(synapse_client=synapse_client).logger.debug( | ||
| f"Deleted file {self.id}" | ||
| ) |
| from synapseclient.operations.factory_operations import ( | ||
| ActivityOptions, | ||
| FileOptions, | ||
| LinkOptions, | ||
| TableOptions, | ||
| get, | ||
| get_async, | ||
| ) | ||
| __all__ = [ | ||
| "ActivityOptions", | ||
| "FileOptions", | ||
| "TableOptions", | ||
| "LinkOptions", | ||
| "get", | ||
| "get_async", | ||
| ] |
| """Factory method for retrieving entities by Synapse ID.""" | ||
| from dataclasses import dataclass | ||
| from typing import TYPE_CHECKING, Optional, Union | ||
| from synapseclient.core.async_utils import wrap_async_to_sync | ||
| from synapseclient.core.exceptions import SynapseNotFoundError | ||
| if TYPE_CHECKING: | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import ( | ||
| Dataset, | ||
| DatasetCollection, | ||
| EntityView, | ||
| File, | ||
| Folder, | ||
| Link, | ||
| MaterializedView, | ||
| Project, | ||
| SubmissionView, | ||
| Table, | ||
| VirtualTable, | ||
| ) | ||
| @dataclass | ||
| class FileOptions: | ||
| """ | ||
| Configuration options specific to File entities when using the factory methods. | ||
| This dataclass allows you to customize how File entities are handled during | ||
| retrieval, including download behavior, file location, and collision handling. | ||
| Attributes: | ||
| download_file: Whether to automatically download the file content when | ||
| retrieving the File entity. If True, the file will be downloaded to | ||
| the local filesystem. If False, only the metadata will be retrieved. | ||
| Default is True. | ||
| download_location: The local directory path where the file should be | ||
| downloaded. If None, the file will be downloaded to the default Synapse | ||
| cache location. If specified, | ||
| must be a valid directory path. Default is None. | ||
| if_collision: Strategy to use when a file with the same name already | ||
| exists at the download location. Valid options are: | ||
| - "keep.both": Keep both files by appending a number to the new file | ||
| - "overwrite.local": Overwrite the existing local file | ||
| - "keep.local": Keep the existing local file and skip download | ||
| Default is "keep.both". | ||
| Example: | ||
| Configure file download options: | ||
| ```python | ||
| from synapseclient.operations import FileOptions | ||
| # Download file to specific location with overwrite | ||
| file_options = FileOptions( | ||
| download_file=True, | ||
| download_location="/path/to/downloads/", | ||
| if_collision="overwrite.local" | ||
| ) | ||
| # Only retrieve metadata, don't download file content | ||
| metadata_only = FileOptions(download_file=False) | ||
| ``` | ||
| """ | ||
| download_file: bool = True | ||
| download_location: Optional[str] = None | ||
| if_collision: str = "keep.both" | ||
| @dataclass | ||
| class ActivityOptions: | ||
| """ | ||
| Configuration options for entities that support activity/provenance tracking. | ||
| This dataclass controls whether activity information (provenance data) should | ||
| be included when retrieving entities. Activity information tracks the computational | ||
| steps, data sources, and relationships that led to the creation of an entity. | ||
| Attributes: | ||
| include_activity: Whether to include activity/provenance information when | ||
| retrieving the entity. If True, the returned entity will have its | ||
| activity attribute populated with provenance data (if available). | ||
| If False, the activity attribute will be None. Including activity | ||
| may result in additional API calls and slower retrieval times. | ||
| Default is False. | ||
| Example: | ||
| Configure activity inclusion: | ||
| ```python | ||
| from synapseclient.operations import ActivityOptions | ||
| # Include activity information | ||
| with_activity = ActivityOptions(include_activity=True) | ||
| # Skip activity information (faster retrieval) | ||
| without_activity = ActivityOptions(include_activity=False) | ||
| ``` | ||
| Note: | ||
| Activity information is only available for entities that support provenance | ||
| tracking (File, Table, Dataset, etc...). For other entity | ||
| types, this option is ignored. | ||
| """ | ||
| include_activity: bool = False | ||
| @dataclass | ||
| class TableOptions: | ||
| """ | ||
| Configuration options for table-like entities when using the factory methods. | ||
| This dataclass controls how table-like entities (Table, Dataset, EntityView, | ||
| MaterializedView, SubmissionView, VirtualTable, and DatasetCollection) are | ||
| retrieved, particularly whether column schema information should be included. | ||
| Attributes: | ||
| include_columns: Whether to include column schema information when | ||
| retrieving table-like entities. If True, the returned entity will | ||
| have its columns attribute populated with Column objects containing | ||
| schema information (name, column_type, etc.). If False, the columns | ||
| attribute will be an empty dict. Including columns may result in | ||
| additional API calls but provides complete table structure information. | ||
| Default is True. | ||
| Example: | ||
| Configure table column inclusion: | ||
| ```python | ||
| from synapseclient.operations import TableOptions | ||
| # Include column schema information | ||
| with_columns = TableOptions(include_columns=True) | ||
| # Skip column information (faster retrieval) | ||
| without_columns = TableOptions(include_columns=False) | ||
| ``` | ||
| """ | ||
| include_columns: bool = True | ||
| @dataclass | ||
| class LinkOptions: | ||
| """ | ||
| Configuration options specific to Link entities when using the factory methods. | ||
| This dataclass controls how Link entities are handled during retrieval, | ||
| particularly whether the link should be followed to return the target entity | ||
| or if the Link entity itself should be returned. | ||
| Attributes: | ||
| follow_link: Whether to follow the link and return the target entity | ||
| instead of the Link entity itself. If True, the factory method will | ||
| return the entity that the Link points to (e.g., if a Link points | ||
| to a File, a File object will be returned). If False, the Link | ||
| entity itself will be returned, allowing you to inspect the link's | ||
| properties such as target_id, target_version, etc. Default is True. | ||
| Example: | ||
| Configure link following behavior: | ||
| ```python | ||
| from synapseclient.operations import LinkOptions | ||
| # Follow the link and return the target entity | ||
| follow_target = LinkOptions(follow_link=True) | ||
| # Return the Link entity itself | ||
| return_link = LinkOptions(follow_link=False) | ||
| ``` | ||
| Note: | ||
| - When follow_link=True, the returned entity type depends on what the | ||
| Link points to (could be File, Project, Folder, etc.) | ||
| - When follow_link=False, a Link entity is always returned | ||
| """ | ||
| follow_link: bool = True | ||
| async def _handle_entity_instance( | ||
| entity, | ||
| version_number: Optional[int] = None, | ||
| activity_options: Optional[ActivityOptions] = None, | ||
| file_options: Optional[FileOptions] = None, | ||
| table_options: Optional[TableOptions] = None, | ||
| link_options: Optional[LinkOptions] = None, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Union[ | ||
| "Dataset", | ||
| "DatasetCollection", | ||
| "EntityView", | ||
| "File", | ||
| "Folder", | ||
| "Link", | ||
| "MaterializedView", | ||
| "Project", | ||
| "SubmissionView", | ||
| "Table", | ||
| "VirtualTable", | ||
| ]: | ||
| """ | ||
| Handle the case where an entity instance is passed directly to get_async. | ||
| This private function encapsulates the logic for applying options and calling | ||
| get_async on an existing entity instance. | ||
| """ | ||
| from synapseclient.models import ( | ||
| Dataset, | ||
| DatasetCollection, | ||
| EntityView, | ||
| File, | ||
| Link, | ||
| MaterializedView, | ||
| SubmissionView, | ||
| Table, | ||
| VirtualTable, | ||
| ) | ||
| if version_number is not None and hasattr(entity, "version_number"): | ||
| entity.version_number = version_number | ||
| get_kwargs = {"synapse_client": synapse_client} | ||
| if activity_options and activity_options.include_activity: | ||
| get_kwargs["include_activity"] = True | ||
| table_like_entities = ( | ||
| Dataset, | ||
| DatasetCollection, | ||
| EntityView, | ||
| MaterializedView, | ||
| SubmissionView, | ||
| Table, | ||
| VirtualTable, | ||
| ) | ||
| if table_options and isinstance(entity, table_like_entities): | ||
| get_kwargs["include_columns"] = table_options.include_columns | ||
| if file_options and isinstance(entity, File): | ||
| if hasattr(file_options, "download_file"): | ||
| entity.download_file = file_options.download_file | ||
| if ( | ||
| hasattr(file_options, "download_location") | ||
| and file_options.download_location | ||
| ): | ||
| entity.path = file_options.download_location | ||
| if hasattr(file_options, "if_collision"): | ||
| entity.if_collision = file_options.if_collision | ||
| if link_options and isinstance(entity, Link): | ||
| if hasattr(link_options, "follow_link"): | ||
| get_kwargs["follow_link"] = link_options.follow_link | ||
| return await entity.get_async(**get_kwargs) | ||
| async def _handle_simple_entity( | ||
| entity_class, | ||
| synapse_id: str, | ||
| version_number: Optional[int] = None, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Union["Project", "Folder"]: | ||
| """ | ||
| Handle simple entities that only need basic setup (Project, Folder, DatasetCollection). | ||
| """ | ||
| entity = entity_class(id=synapse_id) | ||
| if version_number and hasattr(entity, "version_number"): | ||
| entity.version_number = version_number | ||
| return await entity.get_async(synapse_client=synapse_client) | ||
| async def _handle_table_like_entity( | ||
| entity_class, | ||
| synapse_id: str, | ||
| version_number: Optional[int] = None, | ||
| activity_options: Optional[ActivityOptions] = None, | ||
| table_options: Optional[TableOptions] = None, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Union[ | ||
| "Dataset", | ||
| "DatasetCollection", | ||
| "EntityView", | ||
| "MaterializedView", | ||
| "SubmissionView", | ||
| "Table", | ||
| "VirtualTable", | ||
| ]: | ||
| """ | ||
| Handle table-like entities (Table, Dataset, EntityView, MaterializedView, SubmissionView, VirtualTable). | ||
| """ | ||
| entity = entity_class(id=synapse_id) | ||
| if version_number: | ||
| entity.version_number = version_number | ||
| kwargs = {"synapse_client": synapse_client} | ||
| if table_options: | ||
| kwargs["include_columns"] = table_options.include_columns | ||
| if activity_options and activity_options.include_activity: | ||
| kwargs["include_activity"] = True | ||
| return await entity.get_async(**kwargs) | ||
| async def _handle_file_entity( | ||
| synapse_id: str, | ||
| version_number: Optional[int] = None, | ||
| activity_options: Optional[ActivityOptions] = None, | ||
| file_options: Optional[FileOptions] = None, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> "File": | ||
| """ | ||
| Handle File entities with file-specific options. | ||
| """ | ||
| from synapseclient.models import File | ||
| file_kwargs = {"id": synapse_id} | ||
| if version_number: | ||
| file_kwargs["version_number"] = version_number | ||
| if file_options: | ||
| file_kwargs["download_file"] = file_options.download_file | ||
| if file_options.download_location: | ||
| file_kwargs["path"] = file_options.download_location | ||
| file_kwargs["if_collision"] = file_options.if_collision | ||
| entity = File(**file_kwargs) | ||
| get_kwargs = {"synapse_client": synapse_client} | ||
| if activity_options and activity_options.include_activity: | ||
| get_kwargs["include_activity"] = True | ||
| return await entity.get_async(**get_kwargs) | ||
| async def _handle_link_entity( | ||
| synapse_id: str, | ||
| link_options: Optional[LinkOptions] = None, | ||
| file_options: Optional[FileOptions] = None, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Union[ | ||
| "Dataset", | ||
| "DatasetCollection", | ||
| "EntityView", | ||
| "File", | ||
| "Folder", | ||
| "Link", | ||
| "MaterializedView", | ||
| "Project", | ||
| "SubmissionView", | ||
| "Table", | ||
| "VirtualTable", | ||
| ]: | ||
| """ | ||
| Handle Link entities with link-specific options. | ||
| Note: Links don't support versioning, so version_number is not included. | ||
| """ | ||
| from synapseclient.models import Link | ||
| entity = Link(id=synapse_id) | ||
| kwargs = {"synapse_client": synapse_client} | ||
| if link_options: | ||
| kwargs["follow_link"] = link_options.follow_link | ||
| if file_options: | ||
| kwargs["file_options"] = file_options | ||
| return await entity.get_async(**kwargs) | ||
| def get( | ||
| synapse_id: Optional[str] = None, | ||
| *, | ||
| entity_name: Optional[str] = None, | ||
| parent_id: Optional[str] = None, | ||
| version_number: Optional[int] = None, | ||
| activity_options: Optional[ActivityOptions] = None, | ||
| file_options: Optional[FileOptions] = None, | ||
| table_options: Optional[TableOptions] = None, | ||
| link_options: Optional[LinkOptions] = None, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Union[ | ||
| "Dataset", | ||
| "DatasetCollection", | ||
| "EntityView", | ||
| "File", | ||
| "Folder", | ||
| "Link", | ||
| "MaterializedView", | ||
| "Project", | ||
| "SubmissionView", | ||
| "Table", | ||
| "VirtualTable", | ||
| ]: | ||
| """ | ||
| Factory method to retrieve any Synapse entity by its ID or by name and parent ID. | ||
| This method serves as a unified interface for retrieving any type of Synapse entity | ||
| without needing to know the specific entity type beforehand. It automatically | ||
| determines the entity type and returns the appropriate model instance. | ||
| You can retrieve entities in two ways: | ||
| 1. By providing a synapse_id directly | ||
| 2. By providing entity_name and optionally parent_id for lookup | ||
| Arguments: | ||
| synapse_id: The Synapse ID of the entity to retrieve (e.g., 'syn123456'). | ||
| Mutually exclusive with entity_name. | ||
| entity_name: The name of the entity to find. Must be used with this approach | ||
| instead of synapse_id. When looking up projects, parent_id should be None. | ||
| parent_id: The parent entity ID when looking up by name. Set to None when | ||
| looking up projects by name. Only used with entity_name. | ||
| version_number: The specific version number of the entity to retrieve. Only | ||
| applies to versionable entities (File, Table, Dataset). If not specified, | ||
| the most recent version will be retrieved. Ignored for other entity types. | ||
| activity_options: Activity-specific configuration options. Can be applied to | ||
| any entity type to include activity information. | ||
| file_options: File-specific configuration options. Only applies to File entities. | ||
| Ignored for other entity types. | ||
| table_options: Table-specific configuration options. Only applies to Table-like | ||
| entities (Table, Dataset, EntityView, MaterializedView, SubmissionView, | ||
| VirtualTable, DatasetCollection). Ignored for other entity types. | ||
| link_options: Link-specific configuration options. Only applies when the entity | ||
| is a Link. Ignored for other entity types. | ||
| 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 appropriate Synapse entity model instance based on the entity type. | ||
| Raises: | ||
| ValueError: If both synapse_id and entity_name are provided, or if neither is provided. | ||
| ValueError: If entity_name is provided without this being a valid lookup scenario. | ||
| ValueError: If the synapse_id is not a valid Synapse ID format. | ||
| Note: | ||
| When using entity_name lookup: | ||
| - For projects: leave parent_id=None | ||
| - For all other entities: provide the parent_id of the containing folder/project | ||
| Example: Retrieving entities by ID | ||
| Get any entity by Synapse ID: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import File, Project | ||
| from synapseclient.operations import get | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Works for any entity type | ||
| entity = get(synapse_id="syn123456") | ||
| # The returned object will be the appropriate type | ||
| if isinstance(entity, File): | ||
| print(f"File: {entity.name}") | ||
| elif isinstance(entity, Project): | ||
| print(f"Project: {entity.name}") | ||
| ``` | ||
| Example: Retrieving entities by name | ||
| Get an entity by name and parent: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Get a file by name within a folder | ||
| entity = get( | ||
| entity_name="my_file.txt", | ||
| parent_id="syn123456" | ||
| ) | ||
| # Get a project by name (parent_id=None) | ||
| project = get( | ||
| entity_name="My Project", | ||
| parent_id=None | ||
| ) | ||
| ``` | ||
| Example: Retrieving a specific version | ||
| Get a specific version of a versionable entity: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get | ||
| syn = Synapse() | ||
| syn.login() | ||
| entity = get(synapse_id="syn123456", version_number=2) | ||
| ``` | ||
| Example: Retrieving a file with custom options | ||
| Get file metadata with specific download options: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get, FileOptions, ActivityOptions | ||
| syn = Synapse() | ||
| syn.login() | ||
| file_entity = get( | ||
| synapse_id="syn123456", | ||
| activity_options=ActivityOptions(include_activity=True), | ||
| file_options=FileOptions( | ||
| download_file=False | ||
| ) | ||
| ) | ||
| ``` | ||
| Example: Retrieving a table with activity and columns | ||
| Get table with activity and column information: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get, ActivityOptions, TableOptions | ||
| syn = Synapse() | ||
| syn.login() | ||
| table_entity = get( | ||
| synapse_id="syn123456", | ||
| activity_options=ActivityOptions(include_activity=True), | ||
| table_options=TableOptions(include_columns=True) | ||
| ) | ||
| ``` | ||
| Example: Following links | ||
| Get the target of a link entity: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get, LinkOptions | ||
| syn = Synapse() | ||
| syn.login() | ||
| target_entity = get( | ||
| synapse_id="syn123456", | ||
| link_options=LinkOptions(follow_link=True) | ||
| ) | ||
| ``` | ||
| Example: Working with Link entities | ||
| Get a Link entity without following it: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get, LinkOptions | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Get the link entity itself | ||
| link_entity = get( | ||
| synapse_id="syn123456", # Example link ID | ||
| link_options=LinkOptions(follow_link=False) | ||
| ) | ||
| print(f"Link: {link_entity.name} -> {link_entity.target_id}") | ||
| # Then follow the link to get the target | ||
| target_entity = get( | ||
| synapse_id="syn123456", | ||
| link_options=LinkOptions(follow_link=True) | ||
| ) | ||
| print(f"Target: {target_entity.name} (type: {type(target_entity).__name__})") | ||
| ``` | ||
| Example: Comprehensive File options | ||
| Download file with custom location and collision handling: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get, FileOptions | ||
| syn = Synapse() | ||
| syn.login() | ||
| file_entity = get( | ||
| synapse_id="syn123456", | ||
| file_options=FileOptions( | ||
| download_file=True, | ||
| download_location="/path/to/download/", | ||
| if_collision="overwrite.local" | ||
| ) | ||
| ) | ||
| print(f"Downloaded file: {file_entity.name} to {file_entity.path}") | ||
| ``` | ||
| Example: Table options for table-like entities | ||
| Get table entities with column information: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get, TableOptions | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Works for Table, Dataset, EntityView, MaterializedView, | ||
| # SubmissionView, VirtualTable, and DatasetCollection | ||
| table_entity = get( | ||
| synapse_id="syn123456", # Example table ID | ||
| table_options=TableOptions(include_columns=True) | ||
| ) | ||
| print(f"Table: {table_entity.name} with {len(table_entity.columns)} columns") | ||
| ``` | ||
| Example: Combining multiple options | ||
| Get a File with both activity and custom download options: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get, FileOptions, ActivityOptions | ||
| syn = Synapse() | ||
| syn.login() | ||
| file_entity = get( | ||
| synapse_id="syn123456", | ||
| activity_options=ActivityOptions(include_activity=True), | ||
| file_options=FileOptions( | ||
| download_file=False | ||
| ) | ||
| ) | ||
| print(f"File: {file_entity.name} (activity included: {file_entity.activity is not None})") | ||
| ``` | ||
| Example: Working with entity instances | ||
| Pass an existing entity instance to refresh or apply new options: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get, FileOptions | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Get an entity first | ||
| entity = get(synapse_id="syn123456") | ||
| print(f"Original entity: {entity.name}") | ||
| # Then use the entity instance to get it again with different options | ||
| refreshed_entity = get( | ||
| entity, | ||
| file_options=FileOptions(download_file=False) | ||
| ) | ||
| print(f"Refreshed entity: {refreshed_entity.name} (download_file: {refreshed_entity.download_file})") | ||
| ``` | ||
| """ | ||
| return wrap_async_to_sync( | ||
| coroutine=get_async( | ||
| synapse_id=synapse_id, | ||
| entity_name=entity_name, | ||
| parent_id=parent_id, | ||
| version_number=version_number, | ||
| activity_options=activity_options, | ||
| file_options=file_options, | ||
| table_options=table_options, | ||
| link_options=link_options, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| ) | ||
| async def get_async( | ||
| synapse_id: Optional[str] = None, | ||
| *, | ||
| entity_name: Optional[str] = None, | ||
| parent_id: Optional[str] = None, | ||
| version_number: Optional[int] = None, | ||
| activity_options: Optional[ActivityOptions] = None, | ||
| file_options: Optional[FileOptions] = None, | ||
| table_options: Optional[TableOptions] = None, | ||
| link_options: Optional[LinkOptions] = None, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Union[ | ||
| "Dataset", | ||
| "DatasetCollection", | ||
| "EntityView", | ||
| "File", | ||
| "Folder", | ||
| "Link", | ||
| "MaterializedView", | ||
| "Project", | ||
| "SubmissionView", | ||
| "Table", | ||
| "VirtualTable", | ||
| ]: | ||
| """ | ||
| Factory method to retrieve any Synapse entity by its ID or by name and parent ID. | ||
| This method serves as a unified interface for retrieving any type of Synapse entity | ||
| without needing to know the specific entity type beforehand. It automatically | ||
| determines the entity type and returns the appropriate model instance. | ||
| You can retrieve entities in two ways: | ||
| 1. By providing a synapse_id directly | ||
| 2. By providing entity_name and optionally parent_id for lookup | ||
| Arguments: | ||
| synapse_id: The Synapse ID of the entity to retrieve (e.g., 'syn123456'). | ||
| Mutually exclusive with entity_name. | ||
| entity_name: The name of the entity to find. Must be used with this approach | ||
| instead of synapse_id. When looking up projects, parent_id should be None. | ||
| parent_id: The parent entity ID when looking up by name. Set to None when | ||
| looking up projects by name. Only used with entity_name. | ||
| version_number: The specific version number of the entity to retrieve. Only | ||
| applies to versionable entities (File, Table, Dataset). If not specified, | ||
| the most recent version will be retrieved. Ignored for other entity types. | ||
| file_options: File-specific configuration options. Only applies to File entities. | ||
| Ignored for other entity types. | ||
| link_options: Link-specific configuration options. Only applies when the entity | ||
| is a Link. Ignored for other entity types. | ||
| 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 appropriate Synapse entity model instance based on the entity type. | ||
| Raises: | ||
| ValueError: If both synapse_id and entity_name are provided, or if neither is provided. | ||
| ValueError: If entity_name is provided without this being a valid lookup scenario. | ||
| ValueError: If the synapse_id is not a valid Synapse ID format. | ||
| Note: | ||
| When using entity_name lookup: | ||
| - For projects: leave parent_id=None | ||
| - For all other entities: provide the parent_id of the containing folder/project | ||
| Example: Retrieving entities by ID | ||
| Get any entity by Synapse ID: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import File, Project | ||
| from synapseclient.operations import get_async | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Works for any entity type | ||
| entity = await get_async(synapse_id="syn123456") | ||
| # The returned object will be the appropriate type | ||
| if isinstance(entity, File): | ||
| print(f"File: {entity.name}") | ||
| elif isinstance(entity, Project): | ||
| print(f"Project: {entity.name}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| Example: Retrieving entities by name | ||
| Get an entity by name and parent: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get_async | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Get a file by name within a folder | ||
| entity = await get_async( | ||
| entity_name="my_file.txt", | ||
| parent_id="syn123456" | ||
| ) | ||
| # Get a project by name (parent_id=None) | ||
| project = await get_async( | ||
| entity_name="My Project", | ||
| parent_id=None | ||
| ) | ||
| asyncio.run(main()) | ||
| ``` | ||
| Example: Retrieving a specific version | ||
| Get a specific version of a versionable entity: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get_async | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| entity = await get_async(synapse_id="syn123456", version_number=2) | ||
| asyncio.run(main()) | ||
| ``` | ||
| Example: Retrieving a file with custom options | ||
| Get file metadata with specific download options: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get_async, FileOptions, ActivityOptions | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| file_entity = await get_async( | ||
| synapse_id="syn123456", | ||
| activity_options=ActivityOptions(include_activity=True), | ||
| file_options=FileOptions( | ||
| download_file=False | ||
| ) | ||
| ) | ||
| asyncio.run(main()) | ||
| ``` | ||
| Example: Retrieving a table with activity and columns | ||
| Get table with activity and column information: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get_async, ActivityOptions, TableOptions | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| table_entity = await get_async( | ||
| synapse_id="syn123456", | ||
| activity_options=ActivityOptions(include_activity=True), | ||
| table_options=TableOptions(include_columns=True) | ||
| ) | ||
| asyncio.run(main()) | ||
| ``` | ||
| Example: Following links | ||
| Get the target of a link entity: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get_async, LinkOptions | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| target_entity = await get_async( | ||
| synapse_id="syn123456", | ||
| link_options=LinkOptions(follow_link=True) | ||
| ) | ||
| asyncio.run(main()) | ||
| ``` | ||
| Example: Working with Link entities | ||
| Get a Link entity without following it: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get_async, LinkOptions | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Get the link entity itself | ||
| link_entity = await get_async( | ||
| synapse_id="syn123456", # Example link ID | ||
| link_options=LinkOptions(follow_link=False) | ||
| ) | ||
| print(f"Link: {link_entity.name} -> {link_entity.target_id}") | ||
| # Then follow the link to get the target | ||
| target_entity = await get_async( | ||
| synapse_id="syn123456", | ||
| link_options=LinkOptions(follow_link=True) | ||
| ) | ||
| print(f"Target: {target_entity.name} (type: {type(target_entity).__name__})") | ||
| asyncio.run(main()) | ||
| ``` | ||
| Example: Comprehensive File options | ||
| Download file with custom location and collision handling: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get_async, FileOptions | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| file_entity = await get_async( | ||
| synapse_id="syn123456", | ||
| file_options=FileOptions( | ||
| download_file=True, | ||
| download_location="/path/to/download/", | ||
| if_collision="overwrite.local" | ||
| ) | ||
| ) | ||
| print(f"Downloaded file: {file_entity.name} to {file_entity.path}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| Example: Table options for table-like entities | ||
| Get table entities with column information: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get_async, TableOptions | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Works for Table, Dataset, EntityView, MaterializedView, | ||
| # SubmissionView, VirtualTable, and DatasetCollection | ||
| table_entity = await get_async( | ||
| synapse_id="syn123456", # Example table ID | ||
| table_options=TableOptions(include_columns=True) | ||
| ) | ||
| print(f"Table: {table_entity.name} with {len(table_entity.columns)} columns") | ||
| asyncio.run(main()) | ||
| ``` | ||
| Example: Combining multiple options | ||
| Get a File with both activity and custom download options: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get_async, FileOptions, ActivityOptions | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| file_entity = await get_async( | ||
| synapse_id="syn123456", | ||
| activity_options=ActivityOptions(include_activity=True), | ||
| file_options=FileOptions( | ||
| download_file=False | ||
| ) | ||
| ) | ||
| print(f"File: {file_entity.name} (activity included: {file_entity.activity is not None})") | ||
| asyncio.run(main()) | ||
| ``` | ||
| Example: Working with entity instances | ||
| Pass an existing entity instance to refresh or apply new options: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.operations import get_async, FileOptions | ||
| async def main(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Get an entity first | ||
| entity = await get_async(synapse_id="syn123456") | ||
| print(f"Original entity: {entity.name}") | ||
| # Then use the entity instance to get it again with different options | ||
| refreshed_entity = await get_async( | ||
| entity, | ||
| file_options=FileOptions(download_file=False) | ||
| ) | ||
| print(f"Refreshed entity: {refreshed_entity.name} (download_file: {refreshed_entity.download_file})") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| from synapseclient.api.entity_bundle_services_v2 import ( | ||
| get_entity_id_bundle2, | ||
| get_entity_id_version_bundle2, | ||
| ) | ||
| from synapseclient.api.entity_services import get_child, get_entity_type | ||
| from synapseclient.core.constants import concrete_types | ||
| from synapseclient.models import ( | ||
| Dataset, | ||
| DatasetCollection, | ||
| EntityView, | ||
| File, | ||
| Folder, | ||
| Link, | ||
| MaterializedView, | ||
| Project, | ||
| SubmissionView, | ||
| Table, | ||
| VirtualTable, | ||
| ) | ||
| activity_options = activity_options or ActivityOptions() | ||
| file_options = file_options or FileOptions() | ||
| table_options = table_options or TableOptions() | ||
| link_options = link_options or LinkOptions() | ||
| # Handle case where an entity instance is passed directly | ||
| entity_types = ( | ||
| Dataset, | ||
| DatasetCollection, | ||
| EntityView, | ||
| File, | ||
| Folder, | ||
| Link, | ||
| MaterializedView, | ||
| Project, | ||
| SubmissionView, | ||
| Table, | ||
| VirtualTable, | ||
| ) | ||
| if isinstance(synapse_id, entity_types): | ||
| return await _handle_entity_instance( | ||
| entity=synapse_id, | ||
| version_number=version_number, | ||
| activity_options=activity_options, | ||
| file_options=file_options, | ||
| table_options=table_options, | ||
| link_options=link_options, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| # Validate input parameters | ||
| if synapse_id is not None and entity_name is not None: | ||
| raise ValueError( | ||
| "Cannot specify both synapse_id and entity_name. " | ||
| "Use synapse_id for direct lookup or entity_name with optional parent_id for name-based lookup." | ||
| ) | ||
| if synapse_id is None and entity_name is None: | ||
| raise ValueError( | ||
| "Must specify either synapse_id or entity_name. " | ||
| "Use synapse_id for direct lookup or entity_name with optional parent_id for name-based lookup." | ||
| ) | ||
| # If looking up by name, get the synapse_id first | ||
| if entity_name is not None and synapse_id is None: | ||
| synapse_id = await get_child( | ||
| entity_name=entity_name, parent_id=parent_id, synapse_client=synapse_client | ||
| ) | ||
| if synapse_id is None: | ||
| if parent_id is None: | ||
| raise SynapseNotFoundError( | ||
| f"Project with name '{entity_name}' not found." | ||
| ) | ||
| else: | ||
| raise SynapseNotFoundError( | ||
| f"Entity with name '{entity_name}' not found in parent '{parent_id}'." | ||
| ) | ||
| entity_header = await get_entity_type( | ||
| entity_id=synapse_id, synapse_client=synapse_client | ||
| ) | ||
| entity_type = entity_header.type | ||
| if entity_type == concrete_types.LINK_ENTITY: | ||
| return await _handle_link_entity( | ||
| synapse_id=synapse_id, | ||
| link_options=link_options, | ||
| file_options=file_options, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| elif entity_type == concrete_types.FILE_ENTITY: | ||
| return await _handle_file_entity( | ||
| synapse_id=synapse_id, | ||
| version_number=version_number, | ||
| activity_options=activity_options, | ||
| file_options=file_options, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| elif entity_type == concrete_types.PROJECT_ENTITY: | ||
| return await _handle_simple_entity( | ||
| entity_class=Project, | ||
| synapse_id=synapse_id, | ||
| version_number=version_number, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| elif entity_type == concrete_types.FOLDER_ENTITY: | ||
| return await _handle_simple_entity( | ||
| entity_class=Folder, | ||
| synapse_id=synapse_id, | ||
| version_number=version_number, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| elif entity_type == concrete_types.TABLE_ENTITY: | ||
| return await _handle_table_like_entity( | ||
| entity_class=Table, | ||
| synapse_id=synapse_id, | ||
| version_number=version_number, | ||
| activity_options=activity_options, | ||
| table_options=table_options, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| elif entity_type == concrete_types.DATASET_ENTITY: | ||
| return await _handle_table_like_entity( | ||
| entity_class=Dataset, | ||
| synapse_id=synapse_id, | ||
| version_number=version_number, | ||
| activity_options=activity_options, | ||
| table_options=table_options, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| elif entity_type == concrete_types.DATASET_COLLECTION_ENTITY: | ||
| return await _handle_table_like_entity( | ||
| entity_class=DatasetCollection, | ||
| synapse_id=synapse_id, | ||
| version_number=version_number, | ||
| activity_options=activity_options, | ||
| table_options=table_options, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| elif entity_type == concrete_types.ENTITY_VIEW: | ||
| return await _handle_table_like_entity( | ||
| entity_class=EntityView, | ||
| synapse_id=synapse_id, | ||
| version_number=version_number, | ||
| activity_options=activity_options, | ||
| table_options=table_options, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| elif entity_type == concrete_types.MATERIALIZED_VIEW: | ||
| return await _handle_table_like_entity( | ||
| entity_class=MaterializedView, | ||
| synapse_id=synapse_id, | ||
| version_number=version_number, | ||
| activity_options=activity_options, | ||
| table_options=table_options, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| elif entity_type == concrete_types.SUBMISSION_VIEW: | ||
| return await _handle_table_like_entity( | ||
| entity_class=SubmissionView, | ||
| synapse_id=synapse_id, | ||
| version_number=version_number, | ||
| activity_options=activity_options, | ||
| table_options=table_options, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| elif entity_type == concrete_types.VIRTUAL_TABLE: | ||
| return await _handle_table_like_entity( | ||
| entity_class=VirtualTable, | ||
| synapse_id=synapse_id, | ||
| version_number=version_number, | ||
| activity_options=activity_options, | ||
| table_options=table_options, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| else: | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| client.logger.warning( | ||
| "Unknown entity type: %s. Falling back to returning %s as a dictionary bundle matching " | ||
| "https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/entitybundle/v2/EntityBundle.html", | ||
| entity_type, | ||
| synapse_id, | ||
| ) | ||
| # This allows the function to handle new entity types that may be added in the future | ||
| if version_number is not None: | ||
| return await get_entity_id_version_bundle2( | ||
| entity_id=synapse_id, | ||
| version=version_number, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| else: | ||
| return await get_entity_id_bundle2( | ||
| entity_id=synapse_id, synapse_client=synapse_client | ||
| ) |
+18
-14
| Metadata-Version: 2.4 | ||
| Name: synapseclient | ||
| Version: 4.9.0 | ||
| Version: 4.10.0 | ||
| Summary: A client for Synapse, a collaborative, open-source research platform that allows teams to share data, track analyses, and collaborate. | ||
@@ -54,3 +54,3 @@ Home-page: https://www.synapse.org | ||
| Requires-Dist: pytest-socket~=0.6.0; extra == "dev" | ||
| Requires-Dist: pytest-asyncio<1.0,>=0.25.0; extra == "dev" | ||
| Requires-Dist: pytest-asyncio<2.0,>=1.2.0; extra == "dev" | ||
| Requires-Dist: flake8<4.0,>=3.7.0; extra == "dev" | ||
@@ -68,3 +68,3 @@ Requires-Dist: pytest-xdist[psutil]<3.0.0,>=2.2; extra == "dev" | ||
| Requires-Dist: pytest-socket~=0.6.0; extra == "tests" | ||
| Requires-Dist: pytest-asyncio<1.0,>=0.25.0; extra == "tests" | ||
| Requires-Dist: pytest-asyncio<2.0,>=1.2.0; extra == "tests" | ||
| Requires-Dist: flake8<4.0,>=3.7.0; extra == "tests" | ||
@@ -80,2 +80,3 @@ Requires-Dist: pytest-xdist[psutil]<3.0.0,>=2.2; extra == "tests" | ||
| Requires-Dist: pysftp<0.3,>=0.2.8; extra == "pysftp" | ||
| Requires-Dist: paramiko<4.0.0; extra == "pysftp" | ||
| Provides-Extra: boto3 | ||
@@ -97,8 +98,8 @@ Requires-Dist: boto3<2.0,>=1.7.0; extra == "boto3" | ||
| --------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ||
| develop | [](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Adevelop) | ||
| master | [](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Amaster) | ||
| develop | [](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Adevelop) | ||
| master | [](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Amaster) | ||
| [](https://pypi.python.org/pypi/synapseclient/) [](https://pypi.python.org/pypi/synapseclient/) | ||
| A Python client for [Sage Bionetworks'](https://www.sagebase.org) [Synapse](https://www.synapse.org/), a collaborative, open-source research platform that allows teams to share data, track analyses, and collaborate. The Python client can be used as a library for development of software that communicates with Synapse or as a command-line utility. | ||
| A Python client for [Sage Bionetworks'](https://sagebionetworks.org/) [Synapse](https://www.synapse.org/), a collaborative, open-source research platform that allows teams to share data, track analyses, and collaborate. The Python client can be used as a library for development of software that communicates with Synapse or as a command-line utility. | ||
@@ -251,3 +252,3 @@ There is also a [Synapse client for R](https://github.com/Sage-Bionetworks/synapser/). | ||
| import synapseclient | ||
| from synapseclient.entity import Project | ||
| from synapseclient.models import Project | ||
@@ -260,3 +261,3 @@ syn = synapseclient.Synapse() | ||
| project = Project('My uniquely named project') | ||
| project = syn.store(project) | ||
| project.store() | ||
@@ -270,2 +271,3 @@ print(project.id) | ||
| import synapseclient | ||
| from synapseclient.models import Folder | ||
@@ -277,4 +279,4 @@ syn = synapseclient.Synapse() | ||
| folder = Folder(name='my_folder', parent="syn123") | ||
| folder = syn.store(folder) | ||
| folder = Folder(name='my_folder', parent_id="syn123") | ||
| folder.store() | ||
@@ -289,2 +291,3 @@ print(folder.id) | ||
| import synapseclient | ||
| from synapseclient.models import File | ||
@@ -297,6 +300,6 @@ syn = synapseclient.Synapse() | ||
| file = File( | ||
| path=filepath, | ||
| parent="syn123", | ||
| path="path/to/file.txt", | ||
| parent_id="syn123", | ||
| ) | ||
| file = syn.store(file) | ||
| file.store() | ||
@@ -310,2 +313,3 @@ print(file.id) | ||
| import synapseclient | ||
| from synapseclient.models import File | ||
@@ -318,3 +322,3 @@ syn = synapseclient.Synapse() | ||
| ## retrieve a 100 by 4 matrix | ||
| matrix = syn.get('syn1901033') | ||
| matrix = File(id='syn1901033').get() | ||
@@ -321,0 +325,0 @@ ## inspect its properties |
+14
-11
@@ -6,8 +6,8 @@ Python Synapse Client | ||
| --------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ||
| develop | [](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Adevelop) | ||
| master | [](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Amaster) | ||
| develop | [](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Adevelop) | ||
| master | [](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Amaster) | ||
| [](https://pypi.python.org/pypi/synapseclient/) [](https://pypi.python.org/pypi/synapseclient/) | ||
| A Python client for [Sage Bionetworks'](https://www.sagebase.org) [Synapse](https://www.synapse.org/), a collaborative, open-source research platform that allows teams to share data, track analyses, and collaborate. The Python client can be used as a library for development of software that communicates with Synapse or as a command-line utility. | ||
| A Python client for [Sage Bionetworks'](https://sagebionetworks.org/) [Synapse](https://www.synapse.org/), a collaborative, open-source research platform that allows teams to share data, track analyses, and collaborate. The Python client can be used as a library for development of software that communicates with Synapse or as a command-line utility. | ||
@@ -160,3 +160,3 @@ There is also a [Synapse client for R](https://github.com/Sage-Bionetworks/synapser/). | ||
| import synapseclient | ||
| from synapseclient.entity import Project | ||
| from synapseclient.models import Project | ||
@@ -169,3 +169,3 @@ syn = synapseclient.Synapse() | ||
| project = Project('My uniquely named project') | ||
| project = syn.store(project) | ||
| project.store() | ||
@@ -179,2 +179,3 @@ print(project.id) | ||
| import synapseclient | ||
| from synapseclient.models import Folder | ||
@@ -186,4 +187,4 @@ syn = synapseclient.Synapse() | ||
| folder = Folder(name='my_folder', parent="syn123") | ||
| folder = syn.store(folder) | ||
| folder = Folder(name='my_folder', parent_id="syn123") | ||
| folder.store() | ||
@@ -198,2 +199,3 @@ print(folder.id) | ||
| import synapseclient | ||
| from synapseclient.models import File | ||
@@ -206,6 +208,6 @@ syn = synapseclient.Synapse() | ||
| file = File( | ||
| path=filepath, | ||
| parent="syn123", | ||
| path="path/to/file.txt", | ||
| parent_id="syn123", | ||
| ) | ||
| file = syn.store(file) | ||
| file.store() | ||
@@ -219,2 +221,3 @@ print(file.id) | ||
| import synapseclient | ||
| from synapseclient.models import File | ||
@@ -227,3 +230,3 @@ syn = synapseclient.Synapse() | ||
| ## retrieve a 100 by 4 matrix | ||
| matrix = syn.get('syn1901033') | ||
| matrix = File(id='syn1901033').get() | ||
@@ -230,0 +233,0 @@ ## inspect its properties |
+4
-3
@@ -63,3 +63,3 @@ [metadata] | ||
| pytest-socket~=0.6.0 | ||
| pytest-asyncio>=0.25.0,<1.0 | ||
| pytest-asyncio>=1.2.0,<2.0 | ||
| flake8>=3.7.0,<4.0 | ||
@@ -77,3 +77,3 @@ pytest-xdist[psutil]>=2.2,<3.0.0 | ||
| pytest-socket~=0.6.0 | ||
| pytest-asyncio>=0.25.0,<1.0 | ||
| pytest-asyncio>=1.2.0,<2.0 | ||
| flake8>=3.7.0,<4.0 | ||
@@ -91,3 +91,3 @@ pytest-xdist[psutil]>=2.2,<3.0.0 | ||
| pytest-socket~=0.6.0 | ||
| pytest-asyncio>=0.25.0,<1.0 | ||
| pytest-asyncio>=1.2.0,<2.0 | ||
| flake8>=3.7.0,<4.0 | ||
@@ -103,2 +103,3 @@ pytest-xdist[psutil]>=2.2,<3.0.0 | ||
| pysftp>=0.2.8,<0.3 | ||
| paramiko<4.0.0 | ||
| boto3 = | ||
@@ -105,0 +106,0 @@ boto3>=1.7.0,<2.0 |
| Metadata-Version: 2.4 | ||
| Name: synapseclient | ||
| Version: 4.9.0 | ||
| Version: 4.10.0 | ||
| Summary: A client for Synapse, a collaborative, open-source research platform that allows teams to share data, track analyses, and collaborate. | ||
@@ -54,3 +54,3 @@ Home-page: https://www.synapse.org | ||
| Requires-Dist: pytest-socket~=0.6.0; extra == "dev" | ||
| Requires-Dist: pytest-asyncio<1.0,>=0.25.0; extra == "dev" | ||
| Requires-Dist: pytest-asyncio<2.0,>=1.2.0; extra == "dev" | ||
| Requires-Dist: flake8<4.0,>=3.7.0; extra == "dev" | ||
@@ -68,3 +68,3 @@ Requires-Dist: pytest-xdist[psutil]<3.0.0,>=2.2; extra == "dev" | ||
| Requires-Dist: pytest-socket~=0.6.0; extra == "tests" | ||
| Requires-Dist: pytest-asyncio<1.0,>=0.25.0; extra == "tests" | ||
| Requires-Dist: pytest-asyncio<2.0,>=1.2.0; extra == "tests" | ||
| Requires-Dist: flake8<4.0,>=3.7.0; extra == "tests" | ||
@@ -80,2 +80,3 @@ Requires-Dist: pytest-xdist[psutil]<3.0.0,>=2.2; extra == "tests" | ||
| Requires-Dist: pysftp<0.3,>=0.2.8; extra == "pysftp" | ||
| Requires-Dist: paramiko<4.0.0; extra == "pysftp" | ||
| Provides-Extra: boto3 | ||
@@ -97,8 +98,8 @@ Requires-Dist: boto3<2.0,>=1.7.0; extra == "boto3" | ||
| --------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ||
| develop | [](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Adevelop) | ||
| master | [](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Amaster) | ||
| develop | [](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Adevelop) | ||
| master | [](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Amaster) | ||
| [](https://pypi.python.org/pypi/synapseclient/) [](https://pypi.python.org/pypi/synapseclient/) | ||
| A Python client for [Sage Bionetworks'](https://www.sagebase.org) [Synapse](https://www.synapse.org/), a collaborative, open-source research platform that allows teams to share data, track analyses, and collaborate. The Python client can be used as a library for development of software that communicates with Synapse or as a command-line utility. | ||
| A Python client for [Sage Bionetworks'](https://sagebionetworks.org/) [Synapse](https://www.synapse.org/), a collaborative, open-source research platform that allows teams to share data, track analyses, and collaborate. The Python client can be used as a library for development of software that communicates with Synapse or as a command-line utility. | ||
@@ -251,3 +252,3 @@ There is also a [Synapse client for R](https://github.com/Sage-Bionetworks/synapser/). | ||
| import synapseclient | ||
| from synapseclient.entity import Project | ||
| from synapseclient.models import Project | ||
@@ -260,3 +261,3 @@ syn = synapseclient.Synapse() | ||
| project = Project('My uniquely named project') | ||
| project = syn.store(project) | ||
| project.store() | ||
@@ -270,2 +271,3 @@ print(project.id) | ||
| import synapseclient | ||
| from synapseclient.models import Folder | ||
@@ -277,4 +279,4 @@ syn = synapseclient.Synapse() | ||
| folder = Folder(name='my_folder', parent="syn123") | ||
| folder = syn.store(folder) | ||
| folder = Folder(name='my_folder', parent_id="syn123") | ||
| folder.store() | ||
@@ -289,2 +291,3 @@ print(folder.id) | ||
| import synapseclient | ||
| from synapseclient.models import File | ||
@@ -297,6 +300,6 @@ syn = synapseclient.Synapse() | ||
| file = File( | ||
| path=filepath, | ||
| parent="syn123", | ||
| path="path/to/file.txt", | ||
| parent_id="syn123", | ||
| ) | ||
| file = syn.store(file) | ||
| file.store() | ||
@@ -310,2 +313,3 @@ print(file.id) | ||
| import synapseclient | ||
| from synapseclient.models import File | ||
@@ -318,3 +322,3 @@ syn = synapseclient.Synapse() | ||
| ## retrieve a 100 by 4 matrix | ||
| matrix = syn.get('syn1901033') | ||
| matrix = File(id='syn1901033').get() | ||
@@ -321,0 +325,0 @@ ## inspect its properties |
@@ -26,3 +26,3 @@ requests<3.0,>=2.22.0 | ||
| pytest-socket~=0.6.0 | ||
| pytest-asyncio<1.0,>=0.25.0 | ||
| pytest-asyncio<2.0,>=1.2.0 | ||
| flake8<4.0,>=3.7.0 | ||
@@ -51,2 +51,3 @@ pytest-xdist[psutil]<3.0.0,>=2.2 | ||
| pysftp<0.3,>=0.2.8 | ||
| paramiko<4.0.0 | ||
@@ -57,3 +58,3 @@ [tests] | ||
| pytest-socket~=0.6.0 | ||
| pytest-asyncio<1.0,>=0.25.0 | ||
| pytest-asyncio<2.0,>=1.2.0 | ||
| flake8<4.0,>=3.7.0 | ||
@@ -60,0 +61,0 @@ pytest-xdist[psutil]<3.0.0,>=2.2 |
@@ -30,2 +30,3 @@ MANIFEST.in | ||
| synapseclient/api/configuration_services.py | ||
| synapseclient/api/curation_services.py | ||
| synapseclient/api/entity_bundle_services_v2.py | ||
@@ -84,2 +85,3 @@ synapseclient/api/entity_factory.py | ||
| synapseclient/models/annotations.py | ||
| synapseclient/models/curation.py | ||
| synapseclient/models/dataset.py | ||
@@ -90,4 +92,6 @@ synapseclient/models/entityview.py | ||
| synapseclient/models/folder.py | ||
| synapseclient/models/link.py | ||
| synapseclient/models/materializedview.py | ||
| synapseclient/models/project.py | ||
| synapseclient/models/recordset.py | ||
| synapseclient/models/submissionview.py | ||
@@ -123,2 +127,4 @@ synapseclient/models/table.py | ||
| synapseclient/models/services/storable_entity_components.py | ||
| synapseclient/operations/__init__.py | ||
| synapseclient/operations/factory_operations.py | ||
| synapseclient/services/__init__.py | ||
@@ -125,0 +131,0 @@ synapseclient/services/json_schema.py |
@@ -77,2 +77,4 @@ """ | ||
| from deprecated import deprecated | ||
| from synapseclient.core.utils import ( | ||
@@ -288,2 +290,9 @@ from_unix_epoch_time, | ||
| @deprecated( | ||
| version="4.9.0", | ||
| reason="To be removed in 5.0.0. Use the dataclass model attributes instead. " | ||
| "All dataclass models support annotations: File, Folder, Project, Table, EntityView, Dataset, " | ||
| "DatasetCollection, MaterializedView, SubmissionView, VirtualTable. " | ||
| "Access annotations directly via `instance.annotations` attribute.", | ||
| ) | ||
| class Annotations(dict): | ||
@@ -300,11 +309,38 @@ """ | ||
| Example: Creating a few instances | ||
| Creating and setting annotations | ||
| Example: Migrating from this class to dataclass models | ||
| **Legacy approach (deprecated):** | ||
| ```python | ||
| from synapseclient import Annotations | ||
| from synapseclient import Annotations | ||
| example1 = Annotations('syn123','40256475-6fb3-11ea-bb0a-9cb6d0d8d984', {'foo':'bar'}) | ||
| example2 = Annotations('syn123','40256475-6fb3-11ea-bb0a-9cb6d0d8d984', foo='bar') | ||
| example3 = Annotations('syn123','40256475-6fb3-11ea-bb0a-9cb6d0d8d984') | ||
| example3['foo'] = 'bar' | ||
| ``` | ||
| example1 = Annotations('syn123','40256475-6fb3-11ea-bb0a-9cb6d0d8d984', {'foo':'bar'}) | ||
| example2 = Annotations('syn123','40256475-6fb3-11ea-bb0a-9cb6d0d8d984', foo='bar') | ||
| example3 = Annotations('syn123','40256475-6fb3-11ea-bb0a-9cb6d0d8d984') | ||
| example3['foo'] = 'bar' | ||
| **New approach using dataclass models:** | ||
| ```python | ||
| import synapseclient | ||
| from synapseclient.models import ( | ||
| File, Folder, Project, Table, EntityView, Dataset, | ||
| DatasetCollection, MaterializedView, SubmissionView, VirtualTable | ||
| ) | ||
| # Create client and login | ||
| syn = synapseclient.Synapse() | ||
| syn.login() | ||
| # File - don't download the file content, just get metadata | ||
| file_instance = File(id="syn12345", download_file=False) | ||
| file_instance = file_instance.get() | ||
| file_instance.annotations = { | ||
| "foo": ["bar"], | ||
| "species": ["Homo sapiens"] | ||
| } | ||
| file_instance = file_instance.store() | ||
| print(f"File annotations: {file_instance.annotations}") | ||
| # All other dataclass models work the same way | ||
| # annotations is always a dict by default (empty {} if no annotations exist) | ||
| ``` | ||
| """ | ||
@@ -311,0 +347,0 @@ |
@@ -11,3 +11,3 @@ # These are all of the models that are used by the Synapse client. | ||
| from .annotations import set_annotations, set_annotations_async | ||
| from .api_client import rest_post_paginated_async | ||
| from .api_client import rest_get_paginated_async, rest_post_paginated_async | ||
| from .configuration_services import ( | ||
@@ -20,2 +20,11 @@ get_client_authenticated_s3_profile, | ||
| ) | ||
| from .curation_services import ( | ||
| create_curation_task, | ||
| delete_curation_task, | ||
| delete_grid_session, | ||
| get_curation_task, | ||
| list_curation_tasks, | ||
| list_grid_sessions, | ||
| update_curation_task, | ||
| ) | ||
| from .entity_bundle_services_v2 import ( | ||
@@ -30,13 +39,30 @@ get_entity_id_bundle2, | ||
| create_access_requirements_if_none, | ||
| create_activity, | ||
| delete_entity, | ||
| delete_entity_acl, | ||
| delete_entity_generated_by, | ||
| delete_entity_provenance, | ||
| get_activity, | ||
| get_child, | ||
| get_children, | ||
| get_entities_by_md5, | ||
| get_entity, | ||
| get_entity_acl, | ||
| get_entity_acl_list, | ||
| get_entity_acl_with_benefactor, | ||
| get_entity_benefactor, | ||
| get_entity_path, | ||
| get_entity_permissions, | ||
| get_entity_provenance, | ||
| get_entity_type, | ||
| get_upload_destination, | ||
| get_upload_destination_location, | ||
| post_entity, | ||
| post_entity_acl, | ||
| put_entity, | ||
| put_entity_acl, | ||
| set_entity_permissions, | ||
| set_entity_provenance, | ||
| update_activity, | ||
| update_entity_acl, | ||
| ) | ||
@@ -58,7 +84,20 @@ from .file_services import ( | ||
| bind_json_schema_to_entity, | ||
| create_organization, | ||
| delete_json_schema, | ||
| delete_json_schema_from_entity, | ||
| delete_organization, | ||
| get_invalid_json_schema_validation, | ||
| get_json_schema_body, | ||
| get_json_schema_derived_keys, | ||
| get_json_schema_from_entity, | ||
| get_json_schema_validation_statistics, | ||
| get_organization, | ||
| get_organization_acl, | ||
| list_json_schema_versions, | ||
| list_json_schema_versions_sync, | ||
| list_json_schemas, | ||
| list_json_schemas_sync, | ||
| list_organizations, | ||
| list_organizations_sync, | ||
| update_organization_acl, | ||
| validate_entity_with_json_schema, | ||
@@ -69,8 +108,32 @@ ) | ||
| ViewTypeMask, | ||
| create_table_snapshot, | ||
| get_column, | ||
| get_columns, | ||
| get_default_columns, | ||
| list_columns, | ||
| list_columns_sync, | ||
| post_columns, | ||
| ) | ||
| from .team_services import post_team_list | ||
| from .user_services import get_user_group_headers_batch | ||
| from .team_services import ( | ||
| create_team, | ||
| delete_membership_invitation, | ||
| delete_team, | ||
| find_team, | ||
| get_membership_status, | ||
| get_team, | ||
| get_team_members, | ||
| get_team_open_invitations, | ||
| get_teams_for_user, | ||
| invite_to_team, | ||
| post_team_list, | ||
| send_membership_invitation, | ||
| ) | ||
| from .user_services import ( | ||
| get_user_bundle, | ||
| get_user_by_principal_id_or_name, | ||
| get_user_group_headers_batch, | ||
| get_user_profile_by_id, | ||
| get_user_profile_by_username, | ||
| is_user_certified, | ||
| ) | ||
@@ -105,2 +168,7 @@ __all__ = [ | ||
| "get_entity_acl", | ||
| "get_entity_acl_list", | ||
| "get_entity_acl_with_benefactor", | ||
| "get_entity_benefactor", | ||
| "get_entity_permissions", | ||
| "get_entity_type", | ||
| "get_upload_destination", | ||
@@ -112,2 +180,14 @@ "get_upload_destination_location", | ||
| "get_entities_by_md5", | ||
| "get_entity_provenance", | ||
| "set_entity_provenance", | ||
| "delete_entity_provenance", | ||
| "get_activity", | ||
| "create_activity", | ||
| "update_activity", | ||
| "get_child", | ||
| "get_children", | ||
| "post_entity_acl", | ||
| "put_entity_acl", | ||
| "set_entity_permissions", | ||
| "update_entity_acl", | ||
| # configuration_services | ||
@@ -131,2 +211,5 @@ "get_config_file", | ||
| "post_columns", | ||
| "get_column", | ||
| "list_columns", | ||
| "list_columns_sync", | ||
| "get_default_columns", | ||
@@ -143,8 +226,48 @@ "ViewTypeMask", | ||
| "get_json_schema_derived_keys", | ||
| "create_organization", | ||
| "get_organization", | ||
| "list_organizations", | ||
| "list_organizations_sync", | ||
| "delete_organization", | ||
| "get_organization_acl", | ||
| "update_organization_acl", | ||
| "list_json_schemas", | ||
| "list_json_schemas_sync", | ||
| "list_json_schema_versions", | ||
| "list_json_schema_versions_sync", | ||
| "get_json_schema_body", | ||
| "delete_json_schema", | ||
| # api client | ||
| "rest_post_paginated_async", | ||
| "rest_get_paginated_async", | ||
| # team_services | ||
| "post_team_list", | ||
| "create_team", | ||
| "delete_team", | ||
| "get_team", | ||
| "find_team", | ||
| "get_team_members", | ||
| "get_teams_for_user", | ||
| "send_membership_invitation", | ||
| "get_team_open_invitations", | ||
| "get_membership_status", | ||
| "delete_membership_invitation", | ||
| "invite_to_team", | ||
| # curation_services | ||
| "create_curation_task", | ||
| "delete_curation_task", | ||
| "delete_grid_session", | ||
| "get_curation_task", | ||
| "list_curation_tasks", | ||
| "list_grid_sessions", | ||
| "update_curation_task", | ||
| # user_services | ||
| "get_user_bundle", | ||
| "get_user_by_principal_id_or_name", | ||
| "get_user_group_headers_batch", | ||
| "get_user_profile_by_id", | ||
| "get_user_profile_by_username", | ||
| "is_user_certified", | ||
| # table_services | ||
| "create_table_snapshot", | ||
| ] |
@@ -0,1 +1,3 @@ | ||
| import json | ||
| import sys | ||
| from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, Optional | ||
@@ -14,3 +16,3 @@ | ||
| headers: Optional[httpx.Headers] = None, | ||
| retry_policy: Optional[Dict[str, Any]] = {}, | ||
| retry_policy: Optional[Dict[str, Any]] = None, | ||
| requests_session_async_synapse: Optional[httpx.AsyncClient] = None, | ||
@@ -38,2 +40,5 @@ *, | ||
| if not retry_policy: | ||
| retry_policy = {} | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
@@ -47,3 +52,3 @@ next_page_token = None | ||
| uri=uri, | ||
| body=body, | ||
| body=json.dumps(body), | ||
| endpoint=endpoint, | ||
@@ -60,1 +65,58 @@ headers=headers, | ||
| break | ||
| async def rest_get_paginated_async( | ||
| uri: str, | ||
| limit: int = 20, | ||
| offset: int = 0, | ||
| endpoint: Optional[str] = None, | ||
| headers: Optional[httpx.Headers] = None, | ||
| retry_policy: Optional[Dict[str, Any]] = None, | ||
| requests_session_async_synapse: Optional[httpx.AsyncClient] = None, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| **kwargs, | ||
| ) -> AsyncGenerator[Dict[str, str], None]: | ||
| """ | ||
| Asynchronously yield items from a paginated GET endpoint. | ||
| Arguments: | ||
| uri: Endpoint URI for the GET request. | ||
| limit: How many records should be returned per request | ||
| offset: At what record offset from the first should iteration start | ||
| endpoint: Optional server endpoint override. | ||
| headers: Optional HTTP headers. | ||
| retry_policy: Optional retry settings. | ||
| requests_session_async_synapse: Optional async HTTPX client session. | ||
| kwargs: Additional keyword arguments for the request. | ||
| synapse_client: Optional Synapse client instance for authentication. | ||
| Yields: | ||
| Individual items from each page of the response. | ||
| The limit parameter is set at 20 by default. Using a larger limit results in fewer calls to the service, but if | ||
| responses are large enough to be a burden on the service they may be truncated. | ||
| """ | ||
| from synapseclient import Synapse | ||
| from synapseclient.core import utils | ||
| if not retry_policy: | ||
| retry_policy = {} | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| prev_num_results = sys.maxsize | ||
| while prev_num_results > 0: | ||
| paginated_uri = utils._limit_and_offset(uri, limit=limit, offset=offset) | ||
| response = await client.rest_get_async( | ||
| uri=paginated_uri, | ||
| endpoint=endpoint, | ||
| headers=headers, | ||
| retry_policy=retry_policy, | ||
| requests_session_async_synapse=requests_session_async_synapse, | ||
| **kwargs, | ||
| ) | ||
| results = response["results"] if "results" in response else response["children"] | ||
| prev_num_results = len(results) | ||
| for result in results: | ||
| offset += 1 | ||
| yield result |
@@ -25,3 +25,3 @@ """Factory type functions to create and retrieve entities from Synapse""" | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Dataset, File, Folder, Project, Table | ||
| from synapseclient.models import Dataset, File, Folder, Project, RecordSet, Table | ||
@@ -237,3 +237,3 @@ | ||
| async def _handle_file_entity( | ||
| entity_instance: "File", | ||
| entity_instance: Union["File", "RecordSet"], | ||
| entity_bundle: Dict[str, Any], | ||
@@ -249,5 +249,3 @@ download_file: bool, | ||
| entity_instance.fill_from_dict( | ||
| synapse_file=entity_bundle["entity"], set_annotations=False | ||
| ) | ||
| entity_instance.fill_from_dict(entity_bundle["entity"], set_annotations=False) | ||
@@ -346,4 +344,6 @@ # Update entity with FileHandle metadata | ||
| Folder, | ||
| Link, | ||
| MaterializedView, | ||
| Project, | ||
| RecordSet, | ||
| SubmissionView, | ||
@@ -382,4 +382,6 @@ Table, | ||
| concrete_types.MATERIALIZED_VIEW: MaterializedView, | ||
| concrete_types.RECORD_SET_ENTITY: RecordSet, | ||
| concrete_types.SUBMISSION_VIEW: SubmissionView, | ||
| concrete_types.VIRTUAL_TABLE: VirtualTable, | ||
| concrete_types.LINK_ENTITY: Link, | ||
| } | ||
@@ -397,3 +399,6 @@ | ||
| # Handle special case for File entities | ||
| if entity["concreteType"] == concrete_types.FILE_ENTITY: | ||
| if ( | ||
| entity["concreteType"] == concrete_types.FILE_ENTITY | ||
| or entity["concreteType"] == concrete_types.RECORD_SET_ENTITY | ||
| ): | ||
| entity_instance = await _handle_file_entity( | ||
@@ -400,0 +405,0 @@ entity_instance=entity_instance, |
@@ -7,6 +7,8 @@ """This module is responsible for exposing the services defined at: | ||
| from dataclasses import dataclass | ||
| from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union | ||
| from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, List, Optional, Union | ||
| from async_lru import alru_cache | ||
| from synapseclient.api.api_client import rest_post_paginated_async | ||
| from synapseclient.core.exceptions import SynapseHTTPError | ||
| from synapseclient.core.utils import get_synid_and_version | ||
@@ -435,2 +437,6 @@ | ||
| instance from the Synapse class constructor. | ||
| Returns: | ||
| A dictionary of the Entity's ACL. | ||
| <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html> | ||
| """ | ||
@@ -445,2 +451,244 @@ from synapseclient import Synapse | ||
| async def get_entity_acl_with_benefactor( | ||
| entity_id: str, | ||
| check_benefactor: bool = True, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Union[str, List[Dict[str, Union[int, List[str]]]]]]: | ||
| """ | ||
| Get the effective Access Control List (ACL) for a Synapse Entity. | ||
| Arguments: | ||
| entity_id: The ID of the entity. | ||
| check_benefactor: If True (default), check the benefactor for the entity | ||
| to get the ACL. If False, only check the entity itself. | ||
| This is useful for checking the ACL of an entity that has local sharing | ||
| settings, but you want to check the ACL of the entity itself and not | ||
| the benefactor it may inherit from. | ||
| 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 of the Entity's ACL. | ||
| https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html | ||
| If the entity does not have its own ACL and check_benefactor is False, | ||
| returns {"resourceAccess": []}. | ||
| Example: Get ACL with benefactor checking | ||
| Get the effective ACL for entity `syn123`. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import get_entity_acl_with_benefactor | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| # Get ACL from benefactor (default behavior) | ||
| acl = await get_entity_acl_with_benefactor(entity_id="syn123") | ||
| print(f"ACL from benefactor: {acl}") | ||
| # Get ACL from entity only | ||
| acl = await get_entity_acl_with_benefactor( | ||
| entity_id="syn123", | ||
| check_benefactor=False | ||
| ) | ||
| print(f"ACL from entity only: {acl}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| if check_benefactor: | ||
| # Get the ACL from the benefactor (which may be the entity itself) | ||
| benefactor = await get_entity_benefactor( | ||
| entity_id=entity_id, synapse_client=synapse_client | ||
| ) | ||
| return await get_entity_acl( | ||
| entity_id=benefactor.id, synapse_client=synapse_client | ||
| ) | ||
| else: | ||
| try: | ||
| return await get_entity_acl( | ||
| entity_id=entity_id, synapse_client=synapse_client | ||
| ) | ||
| except SynapseHTTPError as e: | ||
| # If entity doesn't have its own ACL and check_benefactor is False, | ||
| # return empty ACL structure indicating no local permissions | ||
| if ( | ||
| "The requested ACL does not exist. This entity inherits its permissions from:" | ||
| in str(e) | ||
| ): | ||
| return {"resourceAccess": []} | ||
| raise e | ||
| async def put_entity_acl( | ||
| entity_id: str, | ||
| acl: Dict[str, Union[str, List[Dict[str, Union[int, List[str]]]]]], | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Union[str, List[Dict[str, Union[int, List[str]]]]]]: | ||
| """ | ||
| Update the Access Control List (ACL) for an entity. | ||
| API Matches <https://rest-docs.synapse.org/rest/PUT/entity/id/acl.html>. | ||
| Note: The caller must be granted `CHANGE_PERMISSIONS` on the Entity to call this method. | ||
| Arguments: | ||
| entity_id: The ID of the entity. | ||
| acl: The ACL to set for the entity. | ||
| 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 updated ACL matching | ||
| https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html | ||
| Example: Update ACL for an entity | ||
| Update the ACL for entity `syn123`. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import put_entity_acl | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| acl = { | ||
| "id": "syn123", | ||
| "etag": "12345", | ||
| "resourceAccess": [ | ||
| { | ||
| "principalId": 12345, | ||
| "accessType": ["READ", "DOWNLOAD"] | ||
| } | ||
| ] | ||
| } | ||
| updated_acl = await put_entity_acl(entity_id="syn123", acl=acl) | ||
| print(f"Updated ACL: {updated_acl}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| return await client.rest_put_async( | ||
| uri=f"/entity/{entity_id}/acl", | ||
| body=json.dumps(acl), | ||
| ) | ||
| async def post_entity_acl( | ||
| entity_id: str, | ||
| acl: Dict[str, Union[str, List[Dict[str, Union[int, List[str]]]]]], | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Union[str, List[Dict[str, Union[int, List[str]]]]]]: | ||
| """ | ||
| Create a new Access Control List (ACL) for an entity. | ||
| API Matches <https://rest-docs.synapse.org/rest/POST/entity/id/acl.html>. | ||
| Note: The caller must be granted `CHANGE_PERMISSIONS` on the Entity to call this method. | ||
| Arguments: | ||
| entity_id: The ID of the entity. | ||
| acl: The ACL to create for the entity. | ||
| 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 created ACL matching | ||
| <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html>. | ||
| Example: Create ACL for an entity | ||
| Create a new ACL for entity `syn123`. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import post_entity_acl | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| acl = { | ||
| "id": "syn123", | ||
| "etag": "12345", | ||
| "resourceAccess": [ | ||
| { | ||
| "principalId": 12345, | ||
| "accessType": ["READ", "DOWNLOAD"] | ||
| } | ||
| ] | ||
| } | ||
| created_acl = await post_entity_acl(entity_id="syn123", acl=acl) | ||
| print(f"Created ACL: {created_acl}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| return await client.rest_post_async( | ||
| uri=f"/entity/{entity_id}/acl", | ||
| body=json.dumps(acl), | ||
| ) | ||
| async def get_entity_permissions( | ||
| entity_id: str, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Union[str, List[str], bool]]: | ||
| """ | ||
| Get the permissions that the caller has on an Entity. | ||
| Arguments: | ||
| entity_id: The ID of the entity. | ||
| 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 containing the permissions that the caller has on the entity. | ||
| <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/auth/UserEntityPermissions.html> | ||
| Example: Get permissions for an entity | ||
| Get the permissions that the caller has on entity `syn123`. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import get_entity_permissions | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| permissions = await get_entity_permissions(entity_id="syn123") | ||
| print(f"Permissions: {permissions}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| return await client.rest_get_async( | ||
| uri=f"/entity/{entity_id}/permissions", | ||
| ) | ||
| async def get_entity_benefactor( | ||
@@ -522,2 +770,54 @@ entity_id: str, | ||
| async def get_entity_type( | ||
| entity_id: str, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> EntityHeader: | ||
| """ | ||
| Get the EntityHeader of an Entity given its ID. The EntityHeader is a light weight | ||
| object with basic information about an Entity includes its type. | ||
| Implements: | ||
| <https://rest-docs.synapse.org/rest/GET/entity/id/type.html> | ||
| Arguments: | ||
| entity_id: The ID of the entity. | ||
| 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: | ||
| EntityHeader object containing basic information about the entity. | ||
| Example: Get entity type information | ||
| Get the EntityHeader for entity `syn123`. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import get_entity_type | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| entity_header = await get_entity_type(entity_id="syn123") | ||
| print(f"Entity type: {entity_header.type}") | ||
| print(f"Entity name: {entity_header.name}") | ||
| print(f"Entity ID: {entity_header.id}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| response = await client.rest_get_async( | ||
| uri=f"/entity/{entity_id}/type", | ||
| ) | ||
| entity_header = EntityHeader() | ||
| return entity_header.fill_from_dict(response) | ||
| async def get_entities_by_md5( | ||
@@ -549,1 +849,818 @@ md5: str, | ||
| ) | ||
| async def get_entity_provenance( | ||
| entity_id: str, | ||
| version_number: Optional[int] = None, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| Retrieve provenance information for a Synapse Entity. | ||
| Arguments: | ||
| entity_id: The ID of the entity. This may include version `syn123.0` or `syn123`. | ||
| If the version is included in `entity_id` and `version_number` is also | ||
| passed in, then the version in `entity_id` will be used. | ||
| version_number: The version of the Entity to retrieve. Gets the most recent version if omitted. | ||
| 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: | ||
| Activity object as a dictionary or raises exception if no provenance record exists. | ||
| Example: Get provenance for an entity | ||
| Get the provenance information for entity `syn123`. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import get_entity_provenance | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| activity = await get_entity_provenance(entity_id="syn123") | ||
| print(f"Activity: {activity}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| Example: Get provenance for a specific version | ||
| Get the provenance information for version 3 of entity `syn123`. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import get_entity_provenance | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| activity = await get_entity_provenance(entity_id="syn123", version_number=3) | ||
| print(f"Activity: {activity}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| syn_id, syn_version = get_synid_and_version(entity_id) | ||
| if not syn_version: | ||
| syn_version = version_number | ||
| if syn_version: | ||
| uri = f"/entity/{syn_id}/version/{syn_version}/generatedBy" | ||
| else: | ||
| uri = f"/entity/{syn_id}/generatedBy" | ||
| return await client.rest_get_async(uri=uri) | ||
| async def set_entity_provenance( | ||
| entity_id: str, | ||
| activity: Dict[str, Any], | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| Stores a record of the code and data used to derive a Synapse entity. | ||
| Arguments: | ||
| entity_id: The ID of the entity. | ||
| activity: A dictionary representing an Activity object. | ||
| 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: | ||
| An updated Activity object as a dictionary. | ||
| Example: Set provenance for an entity | ||
| Set the provenance for entity `syn123` with an activity. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import set_entity_provenance, create_activity | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| # First create or get an activity | ||
| activity = await create_activity({ | ||
| "name": "Analysis Step", | ||
| "description": "Data processing step" | ||
| }) | ||
| # Set the provenance | ||
| updated_activity = await set_entity_provenance( | ||
| entity_id="syn123", | ||
| activity=activity | ||
| ) | ||
| print(f"Updated activity: {updated_activity}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| if "id" in activity: | ||
| saved_activity = await update_activity(activity, synapse_client=synapse_client) | ||
| else: | ||
| saved_activity = await create_activity(activity, synapse_client=synapse_client) | ||
| uri = f"/entity/{entity_id}/generatedBy?generatedBy={saved_activity['id']}" | ||
| return await client.rest_put_async(uri=uri) | ||
| async def delete_entity_provenance( | ||
| entity_id: str, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> None: | ||
| """ | ||
| Removes provenance information from an Entity and deletes the associated Activity. | ||
| Arguments: | ||
| entity_id: The ID of the entity. | ||
| 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 provenance for an entity | ||
| Delete the provenance for entity `syn123`. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import delete_entity_provenance | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| await delete_entity_provenance(entity_id="syn123") | ||
| asyncio.run(main()) | ||
| ``` | ||
| Returns: None | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| try: | ||
| activity = await get_entity_provenance(entity_id, synapse_client=synapse_client) | ||
| except SynapseHTTPError: | ||
| # If no provenance exists, nothing to delete | ||
| return | ||
| if not activity: | ||
| return | ||
| await client.rest_delete_async(uri=f"/entity/{entity_id}/generatedBy") | ||
| # If the activity is shared by more than one entity you recieve an HTTP 400 error: | ||
| # "If you wish to delete this activity, please first delete all Entities generated by this Activity."" | ||
| await client.rest_delete_async(uri=f"/activity/{activity['id']}") | ||
| async def create_activity( | ||
| activity: Dict[str, Any], | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| Create a new Activity in Synapse. | ||
| Arguments: | ||
| activity: A dictionary representing an Activity object. | ||
| 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 created Activity object as a dictionary. | ||
| Example: Create a new activity | ||
| Create a new activity in Synapse. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import create_activity | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| activity = await create_activity({ | ||
| "name": "Data Analysis", | ||
| "description": "Processing raw data" | ||
| }) | ||
| print(f"Created activity: {activity}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| return await client.rest_post_async(uri="/activity", body=json.dumps(activity)) | ||
| async def update_activity( | ||
| activity: Dict[str, Any], | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| Modifies an existing Activity. | ||
| Arguments: | ||
| activity: The Activity to be updated. Must contain an 'id' field. | ||
| 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: | ||
| An updated Activity object as a dictionary. | ||
| Raises: | ||
| ValueError: If the activity does not contain an 'id' field. | ||
| Example: Update an existing activity | ||
| Update an existing activity in Synapse. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import update_activity | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| activity = { | ||
| "id": "12345", | ||
| "name": "Updated Analysis", | ||
| "description": "Updated processing step" | ||
| } | ||
| updated_activity = await update_activity(activity) | ||
| print(f"Updated activity: {updated_activity}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| from synapseclient import Synapse | ||
| if "id" not in activity: | ||
| raise ValueError("The activity you want to update must exist on Synapse") | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| uri = f"/activity/{activity['id']}" | ||
| return await client.rest_put_async(uri=uri, body=json.dumps(activity)) | ||
| async def get_activity( | ||
| activity_id: str, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| Retrieve an Activity by its ID. | ||
| Arguments: | ||
| activity_id: The ID of the activity to retrieve. | ||
| 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: | ||
| Activity object as a dictionary. | ||
| Example: Get activity by ID | ||
| Retrieve an activity using its ID. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import get_activity | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| activity = await get_activity(activity_id="12345") | ||
| print(f"Activity: {activity}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| return await client.rest_get_async(uri=f"/activity/{activity_id}") | ||
| async def get_children( | ||
| parent: Optional[str] = None, | ||
| include_types: List[str] = None, | ||
| sort_by: str = "NAME", | ||
| sort_direction: str = "ASC", | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> AsyncGenerator[Dict[str, Any], None]: | ||
| """ | ||
| Retrieve all entities stored within a parent such as folder or project. | ||
| Arguments: | ||
| parent: The ID of a Synapse container (folder or project) or None to retrieve all projects | ||
| include_types: List of entity types to include (e.g., ["folder", "file"]). | ||
| Available types can be found at: | ||
| https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/EntityType.html | ||
| sort_by: How results should be sorted. Can be "NAME" or "CREATED_ON" | ||
| sort_direction: The direction of the result sort. Can be "ASC" or "DESC" | ||
| 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. | ||
| Yields: | ||
| An async generator that yields entity children dictionaries. | ||
| Example: Getting children of a folder | ||
| Retrieve all children of a folder: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import get_children | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| async for child in get_children(parent="syn123456"): | ||
| print(f"Child: {child['name']} (ID: {child['id']})") | ||
| asyncio.run(main()) | ||
| ``` | ||
| Example: Getting children with specific types | ||
| Retrieve only files and folders: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import get_children | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| async for child in get_children( | ||
| parent="syn123456", | ||
| include_types=["file", "folder"], | ||
| sort_by="NAME", | ||
| sort_direction="ASC" | ||
| ): | ||
| print(f"Child: {child['name']} (Type: {child['type']})") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| if include_types is None: | ||
| include_types = [ | ||
| "folder", | ||
| "file", | ||
| "table", | ||
| "link", | ||
| "entityview", | ||
| "dockerrepo", | ||
| "submissionview", | ||
| "dataset", | ||
| "materializedview", | ||
| ] | ||
| request_body = { | ||
| "parentId": parent, | ||
| "includeTypes": include_types, | ||
| "sortBy": sort_by, | ||
| "sortDirection": sort_direction, | ||
| } | ||
| response = rest_post_paginated_async( | ||
| uri="/entity/children", | ||
| body=request_body, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| async for child in response: | ||
| yield child | ||
| async def get_child( | ||
| entity_name: str, | ||
| parent_id: Optional[str] = None, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Optional[str]: | ||
| """ | ||
| Retrieve an entityId for a given parent ID and entity name. | ||
| This service can also be used to lookup projectId by setting the parentId to None. | ||
| This calls to the REST API found here: <https://rest-docs.synapse.org/rest/POST/entity/child.html> | ||
| Arguments: | ||
| entity_name: The name of the entity to find | ||
| parent_id: The parent ID. Set to None when looking up a project by name. | ||
| 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 entity ID if found, None if not found. | ||
| Raises: | ||
| SynapseHTTPError: If there's an error other than "not found" (404). | ||
| Example: Getting a child entity ID | ||
| Find a file by name within a folder: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import get_child | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| entity_id = await get_child( | ||
| entity_name="my_file.txt", | ||
| parent_id="syn123456" | ||
| ) | ||
| if entity_id: | ||
| print(f"Found entity: {entity_id}") | ||
| else: | ||
| print("Entity not found") | ||
| asyncio.run(main()) | ||
| ``` | ||
| Example: Getting a project by name | ||
| Find a project by name: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import get_child | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| project_id = await get_child( | ||
| entity_name="My Project", | ||
| parent_id=None # None for projects | ||
| ) | ||
| if project_id: | ||
| print(f"Found project: {project_id}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| entity_lookup_request = { | ||
| "parentId": parent_id, | ||
| "entityName": entity_name, | ||
| } | ||
| try: | ||
| response = await client.rest_post_async( | ||
| uri="/entity/child", body=json.dumps(entity_lookup_request) | ||
| ) | ||
| return response.get("id") | ||
| except SynapseHTTPError as e: | ||
| if e.response.status_code == 404: | ||
| # Entity not found | ||
| return None | ||
| raise | ||
| async def set_entity_permissions( | ||
| entity_id: str, | ||
| principal_id: Optional[str] = None, | ||
| access_type: Optional[List[str]] = None, | ||
| modify_benefactor: bool = False, | ||
| warn_if_inherits: bool = True, | ||
| overwrite: bool = True, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Union[str, List[Dict[str, Union[int, List[str]]]]]]: | ||
| """ | ||
| Set permissions for a user or group on an entity. | ||
| Arguments: | ||
| entity_id: The ID of the entity. | ||
| principal_id: Identifier of a user or group. '273948' is for all registered Synapse users | ||
| and '273949' is for public access. None implies public access. | ||
| access_type: Type of permission to be granted. One or more of CREATE, READ, DOWNLOAD, UPDATE, | ||
| DELETE, CHANGE_PERMISSIONS. If None or empty list, removes permissions. | ||
| modify_benefactor: Set as True when modifying a benefactor's ACL. | ||
| warn_if_inherits: When modify_benefactor is False, and warn_if_inherits is True, | ||
| a warning log message is produced if the benefactor for the entity | ||
| you passed into the function is not itself. | ||
| overwrite: By default this function overwrites existing permissions for the specified user. | ||
| Set this flag to False to add new permissions non-destructively. | ||
| 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 updated ACL matching | ||
| https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html | ||
| Example: Set permissions for an entity | ||
| Grant all registered users download access. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import set_entity_permissions | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| # Grant all registered users download access | ||
| acl = await set_entity_permissions( | ||
| entity_id="syn123", | ||
| principal_id="273948", | ||
| access_type=["READ", "DOWNLOAD"] | ||
| ) | ||
| print(f"Updated ACL: {acl}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| # Get the benefactor for the entity | ||
| benefactor = await get_entity_benefactor(entity_id, synapse_client=synapse_client) | ||
| if benefactor.id != entity_id: | ||
| if modify_benefactor: | ||
| entity_id = benefactor.id | ||
| elif warn_if_inherits: | ||
| client.logger.warning( | ||
| f"Creating an ACL for entity {entity_id}, which formerly inherited access control from a" | ||
| f' benefactor entity, "{benefactor.name}" ({benefactor.id}).' | ||
| ) | ||
| try: | ||
| acl = await get_entity_acl_with_benefactor( | ||
| entity_id=entity_id, synapse_client=synapse_client | ||
| ) | ||
| except SynapseHTTPError as e: | ||
| if ( | ||
| "The requested ACL does not exist. This entity inherits its permissions from:" | ||
| in str(e) | ||
| ): | ||
| acl = {"resourceAccess": []} | ||
| else: | ||
| raise e | ||
| # Get the principal ID as an integer | ||
| from synapseclient.api.user_services import get_user_by_principal_id_or_name | ||
| principal_id_int = await get_user_by_principal_id_or_name( | ||
| principal_id=principal_id, synapse_client=synapse_client | ||
| ) | ||
| # Find existing permissions for this principal | ||
| permissions_to_update = None | ||
| for permissions in acl["resourceAccess"]: | ||
| if ( | ||
| "principalId" in permissions | ||
| and permissions["principalId"] == principal_id_int | ||
| ): | ||
| permissions_to_update = permissions | ||
| break | ||
| if access_type is None or access_type == []: | ||
| # Remove permissions | ||
| if permissions_to_update and overwrite: | ||
| acl["resourceAccess"].remove(permissions_to_update) | ||
| else: | ||
| # Add or update permissions | ||
| if not permissions_to_update: | ||
| permissions_to_update = {"accessType": [], "principalId": principal_id_int} | ||
| acl["resourceAccess"].append(permissions_to_update) | ||
| if overwrite: | ||
| permissions_to_update["accessType"] = access_type | ||
| else: | ||
| permissions_to_update["accessType"] = list( | ||
| set(permissions_to_update["accessType"]) | set(access_type) | ||
| ) | ||
| benefactor_for_store = await get_entity_benefactor( | ||
| entity_id, synapse_client=synapse_client | ||
| ) | ||
| if benefactor_for_store.id == entity_id: | ||
| # Entity is its own benefactor, use PUT | ||
| return await put_entity_acl( | ||
| entity_id=entity_id, acl=acl, synapse_client=synapse_client | ||
| ) | ||
| else: | ||
| # Entity inherits ACL, use POST to create new ACL | ||
| return await post_entity_acl( | ||
| entity_id=entity_id, acl=acl, synapse_client=synapse_client | ||
| ) | ||
| async def get_entity_acl_list( | ||
| entity_id: str, | ||
| principal_id: Optional[str] = None, | ||
| check_benefactor: bool = True, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> List[str]: | ||
| """ | ||
| Get a list of permissions for a user or group on an entity. | ||
| Arguments: | ||
| entity_id: The ID of the entity. | ||
| principal_id: Identifier of a user or group to check permissions for. | ||
| If None, returns permissions for the current user. | ||
| check_benefactor: If True (default), check the benefactor for the entity | ||
| to get the ACL. If False, only check the entity itself. | ||
| 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 access types that the specified principal has on the entity. | ||
| Example: Get ACL list for a user | ||
| Get the permissions that a user has on an entity. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import get_entity_acl_list | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| # Get permissions for current user | ||
| permissions = await get_entity_acl_list(entity_id="syn123") | ||
| print(f"Current user permissions: {permissions}") | ||
| # Get permissions for specific user | ||
| permissions = await get_entity_acl_list( | ||
| entity_id="syn123", | ||
| principal_id="12345" | ||
| ) | ||
| print(f"User 12345 permissions: {permissions}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| from synapseclient import AUTHENTICATED_USERS, PUBLIC | ||
| from synapseclient.api.team_services import get_teams_for_user | ||
| from synapseclient.api.user_services import ( | ||
| get_user_bundle, | ||
| get_user_by_principal_id_or_name, | ||
| ) | ||
| # Get the ACL for the entity | ||
| acl = await get_entity_acl_with_benefactor( | ||
| entity_id=entity_id, | ||
| check_benefactor=check_benefactor, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| # Get the principal ID as an integer (None defaults to PUBLIC) | ||
| principal_id_int = await get_user_by_principal_id_or_name( | ||
| principal_id=principal_id, synapse_client=synapse_client | ||
| ) | ||
| # Get teams that the user belongs to | ||
| team_ids = [] | ||
| async for team in get_teams_for_user( | ||
| user_id=str(principal_id_int), synapse_client=synapse_client | ||
| ): | ||
| team_ids.append(int(team["id"])) | ||
| user_profile_bundle = await get_user_bundle( | ||
| user_id=principal_id_int, mask=1, synapse_client=synapse_client | ||
| ) | ||
| effective_permission_set = set() | ||
| # Loop over all permissions in the returned ACL and add it to the effective_permission_set | ||
| # if the principalId in the ACL matches | ||
| # 1) the one we are looking for, | ||
| # 2) a team the user is a member of, | ||
| # 3) PUBLIC | ||
| # 4) AUTHENTICATED_USERS (if user_profile_bundle exists for the principal_id) | ||
| for permissions in acl["resourceAccess"]: | ||
| if "principalId" in permissions and ( | ||
| permissions["principalId"] == principal_id_int | ||
| or permissions["principalId"] in team_ids | ||
| or permissions["principalId"] == PUBLIC | ||
| or ( | ||
| permissions["principalId"] == AUTHENTICATED_USERS | ||
| and user_profile_bundle is not None | ||
| ) | ||
| ): | ||
| effective_permission_set = effective_permission_set.union( | ||
| permissions["accessType"] | ||
| ) | ||
| return list(effective_permission_set) | ||
| async def update_entity_acl( | ||
| entity_id: str, | ||
| acl: Dict[str, Union[str, List[Dict[str, Union[int, List[str]]]]]], | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Union[str, List[Dict[str, Union[int, List[str]]]]]]: | ||
| """ | ||
| Create or update the Access Control List(ACL) for an entity. | ||
| Arguments: | ||
| entity_id: The ID of the entity. | ||
| acl: The ACL to be applied to the entity. Should match the format: | ||
| {'resourceAccess': [ | ||
| {'accessType': ['READ', 'DOWNLOAD'], | ||
| 'principalId': 222222} | ||
| ]} | ||
| 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 created or updated ACL matching | ||
| https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html | ||
| Example: Update entity ACL | ||
| Update the ACL for entity `syn123`. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import update_entity_acl | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| acl = { | ||
| 'resourceAccess': [ | ||
| {'accessType': ['READ', 'DOWNLOAD'], | ||
| 'principalId': 273948} | ||
| ] | ||
| } | ||
| updated_acl = await update_entity_acl(entity_id="syn123", acl=acl) | ||
| print(f"Updated ACL: {updated_acl}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| # Get the benefactor to determine whether to use PUT or POST | ||
| benefactor = await get_entity_benefactor( | ||
| entity_id=entity_id, synapse_client=synapse_client | ||
| ) | ||
| uri = f"/entity/{entity_id}/acl" | ||
| if benefactor.id == entity_id: | ||
| # Entity is its own benefactor, use PUT to update existing ACL | ||
| return await client.rest_put_async(uri=uri, body=json.dumps(acl)) | ||
| else: | ||
| # Entity inherits from a benefactor, use POST to create new ACL | ||
| return await client.rest_post_async(uri=uri, body=json.dumps(acl)) |
@@ -0,1 +1,8 @@ | ||
| """ | ||
| JSON Schema Services for Synapse | ||
| This module provides functions for interacting with JSON schemas in Synapse, | ||
| including managing organizations, schemas, and entity bindings. | ||
| """ | ||
| import json | ||
@@ -9,3 +16,5 @@ from typing import ( | ||
| List, | ||
| Mapping, | ||
| Optional, | ||
| Sequence, | ||
| Union, | ||
@@ -26,16 +35,23 @@ ) | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Union[Dict[str, Any], str]: | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| <https://rest-docs.synapse.org/rest/PUT/entity/id/schema/binding.html> | ||
| Bind a JSON schema to a Synapse entity. | ||
| Bind a JSON schema to an entity | ||
| This creates a binding between an entity and a JSON schema, which enables | ||
| schema validation for the entity. When bound, the entity's annotations | ||
| will be validated against the schema requirements. | ||
| Arguments: | ||
| synapse_id: Synapse Entity or Synapse Id | ||
| json_schema_uri: JSON schema URI | ||
| enable_derived_annotations: If True, derived annotations will be enabled for this entity | ||
| 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_id: The Synapse ID of the entity to bind the schema to | ||
| json_schema_uri: The $id URI of the JSON schema to bind (e.g., "my.org-schema.name-1.0.0") | ||
| enable_derived_annotations: If True, enables automatic generation of derived annotations | ||
| from the schema for this entity. Defaults to False. | ||
| 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 JsonSchemaObjectBinding object containing the binding details. | ||
| Object matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchemaObjectBinding.html> | ||
@@ -58,15 +74,23 @@ """ | ||
| synapse_id: str, *, synapse_client: Optional["Synapse"] = None | ||
| ) -> Union[Dict[str, Any], str]: | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| Get the JSON schema binding for a Synapse entity. | ||
| <https://rest-docs.synapse.org/rest/GET/entity/id/schema/binding.html> | ||
| Get bound schema from entity | ||
| Retrieves information about any JSON schema that is currently bound to the specified entity. | ||
| Arguments: | ||
| synapse_id: Synapse Id | ||
| 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_id: The Synapse ID of the entity to check for schema bindings | ||
| 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 JsonSchemaObjectBinding object if a schema is bound, containing: | ||
| - entityId: The entity ID | ||
| - schema$id: The URI of the bound schema | ||
| - enableDerivedAnnotations: Whether derived annotations are enabled | ||
| Object matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchemaObjectBinding.html> | ||
@@ -86,9 +110,12 @@ """ | ||
| Delete bound schema from entity | ||
| Remove the JSON schema binding from a Synapse entity. | ||
| This unbinds any JSON schema from the specified entity, removing schema validation | ||
| requirements and stopping the generation of derived annotations. | ||
| Arguments: | ||
| synapse_id: Synapse Id | ||
| 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_id: The Synapse ID of the entity to unbind the schema from | ||
| 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 | ||
| """ | ||
@@ -103,15 +130,28 @@ from synapseclient import Synapse | ||
| synapse_id: str, *, synapse_client: Optional["Synapse"] = None | ||
| ) -> Union[Dict[str, Union[str, bool]], str]: | ||
| ) -> Dict[str, Union[str, bool]]: | ||
| """ | ||
| <https://rest-docs.synapse.org/rest/GET/entity/id/schema/validation.html> | ||
| Get validation results of an entity against bound JSON schema | ||
| Validate a Synapse entity against its bound JSON schema. | ||
| Checks whether the entity's annotations conform to the requirements of its bound JSON schema. | ||
| The entity must have a schema binding for this operation to work. | ||
| Arguments: | ||
| synapse_id: Synapse Id | ||
| 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_id: The Synapse ID of the entity to validate | ||
| 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: | ||
| Object matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/ValidationResults.html> | ||
| A ValidationResults object containing: | ||
| - objectId: The entity ID that was validated | ||
| - objectType: The type of object (typically "entity") | ||
| - isValid: Boolean indicating if the entity passes validation | ||
| - validatedOn: Timestamp of when validation was performed | ||
| - validationErrorMessage: Error details if validation failed | ||
| - validationException: Exception details if validation failed | ||
| """ | ||
@@ -128,15 +168,22 @@ from synapseclient import Synapse | ||
| """ | ||
| Get validation statistics for a container entity (Project or Folder). | ||
| Returns summary statistics about JSON schema validation results for all child entities | ||
| of the specified container that have schema bindings. | ||
| <https://rest-docs.synapse.org/rest/GET/entity/id/schema/validation/statistics.html> | ||
| Get the summary statistic of json schema validation results for | ||
| a container entity | ||
| Arguments: | ||
| synapse_id: Synapse Id | ||
| 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_id: The Synapse ID of the container entity (Project or Folder) | ||
| 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: | ||
| Object matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/ValidationSummaryStatistics.html> | ||
| A ValidationSummaryStatistics object containing: | ||
| - containerId: The container entity ID | ||
| - totalNumberOfChildren: Total child entities in the container | ||
| - numberOfValidChildren: Number of children that pass validation | ||
| - numberOfInvalidChildren: Number of children that fail validation | ||
| - numberOfUnknownChildren: Number of children with unknown validation status | ||
| """ | ||
@@ -155,21 +202,30 @@ from synapseclient import Synapse | ||
| """ | ||
| <https://rest-docs.synapse.org/rest/POST/entity/id/schema/validation/invalid.html> | ||
| Get a single page of invalid JSON schema validation results for a container Entity | ||
| (Project or Folder). | ||
| Get all invalid JSON schema validation results for a container entity. | ||
| Returns detailed validation results for all child entities of the specified container | ||
| that fail their JSON schema validation. Results are paginated automatically. | ||
| Arguments: | ||
| synapse_id: Synapse Id | ||
| 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: | ||
| synapse_id: The Synapse ID of the container entity (Project or Folder) | ||
| 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 | ||
| Yields: | ||
| Object matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/ValidationResults.html> | ||
| ValidationResults objects for each invalid child entity, containing: | ||
| - objectId: The child entity ID | ||
| - objectType: The type of object | ||
| - isValid: Always False for this endpoint | ||
| - validationErrorMessage: Details about why validation failed | ||
| - validationException: Exception details | ||
| """ | ||
| request_body = {"containerId": synapse_id} | ||
| response = rest_post_paginated_async( | ||
| f"/entity/{synapse_id}/schema/validation/invalid", | ||
| body=json.dumps(request_body), | ||
| body=request_body, | ||
| synapse_client=synapse_client, | ||
@@ -185,14 +241,16 @@ ) | ||
| """ | ||
| <https://rest-docs.synapse.org/rest/POST/entity/id/schema/validation/invalid.html> | ||
| Get a single page of invalid JSON schema validation results for a container Entity | ||
| Get a single page of invalid JSON schema validation results for a container entity | ||
| (Project or Folder). | ||
| Arguments: | ||
| synapse_id: Synapse Id | ||
| 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: | ||
| synapse_id: The Synapse ID of the container entity (Project or Folder) | ||
| 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 | ||
| Yields: | ||
| ValidationResults objects for each invalid child entity. | ||
| Object matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/ValidationResults.html> | ||
@@ -218,8 +276,16 @@ """ | ||
| Retrieve derived JSON schema keys for a given Synapse entity. | ||
| Get the derived annotation keys for a Synapse entity with a bound JSON schema. | ||
| When an entity has a JSON schema binding with derived annotations enabled, | ||
| Synapse can automatically generate annotation keys based on the schema structure. | ||
| This function retrieves those derived keys. | ||
| Arguments: | ||
| synapse_id (str): The Synapse ID of the entity for which to retrieve derived keys. | ||
| synapse_id: The Synapse ID of the entity to get derived keys for | ||
| 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 Keys object containing a list of derived annotation key names. | ||
| Object matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/annotation/v2/Keys.html> | ||
@@ -231,1 +297,446 @@ """ | ||
| return await client.rest_get_async(uri=f"/entity/{synapse_id}/derivedKeys") | ||
| async def create_organization( | ||
| organization_name: str, *, synapse_client: Optional["Synapse"] = None | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| Create a new JSON schema organization. | ||
| Creates a new organization with a unique name that will serve as a namespace for JSON schemas. | ||
| The new organization will have an auto-generated AccessControlList (ACL) granting the caller | ||
| all relevant permissions. Organization names must be at least 6 characters and follow specific | ||
| naming conventions. | ||
| Arguments: | ||
| organization_name: Unique name for the organization. Must be at least 6 characters, | ||
| cannot start with a number, and should follow dot-separated alphanumeric | ||
| format (e.g., "my.organization") | ||
| 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: | ||
| An Organization object containing: | ||
| - id: The numeric identifier of the organization | ||
| - name: The organization name | ||
| - createdOn: Creation timestamp | ||
| - createdBy: ID of the user who created the organization | ||
| Object matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/Organization.html> | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| request_body = {"organizationName": organization_name} | ||
| return await client.rest_post_async( | ||
| uri="/schema/organization", body=json.dumps(request_body) | ||
| ) | ||
| async def get_organization( | ||
| organization_name: str, *, synapse_client: Optional["Synapse"] = None | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| Get an organization by name. | ||
| Looks up an existing JSON schema organization by its unique name. | ||
| Arguments: | ||
| organization_name: The name of the organization to retrieve | ||
| 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: | ||
| An Organization object containing: | ||
| - id: The numeric identifier of the organization | ||
| - name: The organization name | ||
| - createdOn: Creation timestamp | ||
| - createdBy: ID of the user who created the organization | ||
| Object matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/Organization.html> | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| return await client.rest_get_async( | ||
| uri=f"/schema/organization?name={organization_name}" | ||
| ) | ||
| async def list_organizations( | ||
| *, synapse_client: Optional["Synapse"] = None | ||
| ) -> AsyncGenerator[Dict[str, Any], None]: | ||
| """ | ||
| Generator to list all JSON schema organizations. | ||
| Retrieves a list of all organizations that are visible to the caller. This operation | ||
| does not require authentication and will return all publicly visible organizations. | ||
| 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 generator of Organization objects, each containing: | ||
| - id: The numeric identifier of the organization | ||
| - name: The organization name | ||
| - createdOn: Creation timestamp | ||
| - createdBy: ID of the user who created the organization | ||
| Object matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/Organization.html> | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| request_body = {} | ||
| async for item in rest_post_paginated_async( | ||
| "/schema/organization/list", body=request_body, synapse_client=client | ||
| ): | ||
| yield item | ||
| def list_organizations_sync( | ||
| *, synapse_client: Optional["Synapse"] = None | ||
| ) -> Generator[Dict[str, Any], None, None]: | ||
| """ | ||
| Generator to list all JSON schema organizations. | ||
| Retrieves a list of all organizations that are visible to the caller. This operation | ||
| does not require authentication and will return all publicly visible organizations. | ||
| 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 generator of Organization objects, each containing: | ||
| - id: The numeric identifier of the organization | ||
| - name: The organization name | ||
| - createdOn: Creation timestamp | ||
| - createdBy: ID of the user who created the organization | ||
| Object matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/Organization.html> | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| request_body = {} | ||
| for item in client._POST_paginated("/schema/organization/list", body=request_body): | ||
| yield item | ||
| async def delete_organization( | ||
| organization_id: str, *, synapse_client: Optional["Synapse"] = None | ||
| ) -> None: | ||
| """ | ||
| Delete a JSON schema organization. | ||
| Deletes the specified organization. All schemas defined within the organization's | ||
| namespace must be deleted first before the organization can be deleted. The caller | ||
| must have ACCESS_TYPE.DELETE permission on the organization. | ||
| Arguments: | ||
| organization_id: The numeric identifier of the organization to delete | ||
| 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 | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| await client.rest_delete_async(uri=f"/schema/organization/{organization_id}") | ||
| async def get_organization_acl( | ||
| organization_id: str, *, synapse_client: Optional["Synapse"] = None | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| Get the Access Control List (ACL) for a JSON schema organization. | ||
| Retrieves the permissions associated with the specified organization. The caller | ||
| must have ACCESS_TYPE.READ permission on the organization to view its ACL. | ||
| Arguments: | ||
| organization_id: The numeric identifier of the organization | ||
| 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: | ||
| An AccessControlList object containing: | ||
| - id: The organization ID | ||
| - creationDate: The date the ACL was created | ||
| - etag: The etag for concurrency control | ||
| - resourceAccess: List of ResourceAccess objects with principalId and accessType arrays matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/ResourceAccess.html> | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| return await client.rest_get_async( | ||
| uri=f"/schema/organization/{organization_id}/acl" | ||
| ) | ||
| async def update_organization_acl( | ||
| organization_id: str, | ||
| resource_access: Sequence[Mapping[str, Sequence[str]]], | ||
| etag: str, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| Update the Access Control List (ACL) for a JSON schema organization. | ||
| Updates the permissions for the specified organization. The caller must have | ||
| ACCESS_TYPE.CHANGE_PERMISSIONS permission on the organization. The etag from | ||
| a previous get_organization_acl() call is required for concurrency control. | ||
| Arguments: | ||
| organization_id: The numeric identifier of the organization | ||
| resource_access: List of ResourceAccess objects, each containing: | ||
| - principalId: The user or team ID | ||
| - accessType: List of permission types (e.g., ["READ", "CREATE", "DELETE"]) | ||
| 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 | ||
| Returns: | ||
| The updated AccessControlList object matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/AccessControlList.html> | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| request_body = {"resourceAccess": resource_access, "etag": etag} | ||
| return await client.rest_put_async( | ||
| uri=f"/schema/organization/{organization_id}/acl", body=json.dumps(request_body) | ||
| ) | ||
| async def list_json_schemas( | ||
| organization_name: str, *, synapse_client: Optional["Synapse"] = None | ||
| ) -> AsyncGenerator[Dict[str, Any], None]: | ||
| """ | ||
| List all JSON schemas for an organization. | ||
| Retrieves all JSON schemas that belong to the specified organization. This operation | ||
| does not require authentication and will return all publicly visible schemas. | ||
| Arguments: | ||
| organization_name: The name of the organization to list schemas for | ||
| 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 generator of JsonSchemaInfo objects, each containing: | ||
| - organizationId: The Synapse issued numeric identifier for the organization. | ||
| - organizationName: The name of the organization to which this schema belongs. | ||
| - schemaId: The Synapse issued numeric identifier for the schema. | ||
| - schemaName: The name of the this schema. | ||
| - createdOn: The date this JSON schema was created. | ||
| - createdBy: The ID of the user that created this JSON schema. | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| request_body = {"organizationName": organization_name} | ||
| async for item in rest_post_paginated_async( | ||
| "/schema/list", body=request_body, synapse_client=client | ||
| ): | ||
| yield item | ||
| def list_json_schemas_sync( | ||
| organization_name: str, *, synapse_client: Optional["Synapse"] = None | ||
| ) -> Generator[Dict[str, Any], None, None]: | ||
| """ | ||
| List all JSON schemas for an organization. | ||
| Retrieves all JSON schemas that belong to the specified organization. This operation | ||
| does not require authentication and will return all publicly visible schemas. | ||
| Arguments: | ||
| organization_name: The name of the organization to list schemas for | ||
| 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 generator of JsonSchemaInfo objects, each containing: | ||
| - organizationId: The Synapse issued numeric identifier for the organization. | ||
| - organizationName: The name of the organization to which this schema belongs. | ||
| - schemaId: The Synapse issued numeric identifier for the schema. | ||
| - schemaName: The name of the this schema. | ||
| - createdOn: The date this JSON schema was created. | ||
| - createdBy: The ID of the user that created this JSON schema. | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| request_body = {"organizationName": organization_name} | ||
| for item in client._POST_paginated("/schema/list", body=request_body): | ||
| yield item | ||
| async def list_json_schema_versions( | ||
| organization_name: str, | ||
| json_schema_name: str, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> AsyncGenerator[Dict[str, Any], None]: | ||
| """ | ||
| List version information for a JSON schema. | ||
| Retrieves version information for all versions of the specified JSON schema within | ||
| an organization. This shows the history and available versions of a schema. | ||
| Arguments: | ||
| organization_name: The name of the organization containing the schema | ||
| json_schema_name: The name of the JSON schema to list versions for | ||
| 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 generator of JsonSchemaVersionInfo objects, each containing: | ||
| - organizationId: The Synapse issued numeric identifier for the organization. | ||
| - organizationName: The name of the organization to which this schema belongs. | ||
| - schemaName: The name of the this schema. | ||
| - schemaId: The Synapse issued numeric identifier for the schema. | ||
| - versionId: The Synapse issued numeric identifier for this version. | ||
| - $id: The full '$id' of this schema version | ||
| - semanticVersion: The semantic version label provided when this version was created. Can be null if a semantic version was not provided when this version was created. | ||
| - createdOn: The date this JSON schema version was created. | ||
| - createdBy: The ID of the user that created this JSON schema version. | ||
| - jsonSHA256Hex: The SHA-256 hexadecimal hash of the UTF-8 encoded JSON schema. | ||
| Object matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchemaVersionInfo.html> | ||
| """ | ||
| request_body = { | ||
| "organizationName": organization_name, | ||
| "schemaName": json_schema_name, | ||
| } | ||
| async for item in rest_post_paginated_async( | ||
| "/schema/version/list", body=request_body, synapse_client=synapse_client | ||
| ): | ||
| yield item | ||
| def list_json_schema_versions_sync( | ||
| organization_name: str, | ||
| json_schema_name: str, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Generator[Dict[str, Any], None, None]: | ||
| """ | ||
| List version information for a JSON schema. | ||
| Retrieves version information for all versions of the specified JSON schema within | ||
| an organization. This shows the history and available versions of a schema. | ||
| Arguments: | ||
| organization_name: The name of the organization containing the schema | ||
| json_schema_name: The name of the JSON schema to list versions for | ||
| 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 generator of JsonSchemaVersionInfo objects, each containing: | ||
| - organizationId: The Synapse issued numeric identifier for the organization. | ||
| - organizationName: The name of the organization to which this schema belongs. | ||
| - schemaName: The name of the this schema. | ||
| - schemaId: The Synapse issued numeric identifier for the schema. | ||
| - versionId: The Synapse issued numeric identifier for this version. | ||
| - $id: The full '$id' of this schema version | ||
| - semanticVersion: The semantic version label provided when this version was created. Can be null if a semantic version was not provided when this version was created. | ||
| - createdOn: The date this JSON schema version was created. | ||
| - createdBy: The ID of the user that created this JSON schema version. | ||
| - jsonSHA256Hex: The SHA-256 hexadecimal hash of the UTF-8 encoded JSON schema. | ||
| Object matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchemaVersionInfo.html> | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| request_body = { | ||
| "organizationName": organization_name, | ||
| "schemaName": json_schema_name, | ||
| } | ||
| for item in client._POST_paginated("/schema/version/list", body=request_body): | ||
| yield item | ||
| async def get_json_schema_body( | ||
| json_schema_uri: str, *, synapse_client: Optional["Synapse"] = None | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| Get a registered JSON schema by its $id URI. | ||
| Retrieves the full JSON schema content using its unique $id identifier. This operation | ||
| does not require authentication for publicly registered schemas. | ||
| Arguments: | ||
| json_schema_uri: The relative $id of the JSON schema to get (e.g., "my.org-schema.name-1.0.0") | ||
| 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 complete JSON schema object as a dictionary, matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/schema/JsonSchema.html> | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| return await client.rest_get_async(uri=f"/schema/type/registered/{json_schema_uri}") | ||
| async def delete_json_schema( | ||
| json_schema_uri: str, *, synapse_client: Optional["Synapse"] = None | ||
| ) -> None: | ||
| """ | ||
| Delete a JSON schema by its $id URI. | ||
| Deletes the specified JSON schema. If the $id excludes a semantic version, all versions | ||
| of the schema will be deleted. If the $id includes a semantic version, only that specific | ||
| version will be deleted. The caller must have ACCESS_TYPE.DELETE permission on the | ||
| schema's organization. | ||
| Arguments: | ||
| json_schema_uri: The $id URI of the schema to delete. Examples: | ||
| - "my.org-schema.name" (deletes all versions) | ||
| - "my.org-schema.name-1.0.0" (deletes only version 1.0.0) | ||
| 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 | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| await client.rest_delete_async(uri=f"/schema/type/registered/{json_schema_uri}") |
@@ -8,4 +8,6 @@ """ | ||
| from enum import Enum | ||
| from typing import TYPE_CHECKING, List, Optional | ||
| from typing import TYPE_CHECKING, AsyncGenerator, Dict, Generator, List, Optional, Union | ||
| from synapseclient.core.utils import delete_none_keys, id_of | ||
| if TYPE_CHECKING: | ||
@@ -47,2 +49,50 @@ from synapseclient import Synapse | ||
| async def create_table_snapshot( | ||
| table_id: str, | ||
| comment: Optional[str] = None, | ||
| label: Optional[str] = None, | ||
| activity_id: Optional[str] = None, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Union[str, int]]: | ||
| """ | ||
| Creates a table snapshot using the Synapse REST API. | ||
| Arguments: | ||
| table_id: Table ID to create a snapshot for. | ||
| comment: Optional snapshot comment. | ||
| label: Optional snapshot label. | ||
| activity_id: Optional activity ID or activity instance applied to snapshot version. | ||
| 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 containing the snapshot response. | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| if activity_id and not isinstance(activity_id, str): | ||
| activity_id = str(activity_id) | ||
| snapshot_body = { | ||
| "snapshotComment": comment, | ||
| "snapshotLabel": label, | ||
| "snapshotActivityId": activity_id, | ||
| } | ||
| delete_none_keys(snapshot_body) | ||
| table_id = id_of(table_id) | ||
| uri = f"/entity/{table_id}/table/snapshot" | ||
| snapshot_response = await client.rest_post_async( | ||
| uri, body=json.dumps(snapshot_body) | ||
| ) | ||
| return snapshot_response | ||
| async def get_columns( | ||
@@ -78,2 +128,93 @@ table_id: str, | ||
| async def get_column( | ||
| column_id: str, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Union[str, int]]: | ||
| """Get a single column by ID. | ||
| Arguments: | ||
| column_id: The ID of the column to retrieve. | ||
| 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 column matching <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/ColumnModel.html> | ||
| """ | ||
| from synapseclient import Synapse | ||
| return await Synapse.get_client(synapse_client=synapse_client).rest_get_async( | ||
| f"/column/{column_id}", | ||
| ) | ||
| async def list_columns( | ||
| prefix: Optional[str] = None, | ||
| limit: int = 100, | ||
| offset: int = 0, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> AsyncGenerator["Column", None]: | ||
| """List columns with optional prefix filtering. | ||
| Arguments: | ||
| prefix: Optional prefix to filter columns by name. | ||
| limit: Number of columns to retrieve per request to Synapse (pagination parameter). | ||
| The function will continue retrieving results until all matching columns are returned. | ||
| offset: The index of the first column to return (pagination parameter). | ||
| 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. | ||
| Yields: Column instances. | ||
| """ | ||
| from synapseclient.api.api_client import rest_get_paginated_async | ||
| from synapseclient.models import Column | ||
| if prefix is None: | ||
| uri = "/column" | ||
| else: | ||
| uri = f"/column?prefix={prefix}" | ||
| async for result in rest_get_paginated_async( | ||
| uri, limit=limit, offset=offset, synapse_client=synapse_client | ||
| ): | ||
| yield Column().fill_from_dict(synapse_column=result) | ||
| def list_columns_sync( | ||
| prefix: Optional[str] = None, | ||
| limit: int = 100, | ||
| offset: int = 0, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Generator["Column", None, None]: | ||
| """List columns with optional prefix filtering (synchronous version). | ||
| Arguments: | ||
| prefix: Optional prefix to filter columns by name. | ||
| limit: Number of columns to retrieve per request to Synapse (pagination parameter). | ||
| The function will continue retrieving results until all matching columns are returned. | ||
| offset: The index of the first column to return (pagination parameter). | ||
| 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. | ||
| Yields: Column instances. | ||
| """ | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Column | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| if prefix is None: | ||
| uri = "/column" | ||
| else: | ||
| uri = f"/column?prefix={prefix}" | ||
| for result in client._GET_paginated(uri, limit=limit, offset=offset): | ||
| yield Column().fill_from_dict(synapse_column=result) | ||
| async def post_columns( | ||
@@ -80,0 +221,0 @@ columns: List["Column"], *, synapse_client: Optional["Synapse"] = None |
@@ -6,4 +6,8 @@ """This module is responsible for exposing the services defined at: | ||
| import json | ||
| from typing import TYPE_CHECKING, Dict, List, Optional, Union | ||
| from typing import TYPE_CHECKING, AsyncGenerator, Dict, List, Optional, Union | ||
| from synapseclient.api import rest_get_paginated_async | ||
| from synapseclient.core.exceptions import SynapseNotFoundError | ||
| from synapseclient.core.utils import id_of | ||
| if TYPE_CHECKING: | ||
@@ -45,1 +49,414 @@ from synapseclient import Synapse | ||
| return None | ||
| async def create_team( | ||
| name: str, | ||
| description: Optional[str] = None, | ||
| icon: Optional[str] = None, | ||
| can_public_join: bool = False, | ||
| can_request_membership: bool = True, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict: | ||
| """ | ||
| Creates a new team. | ||
| Arguments: | ||
| name: The name of the team to create. | ||
| description: A description of the team. | ||
| icon: The FileHandleID of the icon to be used for the team. | ||
| can_public_join: Whether the team can be joined by anyone. Defaults to False. | ||
| can_request_membership: Whether the team can request membership. Defaults to True. | ||
| 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: | ||
| Dictionary representing the created team | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| request_body = { | ||
| "name": name, | ||
| "description": description, | ||
| "icon": icon, | ||
| "canPublicJoin": can_public_join, | ||
| "canRequestMembership": can_request_membership, | ||
| } | ||
| response = await client.rest_post_async(uri="/team", body=json.dumps(request_body)) | ||
| return response | ||
| async def delete_team( | ||
| id: int, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> None: | ||
| """ | ||
| Deletes a team. | ||
| Arguments: | ||
| id: The ID of the team to delete. | ||
| 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. | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| await client.rest_delete_async(uri=f"/team/{id}") | ||
| async def get_teams_for_user( | ||
| user_id: str, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> AsyncGenerator[Dict, None]: | ||
| """ | ||
| Retrieve teams for the matching user ID as an async generator. | ||
| This function yields team dictionaries one by one as they are retrieved from the | ||
| paginated API response, allowing for memory-efficient processing of large result sets. | ||
| Arguments: | ||
| user_id: Identifier of a user. | ||
| 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. | ||
| Yields: | ||
| Team dictionaries that the user is a member of. Each dictionary matches the | ||
| <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/Team.html> | ||
| structure. | ||
| """ | ||
| async for result in rest_get_paginated_async( | ||
| uri=f"/user/{user_id}/team", synapse_client=synapse_client | ||
| ): | ||
| yield result | ||
| async def get_team( | ||
| id: Union[int, str], | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict: | ||
| """ | ||
| Finds a team with a given ID or name. | ||
| Arguments: | ||
| id: The ID or name of the team to retrieve. | ||
| 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: | ||
| Dictionary representing the team | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| # Retrieves team id | ||
| teamid = id_of(id) | ||
| try: | ||
| int(teamid) | ||
| except (TypeError, ValueError): | ||
| if isinstance(id, str): | ||
| teams = await find_team(id, synapse_client=client) | ||
| for team in teams: | ||
| if team.get("name") == id: | ||
| teamid = team.get("id") | ||
| break | ||
| else: | ||
| raise ValueError(f'Can\'t find team "{teamid}"') | ||
| else: | ||
| raise ValueError(f'Can\'t find team "{teamid}"') | ||
| response = await client.rest_get_async(uri=f"/team/{teamid}") | ||
| return response | ||
| async def find_team( | ||
| name: str, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> List[Dict]: | ||
| """ | ||
| Retrieve a Teams matching the supplied name fragment | ||
| Arguments: | ||
| name: A team name | ||
| 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: | ||
| List of team dictionaries | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| results = [] | ||
| async for result in rest_get_paginated_async( | ||
| uri=f"/teams?fragment={name}", synapse_client=client | ||
| ): | ||
| results.append(result) | ||
| return results | ||
| async def get_team_members( | ||
| team: Union[int, str], | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> List[Dict]: | ||
| """ | ||
| Lists the members of the given team. | ||
| Arguments: | ||
| team: A team ID or name. | ||
| 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: | ||
| List of team member dictionaries | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| team_id = id_of(team) | ||
| results = [] | ||
| async for result in rest_get_paginated_async( | ||
| uri=f"/teamMembers/{team_id}", synapse_client=client | ||
| ): | ||
| results.append(result) | ||
| return results | ||
| async def send_membership_invitation( | ||
| team_id: int, | ||
| invitee_id: Optional[str] = None, | ||
| invitee_email: Optional[str] = None, | ||
| message: Optional[str] = None, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict: | ||
| """ | ||
| Create a membership invitation and send an email notification to the invitee. | ||
| Arguments: | ||
| team_id: Synapse team ID | ||
| invitee_id: Synapse username or profile id of user | ||
| invitee_email: Email of user | ||
| message: Additional message for the user getting invited to the team. | ||
| 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: | ||
| MembershipInvitation dictionary | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| invite_request = {"teamId": str(team_id), "message": message} | ||
| if invitee_email is not None: | ||
| invite_request["inviteeEmail"] = str(invitee_email) | ||
| if invitee_id is not None: | ||
| invite_request["inviteeId"] = str(invitee_id) | ||
| response = await client.rest_post_async( | ||
| uri="/membershipInvitation", body=json.dumps(invite_request) | ||
| ) | ||
| return response | ||
| async def get_team_open_invitations( | ||
| team: Union[int, str], | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> List[Dict]: | ||
| """ | ||
| Retrieve the open requests submitted to a Team | ||
| Arguments: | ||
| team: A team ID or name. | ||
| 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: | ||
| List of MembershipRequest dictionaries | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| team_id = id_of(team) | ||
| results = [] | ||
| async for result in rest_get_paginated_async( | ||
| uri=f"/team/{team_id}/openInvitation", synapse_client=client | ||
| ): | ||
| results.append(result) | ||
| return results | ||
| async def get_membership_status( | ||
| user_id: str, | ||
| team: Union[int, str], | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict: | ||
| """ | ||
| Retrieve a user's Team Membership Status bundle. | ||
| Arguments: | ||
| user_id: Synapse user ID | ||
| team: A team ID or name. | ||
| 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: | ||
| Dictionary of TeamMembershipStatus: | ||
| <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/TeamMembershipStatus.html> | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| team_id = id_of(team) | ||
| uri = f"/team/{team_id}/member/{user_id}/membershipStatus" | ||
| response = await client.rest_get_async(uri=uri) | ||
| return response | ||
| async def delete_membership_invitation( | ||
| invitation_id: str, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> None: | ||
| """ | ||
| Delete an invitation. Note: The client must be an administrator of the | ||
| Team referenced by the invitation or the invitee to make this request. | ||
| Arguments: | ||
| invitation_id: Open invitation id | ||
| 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. | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| await client.rest_delete_async(uri=f"/membershipInvitation/{invitation_id}") | ||
| async def invite_to_team( | ||
| team: Union[int, str], | ||
| user: Optional[str] = None, | ||
| invitee_email: Optional[str] = None, | ||
| message: Optional[str] = None, | ||
| force: bool = False, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Optional[Dict]: | ||
| """ | ||
| Invite user to a Synapse team via Synapse username or email (choose one or the other) | ||
| Arguments: | ||
| team: A team ID or name. | ||
| user: Synapse username or profile id of user | ||
| invitee_email: Email of user | ||
| message: Additional message for the user getting invited to the team. | ||
| force: If an open invitation exists for the invitee, the old invite will be cancelled. | ||
| 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: | ||
| MembershipInvitation or None if user is already a member | ||
| """ | ||
| from synapseclient.models import UserProfile | ||
| # Input validation | ||
| id_email_specified = invitee_email is not None and user is not None | ||
| id_email_notspecified = invitee_email is None and user is None | ||
| if id_email_specified or id_email_notspecified: | ||
| raise ValueError("Must specify either 'user' or 'inviteeEmail'") | ||
| team_id = id_of(team) | ||
| is_member = False | ||
| open_invitations = await get_team_open_invitations( | ||
| team_id, synapse_client=synapse_client | ||
| ) | ||
| if user is not None: | ||
| try: | ||
| user_profile = await UserProfile(username=str(user)).get_async( | ||
| synapse_client=synapse_client | ||
| ) | ||
| except SynapseNotFoundError: | ||
| try: | ||
| user_profile = await UserProfile(id=int(user)).get_async( | ||
| synapse_client=synapse_client | ||
| ) | ||
| except (ValueError, TypeError) as ex: | ||
| raise SynapseNotFoundError(f'Can\'t find user "{user}"') from ex | ||
| invitee_id = user_profile.id | ||
| membership_status = await get_membership_status( | ||
| user_id=invitee_id, team=team_id, synapse_client=synapse_client | ||
| ) | ||
| is_member = membership_status["isMember"] | ||
| open_invites_to_user = [ | ||
| invitation | ||
| for invitation in open_invitations | ||
| if int(invitation.get("inviteeId")) == invitee_id | ||
| ] | ||
| else: | ||
| invitee_id = None | ||
| open_invites_to_user = [ | ||
| invitation | ||
| for invitation in open_invitations | ||
| if invitation.get("inviteeEmail") == invitee_email | ||
| ] | ||
| # Only invite if the invitee is not a member and | ||
| # if invitee doesn't have an open invitation unless force=True | ||
| if not is_member and (not open_invites_to_user or force): | ||
| # Delete all old invitations | ||
| for invite in open_invites_to_user: | ||
| await delete_membership_invitation( | ||
| invitation_id=invite["id"], synapse_client=synapse_client | ||
| ) | ||
| return await send_membership_invitation( | ||
| team_id, | ||
| invitee_id=invitee_id, | ||
| invitee_email=invitee_email, | ||
| message=message, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| else: | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| if is_member: | ||
| not_sent_reason = f"`{user_profile.username}` is already a member" | ||
| else: | ||
| not_sent_reason = ( | ||
| f"`{user_profile.username}` already has an open invitation " | ||
| "Set `force=True` to send new invite." | ||
| ) | ||
| client.logger.warning("No invitation sent: {}".format(not_sent_reason)) | ||
| return None |
@@ -5,4 +5,12 @@ """This module is responsible for exposing the services defined at: | ||
| import urllib.parse as urllib_urlparse | ||
| from typing import TYPE_CHECKING, Dict, List, Optional, Union | ||
| from synapseclient.api import rest_get_paginated_async | ||
| from synapseclient.core.exceptions import ( | ||
| SynapseError, | ||
| SynapseHTTPError, | ||
| SynapseNotFoundError, | ||
| ) | ||
| if TYPE_CHECKING: | ||
@@ -44,1 +52,287 @@ from synapseclient import Synapse | ||
| return [] | ||
| async def get_user_profile_by_id( | ||
| id: Optional[int] = None, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict: | ||
| """ | ||
| Get the details about a Synapse user by ID. | ||
| Retrieves information on the current user if 'id' is omitted. | ||
| Arguments: | ||
| id: The ownerId of a user | ||
| 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 user profile for the user of interest. | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| if id: | ||
| if not isinstance(id, int): | ||
| raise TypeError("id must be an 'ownerId' integer") | ||
| else: | ||
| id = "" | ||
| uri = f"/userProfile/{id}" | ||
| response = await client.rest_get_async(uri=uri) | ||
| return response | ||
| async def get_user_profile_by_username( | ||
| username: Optional[str] = None, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict: | ||
| """ | ||
| Get the details about a Synapse user by username. | ||
| Retrieves information on the current user if 'username' is omitted or empty string. | ||
| Arguments: | ||
| username: The userName of a user | ||
| 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 user profile for the user of interest. | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| is_none = username is None | ||
| is_str = isinstance(username, str) | ||
| if not is_str and not is_none: | ||
| raise TypeError("username must be string or None") | ||
| if is_str: | ||
| principals = await _find_principals(username, synapse_client=synapse_client) | ||
| for principal in principals: | ||
| if principal.get("userName", None).lower() == username.lower(): | ||
| id = principal["ownerId"] | ||
| break | ||
| else: | ||
| raise SynapseNotFoundError(f"Can't find user '{username}'") | ||
| else: | ||
| id = "" | ||
| uri = f"/userProfile/{id}" | ||
| response = await client.rest_get_async(uri=uri) | ||
| return response | ||
| async def is_user_certified( | ||
| user: Union[str, int], | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> bool: | ||
| """ | ||
| Determines whether a Synapse user is a certified user. | ||
| Arguments: | ||
| user: Synapse username or Id | ||
| 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: | ||
| True if the Synapse user is certified | ||
| """ | ||
| # Check if userid or username exists - get user profile first | ||
| try: | ||
| # if id is unset or a userID, this will succeed | ||
| user_id = "" if user is None else int(user) | ||
| except (TypeError, ValueError): | ||
| # It's a username, need to look it up | ||
| if isinstance(user, str): | ||
| principals = await _find_principals(user, synapse_client=synapse_client) | ||
| for principal in principals: | ||
| if principal.get("userName", None).lower() == user.lower(): | ||
| user_id = principal["ownerId"] | ||
| break | ||
| else: # no break | ||
| raise ValueError(f'Can\'t find user "{user}": ') | ||
| else: | ||
| raise ValueError(f"Invalid user identifier: {user}") | ||
| # Get passing record | ||
| try: | ||
| certification_status = await _get_certified_passing_record( | ||
| user_id, synapse_client=synapse_client | ||
| ) | ||
| return certification_status["passed"] | ||
| except SynapseHTTPError as ex: | ||
| if ex.response.status_code == 404: | ||
| # user hasn't taken the quiz | ||
| return False | ||
| raise | ||
| async def _find_principals( | ||
| query_string: str, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> List[Dict]: | ||
| """ | ||
| Find users or groups by name or email. | ||
| Arguments: | ||
| query_string: The string to search for | ||
| 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 userGroupHeader objects with fields displayName, email, firstName, lastName, isIndividual, ownerId | ||
| """ | ||
| uri = "/userGroupHeaders?prefix=%s" % urllib_urlparse.quote(query_string) | ||
| # Collect all results from the paginated endpoint | ||
| results = [] | ||
| async for result in rest_get_paginated_async( | ||
| uri=uri, synapse_client=synapse_client | ||
| ): | ||
| results.append(result) | ||
| return results | ||
| async def _get_certified_passing_record( | ||
| userid: int, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Dict[str, Union[str, int, bool, list]]: | ||
| """ | ||
| Retrieve the Passing Record on the User Certification test for the given user. | ||
| Arguments: | ||
| userid: Synapse user Id | ||
| 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: | ||
| Synapse Passing Record. A record of whether a given user passed a given test. | ||
| <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/quiz/PassingRecord.html> | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| response = await client.rest_get_async( | ||
| uri=f"/user/{userid}/certifiedUserPassingRecord" | ||
| ) | ||
| return response | ||
| async def get_user_by_principal_id_or_name( | ||
| principal_id: Optional[Union[str, int]] = None, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> int: | ||
| """ | ||
| Given either a string, int or None finds the corresponding user where None implies PUBLIC. | ||
| Arguments: | ||
| principal_id: Identifier of a user or group. '273948' is for all registered Synapse users | ||
| and '273949' is for public access. None implies public access. | ||
| 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 integer ID of the user. | ||
| Raises: | ||
| SynapseError: If the user cannot be found or is ambiguous. | ||
| Example: Get user ID by principal ID | ||
| Get the user ID for a given principal ID. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import get_user_by_principal_id_or_name | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| # Get public user ID | ||
| public_id = await get_user_by_principal_id_or_name(principal_id=None) | ||
| print(f"Public user ID: {public_id}") | ||
| # Get user ID by username | ||
| user_id = await get_user_by_principal_id_or_name(principal_id="username") | ||
| print(f"User ID for 'username': {user_id}") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| from synapseclient import PUBLIC, Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| if principal_id is None or principal_id == "PUBLIC": | ||
| return PUBLIC | ||
| try: | ||
| return int(principal_id) | ||
| # If principal_id is not a number assume it is a name or email | ||
| except ValueError as ex: | ||
| user_profiles = await client.rest_get_async( | ||
| uri=f"/userGroupHeaders?prefix={principal_id}" | ||
| ) | ||
| total_results = len(user_profiles["children"]) | ||
| if total_results == 1: | ||
| return int(user_profiles["children"][0]["ownerId"]) | ||
| elif total_results > 1: | ||
| for profile in user_profiles["children"]: | ||
| if profile["userName"] == principal_id: | ||
| return int(profile["ownerId"]) | ||
| supplemental_message = ( | ||
| "Please be more specific" if total_results > 1 else "No matches" | ||
| ) | ||
| raise SynapseError( | ||
| f"Unknown Synapse user ({principal_id}). {supplemental_message}." | ||
| ) from ex | ||
| async def get_user_bundle( | ||
| user_id: int, | ||
| mask: int, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Optional[Dict[str, Union[str, dict]]]: | ||
| """ | ||
| Retrieve the user bundle for the given user. | ||
| Arguments: | ||
| user_id: Synapse user Id | ||
| mask: Bit field indicating which components to include in the bundle. | ||
| 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: | ||
| Synapse User Bundle or None if user not found | ||
| <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/UserBundle.html> | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| try: | ||
| return await client.rest_get_async(uri=f"/user/{user_id}/bundle?mask={mask}") | ||
| except SynapseHTTPError as ex: | ||
| if ex.response.status_code == 404: | ||
| return None | ||
| raise |
@@ -5,3 +5,3 @@ """This utility class is to hold any utilities that are needed for async operations.""" | ||
| import functools | ||
| from typing import TYPE_CHECKING, Any, Callable, Coroutine, Union | ||
| from typing import Any, Callable, Coroutine, Union | ||
@@ -11,5 +11,2 @@ import nest_asyncio | ||
| if TYPE_CHECKING: | ||
| from synapseclient import Synapse | ||
| tracer = trace.get_tracer("synapseclient") | ||
@@ -82,3 +79,3 @@ | ||
| def wrap_async_to_sync(coroutine: Coroutine[Any, Any, Any], syn: "Synapse") -> Any: | ||
| def wrap_async_to_sync(coroutine: Coroutine[Any, Any, Any]) -> Any: | ||
| """Wrap an async function to be called in a sync context.""" | ||
@@ -99,2 +96,64 @@ loop = None | ||
| def wrap_async_generator_to_sync_generator(async_gen_func: Callable, *args, **kwargs): | ||
| """ | ||
| Wrap an async generator function to be called in a sync context, returning a sync generator. | ||
| This function takes an async generator function and its arguments, then yields items | ||
| synchronously by running the async generator in the appropriate event loop. | ||
| Arguments: | ||
| async_gen_func: The async generator function to wrap | ||
| *args: Positional arguments to pass to the async generator function | ||
| **kwargs: Keyword arguments to pass to the async generator function | ||
| Yields: | ||
| Items from the async generator, yielded synchronously | ||
| """ | ||
| loop = None | ||
| try: | ||
| loop = asyncio.get_running_loop() | ||
| except RuntimeError: | ||
| pass | ||
| if loop: | ||
| nest_asyncio.apply(loop=loop) | ||
| # Create the async generator | ||
| async_gen = async_gen_func(*args, **kwargs) | ||
| # Yield items from the async generator synchronously | ||
| try: | ||
| while True: | ||
| try: | ||
| item = loop.run_until_complete(async_gen.__anext__()) | ||
| yield item | ||
| except StopAsyncIteration: | ||
| break | ||
| finally: | ||
| # Ensure the generator is properly closed | ||
| try: | ||
| loop.run_until_complete(async_gen.aclose()) | ||
| except (RuntimeError, StopAsyncIteration): | ||
| pass | ||
| else: | ||
| # No running loop, create a new one | ||
| async def run_generator(): | ||
| async_gen = async_gen_func(*args, **kwargs) | ||
| items = [] | ||
| try: | ||
| async for item in async_gen: | ||
| items.append(item) | ||
| finally: | ||
| try: | ||
| await async_gen.aclose() | ||
| except (RuntimeError, StopAsyncIteration): | ||
| pass | ||
| return items | ||
| items = asyncio.run(run_generator()) | ||
| for item in items: | ||
| yield item | ||
| # Adapted from | ||
@@ -101,0 +160,0 @@ # https://github.com/keflavich/astroquery/blob/30deafc3aa057916bcdca70733cba748f1b36b64/astroquery/utils/process_asyncs.py#L11 |
@@ -68,2 +68,3 @@ """ | ||
| PROJECT_ENTITY = "org.sagebionetworks.repo.model.Project" | ||
| RECORD_SET_ENTITY = "org.sagebionetworks.repo.model.RecordSet" | ||
| TABLE_ENTITY = "org.sagebionetworks.repo.model.table.TableEntity" | ||
@@ -89,1 +90,42 @@ DATASET_ENTITY = "org.sagebionetworks.repo.model.table.Dataset" | ||
| AGENT_CHAT_REQUEST = "org.sagebionetworks.repo.model.agent.AgentChatRequest" | ||
| # JSON Schema | ||
| GET_VALIDATION_SCHEMA_REQUEST = ( | ||
| "org.sagebionetworks.repo.model.schema.GetValidationSchemaRequest" | ||
| ) | ||
| CREATE_SCHEMA_REQUEST = "org.sagebionetworks.repo.model.schema.CreateSchemaRequest" | ||
| # Query Table as a CSV | ||
| # https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/DownloadFromTableResult.html | ||
| QUERY_TABLE_CSV_REQUEST = ( | ||
| "org.sagebionetworks.repo.model.table.DownloadFromTableRequest" | ||
| ) | ||
| # Query Table Bundle Request | ||
| # https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/QueryBundleRequest.html | ||
| QUERY_BUNDLE_REQUEST = "org.sagebionetworks.repo.model.table.QueryBundleRequest" | ||
| QUERY_RESULT = "org.sagebionetworks.repo.model.table.QueryResult" | ||
| QUERY_TABLE_CSV_RESULT = "org.sagebionetworks.repo.model.table.DownloadFromTableResult" | ||
| # Curation Task Types | ||
| CURATION_TASK = "org.sagebionetworks.repo.model.curation.CurationTask" | ||
| FILE_BASED_METADATA_TASK_PROPERTIES = ( | ||
| "org.sagebionetworks.repo.model.curation.metadata.FileBasedMetadataTaskProperties" | ||
| ) | ||
| RECORD_BASED_METADATA_TASK_PROPERTIES = ( | ||
| "org.sagebionetworks.repo.model.curation.metadata.RecordBasedMetadataTaskProperties" | ||
| ) | ||
| # Grid Session Types | ||
| CREATE_GRID_REQUEST = "org.sagebionetworks.repo.model.grid.CreateGridRequest" | ||
| GRID_RECORD_SET_EXPORT_REQUEST = ( | ||
| "org.sagebionetworks.repo.model.grid.GridRecordSetExportRequest" | ||
| ) | ||
| LIST_GRID_SESSIONS_REQUEST = ( | ||
| "org.sagebionetworks.repo.model.grid.ListGridSessionsRequest" | ||
| ) | ||
| LIST_GRID_SESSIONS_RESPONSE = ( | ||
| "org.sagebionetworks.repo.model.grid.ListGridSessionsResponse" | ||
| ) |
@@ -52,17 +52,19 @@ """Logic required for the actual transferring of files.""" | ||
| Attributes: | ||
| file_handle_id : The file handle ID to download. | ||
| object_id : The Synapse object this file associated to. | ||
| file_handle_id : The file handle ID to download. Defaults to None. | ||
| object_id : The Synapse object this file associated to. Defaults to None. | ||
| object_type : The type of the associated Synapse object. Any of | ||
| <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/file/FileHandleAssociateType.html> | ||
| <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/file/FileHandleAssociateType.html>. Defaults to None. | ||
| path : The local path to download the file to. | ||
| This path can be either an absolute path or | ||
| a relative path from where the code is executed to the download location. | ||
| debug: A boolean to specify if debug mode is on. | ||
| a relative path from where the code is executed to the download location. Defaults to None. | ||
| debug: A boolean to specify if debug mode is on. Defaults to False. | ||
| presigned_url: Optional information about a presigned url to download the file. Defaults to None. | ||
| """ | ||
| file_handle_id: int | ||
| object_id: str | ||
| object_type: str | ||
| path: str | ||
| file_handle_id: int = None | ||
| object_id: str = None | ||
| object_type: str = None | ||
| path: str = None | ||
| debug: bool = False | ||
| presigned_url: Optional["PresignedUrlInfo"] = None | ||
@@ -299,3 +301,12 @@ | ||
| """ | ||
| url_provider = PresignedUrlProvider(self._syn, request=self._download_request) | ||
| if self._download_request.presigned_url is not None: | ||
| url_provider = PresignedUrlProvider( | ||
| self._syn, | ||
| request=self._download_request, | ||
| _cached_info=self._download_request.presigned_url, | ||
| ) | ||
| else: | ||
| url_provider = PresignedUrlProvider( | ||
| self._syn, request=self._download_request | ||
| ) | ||
@@ -312,5 +323,10 @@ file_size = await with_retry_time_based_async( | ||
| ) | ||
| # set postfix to object_id if not presigned url, otherwise set to file_name | ||
| if self._download_request.presigned_url is None: | ||
| postfix = self._download_request.object_id | ||
| else: | ||
| postfix = self._download_request.presigned_url.file_name | ||
| self._progress_bar = get_or_create_download_progress_bar( | ||
| file_size=file_size, | ||
| postfix=self._download_request.object_id, | ||
| postfix=postfix, | ||
| synapse_client=self._syn, | ||
@@ -317,0 +333,0 @@ ) |
@@ -34,2 +34,3 @@ """This module handles the various ways that a user can download a file to Synapse.""" | ||
| DownloadRequest, | ||
| PresignedUrlInfo, | ||
| PresignedUrlProvider, | ||
@@ -685,9 +686,10 @@ _pre_signed_url_expiration_time, | ||
| async def download_from_url_multi_threaded( | ||
| file_handle_id: str, | ||
| object_id: str, | ||
| object_type: str, | ||
| destination: str, | ||
| file_handle_id: Optional[str] = None, | ||
| object_id: Optional[str] = None, | ||
| object_type: Optional[str] = None, | ||
| *, | ||
| expected_md5: str = None, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| presigned_url: Optional[PresignedUrlInfo] = None, | ||
| ) -> str: | ||
@@ -698,14 +700,14 @@ """ | ||
| Arguments: | ||
| file_handle_id: The id of the FileHandle to download | ||
| object_id: The id of the Synapse object that uses the FileHandle | ||
| destination: The destination on local file system | ||
| file_handle_id: Optional. The id of the FileHandle to download | ||
| object_id: Optional. The id of the Synapse object that uses the FileHandle | ||
| e.g. "syn123" | ||
| object_type: The type of the Synapse object that uses the | ||
| object_type: Optional. The type of the Synapse object that uses the | ||
| FileHandle e.g. "FileEntity". Any of | ||
| <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/file/FileHandleAssociateType.html> | ||
| destination: The destination on local file system | ||
| expected_md5: The expected MD5 | ||
| content_size: The size of the content | ||
| 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. | ||
| presigned_url: Optional. PresignedUrlInfo object if given, the URL is already a pre-signed URL. | ||
| Raises: | ||
@@ -724,15 +726,34 @@ SynapseMd5MismatchError: If the actual MD5 does not match expected MD5. | ||
| ) | ||
| # check if the presigned url is expired | ||
| if presigned_url is not None: | ||
| if ( | ||
| presigned_url.expiration_utc | ||
| < datetime.datetime.now(tz=datetime.timezone.utc) | ||
| + PresignedUrlProvider._TIME_BUFFER | ||
| ): | ||
| raise SynapseError( | ||
| "The provided pre-signed URL has expired. Please provide a new pre-signed URL." | ||
| ) | ||
| request = DownloadRequest( | ||
| file_handle_id=int(file_handle_id), | ||
| object_id=object_id, | ||
| object_type=object_type, | ||
| path=temp_destination, | ||
| debug=client.debug, | ||
| ) | ||
| if not presigned_url.file_name: | ||
| raise SynapseError("The provided pre-signed URL is missing the file name.") | ||
| await download_file( | ||
| client=client, | ||
| download_request=request, | ||
| ) | ||
| if os.path.isdir(destination): | ||
| # If the destination is a directory, then the file name should be the same as the file name in the presigned url | ||
| # This is added to ensure the temp file can be copied to the desired destination without changing the file name | ||
| destination = os.path.join(destination, presigned_url.file_name) | ||
| request = DownloadRequest( | ||
| path=temp_destination, | ||
| debug=client.debug, | ||
| presigned_url=presigned_url, | ||
| ) | ||
| else: | ||
| request = DownloadRequest( | ||
| file_handle_id=int(file_handle_id), | ||
| object_id=object_id, | ||
| object_type=object_type, | ||
| path=temp_destination, | ||
| debug=client.debug, | ||
| ) | ||
| await download_file(client=client, download_request=request) | ||
@@ -760,7 +781,8 @@ if expected_md5: # if md5 not set (should be the case for all except http download) | ||
| destination: str, | ||
| entity_id: Optional[str], | ||
| file_handle_associate_type: Optional[str], | ||
| entity_id: Optional[str] = None, | ||
| file_handle_associate_type: Optional[str] = None, | ||
| file_handle_id: Optional[str] = None, | ||
| expected_md5: Optional[str] = None, | ||
| progress_bar: Optional[tqdm] = None, | ||
| url_is_presigned: Optional[bool] = False, | ||
| *, | ||
@@ -775,5 +797,5 @@ synapse_client: Optional["Synapse"] = None, | ||
| destination: The destination on local file system | ||
| entity_id: The id of the Synapse object that uses the FileHandle | ||
| entity_id: Optional. The id of the Synapse object that uses the FileHandle | ||
| e.g. "syn123" | ||
| file_handle_associate_type: The type of the Synapse object that uses the | ||
| file_handle_associate_type: Optional. The type of the Synapse object that uses the | ||
| FileHandle e.g. "FileEntity". Any of | ||
@@ -785,2 +807,4 @@ <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/file/FileHandleAssociateType.html> | ||
| expected_md5: Optional. If given, check that the MD5 of the downloaded file matches the expected MD5 | ||
| progress_bar: Optional progress bar to update during download | ||
| url_is_presigned: If True, the URL is already a pre-signed URL. | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -807,3 +831,6 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| delete_on_md5_mismatch = True | ||
| client.logger.debug(f"[{entity_id}]: Downloading from {url} to {destination}") | ||
| if entity_id: | ||
| client.logger.debug(f"[{entity_id}]: Downloading from {url} to {destination}") | ||
| else: | ||
| client.logger.debug(f"Downloading from {url} to {destination}") | ||
| span = trace.get_current_span() | ||
@@ -915,9 +942,14 @@ | ||
| if url_is_expired: | ||
| response = get_file_handle_for_download( | ||
| file_handle_id=file_handle_id, | ||
| synapse_id=entity_id, | ||
| entity_type=file_handle_associate_type, | ||
| synapse_client=client, | ||
| ) | ||
| url = response["preSignedURL"] | ||
| if url_is_presigned: | ||
| raise SynapseError( | ||
| "The provided pre-signed URL has expired. Please provide a new pre-signed URL." | ||
| ) | ||
| else: | ||
| response = get_file_handle_for_download( | ||
| file_handle_id=file_handle_id, | ||
| synapse_id=entity_id, | ||
| entity_type=file_handle_associate_type, | ||
| synapse_client=client, | ||
| ) | ||
| url = response["preSignedURL"] | ||
| response = with_retry( | ||
@@ -924,0 +956,0 @@ lambda url=url, range_header=range_header, auth=auth: client._requests_session.get( |
@@ -197,2 +197,3 @@ """Wrappers for remote file storage clients like S3 and SFTP.""" | ||
| import boto3.s3.transfer | ||
| from boto3.exceptions import S3UploadFailedError | ||
@@ -226,9 +227,23 @@ transfer_config = boto3.s3.transfer.TransferConfig( | ||
| # automatically determines whether to perform multi-part upload | ||
| s3.Bucket(bucket).upload_file( | ||
| upload_file_path, | ||
| remote_file_key, | ||
| Callback=progress_callback, | ||
| Config=transfer_config, | ||
| ExtraArgs={"ACL": "bucket-owner-full-control"}, | ||
| ) | ||
| try: | ||
| s3.Bucket(bucket).upload_file( | ||
| upload_file_path, | ||
| remote_file_key, | ||
| Callback=progress_callback, | ||
| Config=transfer_config, | ||
| ExtraArgs={"ACL": "bucket-owner-full-control"}, | ||
| ) | ||
| except S3UploadFailedError as upload_error: | ||
| if "Invalid canned ACL" in str(upload_error): | ||
| s3.Bucket(bucket).upload_file( | ||
| upload_file_path, | ||
| remote_file_key, | ||
| Callback=progress_callback, | ||
| Config=transfer_config, | ||
| # https://sagebionetworks.jira.com/browse/SYNPY-1198 | ||
| # IBM Based buckets enforce this by default, and does not support this additional setting | ||
| # ExtraArgs={"ACL": "bucket-owner-full-control"}, | ||
| ) | ||
| else: | ||
| raise upload_error | ||
| if progress_bar is not None: | ||
@@ -235,0 +250,0 @@ progress_bar.close() |
@@ -76,2 +76,6 @@ """A helper tool that allows the Python client to | ||
| NON_RETRYABLE_ERRORS = [ | ||
| "is not a table or view", | ||
| ] | ||
| DEBUG_EXCEPTION = "calling %s resulted in an Exception" | ||
@@ -246,2 +250,3 @@ | ||
| verbose: bool = False, | ||
| non_retryable_errors: List[str] = None, | ||
| ) -> Tuple[List[int], List[int], List[str], List[Union[Exception, str]], Logger]: | ||
@@ -257,2 +262,4 @@ """Assigns default values to the retry parameters.""" | ||
| retry_exceptions = [] | ||
| if not non_retryable_errors: | ||
| non_retryable_errors = NON_RETRYABLE_ERRORS | ||
@@ -269,2 +276,3 @@ if verbose: | ||
| logger, | ||
| non_retryable_errors, | ||
| ) | ||
@@ -287,2 +295,3 @@ | ||
| read_response_content: bool = True, | ||
| non_retryable_errors: List[str] = None, | ||
| ) -> Union[Exception, httpx.Response, Any, None]: | ||
@@ -314,2 +323,4 @@ """ | ||
| read_response_content: Whether to read the response content for HTTP requests. | ||
| non_retryable_errors: List of strings that if found in the response or exception | ||
| message will prevent a retry from occurring. | ||
@@ -330,2 +341,3 @@ Example: Using with_retry | ||
| logger, | ||
| non_retry_errors, | ||
| ) = _assign_default_values( | ||
@@ -337,2 +349,3 @@ retry_status_codes=retry_status_codes, | ||
| verbose=verbose, | ||
| non_retryable_errors=non_retryable_errors, | ||
| ) | ||
@@ -367,2 +380,3 @@ | ||
| retry_errors=retry_errors, | ||
| non_retryable_errors=non_retry_errors, | ||
| ) | ||
@@ -420,2 +434,3 @@ | ||
| read_response_content: bool = True, | ||
| non_retryable_errors: List[str] = None, | ||
| ) -> Union[Exception, httpx.Response, Any, None]: | ||
@@ -447,2 +462,4 @@ """ | ||
| read_response_content: Whether to read the response content for HTTP requests. | ||
| non_retryable_errors: List of strings that if found in the response or exception | ||
| message will prevent a retry from occurring. | ||
@@ -463,2 +480,3 @@ Example: Using with_retry | ||
| logger, | ||
| non_retry_errors, | ||
| ) = _assign_default_values( | ||
@@ -470,2 +488,3 @@ retry_status_codes=retry_status_codes, | ||
| verbose=verbose, | ||
| non_retryable_errors=non_retryable_errors, | ||
| ) | ||
@@ -500,2 +519,3 @@ | ||
| retry_errors=retry_errors, | ||
| non_retryable_errors=non_retry_errors, | ||
| ) | ||
@@ -547,2 +567,3 @@ | ||
| retry_errors: List[str], | ||
| non_retryable_errors: List[str], | ||
| ) -> bool: | ||
@@ -560,2 +581,3 @@ """Determines if a request should be retried based on the response and caught | ||
| retry_errors: The errors that should be retried. | ||
| non_retryable_errors: The errors that should not be retried. | ||
@@ -565,4 +587,14 @@ Returns: | ||
| """ | ||
| response_message = None | ||
| # Check if we got a retry-able HTTP error | ||
| if response is not None and hasattr(response, "status_code"): | ||
| # First check for non-retryable error patterns even in retry status codes | ||
| if response.status_code in retry_status_codes: | ||
| response_message = response_message or _get_message(response) | ||
| # Check for non-retryable error patterns that should never be retried | ||
| if response_message and any( | ||
| [pattern in response_message for pattern in non_retryable_errors] | ||
| ): | ||
| return False | ||
| if ( | ||
@@ -575,3 +607,3 @@ expected_status_codes and response.status_code not in expected_status_codes | ||
| # For all other non 200 messages look for retryable errors in the body or reason field | ||
| response_message = _get_message(response) | ||
| response_message = response_message or _get_message(response) | ||
| if ( | ||
@@ -578,0 +610,0 @@ any([msg.lower() in response_message.lower() for msg in retry_errors]) |
@@ -332,4 +332,3 @@ """Implements the client side of Synapse's | ||
| list(self._pre_signed_part_urls.keys()), | ||
| ), | ||
| syn=self._syn, | ||
| ) | ||
| ) | ||
@@ -336,0 +335,0 @@ |
@@ -10,2 +10,8 @@ # These are all of the models that are used by the Synapse client. | ||
| from synapseclient.models.annotations import Annotations | ||
| from synapseclient.models.curation import ( | ||
| CurationTask, | ||
| FileBasedMetadataTaskProperties, | ||
| Grid, | ||
| RecordBasedMetadataTaskProperties, | ||
| ) | ||
| from synapseclient.models.dataset import Dataset, DatasetCollection, EntityRef | ||
@@ -15,5 +21,7 @@ from synapseclient.models.entityview import EntityView, ViewTypeMask | ||
| from synapseclient.models.folder import Folder | ||
| from synapseclient.models.link import Link | ||
| from synapseclient.models.materializedview import MaterializedView | ||
| from synapseclient.models.mixins.table_components import QueryMixin | ||
| from synapseclient.models.project import Project | ||
| from synapseclient.models.recordset import RecordSet | ||
| from synapseclient.models.services import FailureStrategy | ||
@@ -23,2 +31,3 @@ from synapseclient.models.submissionview import SubmissionView | ||
| from synapseclient.models.table_components import ( | ||
| ActionRequiredCount, | ||
| AppendableRowSetRequest, | ||
@@ -34,4 +43,14 @@ Column, | ||
| PartialRowSet, | ||
| Query, | ||
| QueryBundleRequest, | ||
| QueryJob, | ||
| QueryNextPageToken, | ||
| QueryResult, | ||
| QueryResultBundle, | ||
| QueryResultOutput, | ||
| Row, | ||
| RowSet, | ||
| SchemaStorageStrategy, | ||
| SelectColumn, | ||
| SumFileSizes, | ||
| TableSchemaChangeRequest, | ||
@@ -41,4 +60,4 @@ TableUpdateTransaction, | ||
| ) | ||
| from synapseclient.models.team import Team, TeamMember | ||
| from synapseclient.models.user import UserPreference, UserProfile | ||
| from synapseclient.models.team import Team, TeamMember, TeamMembershipStatus | ||
| from synapseclient.models.user import UserGroupHeader, UserPreference, UserProfile | ||
| from synapseclient.models.virtualtable import VirtualTable | ||
@@ -54,8 +73,16 @@ | ||
| "Folder", | ||
| "Link", | ||
| "Project", | ||
| "RecordSet", | ||
| "Annotations", | ||
| "Team", | ||
| "TeamMember", | ||
| "TeamMembershipStatus", | ||
| "CurationTask", | ||
| "FileBasedMetadataTaskProperties", | ||
| "RecordBasedMetadataTaskProperties", | ||
| "Grid", | ||
| "UserProfile", | ||
| "UserPreference", | ||
| "UserGroupHeader", | ||
| "Agent", | ||
@@ -74,5 +101,5 @@ "AgentSession", | ||
| "ColumnType", | ||
| "SumFileSizes", | ||
| "FacetType", | ||
| "JsonSubColumn", | ||
| "QueryResultBundle", | ||
| "query_async", | ||
@@ -92,2 +119,13 @@ "query", | ||
| "VirtualTable", | ||
| "ActionRequiredCount", | ||
| "QueryBundleRequest", | ||
| "QueryNextPageToken", | ||
| "QueryResult", | ||
| "QueryResultBundle", | ||
| "QueryResultOutput", | ||
| "QueryJob", | ||
| "Query", | ||
| "Row", | ||
| "RowSet", | ||
| "SelectColumn", | ||
| # Dataset models | ||
@@ -94,0 +132,0 @@ "Dataset", |
@@ -1,2 +0,1 @@ | ||
| import asyncio | ||
| from dataclasses import dataclass, field | ||
@@ -7,7 +6,15 @@ from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Union | ||
| from synapseclient.activity import Activity as Synapse_Activity | ||
| from synapseclient.api import delete_entity_generated_by | ||
| from synapseclient.api import ( | ||
| create_activity, | ||
| delete_entity_generated_by, | ||
| delete_entity_provenance, | ||
| get_activity, | ||
| get_entity_provenance, | ||
| set_entity_provenance, | ||
| update_activity, | ||
| ) | ||
| from synapseclient.core.async_utils import async_to_sync, otel_trace_method | ||
| from synapseclient.core.constants.concrete_types import USED_ENTITY, USED_URL | ||
| from synapseclient.core.exceptions import SynapseHTTPError | ||
| from synapseclient.core.utils import delete_none_keys, get_synid_and_version | ||
| from synapseclient.models.protocols.activity_protocol import ActivitySynchronousProtocol | ||
@@ -50,3 +57,24 @@ | ||
| def to_synapse_request( | ||
| self, was_executed: bool = False | ||
| ) -> Dict[str, Union[str, bool, Dict]]: | ||
| """ | ||
| Converts the UsedEntity to a request expected by the Synapse REST API. | ||
| Arguments: | ||
| was_executed: Whether the entity was executed (vs. just used). | ||
| Returns: | ||
| A dictionary representation matching the Synapse REST API format. | ||
| """ | ||
| return { | ||
| "concreteType": USED_ENTITY, | ||
| "reference": { | ||
| "targetId": self.target_id, | ||
| "targetVersionNumber": self.target_version_number, | ||
| }, | ||
| "wasExecuted": was_executed, | ||
| } | ||
| @dataclass | ||
@@ -85,3 +113,22 @@ class UsedURL: | ||
| def to_synapse_request( | ||
| self, was_executed: bool = False | ||
| ) -> Dict[str, Union[str, bool]]: | ||
| """ | ||
| Converts the UsedURL to a request expected by the Synapse REST API. | ||
| Arguments: | ||
| was_executed: Whether the URL was executed (vs. just used). | ||
| Returns: | ||
| A dictionary representation matching the Synapse REST API format. | ||
| """ | ||
| return { | ||
| "concreteType": USED_URL, | ||
| "name": self.name, | ||
| "url": self.url, | ||
| "wasExecuted": was_executed, | ||
| } | ||
| class UsedAndExecutedSynapseActivities(NamedTuple): | ||
@@ -161,3 +208,3 @@ """ | ||
| def fill_from_dict( | ||
| self, synapse_activity: Union[Synapse_Activity, Dict] | ||
| self, synapse_activity: Dict[str, Union[str, List[Dict[str, Union[str, bool]]]]] | ||
| ) -> "Activity": | ||
@@ -227,15 +274,7 @@ """ | ||
| synapse_activity_used.append( | ||
| { | ||
| "reference": { | ||
| "targetId": used.target_id, | ||
| "targetVersionNumber": used.target_version_number, | ||
| } | ||
| } | ||
| used.to_synapse_request(was_executed=False) | ||
| ) | ||
| elif isinstance(used, UsedURL): | ||
| synapse_activity_used.append( | ||
| { | ||
| "name": used.name, | ||
| "url": used.url, | ||
| } | ||
| used.to_synapse_request(was_executed=False) | ||
| ) | ||
@@ -246,14 +285,9 @@ | ||
| synapse_activity_executed.append( | ||
| { | ||
| "reference": { | ||
| "targetId": executed.target_id, | ||
| "targetVersionNumber": executed.target_version_number, | ||
| }, | ||
| "wasExecuted": True, | ||
| } | ||
| executed.to_synapse_request(was_executed=True) | ||
| ) | ||
| elif isinstance(executed, UsedURL): | ||
| synapse_activity_executed.append( | ||
| {"name": executed.name, "url": executed.url, "wasExecuted": True} | ||
| executed.to_synapse_request(was_executed=True) | ||
| ) | ||
| return UsedAndExecutedSynapseActivities( | ||
@@ -263,2 +297,33 @@ used=synapse_activity_used, executed=synapse_activity_executed | ||
| def to_synapse_request(self) -> Dict[str, Union[str, List[Dict]]]: | ||
| """ | ||
| Converts the Activity to a request expected by the Synapse REST API. | ||
| Returns: | ||
| A dictionary representation matching the Synapse REST API format. | ||
| """ | ||
| used_and_executed_activities = ( | ||
| self._create_used_and_executed_synapse_activities() | ||
| ) | ||
| combined_used = ( | ||
| used_and_executed_activities.used + used_and_executed_activities.executed | ||
| ) | ||
| request = { | ||
| "id": self.id, | ||
| "name": self.name, | ||
| "description": self.description, | ||
| "etag": self.etag, | ||
| "createdOn": self.created_on, | ||
| "modifiedOn": self.modified_on, | ||
| "createdBy": self.created_by, | ||
| "modifiedBy": self.modified_by, | ||
| "used": combined_used, | ||
| } | ||
| delete_none_keys(request) | ||
| return request | ||
| @otel_trace_method( | ||
@@ -269,3 +334,3 @@ method_to_trace_name=lambda self, **kwargs: f"Activity_store: {self.name}" | ||
| self, | ||
| parent: Optional[Union["Table", "File", "EntityView", "Dataset"]] = None, | ||
| parent: Optional[Union["Table", "File", "EntityView", "Dataset", str]] = None, | ||
| *, | ||
@@ -278,3 +343,4 @@ synapse_client: Optional["Synapse"] = None, | ||
| Arguments: | ||
| parent: The parent entity to associate this activity with. | ||
| parent: The parent entity to associate this activity with. Can be an entity | ||
| object or a string ID (e.g., "syn123"). | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -296,43 +362,29 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| # TODO: Input validation: SYNPY-1400 | ||
| used_and_executed_activities = ( | ||
| self._create_used_and_executed_synapse_activities() | ||
| ) | ||
| if parent: | ||
| parent_id = parent if isinstance(parent, str) else parent.id | ||
| saved_activity = await set_entity_provenance( | ||
| entity_id=parent_id, | ||
| activity=self.to_synapse_request(), | ||
| synapse_client=synapse_client, | ||
| ) | ||
| else: | ||
| if self.id: | ||
| saved_activity = await update_activity( | ||
| self.to_synapse_request(), synapse_client=synapse_client | ||
| ) | ||
| else: | ||
| saved_activity = await create_activity( | ||
| self.to_synapse_request(), synapse_client=synapse_client | ||
| ) | ||
| self.fill_from_dict(synapse_activity=saved_activity) | ||
| synapse_activity = Synapse_Activity( | ||
| name=self.name, | ||
| description=self.description, | ||
| used=used_and_executed_activities.used, | ||
| executed=used_and_executed_activities.executed, | ||
| ) | ||
| loop = asyncio.get_event_loop() | ||
| if self.id: | ||
| # Despite init in `Synapse_Activity` not accepting an ID/ETAG the | ||
| # `updateActivity` method expects that it exists on the dict | ||
| # and `setProvenance` accepts it as well. | ||
| synapse_activity["id"] = self.id | ||
| synapse_activity["etag"] = self.etag | ||
| if parent: | ||
| saved_activity = await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client(synapse_client=synapse_client).setProvenance( | ||
| entity=parent.id, | ||
| activity=synapse_activity, | ||
| ), | ||
| parent_display_id = parent if isinstance(parent, str) else parent.id | ||
| Synapse.get_client(synapse_client=synapse_client).logger.info( | ||
| f"[{parent_display_id}]: Stored activity" | ||
| ) | ||
| else: | ||
| saved_activity = await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client( | ||
| synapse_client=synapse_client | ||
| ).updateActivity( | ||
| activity=synapse_activity, | ||
| ), | ||
| Synapse.get_client(synapse_client=synapse_client).logger.info( | ||
| f"[{self.id}]: Stored activity" | ||
| ) | ||
| self.fill_from_dict(synapse_activity=saved_activity) | ||
| Synapse.get_client(synapse_client=synapse_client).logger.info( | ||
| f"[{parent.id}]: Stored activity" | ||
| if parent | ||
| else f"[{self.id}]: Stored activity" | ||
| ) | ||
@@ -344,3 +396,4 @@ return self | ||
| cls, | ||
| parent: Union["Table", "File", "EntityView", "Dataset"], | ||
| parent: Union["Table", "File", "EntityView", "Dataset", str], | ||
| parent_version_number: Optional[int] = None, | ||
| *, | ||
@@ -356,2 +409,6 @@ synapse_client: Optional["Synapse"] = None, | ||
| omitted. | ||
| parent_version_number: The version number of the parent entity. When parent | ||
| is a string with version (e.g., "syn123.4"), the version in the string | ||
| takes precedence. When parent is an object, this parameter takes precedence | ||
| over parent.version_number. Gets the most recent version if omitted. | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -367,16 +424,21 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| """ | ||
| from synapseclient import Synapse | ||
| if isinstance(parent, str): | ||
| parent_id, version = get_synid_and_version(parent) | ||
| if version is None: | ||
| version = parent_version_number | ||
| else: | ||
| parent_id = parent.id | ||
| version = ( | ||
| parent_version_number | ||
| if parent_version_number is not None | ||
| else parent.version_number | ||
| ) | ||
| # TODO: Input validation: SYNPY-1400 | ||
| with tracer.start_as_current_span(name=f"Activity_get: Parent_ID: {parent.id}"): | ||
| loop = asyncio.get_event_loop() | ||
| with tracer.start_as_current_span(name=f"Activity_get: Parent_ID: {parent_id}"): | ||
| try: | ||
| synapse_activity = await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client( | ||
| synapse_client=synapse_client | ||
| ).getProvenance( | ||
| entity=parent.id, | ||
| version=parent.version_number, | ||
| ), | ||
| synapse_activity = await get_entity_provenance( | ||
| entity_id=parent_id, | ||
| version_number=version, | ||
| synapse_client=synapse_client, | ||
| ) | ||
@@ -396,3 +458,3 @@ except SynapseHTTPError as ex: | ||
| cls, | ||
| parent: Union["Table", "File"], | ||
| parent: Union["Table", "File", str], | ||
| *, | ||
@@ -418,18 +480,12 @@ synapse_client: Optional["Synapse"] = None, | ||
| """ | ||
| from synapseclient import Synapse | ||
| parent_id = parent if isinstance(parent, str) else parent.id | ||
| # TODO: Input validation: SYNPY-1400 | ||
| with tracer.start_as_current_span( | ||
| name=f"Activity_delete: Parent_ID: {parent.id}" | ||
| name=f"Activity_delete: Parent_ID: {parent_id}" | ||
| ): | ||
| loop = asyncio.get_event_loop() | ||
| await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client( | ||
| synapse_client=synapse_client | ||
| ).deleteProvenance( | ||
| entity=parent.id, | ||
| ), | ||
| await delete_entity_provenance( | ||
| entity_id=parent_id, synapse_client=synapse_client | ||
| ) | ||
| parent.activity = None | ||
| if not isinstance(parent, str): | ||
| parent.activity = None | ||
@@ -439,3 +495,3 @@ @classmethod | ||
| cls, | ||
| parent: Union["Table", "File"], | ||
| parent: Union["Table", "File", str], | ||
| *, | ||
@@ -458,9 +514,142 @@ synapse_client: Optional["Synapse"] = None, | ||
| """ | ||
| parent_id = parent if isinstance(parent, str) else parent.id | ||
| # TODO: Input validation: SYNPY-1400 | ||
| with tracer.start_as_current_span( | ||
| name=f"Activity_disassociate: Parent_ID: {parent.id}" | ||
| name=f"Activity_disassociate: Parent_ID: {parent_id}" | ||
| ): | ||
| await delete_entity_generated_by( | ||
| entity_id=parent.id, synapse_client=synapse_client | ||
| entity_id=parent_id, synapse_client=synapse_client | ||
| ) | ||
| parent.activity = None | ||
| if not isinstance(parent, str): | ||
| parent.activity = None | ||
| @classmethod | ||
| async def get_async( | ||
| cls, | ||
| activity_id: Optional[str] = None, | ||
| parent_id: Optional[str] = None, | ||
| parent_version_number: Optional[int] = None, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> Union["Activity", None]: | ||
| """ | ||
| Get an Activity from Synapse by either activity ID or parent entity ID. | ||
| Arguments: | ||
| activity_id: The ID of the activity to retrieve. If provided, this takes | ||
| precedence over parent_id. | ||
| parent_id: The ID of the parent entity to get the activity for. | ||
| Only used if activity_id is not provided (ignored when activity_id is provided). | ||
| parent_version_number: The version number of the parent entity. Only used when | ||
| parent_id is provided (ignored when activity_id is provided). Gets the | ||
| most recent version if omitted. | ||
| 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 activity object or None if it does not exist. | ||
| Raises: | ||
| ValueError: If neither activity_id nor parent_id is provided. | ||
| Example: Get activity by activity ID | ||
| Retrieve an activity using its ID. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Activity | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| activity = await Activity.get_async(activity_id="12345") | ||
| if activity: | ||
| print(f"Activity: {activity.name}") | ||
| else: | ||
| print("Activity not found") | ||
| asyncio.run(main()) | ||
| ``` | ||
| Example: Get activity by parent entity ID | ||
| Retrieve an activity using the parent entity ID. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Activity | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| activity = await Activity.get_async(parent_id="syn123") | ||
| if activity: | ||
| print(f"Activity: {activity.name}") | ||
| else: | ||
| print("No activity found for entity") | ||
| asyncio.run(main()) | ||
| ``` | ||
| Example: Get activity by parent entity ID with version | ||
| Retrieve an activity for a specific version of a parent entity. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Activity | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def main(): | ||
| activity = await Activity.get_async( | ||
| parent_id="syn123", | ||
| parent_version_number=3 | ||
| ) | ||
| if activity: | ||
| print(f"Activity: {activity.name}") | ||
| else: | ||
| print("No activity found for entity version") | ||
| asyncio.run(main()) | ||
| ``` | ||
| """ | ||
| if not activity_id and not parent_id: | ||
| raise ValueError("Either activity_id or parent_id must be provided") | ||
| if activity_id: | ||
| try: | ||
| synapse_activity = await get_activity( | ||
| activity_id=activity_id, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| if synapse_activity: | ||
| return cls().fill_from_dict(synapse_activity=synapse_activity) | ||
| else: | ||
| return None | ||
| except SynapseHTTPError as ex: | ||
| if ex.response.status_code == 404: | ||
| return None | ||
| else: | ||
| raise ex | ||
| else: | ||
| try: | ||
| synapse_activity = await get_entity_provenance( | ||
| entity_id=parent_id, | ||
| version_number=parent_version_number, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| if synapse_activity: | ||
| return cls().fill_from_dict(synapse_activity=synapse_activity) | ||
| else: | ||
| return None | ||
| except SynapseHTTPError as ex: | ||
| if ex.response.status_code == 404: | ||
| return None | ||
| else: | ||
| raise ex |
@@ -386,2 +386,3 @@ from dataclasses import dataclass, field | ||
| *, | ||
| timeout: int = 120, | ||
| synapse_client: Optional[Synapse] = None, | ||
@@ -398,2 +399,4 @@ ) -> AgentPrompt: | ||
| Defaults to None (all results). | ||
| timeout: The number of seconds to wait for the job to complete or progress | ||
| before raising a SynapseTimeoutError. Defaults to 120. | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -427,3 +430,5 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| ).send_job_and_wait_async( | ||
| synapse_client=synapse_client, post_exchange_args={"newer_than": newer_than} | ||
| timeout=timeout, | ||
| synapse_client=synapse_client, | ||
| post_exchange_args={"newer_than": newer_than}, | ||
| ) | ||
@@ -817,2 +822,3 @@ self.chat_history.append(agent_prompt) | ||
| *, | ||
| timeout: int = 120, | ||
| synapse_client: Optional[Synapse] = None, | ||
@@ -830,2 +836,4 @@ ) -> AgentPrompt: | ||
| newer_than: The timestamp to get trace results newer than. Defaults to None (all results). | ||
| timeout: The number of seconds to wait for the job to complete or progress | ||
| before raising a SynapseTimeoutError. Defaults to 120. | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -922,2 +930,3 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| print_response=print_response, | ||
| timeout=timeout, | ||
| synapse_client=synapse_client, | ||
@@ -924,0 +933,0 @@ ) |
+145
-135
@@ -40,3 +40,3 @@ """Script to work with Synapse files.""" | ||
| if TYPE_CHECKING: | ||
| from synapseclient.models import Folder, Project | ||
| from synapseclient.models import Folder, Project, RecordSet | ||
@@ -855,3 +855,3 @@ | ||
| ): | ||
| await self._upload_file(synapse_client=client) | ||
| await _upload_file(entity_to_upload=self, synapse_client=client) | ||
| elif self.data_file_handle_id: | ||
@@ -1243,135 +1243,2 @@ self.path = client.cache.get(file_handle_id=self.data_file_handle_id) | ||
| async def _needs_upload(self, syn: Synapse) -> bool: | ||
| """ | ||
| Determines if a file needs to be uploaded to Synapse. The following conditions | ||
| apply: | ||
| - The file exists and is an ExternalFileHandle and the url has changed | ||
| - The file exists and is a local file and the MD5 has changed | ||
| - The file is not present in Synapse | ||
| If the file is already specifying a data_file_handle_id then it is assumed that | ||
| the file is already uploaded to Synapse. It does not need to be uploaded and | ||
| the only thing that will occur is the File metadata will be added to Synapse | ||
| outside of this upload process. | ||
| Arguments: | ||
| syn: 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: | ||
| True if the file needs to be uploaded, otherwise False. | ||
| """ | ||
| needs_upload = False | ||
| # Check if the file should be uploaded | ||
| if self._last_persistent_instance is not None: | ||
| if ( | ||
| self.file_handle | ||
| and self.file_handle.concrete_type | ||
| == "org.sagebionetworks.repo.model.file.ExternalFileHandle" | ||
| ): | ||
| # switching away from ExternalFileHandle or the url was updated | ||
| needs_upload = self.synapse_store or ( | ||
| self.file_handle.external_url != self.external_url | ||
| ) | ||
| else: | ||
| # Check if we need to upload a new version of an existing | ||
| # file. If the file referred to by entity['path'] has been | ||
| # modified, we want to upload the new version. | ||
| # If synapeStore is false then we must upload a ExternalFileHandle | ||
| needs_upload = ( | ||
| not self.synapse_store | ||
| or not self.file_handle | ||
| or not ( | ||
| exists_in_cache := syn.cache.contains( | ||
| self.file_handle.id, self.path | ||
| ) | ||
| ) | ||
| ) | ||
| md5_stored_in_synapse = ( | ||
| self.file_handle.content_md5 if self.file_handle else None | ||
| ) | ||
| # Check if we got an MD5 checksum from Synapse and compare it to the local file | ||
| if ( | ||
| self.synapse_store | ||
| and needs_upload | ||
| and os.path.isfile(self.path) | ||
| and md5_stored_in_synapse | ||
| ): | ||
| await self._load_local_md5() | ||
| if md5_stored_in_synapse == ( | ||
| local_file_md5_hex := self.content_md5 | ||
| ): | ||
| needs_upload = False | ||
| # If we had a cache miss, but already uploaded to Synapse we | ||
| # can add the file to the cache. | ||
| if ( | ||
| not exists_in_cache | ||
| and self.file_handle | ||
| and self.file_handle.id | ||
| and local_file_md5_hex | ||
| ): | ||
| syn.cache.add( | ||
| file_handle_id=self.file_handle.id, | ||
| path=self.path, | ||
| md5=local_file_md5_hex, | ||
| ) | ||
| elif self.data_file_handle_id is not None: | ||
| needs_upload = False | ||
| else: | ||
| needs_upload = True | ||
| return needs_upload | ||
| async def _upload_file( | ||
| self, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> "File": | ||
| """The upload process for a file. This will upload the file to Synapse if it | ||
| needs to be uploaded. If the file does not need to be uploaded the file | ||
| metadata will be added to Synapse outside of this upload process. | ||
| 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: | ||
| The file object. | ||
| """ | ||
| syn = Synapse.get_client(synapse_client=synapse_client) | ||
| needs_upload = await self._needs_upload(syn=syn) | ||
| if needs_upload: | ||
| parent_id_for_upload = self.parent_id | ||
| if not parent_id_for_upload: | ||
| raise SynapseMalformedEntityError( | ||
| "Entities of type File must have a parentId." | ||
| ) | ||
| updated_file_handle = await upload_file_handle( | ||
| syn=syn, | ||
| parent_entity_id=parent_id_for_upload, | ||
| path=( | ||
| self.path | ||
| if (self.synapse_store or self.external_url is None) | ||
| else self.external_url | ||
| ), | ||
| synapse_store=self.synapse_store, | ||
| md5=self.content_md5, | ||
| file_size=self.content_size, | ||
| mimetype=self.content_type, | ||
| ) | ||
| self.file_handle = FileHandle().fill_from_dict(updated_file_handle) | ||
| self._fill_from_file_handle() | ||
| return self | ||
| def _convert_into_legacy_file(self) -> SynapseFile: | ||
@@ -1406,1 +1273,144 @@ """Convert the file object into a SynapseFile object.""" | ||
| return return_data | ||
| async def _needs_upload( | ||
| syn: Synapse, entity_to_upload: Union["File", "RecordSet"] | ||
| ) -> bool: | ||
| """ | ||
| Determines if a file needs to be uploaded to Synapse. The following conditions | ||
| apply: | ||
| - The file exists and is an ExternalFileHandle and the url has changed | ||
| - The file exists and is a local file and the MD5 has changed | ||
| - The file is not present in Synapse | ||
| If the file is already specifying a data_file_handle_id then it is assumed that | ||
| the file is already uploaded to Synapse. It does not need to be uploaded and | ||
| the only thing that will occur is the File metadata will be added to Synapse | ||
| outside of this upload process. | ||
| Arguments: | ||
| syn: 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: | ||
| True if the file needs to be uploaded, otherwise False. | ||
| """ | ||
| needs_upload = False | ||
| # Check if the file should be uploaded | ||
| if entity_to_upload._last_persistent_instance is not None: | ||
| if ( | ||
| entity_to_upload.file_handle | ||
| and entity_to_upload.file_handle.concrete_type | ||
| == "org.sagebionetworks.repo.model.file.ExternalFileHandle" | ||
| ): | ||
| # switching away from ExternalFileHandle or the url was updated | ||
| needs_upload = entity_to_upload.synapse_store or ( | ||
| entity_to_upload.file_handle.external_url | ||
| != entity_to_upload.external_url | ||
| ) | ||
| else: | ||
| # Check if we need to upload a new version of an existing | ||
| # file. If the file referred to by entity['path'] has been | ||
| # modified, we want to upload the new version. | ||
| # If synapeStore is false then we must upload a ExternalFileHandle | ||
| needs_upload = ( | ||
| not entity_to_upload.synapse_store | ||
| or not entity_to_upload.file_handle | ||
| or not ( | ||
| exists_in_cache := syn.cache.contains( | ||
| entity_to_upload.file_handle.id, entity_to_upload.path | ||
| ) | ||
| ) | ||
| ) | ||
| md5_stored_in_synapse = ( | ||
| entity_to_upload.file_handle.content_md5 | ||
| if entity_to_upload.file_handle | ||
| else None | ||
| ) | ||
| # Check if we got an MD5 checksum from Synapse and compare it to the local file | ||
| if ( | ||
| entity_to_upload.synapse_store | ||
| and needs_upload | ||
| and os.path.isfile(entity_to_upload.path) | ||
| and md5_stored_in_synapse | ||
| ): | ||
| await entity_to_upload._load_local_md5() | ||
| if md5_stored_in_synapse == ( | ||
| local_file_md5_hex := entity_to_upload.content_md5 | ||
| ): | ||
| needs_upload = False | ||
| # If we had a cache miss, but already uploaded to Synapse we | ||
| # can add the file to the cache. | ||
| if ( | ||
| not exists_in_cache | ||
| and entity_to_upload.file_handle | ||
| and entity_to_upload.file_handle.id | ||
| and local_file_md5_hex | ||
| ): | ||
| syn.cache.add( | ||
| file_handle_id=entity_to_upload.file_handle.id, | ||
| path=entity_to_upload.path, | ||
| md5=local_file_md5_hex, | ||
| ) | ||
| elif entity_to_upload.data_file_handle_id is not None: | ||
| needs_upload = False | ||
| else: | ||
| needs_upload = True | ||
| return needs_upload | ||
| async def _upload_file( | ||
| entity_to_upload: Union["File", "RecordSet"], | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> "File": | ||
| """The upload process for a file. This will upload the file to Synapse if it | ||
| needs to be uploaded. If the file does not need to be uploaded the file | ||
| metadata will be added to Synapse outside of this upload process. | ||
| 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: | ||
| The file object. | ||
| """ | ||
| syn = Synapse.get_client(synapse_client=synapse_client) | ||
| needs_upload = await _needs_upload(entity_to_upload=entity_to_upload, syn=syn) | ||
| if needs_upload: | ||
| parent_id_for_upload = entity_to_upload.parent_id | ||
| if not parent_id_for_upload: | ||
| raise SynapseMalformedEntityError( | ||
| "Entities of type File must have a parentId." | ||
| ) | ||
| updated_file_handle = await upload_file_handle( | ||
| syn=syn, | ||
| parent_entity_id=parent_id_for_upload, | ||
| path=( | ||
| entity_to_upload.path | ||
| if ( | ||
| entity_to_upload.synapse_store | ||
| or entity_to_upload.external_url is None | ||
| ) | ||
| else entity_to_upload.external_url | ||
| ), | ||
| synapse_store=entity_to_upload.synapse_store, | ||
| md5=entity_to_upload.content_md5, | ||
| file_size=entity_to_upload.content_size, | ||
| mimetype=entity_to_upload.content_type, | ||
| ) | ||
| entity_to_upload.file_handle = FileHandle().fill_from_dict(updated_file_handle) | ||
| entity_to_upload._fill_from_file_handle() | ||
| return entity_to_upload |
@@ -23,2 +23,3 @@ import asyncio | ||
| from synapseclient.models.services.search import get_id | ||
| from synapseclient.models.services.storable_entity import store_entity | ||
| from synapseclient.models.services.storable_entity_components import ( | ||
@@ -31,3 +32,12 @@ FailureStrategy, | ||
| if TYPE_CHECKING: | ||
| from synapseclient.models import Project | ||
| from synapseclient.models import ( | ||
| Dataset, | ||
| DatasetCollection, | ||
| EntityView, | ||
| MaterializedView, | ||
| Project, | ||
| SubmissionView, | ||
| Table, | ||
| VirtualTable, | ||
| ) | ||
@@ -64,2 +74,9 @@ | ||
| folders: Folders that exist within this folder. | ||
| tables: Tables that exist within this folder. | ||
| entityviews: Entity views that exist within this folder. | ||
| submissionviews: Submission views that exist within this folder. | ||
| datasets: Datasets that exist within this folder. | ||
| datasetcollections: Dataset collections that exist within this folder. | ||
| materializedviews: Materialized views that exist within this folder. | ||
| virtualtables: Virtual tables that exist within this folder. | ||
| annotations: Additional metadata associated with the folder. The key is the name | ||
@@ -122,2 +139,27 @@ of your desired annotations. The value is an object containing a list of | ||
| tables: List["Table"] = field(default_factory=list, compare=False) | ||
| """Tables that exist within this folder.""" | ||
| entityviews: List["EntityView"] = field(default_factory=list, compare=False) | ||
| """Entity views that exist within this folder.""" | ||
| submissionviews: List["SubmissionView"] = field(default_factory=list, compare=False) | ||
| """Submission views that exist within this folder.""" | ||
| datasets: List["Dataset"] = field(default_factory=list, compare=False) | ||
| """Datasets that exist within this folder.""" | ||
| datasetcollections: List["DatasetCollection"] = field( | ||
| default_factory=list, compare=False | ||
| ) | ||
| """Dataset collections that exist within this folder.""" | ||
| materializedviews: List["MaterializedView"] = field( | ||
| default_factory=list, compare=False | ||
| ) | ||
| """Materialized views that exist within this folder.""" | ||
| virtualtables: List["VirtualTable"] = field(default_factory=list, compare=False) | ||
| """Virtual tables that exist within this folder.""" | ||
| annotations: Optional[ | ||
@@ -287,3 +329,2 @@ Dict[ | ||
| if self.has_changed: | ||
| loop = asyncio.get_event_loop() | ||
| synapse_folder = Synapse_Folder( | ||
@@ -297,10 +338,6 @@ id=self.id, | ||
| delete_none_keys(synapse_folder) | ||
| entity = await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client(synapse_client=synapse_client).store( | ||
| obj=synapse_folder, | ||
| set_annotations=False, | ||
| isRestricted=self.is_restricted, | ||
| createOrUpdate=False, | ||
| ), | ||
| entity = await store_entity( | ||
| resource=self, | ||
| entity=synapse_folder, | ||
| synapse_client=synapse_client, | ||
| ) | ||
@@ -307,0 +344,0 @@ |
@@ -15,2 +15,8 @@ import asyncio | ||
| AGENT_CHAT_REQUEST, | ||
| CREATE_GRID_REQUEST, | ||
| CREATE_SCHEMA_REQUEST, | ||
| GET_VALIDATION_SCHEMA_REQUEST, | ||
| GRID_RECORD_SET_EXPORT_REQUEST, | ||
| QUERY_BUNDLE_REQUEST, | ||
| QUERY_TABLE_CSV_REQUEST, | ||
| TABLE_UPDATE_TRANSACTION_REQUEST, | ||
@@ -26,3 +32,9 @@ ) | ||
| AGENT_CHAT_REQUEST: "/agent/chat/async", | ||
| CREATE_GRID_REQUEST: "/grid/session/async", | ||
| GRID_RECORD_SET_EXPORT_REQUEST: "/grid/export/recordset/async", | ||
| TABLE_UPDATE_TRANSACTION_REQUEST: "/entity/{entityId}/table/transaction/async", | ||
| GET_VALIDATION_SCHEMA_REQUEST: "/schema/type/validation/async", | ||
| CREATE_SCHEMA_REQUEST: "/schema/type/create/async", | ||
| QUERY_TABLE_CSV_REQUEST: "/entity/{entityId}/table/download/csv/async", | ||
| QUERY_BUNDLE_REQUEST: "/entity/{entityId}/table/query/async", | ||
| } | ||
@@ -64,3 +76,3 @@ | ||
| post_exchange_args: Optional[Dict[str, Any]] = None, | ||
| timeout: int = 60, | ||
| timeout: int = 120, | ||
| *, | ||
@@ -79,3 +91,3 @@ synapse_client: Optional[Synapse] = None, | ||
| timeout: The number of seconds to wait for the job to complete or progress | ||
| before raising a SynapseTimeoutError. Defaults to 60. | ||
| before raising a SynapseTimeoutError. Defaults to 120. | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -294,3 +306,3 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| endpoint: str = None, | ||
| timeout: int = 60, | ||
| timeout: int = 120, | ||
| *, | ||
@@ -307,3 +319,3 @@ synapse_client: Optional["Synapse"] = None, | ||
| timeout: The number of seconds to wait for the job to complete or progress | ||
| before raising a SynapseTimeoutError. Defaults to 60. | ||
| before raising a SynapseTimeoutError. Defaults to 120. | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -403,3 +415,3 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| sleep: int = 1, | ||
| timeout: int = 60, | ||
| timeout: int = 120, | ||
| request: Dict[str, Any] = None, | ||
@@ -418,3 +430,3 @@ *, | ||
| timeout: The number of seconds to wait for the job to complete or progress | ||
| before raising a SynapseTimeoutError. Defaults to 60. | ||
| before raising a SynapseTimeoutError. Defaults to 120. | ||
| request: The original request that was sent to the server that created the job. | ||
@@ -421,0 +433,0 @@ Required if the request type is one that requires additional information. |
@@ -5,3 +5,13 @@ """Mixin for objects that can have Folders and Files stored in them.""" | ||
| import os | ||
| from typing import TYPE_CHECKING, Dict, List, NoReturn, Optional, Union | ||
| from typing import ( | ||
| TYPE_CHECKING, | ||
| AsyncGenerator, | ||
| Dict, | ||
| Generator, | ||
| List, | ||
| NoReturn, | ||
| Optional, | ||
| Tuple, | ||
| Union, | ||
| ) | ||
@@ -12,7 +22,21 @@ from typing_extensions import Self | ||
| from synapseclient.api import get_entity_id_bundle2 | ||
| from synapseclient.core.async_utils import async_to_sync, otel_trace_method | ||
| from synapseclient.api.entity_services import EntityHeader, get_children | ||
| from synapseclient.core.async_utils import ( | ||
| async_to_sync, | ||
| otel_trace_method, | ||
| skip_async_to_sync, | ||
| wrap_async_generator_to_sync_generator, | ||
| ) | ||
| from synapseclient.core.constants.concrete_types import ( | ||
| DATASET_COLLECTION_ENTITY, | ||
| DATASET_ENTITY, | ||
| ENTITY_VIEW, | ||
| FILE_ENTITY, | ||
| FOLDER_ENTITY, | ||
| LINK_ENTITY, | ||
| MATERIALIZED_VIEW, | ||
| PROJECT_ENTITY, | ||
| SUBMISSION_VIEW, | ||
| TABLE_ENTITY, | ||
| VIRTUAL_TABLE, | ||
| ) | ||
@@ -31,3 +55,14 @@ from synapseclient.core.constants.method_flags import COLLISION_OVERWRITE_LOCAL | ||
| if TYPE_CHECKING: | ||
| from synapseclient.models import File, Folder | ||
| # TODO: Support DockerRepo and Link in https://sagebionetworks.jira.com/browse/SYNPY-1343 epic or later | ||
| from synapseclient.models import ( | ||
| Dataset, | ||
| DatasetCollection, | ||
| EntityView, | ||
| File, | ||
| Folder, | ||
| MaterializedView, | ||
| SubmissionView, | ||
| Table, | ||
| VirtualTable, | ||
| ) | ||
@@ -45,2 +80,9 @@ | ||
| - `folders` | ||
| - `tables` | ||
| - `entityviews` | ||
| - `submissionviews` | ||
| - `datasets` | ||
| - `datasetcollections` | ||
| - `materializedviews` | ||
| - `virtualtables` | ||
| - `_last_persistent_instance` | ||
@@ -56,4 +98,13 @@ - `_synced_from_synapse` | ||
| name: None = None | ||
| files: "File" = None | ||
| folders: "Folder" = None | ||
| files: List["File"] = None | ||
| folders: List["Folder"] = None | ||
| tables: List["Table"] = None | ||
| # links: List["Link"] = None | ||
| entityviews: List["EntityView"] = None | ||
| # dockerrepos: List["DockerRepo"] = None | ||
| submissionviews: List["SubmissionView"] = None | ||
| datasets: List["Dataset"] = None | ||
| datasetcollections: List["DatasetCollection"] = None | ||
| materializedviews: List["MaterializedView"] = None | ||
| virtualtables: List["VirtualTable"] = None | ||
| _last_persistent_instance: None = None | ||
@@ -115,2 +166,3 @@ _synced_from_synapse: bool = False | ||
| queue: asyncio.Queue = None, | ||
| include_types: Optional[List[str]] = None, | ||
| *, | ||
@@ -122,5 +174,6 @@ synapse_client: Optional[Synapse] = None, | ||
| will download the files that are found and it will populate the | ||
| `files` and `folders` attributes with the found files and folders. If you only | ||
| want to retrieve the full tree of metadata about your container specify | ||
| `download_file` as False. | ||
| `files` and `folders` attributes with the found files and folders, along with | ||
| all other entity types (tables, entityviews, etc.) present in the container. | ||
| If you only want to retrieve the full tree of metadata about your | ||
| container specify `download_file` as False. | ||
@@ -131,3 +184,8 @@ This works similar to [synapseutils.syncFromSynapse][], however, this does not | ||
| Only Files and Folders are supported at this time to be synced from synapse. | ||
| Supports syncing Files, Folders, Tables, EntityViews, SubmissionViews, Datasets, | ||
| DatasetCollections, MaterializedViews, and VirtualTables from Synapse. The | ||
| metadata for these entity types will be populated in their respective | ||
| attributes (`files`, `folders`, `tables`, `entityviews`, `submissionviews`, | ||
| `datasets`, `datasetcollections`, `materializedviews`, `virtualtables`) if | ||
| they are found within the container. | ||
@@ -154,2 +212,9 @@ Arguments: | ||
| queue: An optional queue to use to download files in parallel. | ||
| include_types: Must be a list of entity types (ie. ["folder","file"]) which | ||
| can be found | ||
| [here](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/EntityType.html). | ||
| Defaults to | ||
| `["folder", "file", "table", "entityview", "dockerrepo", | ||
| "submissionview", "dataset", "datasetcollection", "materializedview", | ||
| "virtualtable"]`. | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -166,5 +231,8 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Folder | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Folder | ||
| async def my_function(): | ||
| syn = Synapse() | ||
@@ -182,7 +250,19 @@ syn.login() | ||
| for table in my_folder.tables: | ||
| print(table.name) | ||
| for dataset in my_folder.datasets: | ||
| print(dataset.name) | ||
| asyncio.run(my_function()) | ||
| ``` | ||
| Suppose I want to download the immediate children of a folder: | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Folder | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Folder | ||
| async def my_function(): | ||
| syn = Synapse() | ||
@@ -200,8 +280,13 @@ syn.login() | ||
| asyncio.run(my_function()) | ||
| ``` | ||
| Suppose I want to download the immediate all children of a Project and all sub-folders and files: | ||
| Suppose I want to sync only specific entity types from a Project: | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Project | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Project | ||
| async def my_function(): | ||
| syn = Synapse() | ||
@@ -211,5 +296,35 @@ syn.login() | ||
| my_project = Project(id="syn12345") | ||
| await my_project.sync_from_synapse_async( | ||
| path="/path/to/folder", | ||
| include_types=["folder", "file", "table", "dataset"] | ||
| ) | ||
| # Access different entity types | ||
| for table in my_project.tables: | ||
| print(f"Table: {table.name}") | ||
| for dataset in my_project.datasets: | ||
| print(f"Dataset: {dataset.name}") | ||
| asyncio.run(my_function()) | ||
| ``` | ||
| Suppose I want to download all the children of a Project and all sub-folders and files: | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Project | ||
| async def my_function(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| my_project = Project(id="syn12345") | ||
| await my_project.sync_from_synapse_async(path="/path/to/folder") | ||
| asyncio.run(my_function()) | ||
| ``` | ||
| Raises: | ||
@@ -297,2 +412,3 @@ ValueError: If the folder does not have an id set. | ||
| queue=queue, | ||
| include_types=include_types, | ||
| synapse_client=syn, | ||
@@ -312,2 +428,3 @@ ) | ||
| queue: asyncio.Queue = None, | ||
| include_types: Optional[List[str]] = None, | ||
| *, | ||
@@ -329,9 +446,6 @@ synapse_client: Optional[Synapse] = None, | ||
| loop = asyncio.get_event_loop() | ||
| children = await loop.run_in_executor( | ||
| None, | ||
| lambda: self._retrieve_children( | ||
| follow_link=follow_link, | ||
| synapse_client=syn, | ||
| ), | ||
| children = await self._retrieve_children( | ||
| follow_link=follow_link, | ||
| include_types=include_types, | ||
| synapse_client=syn, | ||
| ) | ||
@@ -357,2 +471,9 @@ | ||
| self.files = [] | ||
| self.tables = [] | ||
| self.entityviews = [] | ||
| self.submissionviews = [] | ||
| self.datasets = [] | ||
| self.datasetcollections = [] | ||
| self.materializedviews = [] | ||
| self.virtualtables = [] | ||
@@ -373,2 +494,3 @@ for child in children: | ||
| queue=queue, | ||
| include_types=include_types, | ||
| ) | ||
@@ -474,5 +596,443 @@ ) | ||
| def _retrieve_children( | ||
| @skip_async_to_sync | ||
| async def walk_async( | ||
| self, | ||
| follow_link: bool = False, | ||
| include_types: Optional[List[str]] = None, | ||
| recursive: bool = True, | ||
| display_ascii_tree: bool = False, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| _newpath: Optional[str] = None, | ||
| _tree_prefix: str = "", | ||
| _is_last_in_parent: bool = True, | ||
| _tree_depth: int = 0, | ||
| ) -> AsyncGenerator[ | ||
| Tuple[Tuple[str, str], List[EntityHeader], List[EntityHeader]], None | ||
| ]: | ||
| """ | ||
| Traverse through the hierarchy of entities stored under this container. | ||
| Mimics os.walk() behavior but yields EntityHeader objects, with optional | ||
| ASCII tree display that continues printing as the walk progresses. | ||
| Arguments: | ||
| follow_link: Whether to follow a link entity or not. Links can be used to | ||
| point at other Synapse entities. | ||
| include_types: Must be a list of entity types (ie. ["folder","file"]) which | ||
| can be found | ||
| [here](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/EntityType.html). | ||
| Defaults to | ||
| `["folder", "file", "table", "entityview", "dockerrepo", | ||
| "submissionview", "dataset", "datasetcollection", "materializedview", | ||
| "virtualtable"]`. The "folder" type is always included so the hierarchy | ||
| can be traversed. | ||
| recursive: Whether to recursively traverse subdirectories. Defaults to True. | ||
| display_ascii_tree: If True, display an ASCII tree representation as the | ||
| container structure is traversed. Tree lines are printed incrementally | ||
| as each container is visited. Defaults to False. | ||
| 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. | ||
| _newpath: Used internally to track the current path during recursion. | ||
| _tree_prefix: Used internally to format the ASCII tree structure. | ||
| _is_last_in_parent: Used internally to determine if the current entity is | ||
| the last child in its parent. | ||
| _tree_depth: Used internally to track the current depth in the tree. | ||
| Yields: | ||
| Tuple of (dirpath, dirs, nondirs) where: | ||
| - dirpath: Tuple (directory_name, synapse_id) representing current directory | ||
| - dirs: List of EntityHeader objects for subdirectories (folders) | ||
| - nondirs: List of EntityHeader objects for non-directory entities (files, tables, etc.) | ||
| Example: Traverse all entities in a container | ||
| Basic usage - traverse all entities in a container | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Folder | ||
| async def my_function(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| container = Folder(id="syn12345") | ||
| async for dirpath, dirs, nondirs in container.walk_async(): | ||
| print(f"Directory: {dirpath[0]} ({dirpath[1]})") | ||
| # Print folders | ||
| for folder_entity in dirs: | ||
| print(f" Folder: {folder_entity}") | ||
| # Print files and other entities | ||
| for entity in nondirs: | ||
| print(f" File: {entity}") | ||
| asyncio.run(my_function()) | ||
| ``` | ||
| Example: Display progressive ASCII tree as walk proceeds | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Folder | ||
| async def my_function(): | ||
| syn = Synapse() | ||
| syn.login() | ||
| container = Folder(id="syn12345") | ||
| # Display tree structure progressively as walk proceeds | ||
| async for dirpath, dirs, nondirs in container.walk_async( | ||
| display_ascii_tree=True | ||
| ): | ||
| # Process each directory as usual | ||
| print(f"Processing: {dirpath[0]}") | ||
| for file_entity in nondirs: | ||
| print(f" Found file: {file_entity.name}") | ||
| asyncio.run(my_function()) | ||
| ``` | ||
| Example output: | ||
| ``` | ||
| === Container Structure === | ||
| 📂 my-container-name (syn52948289) [Project] | ||
| ├── 📁 bulk-upload (syn68884548) [Folder] | ||
| │ └── 📁 file_script_folder (syn68884547) [Folder] | ||
| │ └── 📁 file_script_sub_folder (syn68884549) [Folder] | ||
| │ └── 📄 file_in_a_sub_folder.txt (syn68884556) [File] | ||
| ├── 📁 root (syn67590143) [Folder] | ||
| │ └── 📁 subdir1 (syn67590144) [Folder] | ||
| │ ├── 📄 file1.txt (syn67590261) [File] | ||
| │ └── 📄 file2.txt (syn67590287) [File] | ||
| └── 📁 temp-files (syn68884954) [Folder] | ||
| └── 📁 root (syn68884955) [Folder] | ||
| └── 📁 subdir1 (syn68884956) [Folder] | ||
| ├── 📄 file1.txt (syn68884959) [File] | ||
| └── 📄 file2.txt (syn68884999) [File] | ||
| Directory: My uniquely named project about Alzheimer's Disease (syn53185532) | ||
| File: EntityHeader(name='Gene Expression Data', id='syn66227753', | ||
| type='org.sagebionetworks.repo.model.table.TableEntity', version_number=1, | ||
| version_label='in progress', is_latest_version=True, benefactor_id=53185532, | ||
| created_on='2025-04-11T21:24:28.913Z', modified_on='2025-04-11T21:24:34.996Z', | ||
| created_by='3481671', modified_by='3481671') | ||
| Folder: EntityHeader(name='Research Data', id='syn68327923', | ||
| type='org.sagebionetworks.repo.model.Folder', version_number=1, | ||
| version_label='1', is_latest_version=True, benefactor_id=68327923, | ||
| created_on='2025-06-16T21:51:50.460Z', modified_on='2025-06-16T22:19:41.481Z', | ||
| created_by='3481671', modified_by='3481671') | ||
| ``` | ||
| Note: | ||
| Each EntityHeader contains complete metadata including id, name, type, | ||
| creation/modification dates, version information, and other Synapse properties. | ||
| The directory path is built using os.path.join() to create hierarchical paths. | ||
| When display_ascii_tree=True, the ASCII tree structure is displayed in proper | ||
| hierarchical order as the entire structure is traversed sequentially. The tree | ||
| will be printed in the correct parent-child relationships, but results will still | ||
| be yielded as expected during the traversal process. | ||
| like "my-container-name/bulk-upload/file_script_folder/file_script_sub_folder". | ||
| When display_ascii_tree=True, the tree is printed progressively as each | ||
| container is visited during the walk, making it suitable for very large | ||
| and deeply nested structures. | ||
| """ | ||
| if not self.id or not self.name: | ||
| await self.get_async(synapse_client=synapse_client) | ||
| if not include_types: | ||
| include_types = [ | ||
| "folder", | ||
| "file", | ||
| "table", | ||
| "entityview", | ||
| "dockerrepo", | ||
| "submissionview", | ||
| "dataset", | ||
| "datasetcollection", | ||
| "materializedview", | ||
| "virtualtable", | ||
| ] | ||
| if follow_link: | ||
| include_types.append("link") | ||
| else: | ||
| if follow_link and "link" not in include_types: | ||
| include_types.append("link") | ||
| if _newpath is None: | ||
| dirpath = (self.name, self.id) | ||
| else: | ||
| dirpath = (_newpath, self.id) | ||
| all_children: List[EntityHeader] = [] | ||
| async for child in get_children( | ||
| parent=self.id, | ||
| include_types=include_types, | ||
| synapse_client=synapse_client, | ||
| ): | ||
| converted_child = EntityHeader().fill_from_dict(synapse_response=child) | ||
| all_children.append(converted_child) | ||
| if display_ascii_tree and _newpath is None and _tree_depth == 0: | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| client.logger.info("=== Container Structure ===") | ||
| if display_ascii_tree: | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| from synapseclient.models import Project | ||
| current_entity = EntityHeader( | ||
| id=self.id, | ||
| name=self.name, | ||
| type=PROJECT_ENTITY if isinstance(self, Project) else FOLDER_ENTITY, | ||
| ) | ||
| if _tree_depth == 0: | ||
| tree_line = self._format_entity_info_for_tree(entity=current_entity) | ||
| else: | ||
| connector = "└── " if _is_last_in_parent else "├── " | ||
| entity_info = self._format_entity_info_for_tree(entity=current_entity) | ||
| tree_line = f"{_tree_prefix}{connector}{entity_info}" | ||
| client.logger.info(tree_line) | ||
| nondirs = [] | ||
| dir_entities = [] | ||
| for child in all_children: | ||
| if child.type in [ | ||
| FOLDER_ENTITY, | ||
| PROJECT_ENTITY, | ||
| ]: | ||
| dir_entities.append(child) | ||
| else: | ||
| nondirs.append(child) | ||
| if display_ascii_tree and nondirs: | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| if _tree_depth == 0: | ||
| child_prefix = "" | ||
| else: | ||
| child_prefix = _tree_prefix + (" " if _is_last_in_parent else "│ ") | ||
| sorted_nondirs = sorted(nondirs, key=lambda x: x.name) | ||
| for i, child in enumerate(sorted_nondirs): | ||
| is_last_child = (i == len(sorted_nondirs) - 1) and ( | ||
| len(dir_entities) == 0 | ||
| ) | ||
| connector = "└── " if is_last_child else "├── " | ||
| entity_info = self._format_entity_info_for_tree(child) | ||
| tree_line = f"{child_prefix}{connector}{entity_info}" | ||
| client.logger.info(tree_line) | ||
| # Yield the current directory's contents | ||
| yield dirpath, dir_entities, nondirs | ||
| if recursive and dir_entities: | ||
| sorted_dir_entities: List[EntityHeader] = sorted( | ||
| dir_entities, key=lambda x: x.name | ||
| ) | ||
| if _tree_depth == 0: | ||
| subdir_prefix = "" | ||
| else: | ||
| subdir_prefix = _tree_prefix + ( | ||
| " " if _is_last_in_parent else "│ " | ||
| ) | ||
| # Process subdirectories SEQUENTIALLY to maintain tree structure | ||
| for i, child_entity in enumerate(sorted_dir_entities): | ||
| is_last_subdir = i == len(sorted_dir_entities) - 1 | ||
| new_dir_path = os.path.join(dirpath[0], child_entity.name) | ||
| if child_entity.type == FOLDER_ENTITY: | ||
| from synapseclient.models import Folder | ||
| child_container = Folder(id=child_entity.id, name=child_entity.name) | ||
| elif child_entity.type == PROJECT_ENTITY: | ||
| from synapseclient.models import Project | ||
| child_container = Project( | ||
| id=child_entity.id, name=child_entity.name | ||
| ) | ||
| else: | ||
| continue # Skip non-container types | ||
| async for result in child_container.walk_async( | ||
| follow_link=follow_link, | ||
| include_types=include_types, | ||
| recursive=recursive, | ||
| display_ascii_tree=display_ascii_tree, | ||
| _newpath=new_dir_path, | ||
| synapse_client=synapse_client, | ||
| _tree_prefix=subdir_prefix, | ||
| _is_last_in_parent=is_last_subdir, | ||
| _tree_depth=_tree_depth + 1, | ||
| ): | ||
| yield result | ||
| def walk( | ||
| self, | ||
| follow_link: bool = False, | ||
| include_types: Optional[List[str]] = None, | ||
| recursive: bool = True, | ||
| display_ascii_tree: bool = False, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| _newpath: Optional[str] = None, | ||
| _tree_prefix: str = "", | ||
| _is_last_in_parent: bool = True, | ||
| _tree_depth: int = 0, | ||
| ) -> Generator[ | ||
| Tuple[Tuple[str, str], List[EntityHeader], List[EntityHeader]], None, None | ||
| ]: | ||
| """ | ||
| Traverse through the hierarchy of entities stored under this container. | ||
| Mimics os.walk() behavior but yields EntityHeader objects, with optional | ||
| ASCII tree display that continues printing as the walk progresses. | ||
| Arguments: | ||
| follow_link: Whether to follow a link entity or not. Links can be used to | ||
| point at other Synapse entities. | ||
| include_types: Must be a list of entity types (ie. ["folder","file"]) which | ||
| can be found | ||
| [here](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/EntityType.html). | ||
| Defaults to | ||
| `["folder", "file", "table", "entityview", "dockerrepo", | ||
| "submissionview", "dataset", "datasetcollection", "materializedview", | ||
| "virtualtable"]`. The "folder" type is always included so the hierarchy | ||
| can be traversed. | ||
| recursive: Whether to recursively traverse subdirectories. Defaults to True. | ||
| display_ascii_tree: If True, display an ASCII tree representation as the | ||
| container structure is traversed. Tree lines are printed incrementally | ||
| as each container is visited. Defaults to False. | ||
| 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. | ||
| _newpath: Used internally to track the current path during recursion. | ||
| _tree_prefix: Used internally to format the ASCII tree structure. | ||
| _is_last_in_parent: Used internally to determine if the current entity is | ||
| the last child in its parent. | ||
| _tree_depth: Used internally to track the current depth in the tree. | ||
| Yields: | ||
| Tuple of (dirpath, dirs, nondirs) where: | ||
| - dirpath: Tuple (directory_name, synapse_id) representing current directory | ||
| - dirs: List of EntityHeader objects for subdirectories (folders) | ||
| - nondirs: List of EntityHeader objects for non-directory entities (files, tables, etc.) | ||
| Example: Traverse all entities in a container | ||
| Basic usage - traverse all entities in a container | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Folder | ||
| syn = Synapse() | ||
| syn.login() | ||
| container = Folder(id="syn12345") | ||
| for dirpath, dirs, nondirs in container.walk(): | ||
| print(f"Directory: {dirpath[0]} ({dirpath[1]})") | ||
| # Print folders | ||
| for folder_entity in dirs: | ||
| print(f" Folder: {folder_entity}") | ||
| # Print files and other entities | ||
| for entity in nondirs: | ||
| print(f" File: {entity}") | ||
| ``` | ||
| Example: Display progressive ASCII tree as walk proceeds | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Folder | ||
| syn = Synapse() | ||
| syn.login() | ||
| container = Folder(id="syn12345") | ||
| # Display tree structure progressively as walk proceeds | ||
| for dirpath, dirs, nondirs in container.walk( | ||
| display_ascii_tree=True | ||
| ): | ||
| # Process each directory as usual | ||
| print(f"Processing: {dirpath[0]}") | ||
| for file_entity in nondirs: | ||
| print(f" Found file: {file_entity.name}") | ||
| ``` | ||
| Example output: | ||
| ``` | ||
| === Container Structure === | ||
| 📂 my-container-name (syn52948289) [Project] | ||
| ├── 📁 bulk-upload (syn68884548) [Folder] | ||
| │ └── 📁 file_script_folder (syn68884547) [Folder] | ||
| │ └── 📁 file_script_sub_folder (syn68884549) [Folder] | ||
| │ └── 📄 file_in_a_sub_folder.txt (syn68884556) [File] | ||
| ├── 📁 root (syn67590143) [Folder] | ||
| │ └── 📁 subdir1 (syn67590144) [Folder] | ||
| │ ├── 📄 file1.txt (syn67590261) [File] | ||
| │ └── 📄 file2.txt (syn67590287) [File] | ||
| └── 📁 temp-files (syn68884954) [Folder] | ||
| └── 📁 root (syn68884955) [Folder] | ||
| └── 📁 subdir1 (syn68884956) [Folder] | ||
| ├── 📄 file1.txt (syn68884959) [File] | ||
| └── 📄 file2.txt (syn68884999) [File] | ||
| Directory: My uniquely named project about Alzheimer's Disease (syn53185532) | ||
| File: EntityHeader(name='Gene Expression Data', id='syn66227753', | ||
| type='org.sagebionetworks.repo.model.table.TableEntity', version_number=1, | ||
| version_label='in progress', is_latest_version=True, benefactor_id=53185532, | ||
| created_on='2025-04-11T21:24:28.913Z', modified_on='2025-04-11T21:24:34.996Z', | ||
| created_by='3481671', modified_by='3481671') | ||
| Folder: EntityHeader(name='Research Data', id='syn68327923', | ||
| type='org.sagebionetworks.repo.model.Folder', version_number=1, | ||
| version_label='1', is_latest_version=True, benefactor_id=68327923, | ||
| created_on='2025-06-16T21:51:50.460Z', modified_on='2025-06-16T22:19:41.481Z', | ||
| created_by='3481671', modified_by='3481671') | ||
| ``` | ||
| Note: | ||
| Each EntityHeader contains complete metadata including id, name, type, | ||
| creation/modification dates, version information, and other Synapse properties. | ||
| The directory path is built using os.path.join() to create hierarchical paths. | ||
| When display_ascii_tree=True, the ASCII tree structure is displayed in proper | ||
| hierarchical order as the entire structure is traversed sequentially. The tree | ||
| will be printed in the correct parent-child relationships, but results will still | ||
| be yielded as expected during the traversal process. | ||
| like "my-container-name/bulk-upload/file_script_folder/file_script_sub_folder". | ||
| When display_ascii_tree=True, the tree is printed progressively as each | ||
| container is visited during the walk, making it suitable for very large | ||
| and deeply nested structures. | ||
| """ | ||
| yield from wrap_async_generator_to_sync_generator( | ||
| self.walk_async, | ||
| follow_link=follow_link, | ||
| include_types=include_types, | ||
| recursive=recursive, | ||
| display_ascii_tree=display_ascii_tree, | ||
| synapse_client=synapse_client, | ||
| _newpath=_newpath, | ||
| _tree_prefix=_tree_prefix, | ||
| _is_last_in_parent=_is_last_in_parent, | ||
| _tree_depth=_tree_depth, | ||
| ) | ||
| async def _retrieve_children( | ||
| self, | ||
| follow_link: bool, | ||
| include_types: Optional[List[str]] = None, | ||
| *, | ||
@@ -482,3 +1042,3 @@ synapse_client: Optional[Synapse] = None, | ||
| """ | ||
| This wraps the `getChildren` generator to return back a list of children. | ||
| Retrieve children entities using the async get_children API. | ||
@@ -488,17 +1048,41 @@ Arguments: | ||
| point at other Synapse entities. | ||
| include_types: Must be a list of entity types (ie. ["folder","file"]) which | ||
| can be found | ||
| [here](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/EntityType.html). | ||
| Defaults to | ||
| `["folder", "file", "table", "entityview", "dockerrepo", | ||
| "submissionview", "dataset", "datasetcollection", "materializedview", | ||
| "virtualtable"]`. | ||
| 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 child entities. | ||
| """ | ||
| include_types = ["folder", "file"] | ||
| if follow_link: | ||
| include_types.append("link") | ||
| children_objects = Synapse.get_client( | ||
| synapse_client=synapse_client | ||
| ).getChildren( | ||
| if not include_types: | ||
| include_types = [ | ||
| "folder", | ||
| "file", | ||
| "table", | ||
| "entityview", | ||
| "dockerrepo", | ||
| "submissionview", | ||
| "dataset", | ||
| "datasetcollection", | ||
| "materializedview", | ||
| "virtualtable", | ||
| ] | ||
| if follow_link: | ||
| include_types.append("link") | ||
| else: | ||
| if follow_link and "link" not in include_types: | ||
| include_types.append("link") | ||
| children = [] | ||
| async for child in get_children( | ||
| parent=self.id, | ||
| includeTypes=include_types, | ||
| ) | ||
| children = [] | ||
| for child in children_objects: | ||
| include_types=include_types, | ||
| synapse_client=synapse_client, | ||
| ): | ||
| children.append(child) | ||
@@ -519,2 +1103,3 @@ return children | ||
| link_hops: int = 1, | ||
| include_types: Optional[List[str]] = None, | ||
| *, | ||
@@ -545,2 +1130,3 @@ synapse_client: Optional[Synapse] = None, | ||
| queue=queue, | ||
| include_types=include_types, | ||
| ) | ||
@@ -560,2 +1146,3 @@ | ||
| link_hops: int = 1, | ||
| include_types: Optional[List[str]] = None, | ||
| *, | ||
@@ -593,2 +1180,5 @@ synapse_client: Optional[Synapse] = None, | ||
| infinite loops. Be careful if setting this above 1. | ||
| include_types: Must be a list of entity types (ie. ["folder","file"]) which | ||
| can be found | ||
| [here](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/EntityType.html) | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -626,2 +1216,3 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| queue=queue, | ||
| include_types=include_types, | ||
| ) | ||
@@ -671,2 +1262,3 @@ ) | ||
| link_hops=link_hops - 1, | ||
| include_types=include_types, | ||
| queue=queue, | ||
@@ -677,3 +1269,88 @@ ) | ||
| ) | ||
| elif synapse_id and child_type == TABLE_ENTITY: | ||
| # Lazy import to avoid circular import | ||
| from synapseclient.models import Table | ||
| table = Table(id=synapse_id, name=name) | ||
| self.tables.append(table) | ||
| pending_tasks.append( | ||
| asyncio.create_task( | ||
| wrap_coroutine(table.get_async(synapse_client=synapse_client)) | ||
| ) | ||
| ) | ||
| elif synapse_id and child_type == ENTITY_VIEW: | ||
| # Lazy import to avoid circular import | ||
| from synapseclient.models import EntityView | ||
| entityview = EntityView(id=synapse_id, name=name) | ||
| self.entityviews.append(entityview) | ||
| pending_tasks.append( | ||
| asyncio.create_task( | ||
| wrap_coroutine(entityview.get_async(synapse_client=synapse_client)) | ||
| ) | ||
| ) | ||
| elif synapse_id and child_type == SUBMISSION_VIEW: | ||
| # Lazy import to avoid circular import | ||
| from synapseclient.models import SubmissionView | ||
| submissionview = SubmissionView(id=synapse_id, name=name) | ||
| self.submissionviews.append(submissionview) | ||
| pending_tasks.append( | ||
| asyncio.create_task( | ||
| wrap_coroutine( | ||
| submissionview.get_async(synapse_client=synapse_client) | ||
| ) | ||
| ) | ||
| ) | ||
| elif synapse_id and child_type == DATASET_ENTITY: | ||
| # Lazy import to avoid circular import | ||
| from synapseclient.models import Dataset | ||
| dataset = Dataset(id=synapse_id, name=name) | ||
| self.datasets.append(dataset) | ||
| pending_tasks.append( | ||
| asyncio.create_task( | ||
| wrap_coroutine(dataset.get_async(synapse_client=synapse_client)) | ||
| ) | ||
| ) | ||
| elif synapse_id and child_type == DATASET_COLLECTION_ENTITY: | ||
| # Lazy import to avoid circular import | ||
| from synapseclient.models import DatasetCollection | ||
| datasetcollection = DatasetCollection(id=synapse_id, name=name) | ||
| self.datasetcollections.append(datasetcollection) | ||
| pending_tasks.append( | ||
| asyncio.create_task( | ||
| wrap_coroutine( | ||
| datasetcollection.get_async(synapse_client=synapse_client) | ||
| ) | ||
| ) | ||
| ) | ||
| elif synapse_id and child_type == MATERIALIZED_VIEW: | ||
| # Lazy import to avoid circular import | ||
| from synapseclient.models import MaterializedView | ||
| materializedview = MaterializedView(id=synapse_id, name=name) | ||
| self.materializedviews.append(materializedview) | ||
| pending_tasks.append( | ||
| asyncio.create_task( | ||
| wrap_coroutine( | ||
| materializedview.get_async(synapse_client=synapse_client) | ||
| ) | ||
| ) | ||
| ) | ||
| elif synapse_id and child_type == VIRTUAL_TABLE: | ||
| # Lazy import to avoid circular import | ||
| from synapseclient.models import VirtualTable | ||
| virtualtable = VirtualTable(id=synapse_id, name=name) | ||
| self.virtualtables.append(virtualtable) | ||
| pending_tasks.append( | ||
| asyncio.create_task( | ||
| wrap_coroutine( | ||
| virtualtable.get_async(synapse_client=synapse_client) | ||
| ) | ||
| ) | ||
| ) | ||
| return pending_tasks | ||
@@ -693,2 +1370,3 @@ | ||
| link_hops: int = 0, | ||
| include_types: Optional[List[str]] = None, | ||
| *, | ||
@@ -741,2 +1419,3 @@ synapse_client: Optional[Synapse] = None, | ||
| queue=queue, | ||
| include_types=include_types, | ||
| synapse_client=synapse_client, | ||
@@ -754,3 +1433,15 @@ ) | ||
| self, | ||
| result: Union[None, "Folder", "File", BaseException], | ||
| result: Union[ | ||
| None, | ||
| "Folder", | ||
| "File", | ||
| "Table", | ||
| "EntityView", | ||
| "SubmissionView", | ||
| "Dataset", | ||
| "DatasetCollection", | ||
| "MaterializedView", | ||
| "VirtualTable", | ||
| BaseException, | ||
| ], | ||
| failure_strategy: FailureStrategy, | ||
@@ -762,3 +1453,3 @@ *, | ||
| Handle what to do based on what was returned from the latest task to complete. | ||
| We are updating the object in place and appending the returned Folder/Files to | ||
| We are updating the object in place and appending the returned entities to | ||
| the appropriate list. | ||
@@ -769,3 +1460,3 @@ | ||
| failure_strategy: Determines how to handle failures when retrieving children | ||
| under this Folder and an exception occurs. | ||
| under this container and an exception occurs. | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -781,3 +1472,11 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| elif ( | ||
| result.__class__.__name__ == "Folder" or result.__class__.__name__ == "File" | ||
| result.__class__.__name__ == "Folder" | ||
| or result.__class__.__name__ == "File" | ||
| or result.__class__.__name__ == "Table" | ||
| or result.__class__.__name__ == "EntityView" | ||
| or result.__class__.__name__ == "SubmissionView" | ||
| or result.__class__.__name__ == "Dataset" | ||
| or result.__class__.__name__ == "DatasetCollection" | ||
| or result.__class__.__name__ == "MaterializedView" | ||
| or result.__class__.__name__ == "VirtualTable" | ||
| ): | ||
@@ -806,1 +1505,61 @@ # Do nothing as the objects are updated in place and the container has | ||
| raise exception | ||
| def _format_entity_info_for_tree( | ||
| self, | ||
| entity: EntityHeader, | ||
| ) -> str: | ||
| """ | ||
| Format entity information for display in progressive tree output. | ||
| Arguments: | ||
| entity: Dictionary containing entity information. | ||
| Returns: | ||
| String representation of the entity for tree display. | ||
| """ | ||
| name = entity.name or "Unknown" | ||
| entity_id = entity.id or "Unknown" | ||
| entity_type = entity.type or "Unknown" | ||
| type_name = entity_type | ||
| icon = "" | ||
| if entity_type == FILE_ENTITY: | ||
| type_name = "File" | ||
| icon = "📄" | ||
| elif entity_type == FOLDER_ENTITY: | ||
| type_name = "Folder" | ||
| icon = "📁 " | ||
| elif entity_type == PROJECT_ENTITY: | ||
| type_name = "Project" | ||
| icon = "📂 " | ||
| elif entity_type == TABLE_ENTITY: | ||
| type_name = "Table" | ||
| icon = "📊" | ||
| elif entity_type == ENTITY_VIEW: | ||
| type_name = "EntityView" | ||
| icon = "📊" | ||
| elif entity_type == MATERIALIZED_VIEW: | ||
| type_name = "MaterializedView" | ||
| icon = "📊" | ||
| elif entity_type == VIRTUAL_TABLE: | ||
| type_name = "VirtualTable" | ||
| icon = "📊" | ||
| elif entity_type == DATASET_ENTITY: | ||
| type_name = "Dataset" | ||
| icon = "📊" | ||
| elif entity_type == DATASET_COLLECTION_ENTITY: | ||
| type_name = "DatasetCollection" | ||
| icon = "🗂️ " | ||
| elif entity_type == SUBMISSION_VIEW: | ||
| type_name = "SubmissionView" | ||
| icon = "📊" | ||
| elif "." in entity_type: | ||
| type_name = entity_type.split(".")[-1] | ||
| if not icon: | ||
| icon = "❓" | ||
| base_info = f"{icon} {name} ({entity_id}) [{type_name}]" | ||
| return base_info |
@@ -5,3 +5,3 @@ import asyncio | ||
| from datetime import date, datetime | ||
| from typing import Dict, List, Optional, Union | ||
| from typing import TYPE_CHECKING, Dict, List, Optional, Union | ||
@@ -24,2 +24,3 @@ from opentelemetry import trace | ||
| from synapseclient.models.services.search import get_id | ||
| from synapseclient.models.services.storable_entity import store_entity | ||
| from synapseclient.models.services.storable_entity_components import ( | ||
@@ -31,3 +32,14 @@ FailureStrategy, | ||
| if TYPE_CHECKING: | ||
| from synapseclient.models import ( | ||
| Dataset, | ||
| DatasetCollection, | ||
| EntityView, | ||
| MaterializedView, | ||
| SubmissionView, | ||
| Table, | ||
| VirtualTable, | ||
| ) | ||
| @dataclass() | ||
@@ -61,2 +73,9 @@ @async_to_sync | ||
| folders: Any folders that are at the root directory of the project. | ||
| tables: Any tables that are at the root directory of the project. | ||
| entityviews: Any entity views that are at the root directory of the project. | ||
| submissionviews: Any submission views that are at the root directory of the project. | ||
| datasets: Any datasets that are at the root directory of the project. | ||
| datasetcollections: Any dataset collections that are at the root directory of the project. | ||
| materializedviews: Any materialized views that are at the root directory of the project. | ||
| virtualtables: Any virtual tables that are at the root directory of the project. | ||
| annotations: Additional metadata associated with the folder. The key is the name | ||
@@ -156,2 +175,27 @@ of your desired annotations. The value is an object containing a list of | ||
| tables: List["Table"] = field(default_factory=list, compare=False) | ||
| """Any tables that are at the root directory of the project.""" | ||
| entityviews: List["EntityView"] = field(default_factory=list, compare=False) | ||
| """Any entity views that are at the root directory of the project.""" | ||
| submissionviews: List["SubmissionView"] = field(default_factory=list, compare=False) | ||
| """Any submission views that are at the root directory of the project.""" | ||
| datasets: List["Dataset"] = field(default_factory=list, compare=False) | ||
| """Any datasets that are at the root directory of the project.""" | ||
| datasetcollections: List["DatasetCollection"] = field( | ||
| default_factory=list, compare=False | ||
| ) | ||
| """Any dataset collections that are at the root directory of the project.""" | ||
| materializedviews: List["MaterializedView"] = field( | ||
| default_factory=list, compare=False | ||
| ) | ||
| """Any materialized views that are at the root directory of the project.""" | ||
| virtualtables: List["VirtualTable"] = field(default_factory=list, compare=False) | ||
| """Any virtual tables that are at the root directory of the project.""" | ||
| annotations: Optional[ | ||
@@ -319,3 +363,2 @@ Dict[ | ||
| if self.has_changed: | ||
| loop = asyncio.get_event_loop() | ||
| synapse_project = Synapse_Project( | ||
@@ -330,9 +373,6 @@ id=self.id, | ||
| delete_none_keys(synapse_project) | ||
| entity = await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client(synapse_client=synapse_client).store( | ||
| obj=synapse_project, | ||
| set_annotations=False, | ||
| createOrUpdate=False, | ||
| ), | ||
| entity = await store_entity( | ||
| resource=self, | ||
| entity=synapse_project, | ||
| synapse_client=synapse_client, | ||
| ) | ||
@@ -339,0 +379,0 @@ self.fill_from_dict(synapse_project=entity, set_annotations=False) |
@@ -446,2 +446,2 @@ """Protocol for the specific methods of this class that have synchronous counterparts | ||
| """ | ||
| return None | ||
| return AclListResult() |
@@ -9,3 +9,3 @@ """Protocol for the specific methods of this class that have synchronous counterparts | ||
| if TYPE_CHECKING: | ||
| from synapseclient.models import Activity, File, Table | ||
| from synapseclient.models import Activity, Dataset, EntityView, File, Table | ||
@@ -21,3 +21,3 @@ | ||
| self, | ||
| parent: Optional[Union["Table", "File"]] = None, | ||
| parent: Optional[Union["Table", "File", "EntityView", "Dataset", str]] = None, | ||
| *, | ||
@@ -30,3 +30,4 @@ synapse_client: Optional[Synapse] = None, | ||
| Arguments: | ||
| parent: The parent entity to associate this activity with. | ||
| parent: The parent entity to associate this activity with. Can be an entity | ||
| object or a string ID (e.g., "syn123"). | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -50,3 +51,4 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| cls, | ||
| parent: Union["Table", "File"], | ||
| parent: Union["Table", "File", "EntityView", "Dataset", str], | ||
| parent_version_number: Optional[int] = None, | ||
| *, | ||
@@ -62,2 +64,6 @@ synapse_client: Optional[Synapse] = None, | ||
| omitted. | ||
| parent_version_number: The version number of the parent entity. When parent | ||
| is a string with version (e.g., "syn123.4"), the version in the string | ||
| takes precedence. When parent is an object, this parameter takes precedence | ||
| over parent.version_number. Gets the most recent version if omitted. | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -80,3 +86,3 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| cls, | ||
| parent: Union["Table", "File"], | ||
| parent: Union["Table", "File", str], | ||
| *, | ||
@@ -105,5 +111,5 @@ synapse_client: Optional[Synapse] = None, | ||
| @classmethod | ||
| async def disassociate_from_entity( | ||
| def disassociate_from_entity( | ||
| cls, | ||
| parent: Union["Table", "File"], | ||
| parent: Union["Table", "File", str], | ||
| *, | ||
@@ -127,1 +133,35 @@ synapse_client: Optional[Synapse] = None, | ||
| return None | ||
| @classmethod | ||
| def get( | ||
| cls, | ||
| activity_id: Optional[str] = None, | ||
| parent_id: Optional[str] = None, | ||
| parent_version_number: Optional[int] = None, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> Union["Activity", None]: | ||
| """ | ||
| Get an Activity from Synapse by either activity ID or parent entity ID. | ||
| Arguments: | ||
| activity_id: The ID of the activity to retrieve. If provided, this takes | ||
| precedence over parent_id. | ||
| parent_id: The ID of the parent entity to get the activity for. | ||
| Only used if activity_id is not provided (ignored when activity_id is provided). | ||
| parent_version_number: The version number of the parent entity. Only used when | ||
| parent_id is provided (ignored when activity_id is provided). Gets the | ||
| most recent version if omitted. | ||
| 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 activity object or None if it does not exist. | ||
| Raises: | ||
| ValueError: If neither activity_id nor parent_id is provided. | ||
| """ | ||
| from synapseclient.models import Activity | ||
| return Activity() |
@@ -116,2 +116,3 @@ """Protocol for the methods of the Agent and AgentSession classes that have | ||
| *, | ||
| timeout: int = 120, | ||
| synapse_client: Optional[Synapse] = None, | ||
@@ -128,2 +129,4 @@ ) -> "AgentPrompt": | ||
| Defaults to None (all results). | ||
| timeout: The number of seconds to wait for the job to complete or progress | ||
| before raising a SynapseTimeoutError. Defaults to 120. | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -322,2 +325,3 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| *, | ||
| timeout: int = 120, | ||
| synapse_client: Optional[Synapse] = None, | ||
@@ -335,2 +339,4 @@ ) -> "AgentPrompt": | ||
| newer_than: The timestamp to get trace results newer than. Defaults to None (all results). | ||
| timeout: The number of seconds to wait for the job to complete or progress | ||
| before raising a SynapseTimeoutError. Defaults to 120. | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -337,0 +343,0 @@ `Synapse.allow_client_caching(False)` this will use the last created |
| """Protocol for the specific methods of this class that have synchronous counterparts | ||
| generated at runtime.""" | ||
| from typing import Optional, Protocol | ||
| import asyncio | ||
| from typing import List, Optional, Protocol | ||
@@ -26,2 +27,7 @@ from typing_extensions import Self | ||
| failure_strategy: FailureStrategy = FailureStrategy.LOG_EXCEPTION, | ||
| include_activity: bool = True, | ||
| follow_link: bool = False, | ||
| link_hops: int = 1, | ||
| queue: asyncio.Queue = None, | ||
| include_types: Optional[List[str]] = None, | ||
| *, | ||
@@ -33,5 +39,6 @@ synapse_client: Optional[Synapse] = None, | ||
| will download the files that are found and it will populate the | ||
| `files` and `folders` attributes with the found files and folders. If you only | ||
| want to retrieve the full tree of metadata about your container specify | ||
| `download_file` as False. | ||
| `files` and `folders` attributes with the found files and folders, along with | ||
| all other entity types (tables, entityviews, etc.) present in the container. | ||
| If you only want to retrieve the full tree of metadata about your | ||
| container specify `download_file` as False. | ||
@@ -42,3 +49,8 @@ This works similar to [synapseutils.syncFromSynapse][], however, this does not | ||
| Only Files and Folders are supported at this time to be synced from synapse. | ||
| Supports syncing Files, Folders, Tables, EntityViews, SubmissionViews, Datasets, | ||
| DatasetCollections, MaterializedViews, and VirtualTables from Synapse. The | ||
| metadata for these entity types will be populated in their respective | ||
| attributes (`files`, `folders`, `tables`, `entityviews`, `submissionviews`, | ||
| `datasets`, `datasetcollections`, `materializedviews`, `virtualtables`) if | ||
| they are found within the container. | ||
@@ -58,2 +70,12 @@ Arguments: | ||
| under this Folder and an exception occurs. | ||
| include_activity: Whether to include the activity of the files. | ||
| follow_link: Whether to follow a link entity or not. Links can be used to | ||
| point at other Synapse entities. | ||
| link_hops: The number of hops to follow the link. A number of 1 is used to | ||
| prevent circular references. There is nothing in place to prevent | ||
| infinite loops. Be careful if setting this above 1. | ||
| queue: An optional queue to use to download files in parallel. | ||
| include_types: Must be a list of entity types (ie. ["folder","file"]) which | ||
| can be found | ||
| [here](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/EntityType.html) | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -70,47 +92,81 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Folder | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Folder | ||
| syn = Synapse() | ||
| syn.login() | ||
| syn = Synapse() | ||
| syn.login() | ||
| my_folder = Folder(id="syn12345") | ||
| my_folder.sync_from_synapse(download_file=False, recursive=False) | ||
| my_folder = Folder(id="syn12345") | ||
| my_folder.sync_from_synapse(download_file=False, recursive=False) | ||
| for folder in my_folder.folders: | ||
| print(folder.name) | ||
| for folder in my_folder.folders: | ||
| print(folder.name) | ||
| for file in my_folder.files: | ||
| print(file.name) | ||
| for file in my_folder.files: | ||
| print(file.name) | ||
| for table in my_folder.tables: | ||
| print(table.name) | ||
| for dataset in my_folder.datasets: | ||
| print(dataset.name) | ||
| ``` | ||
| Suppose I want to download the immediate children of a folder: | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Folder | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Folder | ||
| syn = Synapse() | ||
| syn.login() | ||
| syn = Synapse() | ||
| syn.login() | ||
| my_folder = Folder(id="syn12345") | ||
| my_folder.sync_from_synapse(path="/path/to/folder", recursive=False) | ||
| my_folder = Folder(id="syn12345") | ||
| my_folder.sync_from_synapse(path="/path/to/folder", recursive=False) | ||
| for folder in my_folder.folders: | ||
| print(folder.name) | ||
| for folder in my_folder.folders: | ||
| print(folder.name) | ||
| for file in my_folder.files: | ||
| print(file.name) | ||
| for file in my_folder.files: | ||
| print(file.name) | ||
| ``` | ||
| Suppose I want to sync only specific entity types from a Project: | ||
| Suppose I want to download the immediate all children of a Project and all sub-folders and files: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Project | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Project | ||
| syn = Synapse() | ||
| syn.login() | ||
| syn = Synapse() | ||
| syn.login() | ||
| my_project = Project(id="syn12345") | ||
| my_project.sync_from_synapse( | ||
| path="/path/to/folder", | ||
| include_types=["folder", "file", "table", "dataset"] | ||
| ) | ||
| my_project = Project(id="syn12345") | ||
| my_project.sync_from_synapse(path="/path/to/folder") | ||
| # Access different entity types | ||
| for table in my_project.tables: | ||
| print(f"Table: {table.name}") | ||
| for dataset in my_project.datasets: | ||
| print(f"Dataset: {dataset.name}") | ||
| ``` | ||
| Suppose I want to download all the children of a Project and all sub-folders and files: | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Project | ||
| syn = Synapse() | ||
| syn.login() | ||
| my_project = Project(id="syn12345") | ||
| my_project.sync_from_synapse(path="/path/to/folder") | ||
| ``` | ||
| Raises: | ||
@@ -117,0 +173,0 @@ ValueError: If the folder does not have an id set. |
| """Protocol for the specific methods of this class that have synchronous counterparts | ||
| generated at runtime.""" | ||
| from typing import Any, Dict, Optional, Protocol | ||
| from typing import Any, Dict, Generator, Optional, Protocol | ||
@@ -17,12 +17,84 @@ from typing_extensions import Self | ||
| def store(self, *, synapse_client: Optional[Synapse] = None) -> Self: | ||
| """Persist the column to Synapse. | ||
| def get(self, *, synapse_client: Optional[Synapse] = None) -> "Self": | ||
| """ | ||
| Get a column by its ID. | ||
| :param synapse_client: If not passed in or None this will use the last client | ||
| from the Synapse class constructor. | ||
| :return: Column | ||
| 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: | ||
| The Column instance. | ||
| Example: Getting a column by ID | ||
| Getting a column by ID | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Column | ||
| syn = Synapse() | ||
| syn.login() | ||
| column = Column(id="123").get() | ||
| """ | ||
| return self | ||
| @staticmethod | ||
| def list( | ||
| prefix: Optional[str] = None, | ||
| limit: int = 100, | ||
| offset: int = 0, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> Generator["Self", None, None]: | ||
| """ | ||
| List columns with optional prefix filtering. | ||
| Arguments: | ||
| prefix: Optional prefix to filter columns by name. | ||
| limit: Number of columns to retrieve per request to Synapse (pagination parameter). | ||
| The function will continue retrieving results until all matching columns are returned. | ||
| offset: The index of the first column to return (pagination parameter). | ||
| 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. | ||
| Yields: | ||
| A generator that yields Column instances. | ||
| Example: Getting all columns | ||
| Getting all columns | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Column | ||
| syn = Synapse() | ||
| syn.login() | ||
| for column in Column.list(): | ||
| print(column.name) | ||
| Example: Getting columns with a prefix | ||
| Getting columns with a prefix | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Column | ||
| syn = Synapse() | ||
| syn.login() | ||
| for column in Column.list(prefix="my_prefix"): | ||
| print(column.name) | ||
| """ | ||
| from synapseclient.api import list_columns_sync | ||
| yield from list_columns_sync( | ||
| prefix=prefix, | ||
| limit=limit, | ||
| offset=offset, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| class TableSynchronousProtocol(Protocol): | ||
@@ -29,0 +101,0 @@ """ |
| """Protocol for the specific methods of this class that have synchronous counterparts | ||
| generated at runtime.""" | ||
| from typing import TYPE_CHECKING, Dict, List, Optional, Protocol | ||
| from typing import TYPE_CHECKING, Dict, List, Optional, Protocol, Union | ||
@@ -9,3 +9,3 @@ from synapseclient import Synapse | ||
| if TYPE_CHECKING: | ||
| from synapseclient.models import Team, TeamMember | ||
| from synapseclient.models import Team, TeamMember, TeamMembershipStatus | ||
@@ -130,8 +130,12 @@ | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> Dict[str, str]: | ||
| ) -> Union[Dict[str, str], None]: | ||
| """Invites a user to a team given the ID field on the Team instance. | ||
| Arguments: | ||
| user: The username of the user to invite. | ||
| user: The username or ID of the user to invite. | ||
| message: The message to send. | ||
| force: If True, will send the invite even if the user is already a member | ||
| or has an open invitation. If False, will not send the invite if the user | ||
| is already a member or has an open invitation. | ||
| Defaults to True. | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -142,3 +146,3 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| Returns: | ||
| dict: The invite response. | ||
| The invite response or None if an invite was not sent. | ||
| """ | ||
@@ -161,1 +165,51 @@ return {} | ||
| return list({}) | ||
| def get_user_membership_status( | ||
| self, | ||
| user_id: str, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> "TeamMembershipStatus": | ||
| """Retrieve a user's Team Membership Status bundle. | ||
| <https://rest-docs.synapse.org/rest/GET/team/id/member/principalId/membershipStatus.html> | ||
| Arguments: | ||
| user_id: The ID of the user whose membership status is being queried. | ||
| 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: | ||
| TeamMembershipStatus object | ||
| Example: | ||
| Check if a user is a member of a team | ||
| This example shows how to check a user's membership status in a team. | ||
| | ||
| ```python | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Team | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Get a team by ID | ||
| team = Team.from_id(123456) | ||
| # Check membership status for a specific user | ||
| user_id = "3350396" # Replace with actual user ID | ||
| status = team.get_user_membership_status(user_id) | ||
| print(f"User ID: {status.user_id}") | ||
| print(f"Is member: {status.is_member}") | ||
| print(f"Can join: {status.can_join}") | ||
| print(f"Has open invitation: {status.has_open_invitation}") | ||
| print(f"Has open request: {status.has_open_request}") | ||
| print(f"Membership approval required: {status.membership_approval_required}") | ||
| ``` | ||
| """ | ||
| from synapseclient.models.team import TeamMembershipStatus | ||
| return TeamMembershipStatus().fill_from_dict({}) |
@@ -51,2 +51,3 @@ """Functional interface for searching for entities in Synapse.""" | ||
| # TODO: Remove this deprecated code with replacement method created in https://sagebionetworks.jira.com/browse/SYNPY-1623 | ||
| loop = asyncio.get_event_loop() | ||
@@ -53,0 +54,0 @@ entity_id = entity.id or await loop.run_in_executor( |
@@ -11,8 +11,13 @@ import asyncio | ||
| Dataset, | ||
| DatasetCollection, | ||
| EntityView, | ||
| File, | ||
| Folder, | ||
| Link, | ||
| MaterializedView, | ||
| Project, | ||
| RecordSet, | ||
| SubmissionView, | ||
| Table, | ||
| VirtualTable, | ||
| ) | ||
@@ -54,3 +59,16 @@ | ||
| async def store_entity_components( | ||
| root_resource: Union["File", "Folder", "Project", "Table", "Dataset", "EntityView"], | ||
| root_resource: Union[ | ||
| "Dataset", | ||
| "DatasetCollection", | ||
| "EntityView", | ||
| "RecordSet", | ||
| "File", | ||
| "Folder", | ||
| "Link", | ||
| "Project", | ||
| "MaterializedView", | ||
| "SubmissionView", | ||
| "Table", | ||
| "VirtualTable", | ||
| ], | ||
| failure_strategy: FailureStrategy = FailureStrategy.LOG_EXCEPTION, | ||
@@ -121,3 +139,2 @@ *, | ||
| # TODO: Double check this logic. This might not be getting set properly from _resolve_store_task | ||
| return re_read_required | ||
@@ -225,3 +242,15 @@ | ||
| async def _store_activity_and_annotations( | ||
| root_resource: Union["File", "Folder", "Project", "Table", "Dataset", "EntityView"], | ||
| root_resource: Union[ | ||
| "Dataset", | ||
| "DatasetCollection", | ||
| "EntityView", | ||
| "File", | ||
| "Folder", | ||
| "Link", | ||
| "Project", | ||
| "MaterializedView", | ||
| "SubmissionView", | ||
| "Table", | ||
| "VirtualTable", | ||
| ], | ||
| *, | ||
@@ -228,0 +257,0 @@ synapse_client: Optional[Synapse] = None, |
@@ -16,7 +16,7 @@ """Script used to store an entity to Synapse.""" | ||
| if TYPE_CHECKING: | ||
| from synapseclient.models import File, Folder, Project | ||
| from synapseclient.models import File, Folder, Link, Project | ||
| async def store_entity( | ||
| resource: Union["File", "Folder", "Project"], | ||
| resource: Union["File", "Folder", "Project", "Link"], | ||
| entity: Dict[str, Union[str, bool, int, float]], | ||
@@ -69,23 +69,2 @@ *, | ||
| else: | ||
| # TODO - When Link is implemented this needs to be completed | ||
| # If Link, get the target name, version number and concrete type and store in link properties | ||
| # if properties["concreteType"] == "org.sagebionetworks.repo.model.Link": | ||
| # target_properties = self._getEntity( | ||
| # properties["linksTo"]["targetId"], | ||
| # version=properties["linksTo"].get("targetVersionNumber"), | ||
| # ) | ||
| # if target_properties["parentId"] == properties["parentId"]: | ||
| # raise ValueError( | ||
| # "Cannot create a Link to an entity under the same parent." | ||
| # ) | ||
| # properties["linksToClassName"] = target_properties["concreteType"] | ||
| # if ( | ||
| # target_properties.get("versionNumber") is not None | ||
| # and properties["linksTo"].get("targetVersionNumber") is not None | ||
| # ): | ||
| # properties["linksTo"]["targetVersionNumber"] = target_properties[ | ||
| # "versionNumber" | ||
| # ] | ||
| # properties["name"] = target_properties["name"] | ||
| updated_entity = await post_entity( | ||
@@ -92,0 +71,0 @@ request=get_properties(entity), |
@@ -186,2 +186,3 @@ import dataclasses | ||
| associate_activity_to_new_version: bool = True, | ||
| timeout: int = 120, | ||
| synapse_client: Optional[Synapse] = None, | ||
@@ -211,2 +212,4 @@ ) -> "TableUpdateTransaction": | ||
| associated with the new version of the table. Defaults to True. | ||
| timeout: The number of seconds to wait for the async job to complete. | ||
| Defaults to 120. | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -730,2 +733,3 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| associate_activity_to_new_version: bool = True, | ||
| timeout: int = 120, | ||
| synapse_client: Optional[Synapse] = None, | ||
@@ -755,2 +759,4 @@ ) -> "TableUpdateTransaction": | ||
| associated with the new version of the table. Defaults to True. | ||
| timeout: The number of seconds to wait for the async job to complete. | ||
| Defaults to 120. | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -828,2 +834,3 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| associate_activity_to_new_version=associate_activity_to_new_version, | ||
| timeout=timeout, | ||
| synapse_client=synapse_client, | ||
@@ -830,0 +837,0 @@ ) |
@@ -0,5 +1,15 @@ | ||
| import json | ||
| import os | ||
| from dataclasses import dataclass, field, replace | ||
| from enum import Enum | ||
| from typing import Any, Dict, List, Optional, TypeVar, Union | ||
| from typing import ( | ||
| TYPE_CHECKING, | ||
| Any, | ||
| AsyncGenerator, | ||
| Dict, | ||
| List, | ||
| Optional, | ||
| TypeVar, | ||
| Union, | ||
| ) | ||
@@ -9,8 +19,17 @@ from typing_extensions import Self | ||
| from synapseclient import Column as Synapse_Column | ||
| from synapseclient.core.async_utils import async_to_sync | ||
| from synapseclient.core.async_utils import async_to_sync, skip_async_to_sync | ||
| from synapseclient.core.constants import concrete_types | ||
| from synapseclient.core.utils import delete_none_keys | ||
| from synapseclient.core.constants.concrete_types import ( | ||
| QUERY_BUNDLE_REQUEST, | ||
| QUERY_RESULT, | ||
| QUERY_TABLE_CSV_REQUEST, | ||
| QUERY_TABLE_CSV_RESULT, | ||
| ) | ||
| from synapseclient.core.utils import delete_none_keys, from_unix_epoch_time | ||
| from synapseclient.models.mixins.asynchronous_job import AsynchronousCommunicator | ||
| from synapseclient.models.protocols.table_protocol import ColumnSynchronousProtocol | ||
| if TYPE_CHECKING: | ||
| from synapseclient import Synapse | ||
| DATA_FRAME_TYPE = TypeVar("pd.DataFrame") | ||
@@ -21,19 +40,19 @@ | ||
| class SumFileSizes: | ||
| sum_file_size_bytes: int | ||
| """ | ||
| A model for the sum of file sizes in a query result bundle. | ||
| This result is modeled from: <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/SumFileSizes.html> | ||
| """ | ||
| sum_file_size_bytes: int = None | ||
| """The sum of the file size in bytes.""" | ||
| greater_than: bool = None | ||
| """When true, the actual sum of the files sizes is greater than the value provided with 'sumFileSizesBytes'. When false, the actual sum of the files sizes is equals the value provided with 'sumFileSizesBytes'""" | ||
| greater_than: bool | ||
| """When true, the actual sum of the files sizes is greater than the value provided | ||
| with 'sum_file_size_bytes'. When false, the actual sum of the files sizes is equals | ||
| the value provided with 'sum_file_size_bytes'""" | ||
| @dataclass | ||
| class QueryResultBundle: | ||
| class QueryResultOutput: | ||
| """ | ||
| The result of querying Synapse with an included `part_mask`. This class contains a | ||
| subnet of the available items that may be returned by specifying a `part_mask`. | ||
| This result is modeled from: <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/QueryResultBundle.html> | ||
| """ | ||
@@ -58,3 +77,33 @@ | ||
| @classmethod | ||
| def fill_from_dict( | ||
| cls, result: "DATA_FRAME_TYPE", data: Dict[str, Any] | ||
| ) -> "QueryResultOutput": | ||
| """ | ||
| Create a QueryResultOutput from a result DataFrame and dictionary response. | ||
| Arguments: | ||
| result: The pandas DataFrame result from the query. | ||
| data: The dictionary response from the REST API containing metadata. | ||
| Returns: | ||
| A QueryResultOutput instance. | ||
| """ | ||
| sum_file_sizes = ( | ||
| SumFileSizes( | ||
| sum_file_size_bytes=data["sum_file_sizes"].sum_file_size_bytes, | ||
| greater_than=data["sum_file_sizes"].greater_than, | ||
| ) | ||
| if data.get("sum_file_sizes") | ||
| else None | ||
| ) | ||
| return cls( | ||
| result=result, | ||
| count=data.get("count", None), | ||
| sum_file_sizes=sum_file_sizes, | ||
| last_updated_on=data.get("last_updated_on", None), | ||
| ) | ||
| @dataclass | ||
@@ -76,3 +125,3 @@ class CsvTableDescriptor: | ||
| is_file_line_header: bool = True | ||
| is_first_line_header: bool = True | ||
| """Is the first line a header? The default value of 'true' will be used if this is not provided by the caller.""" | ||
@@ -87,3 +136,3 @@ | ||
| "lineEnd": self.line_end, | ||
| "isFirstLineHeader": self.is_file_line_header, | ||
| "isFirstLineHeader": self.is_first_line_header, | ||
| } | ||
@@ -93,3 +142,14 @@ delete_none_keys(request) | ||
| def fill_from_dict(self, data: Dict[str, Any]) -> "Self": | ||
| """Converts a response from the REST API into this dataclass.""" | ||
| self.separator = data.get("separator", self.separator) | ||
| self.quote_character = data.get("quoteCharacter", self.quote_character) | ||
| self.escape_character = data.get("escapeCharacter", self.escape_character) | ||
| self.line_end = data.get("lineEnd", self.line_end) | ||
| self.is_first_line_header = data.get( | ||
| "isFirstLineHeader", self.is_first_line_header | ||
| ) | ||
| return self | ||
| @dataclass | ||
@@ -480,2 +540,523 @@ class PartialRow: | ||
| @dataclass | ||
| class Row: | ||
| """ | ||
| Represents a single row of a TableEntity. | ||
| This result is modeled from: <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/Row.html> | ||
| """ | ||
| row_id: Optional[int] = None | ||
| """The immutable ID issued to a new row.""" | ||
| version_number: Optional[int] = None | ||
| """The version number of this row. Each row version is immutable, so when a row | ||
| is updated a new version is created.""" | ||
| etag: Optional[str] = None | ||
| """For queries against EntityViews with query.includeEntityEtag=true, this field | ||
| will contain the etag of the entity. Will be null for all other cases.""" | ||
| values: Optional[List[str]] = None | ||
| """The values for each column of this row. To delete a row, set this to an empty list: []""" | ||
| def to_boolean(value): | ||
| """ | ||
| Convert a string to boolean, case insensitively, | ||
| where true values are: true, t, and 1 and false values are: false, f, 0. | ||
| Raise a ValueError for all other values. | ||
| """ | ||
| if value is None: | ||
| raise ValueError("Can't convert None to boolean.") | ||
| if isinstance(value, bool): | ||
| return value | ||
| if isinstance(value, str): | ||
| lower_value = value.lower() | ||
| if lower_value in ["true", "t", "1"]: | ||
| return True | ||
| if lower_value in ["false", "f", "0"]: | ||
| return False | ||
| raise ValueError(f"Can't convert {value} to boolean.") | ||
| @staticmethod | ||
| def cast_values(values, headers): | ||
| """ | ||
| Convert a row of table query results from strings to the correct column type. | ||
| See: <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/ColumnType.html> | ||
| """ | ||
| if len(values) != len(headers): | ||
| raise ValueError( | ||
| f"The number of columns in the csv file does not match the given headers. {len(values)} fields, {len(headers)} headers" | ||
| ) | ||
| result = [] | ||
| for header, field in zip(headers, values): # noqa: F402 | ||
| columnType = header.get("columnType", "STRING") | ||
| # convert field to column type | ||
| if field is None or field == "": | ||
| result.append(None) | ||
| elif columnType in { | ||
| "STRING", | ||
| "ENTITYID", | ||
| "FILEHANDLEID", | ||
| "LARGETEXT", | ||
| "USERID", | ||
| "LINK", | ||
| }: | ||
| result.append(field) | ||
| elif columnType == "DOUBLE": | ||
| result.append(float(field)) | ||
| elif columnType == "INTEGER": | ||
| result.append(int(field)) | ||
| elif columnType == "BOOLEAN": | ||
| result.append(Row.to_boolean(field)) | ||
| elif columnType == "DATE": | ||
| result.append(from_unix_epoch_time(field)) | ||
| elif columnType in { | ||
| "STRING_LIST", | ||
| "INTEGER_LIST", | ||
| "BOOLEAN_LIST", | ||
| "ENTITYID_LIST", | ||
| "USERID_LIST", | ||
| }: | ||
| result.append(json.loads(field)) | ||
| elif columnType == "DATE_LIST": | ||
| result.append(json.loads(field, parse_int=from_unix_epoch_time)) | ||
| else: | ||
| # default to string for unknown column type | ||
| result.append(field) | ||
| return result | ||
| @classmethod | ||
| def fill_from_dict(cls, data: Dict[str, Any]) -> "Row": | ||
| """Create a Row from a dictionary response.""" | ||
| return cls( | ||
| row_id=data.get("rowId"), | ||
| version_number=data.get("versionNumber"), | ||
| etag=data.get("etag"), | ||
| values=data.get("values"), | ||
| ) | ||
| @dataclass | ||
| class ActionRequiredCount: | ||
| """ | ||
| Represents a single action that the user will need to take in order to download one or more files. | ||
| This result is modeled from: <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/download/ActionRequiredCount.html> | ||
| """ | ||
| action: Optional[Dict[str, Any]] = None | ||
| """An action that the user must take in order to download a file.""" | ||
| count: Optional[int] = None | ||
| """The number of files that require this action.""" | ||
| @classmethod | ||
| def fill_from_dict(cls, data: Dict[str, Any]) -> "ActionRequiredCount": | ||
| """Create an ActionRequiredCount from a dictionary response.""" | ||
| return cls( | ||
| action=data.get("action", None), | ||
| count=data.get("count", None), | ||
| ) | ||
| @dataclass | ||
| class SelectColumn: | ||
| """ | ||
| A column model contains the metadata of a single column of a TableEntity. | ||
| This result is modeled from: <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/SelectColumn.html> | ||
| """ | ||
| name: Optional[str] = None | ||
| """The required display name of the column""" | ||
| column_type: Optional[ColumnType] = None | ||
| """The column type determines the type of data that can be stored in a column. | ||
| Switching between types (using a transaction with TableUpdateTransactionRequest | ||
| in the "changes" list) is generally allowed except for switching to "_LIST" | ||
| suffixed types. In such cases, a new column must be created and data must be | ||
| copied over manually""" | ||
| id: Optional[str] = None | ||
| """The optional ID of the select column, if this is a direct column selected""" | ||
| @classmethod | ||
| def fill_from_dict(cls, data: Dict[str, Any]) -> "SelectColumn": | ||
| """Create a SelectColumn from a dictionary response.""" | ||
| column_type = None | ||
| column_type_value = data.get("columnType") | ||
| if column_type_value: | ||
| try: | ||
| column_type = ColumnType(column_type_value) | ||
| except ValueError: | ||
| column_type = None | ||
| return cls( | ||
| name=data.get("name"), | ||
| column_type=column_type, | ||
| id=data.get("id"), | ||
| ) | ||
| @dataclass | ||
| class QueryNextPageToken: | ||
| """ | ||
| Token for retrieving the next page of query results. | ||
| This result is modeled from: <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/QueryNextPageToken.html> | ||
| """ | ||
| concrete_type: Optional[str] = None | ||
| """The concrete type of this object""" | ||
| entity_id: Optional[str] = None | ||
| """The ID of the entity (table/view) being queried""" | ||
| token: Optional[str] = None | ||
| """The token for the next page.""" | ||
| @classmethod | ||
| def fill_from_dict(cls, data: Dict[str, Any]) -> "QueryNextPageToken": | ||
| """Create a QueryNextPageToken from a dictionary response.""" | ||
| return cls( | ||
| concrete_type=data.get("concreteType"), | ||
| entity_id=data.get("entityId"), | ||
| token=data.get("token"), | ||
| ) | ||
| @dataclass | ||
| class RowSet: | ||
| """ | ||
| Represents a set of row of a TableEntity. | ||
| This result is modeled from: <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/RowSet.html> | ||
| """ | ||
| concrete_type: Optional[str] = None | ||
| """The concrete type of this object""" | ||
| table_id: Optional[str] = None | ||
| """The ID of the TableEntity than owns these rows""" | ||
| etag: Optional[str] = None | ||
| """Any RowSet returned from Synapse will contain the current etag of the change set. | ||
| To update any rows from a RowSet the etag must be provided with the POST.""" | ||
| headers: Optional[List[SelectColumn]] = None | ||
| """The list of SelectColumns that describes the rows of this set.""" | ||
| rows: Optional[List[Row]] = field(default_factory=list) | ||
| """The Rows of this set. The index of each row value aligns with the index of each header.""" | ||
| @classmethod | ||
| def cast_row( | ||
| cls, row: Dict[str, Any], headers: List[Dict[str, Any]] | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| Cast the values in a single row to their appropriate column types. | ||
| This method takes a row dictionary containing string values from a table query | ||
| response and converts them to the correct Python types based on the column | ||
| headers. For example, converts string "123" to integer 123 for INTEGER columns, | ||
| or string "true" to boolean True for BOOLEAN columns. | ||
| Arguments: | ||
| row: A dictionary representing a single table row with keys that need to be cast to proper types. | ||
| headers: A list of header dictionaries, each containing column metadata | ||
| including 'columnType' which determines how to cast the corresponding | ||
| value in the row. | ||
| Returns: | ||
| The same row dictionary with the 'values' field updated to contain | ||
| properly typed values instead of strings. | ||
| """ | ||
| row["values"] = Row.cast_values(row["values"], headers) | ||
| return row | ||
| @classmethod | ||
| def cast_row_set(cls, rows: List[Row], headers: List[Dict[str, Any]]) -> List[Row]: | ||
| """ | ||
| Cast the values in multiple rows to their appropriate column types. | ||
| This method takes a list of row dictionaries containing string values from a table query | ||
| response and converts them to the correct Python types based on the column headers. | ||
| It applies the same type casting logic as `cast_row` to each row in the collection. | ||
| Arguments: | ||
| rows: A list of row dictionaries, each representing a single table row with | ||
| field contains a list of string values that need to be cast to proper types. | ||
| headers: A list of header dictionaries, each containing column metadata | ||
| including 'columnType' which determines how to cast the corresponding | ||
| values in each row. | ||
| Returns: | ||
| A list of row dictionaries with the 'values' field in each row updated to | ||
| contain properly typed values instead of strings. | ||
| """ | ||
| rows = [cls.cast_row(row, headers) for row in rows] | ||
| return rows | ||
| @classmethod | ||
| def fill_from_dict(cls, data: Dict[str, Any]) -> "RowSet": | ||
| """Create a RowSet from a dictionary response.""" | ||
| headers_data = data.get("headers") | ||
| rows_data = data.get("rows") | ||
| # Handle headers - convert to SelectColumn objects | ||
| headers = None | ||
| if headers_data and isinstance(headers_data, list): | ||
| headers = [SelectColumn.fill_from_dict(header) for header in headers_data] | ||
| # Handle rows - cast values and convert to Row objects | ||
| rows = None | ||
| if rows_data and isinstance(rows_data, list): | ||
| # Cast row values based on header types if headers are available | ||
| if headers_data and isinstance(headers_data, list): | ||
| rows_data = cls.cast_row_set(rows_data, headers_data) | ||
| # Convert to Row objects | ||
| rows = [Row.fill_from_dict(row) for row in rows_data] | ||
| return cls( | ||
| concrete_type=data.get("concreteType"), | ||
| table_id=data.get("tableId"), | ||
| etag=data.get("etag"), | ||
| headers=headers, | ||
| rows=rows, | ||
| ) | ||
| @dataclass | ||
| class QueryResult: | ||
| """ | ||
| A page of query result. | ||
| This result is modeled from: <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/QueryResult.html> | ||
| """ | ||
| query_results: RowSet | ||
| """Represents a set of row of a TableEntity (RowSet)""" | ||
| concrete_type: str = QUERY_RESULT | ||
| """The concrete type of this object""" | ||
| next_page_token: Optional[QueryNextPageToken] = None | ||
| """Token for retrieving the next page of results, if available""" | ||
| @classmethod | ||
| def fill_from_dict(cls, data: Dict[str, Any]) -> "QueryResult": | ||
| """Create a QueryResult from a dictionary response.""" | ||
| next_page_token = None | ||
| query_results = data.get("queryResults", None) | ||
| if data.get("nextPageToken", None): | ||
| next_page_token = QueryNextPageToken.fill_from_dict(data["nextPageToken"]) | ||
| if data.get("queryResults", None): | ||
| query_results = RowSet.fill_from_dict(data["queryResults"]) | ||
| return cls( | ||
| concrete_type=data.get("concreteType"), | ||
| query_results=query_results, | ||
| next_page_token=next_page_token, | ||
| ) | ||
| @dataclass | ||
| class QueryJob(AsynchronousCommunicator): | ||
| """ | ||
| A query job that can be submitted to Synapse and return a DownloadFromTableResult. | ||
| This class combines query request parameters with the ability to receive | ||
| query results through the AsynchronousCommunicator pattern. | ||
| Request modeled from: <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/DownloadFromTableRequest.html> | ||
| Response modeled from: <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/DownloadFromTableResult.html> | ||
| """ | ||
| # Request parameters | ||
| entity_id: str | ||
| """The ID of the entity (table/view) being queried""" | ||
| concrete_type: str = QUERY_TABLE_CSV_REQUEST | ||
| "The concrete type of the request (usually DownloadFromTableRequest)" | ||
| write_header: Optional[bool] = True | ||
| """Should the first line contain the columns names as a header in the resulting file? Set to 'true' to include the headers else, 'false'. The default value is 'true'.""" | ||
| include_row_id_and_row_version: Optional[bool] = True | ||
| """Should the first two columns contain the row ID and row version? The default value is 'true'.""" | ||
| csv_table_descriptor: Optional[CsvTableDescriptor] = None | ||
| """The description of a csv for upload or download.""" | ||
| file_name: Optional[str] = None | ||
| """The optional name for the downloaded table.""" | ||
| sql: Optional[str] = None | ||
| """The SQL query to execute""" | ||
| additional_filters: Optional[List[Dict[str, Any]]] = None | ||
| """Appends additional filters to the SQL query. These are applied before facets. Filters within the list have an AND relationship. If a WHERE clause already exists on the SQL query or facets are selected, it will also be ANDed with the query generated by these additional filters.""" | ||
| """TODO: create QueryFilter dataclass: https://sagebionetworks.jira.com/browse/SYNPY-1651""" | ||
| selected_facets: Optional[List[Dict[str, Any]]] = None | ||
| """The selected facet filters.""" | ||
| """TODO: create FacetColumnRequest dataclass: https://sagebionetworks.jira.com/browse/SYNPY-1651""" | ||
| include_entity_etag: Optional[bool] = False | ||
| """"Optional, default false. When true, a query results against views will include the Etag of each entity in the results. Note: The etag is necessary to update Entities in the view.""" | ||
| select_file_column: Optional[int] = None | ||
| """The id of the column used to select file entities (e.g. to fetch the action required for download). The column needs to be an ENTITYID type column and be part of the schema of the underlying table/view.""" | ||
| select_file_version_column: Optional[int] = None | ||
| """The id of the column used as the version for selecting file entities when required (e.g. to add a materialized view query to the download cart with version enabled). The column needs to be an INTEGER type column and be part of the schema of the underlying table/view.""" | ||
| offset: Optional[int] = None | ||
| """The optional offset into the results""" | ||
| limit: Optional[int] = None | ||
| """The optional limit to the results""" | ||
| sort: Optional[List[Dict[str, Any]]] = None | ||
| """The sort order for the query results (ARRAY<SortItem>)""" | ||
| """TODO: Add SortItem dataclass: https://sagebionetworks.jira.com/browse/SYNPY-1651""" | ||
| # Response attributes (filled after job completion) | ||
| job_id: Optional[str] = None | ||
| """The job ID returned from the async job""" | ||
| results_file_handle_id: Optional[str] = None | ||
| """The file handle ID of the results CSV file""" | ||
| table_id: Optional[str] = None | ||
| """The ID of the table that was queried""" | ||
| etag: Optional[str] = None | ||
| """The etag of the table""" | ||
| headers: Optional[List[SelectColumn]] = None | ||
| """The column headers from the query result""" | ||
| response_concrete_type: Optional[str] = QUERY_TABLE_CSV_RESULT | ||
| """The concrete type of the response (usually DownloadFromTableResult)""" | ||
| def to_synapse_request(self) -> Dict[str, Any]: | ||
| """Convert to DownloadFromTableRequest format for async job submission.""" | ||
| csv_table_descriptor = None | ||
| if self.csv_table_descriptor: | ||
| csv_table_descriptor = self.csv_table_descriptor.to_synapse_request() | ||
| synapse_request = { | ||
| "concreteType": QUERY_TABLE_CSV_REQUEST, | ||
| "entityId": self.entity_id, | ||
| "csvTableDescriptor": csv_table_descriptor, | ||
| "sql": self.sql, | ||
| "writeHeader": self.write_header, | ||
| "includeRowIdAndRowVersion": self.include_row_id_and_row_version, | ||
| "includeEntityEtag": self.include_entity_etag, | ||
| "fileName": self.file_name, | ||
| "additionalFilters": self.additional_filters, | ||
| "selectedFacet": self.selected_facets, | ||
| "selectFileColumns": self.select_file_column, | ||
| "selectFileVersionColumns": self.select_file_version_column, | ||
| "offset": self.offset, | ||
| "sort": self.sort, | ||
| } | ||
| delete_none_keys(synapse_request) | ||
| return synapse_request | ||
| def fill_from_dict(self, synapse_response: Dict[str, Any]) -> "Self": | ||
| """Fill the job results from Synapse response.""" | ||
| # Fill response attributes from DownloadFromTableResult | ||
| headers = None | ||
| headers_data = synapse_response.get("headers") | ||
| if headers_data and isinstance(headers_data, list): | ||
| headers = [SelectColumn.fill_from_dict(header) for header in headers_data] | ||
| self.job_id = synapse_response.get("jobId") | ||
| self.response_concrete_type = synapse_response.get("concreteType") | ||
| self.results_file_handle_id = synapse_response.get("resultsFileHandleId") | ||
| self.table_id = synapse_response.get("tableId") | ||
| self.etag = synapse_response.get("etag") | ||
| self.headers = headers | ||
| return self | ||
| @dataclass | ||
| class Query: | ||
| """ | ||
| Represents a SQL query with optional parameters. | ||
| This result is modeled from: <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/Query.html> | ||
| """ | ||
| sql: str | ||
| """The SQL query string""" | ||
| additional_filters: Optional[List[Dict[str, Any]]] = None | ||
| """Appends additional filters to the SQL query. These are applied before facets. | ||
| Filters within the list have an AND relationship. If a WHERE clause already exists | ||
| on the SQL query or facets are selected, it will also be ANDed with the query | ||
| generated by these additional filters.""" | ||
| """TODO: create QueryFilter dataclass: https://sagebionetworks.jira.com/browse/SYNPY-1651""" | ||
| selected_facets: Optional[List[Dict[str, Any]]] = None | ||
| """The selected facet filters""" | ||
| """TODO: create FacetColumnRequest dataclass: https://sagebionetworks.jira.com/browse/SYNPY-1651""" | ||
| include_entity_etag: Optional[bool] = False | ||
| """Optional, default false. When true, a query results against views will include | ||
| the Etag of each entity in the results. Note: The etag is necessary to update | ||
| Entities in the view.""" | ||
| select_file_column: Optional[int] = None | ||
| """The id of the column used to select file entities (e.g. to fetch the action | ||
| required for download). The column needs to be an ENTITYID type column and be | ||
| part of the schema of the underlying table/view.""" | ||
| select_file_version_column: Optional[int] = None | ||
| """The id of the column used as the version for selecting file entities when required | ||
| (e.g. to add a materialized view query to the download cart with version enabled). | ||
| The column needs to be an INTEGER type column and be part of the schema of the | ||
| underlying table/view.""" | ||
| offset: Optional[int] = None | ||
| """The optional offset into the results""" | ||
| limit: Optional[int] = None | ||
| """The optional limit to the results""" | ||
| sort: Optional[List[Dict[str, Any]]] = None | ||
| """The sort order for the query results (ARRAY<SortItem>)""" | ||
| """TODO: Add SortItem dataclass: https://sagebionetworks.jira.com/browse/SYNPY-1651 """ | ||
| def to_synapse_request(self) -> Dict[str, Any]: | ||
| """Converts the Query object into a dictionary that can be passed into the REST API.""" | ||
| result = { | ||
| "sql": self.sql, | ||
| "additionalFilters": self.additional_filters, | ||
| "selectedFacets": self.selected_facets, | ||
| "includeEntityEtag": self.include_entity_etag, | ||
| "selectFileColumn": self.select_file_column, | ||
| "selectFileVersionColumn": self.select_file_version_column, | ||
| "offset": self.offset, | ||
| "limit": self.limit, | ||
| "sort": self.sort, | ||
| } | ||
| delete_none_keys(result) | ||
| return result | ||
| @dataclass | ||
| class JsonSubColumn: | ||
@@ -504,2 +1085,20 @@ """For column of type JSON that represents the combination of multiple | ||
| @classmethod | ||
| def fill_from_dict(cls, synapse_sub_column: Dict[str, Any]) -> "JsonSubColumn": | ||
| """Converts a response from the synapseclient into this dataclass.""" | ||
| return cls( | ||
| name=synapse_sub_column.get("name", ""), | ||
| column_type=( | ||
| ColumnType(synapse_sub_column.get("columnType", None)) | ||
| if synapse_sub_column.get("columnType", None) | ||
| else ColumnType.STRING | ||
| ), | ||
| json_path=synapse_sub_column.get("jsonPath", ""), | ||
| facet_type=( | ||
| FacetType(synapse_sub_column.get("facetType", None)) | ||
| if synapse_sub_column.get("facetType", None) | ||
| else None | ||
| ), | ||
| ) | ||
| def to_synapse_request(self) -> Dict[str, Any]: | ||
@@ -574,2 +1173,111 @@ """Converts the Column object into a dictionary that can be passed into the | ||
| async def get_async( | ||
| self, *, synapse_client: Optional["Synapse"] = None | ||
| ) -> "Column": | ||
| """ | ||
| Get a column by its ID. | ||
| 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: | ||
| The Column instance. | ||
| Example: Getting a column by ID | ||
| Getting a column by ID | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Column | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def get_column(): | ||
| column = await Column(id="123").get_async() | ||
| print(column.name) | ||
| asyncio.run(get_column()) | ||
| """ | ||
| from synapseclient.api import get_column | ||
| if not self.id: | ||
| raise ValueError("Column ID is required to get a column") | ||
| result = await get_column( | ||
| column_id=self.id, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| self.fill_from_dict(result) | ||
| return self | ||
| @skip_async_to_sync | ||
| @staticmethod | ||
| async def list_async( | ||
| prefix: Optional[str] = None, | ||
| limit: int = 100, | ||
| offset: int = 0, | ||
| *, | ||
| synapse_client: Optional["Synapse"] = None, | ||
| ) -> AsyncGenerator["Column", None]: | ||
| """ | ||
| List columns with optional prefix filtering. | ||
| Arguments: | ||
| prefix: Optional prefix to filter columns by name. | ||
| limit: Number of columns to retrieve per request to Synapse (pagination parameter). | ||
| The function will continue retrieving results until all matching columns are returned. | ||
| offset: The index of the first column to return (pagination parameter). | ||
| 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. | ||
| Yields: | ||
| Column instances. | ||
| Example: Getting all columns | ||
| Getting all columns | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Column | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def get_columns(): | ||
| async for column in Column.list_async(): | ||
| print(column.name) | ||
| asyncio.run(get_columns()) | ||
| Example: Getting columns with a prefix | ||
| Getting columns with a prefix | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Column | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def get_columns(): | ||
| async for column in Column.list_async(prefix="my_prefix"): | ||
| print(column.name) | ||
| asyncio.run(get_columns()) | ||
| """ | ||
| from synapseclient.api import list_columns | ||
| async for column in list_columns( | ||
| prefix=prefix, | ||
| limit=limit, | ||
| offset=offset, | ||
| synapse_client=synapse_client, | ||
| ): | ||
| yield column | ||
| def fill_from_dict( | ||
@@ -595,4 +1303,12 @@ self, synapse_column: Union[Synapse_Column, Dict[str, Any]] | ||
| self.enum_values = synapse_column.get("enumValues", None) | ||
| # TODO: This needs to be converted to its Dataclass. It also needs to be tested to verify conversion. | ||
| self.json_sub_columns = synapse_column.get("jsonSubColumns", None) | ||
| json_sub_columns_data = synapse_column.get("jsonSubColumns", None) | ||
| if json_sub_columns_data: | ||
| self.json_sub_columns = [ | ||
| JsonSubColumn.fill_from_dict(sub_column_data) | ||
| for sub_column_data in json_sub_columns_data | ||
| ] | ||
| else: | ||
| self.json_sub_columns = None | ||
| self._set_last_persistent_instance() | ||
@@ -614,3 +1330,5 @@ return self | ||
| self._last_persistent_instance.json_sub_columns = ( | ||
| replace(self.json_sub_columns) if self.json_sub_columns else None | ||
| [replace(sub_col) for sub_col in self.json_sub_columns] | ||
| if self.json_sub_columns | ||
| else None | ||
| ) | ||
@@ -648,2 +1366,211 @@ | ||
| @dataclass | ||
| class QueryResultBundle: | ||
| """ | ||
| A bundle of information about a query result. | ||
| This result is modeled from: <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/QueryResultBundle.html> | ||
| """ | ||
| concrete_type: str = QUERY_TABLE_CSV_REQUEST | ||
| """The concrete type of this object""" | ||
| query_result: QueryResult = None | ||
| """A page of query result""" | ||
| query_count: Optional[int] = None | ||
| """The total number of rows that match the query. Use mask = 0x2 to include in the | ||
| bundle.""" | ||
| select_columns: Optional[List[SelectColumn]] = None | ||
| """The list of SelectColumns from the select clause. Use mask = 0x4 to include in | ||
| the bundle.""" | ||
| max_rows_per_page: Optional[int] = None | ||
| """The maximum number of rows that can be retrieved in a single call. This is a | ||
| function of the columns that are selected in the query. Use mask = 0x8 to include | ||
| in the bundle.""" | ||
| column_models: Optional[List[Column]] = None | ||
| """The list of ColumnModels for the table. Use mask = 0x10 to include in the bundle.""" | ||
| facets: Optional[List[Dict[str, Any]]] = None | ||
| """TODO: create facets dataclass""" | ||
| """The list of facets for the search results. Use mask = 0x20 to include in the bundle.""" | ||
| sum_file_sizes: Optional[SumFileSizes] = None | ||
| """The sum of the file size for all files in the given view query. Use mask = 0x40 | ||
| to include in the bundle.""" | ||
| last_updated_on: Optional[str] = None | ||
| """The date-time when this table/view was last updated. Note: Since views are | ||
| eventually consistent a view might still be out-of-date even if it was recently | ||
| updated. Use mask = 0x80 to include in the bundle. This is returned in the | ||
| ISO8601 format like `2000-01-01T00:00:00.000Z`.""" | ||
| combined_sql: Optional[str] = None | ||
| """The SQL that is combination of a the input SQL, FacetRequests, AdditionalFilters, | ||
| Sorting, and Pagination. Use mask = 0x100 to include in the bundle.""" | ||
| actions_required: Optional[List[ActionRequiredCount]] = None | ||
| """The first 50 actions required to download the files that are part of the query. | ||
| Use mask = 0x200 to include them in the bundle.""" | ||
| @classmethod | ||
| def fill_from_dict(cls, data: Dict[str, Any]) -> "QueryResultBundle": | ||
| """Create a QueryResultBundle from a dictionary response.""" | ||
| # Handle sum_file_sizes | ||
| sum_file_sizes = None | ||
| sum_file_sizes_data = data.get("sumFileSizes") | ||
| if sum_file_sizes_data: | ||
| sum_file_sizes = SumFileSizes( | ||
| sum_file_size_bytes=sum_file_sizes_data.get("sumFileSizesBytes"), | ||
| greater_than=sum_file_sizes_data.get("greaterThan"), | ||
| ) | ||
| # Handle query_result | ||
| query_result = None | ||
| query_result_data = data.get("queryResult") | ||
| if query_result_data: | ||
| query_result = QueryResult.fill_from_dict(query_result_data) | ||
| # Handle select_columns | ||
| select_columns = None | ||
| select_columns_data = data.get("selectColumns") | ||
| if select_columns_data and isinstance(select_columns_data, list): | ||
| select_columns = [ | ||
| SelectColumn.fill_from_dict(col) for col in select_columns_data | ||
| ] | ||
| # Handle actions_required | ||
| actions_required = None | ||
| actions_required_data = data.get("actionsRequired") | ||
| if actions_required_data and isinstance(actions_required_data, list): | ||
| actions_required = [ | ||
| ActionRequiredCount.fill_from_dict(action) | ||
| for action in actions_required_data | ||
| ] | ||
| # Handle column_models | ||
| column_models = None | ||
| column_models_data = data.get("columnModels") | ||
| if column_models_data and isinstance(column_models_data, list): | ||
| column_models = [Column().fill_from_dict(col) for col in column_models_data] | ||
| return cls( | ||
| concrete_type=data.get("concreteType"), | ||
| query_result=query_result, | ||
| query_count=data.get("queryCount"), | ||
| select_columns=select_columns, | ||
| max_rows_per_page=data.get("maxRowsPerPage"), | ||
| column_models=column_models, | ||
| facets=data.get("facets"), | ||
| sum_file_sizes=sum_file_sizes, | ||
| last_updated_on=data.get("lastUpdatedOn"), | ||
| combined_sql=data.get("combinedSql"), | ||
| actions_required=actions_required, | ||
| ) | ||
| @dataclass | ||
| class QueryBundleRequest(AsynchronousCommunicator): | ||
| """ | ||
| A query bundle request that can be submitted to Synapse to retrieve query results with metadata. | ||
| This class combines query request parameters with the ability to receive | ||
| a QueryResultBundle through the AsynchronousCommunicator pattern. | ||
| The partMask determines which parts of the result bundle are included: | ||
| - Query Results (queryResults) = 0x1 | ||
| - Query Count (queryCount) = 0x2 | ||
| - Select Columns (selectColumns) = 0x4 | ||
| - Max Rows Per Page (maxRowsPerPage) = 0x8 | ||
| - The Table Columns (columnModels) = 0x10 | ||
| - Facet statistics for each faceted column (facetStatistics) = 0x20 | ||
| - The sum of the file sizes (sumFileSizesBytes) = 0x40 | ||
| - The last updated on date (lastUpdatedOn) = 0x80 | ||
| - The combined SQL query including additional filters (combinedSql) = 0x100 | ||
| - The list of actions required for any file in the query (actionsRequired) = 0x200 | ||
| This result is modeled from: <https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/table/QueryBundleRequest.html> | ||
| """ | ||
| # Request parameters | ||
| entity_id: str | ||
| """The ID of the entity (table/view) being queried""" | ||
| query: Query | ||
| """The SQL query with parameters""" | ||
| concrete_type: str = QUERY_BUNDLE_REQUEST | ||
| """The concrete type of this request""" | ||
| part_mask: Optional[int] = None | ||
| """Optional integer mask to request specific parts. Default includes all parts if not specified.""" | ||
| # Response attributes (filled after job completion from QueryResultBundle) | ||
| query_result: Optional[QueryResult] = None | ||
| """A page of query result""" | ||
| query_count: Optional[int] = None | ||
| """The total number of rows that match the query""" | ||
| select_columns: Optional[List[SelectColumn]] = None | ||
| """The list of SelectColumns from the select clause""" | ||
| max_rows_per_page: Optional[int] = None | ||
| """The maximum number of rows that can be retrieved in a single call""" | ||
| column_models: Optional[List[Dict[str, Any]]] = None | ||
| """The list of ColumnModels for the table""" | ||
| facets: Optional[List[Dict[str, Any]]] = None | ||
| """The list of facets for the search results""" | ||
| sum_file_sizes: Optional[SumFileSizes] = None | ||
| """The sum of the file size for all files in the given view query""" | ||
| last_updated_on: Optional[str] = None | ||
| """The date-time when this table/view was last updated""" | ||
| combined_sql: Optional[str] = None | ||
| """The SQL that is combination of a the input SQL, FacetRequests, AdditionalFilters, Sorting, and Pagination""" | ||
| actions_required: Optional[List[ActionRequiredCount]] = None | ||
| """The first 50 actions required to download the files that are part of the query""" | ||
| def to_synapse_request(self) -> Dict[str, Any]: | ||
| """Convert to QueryBundleRequest format for async job submission.""" | ||
| result = { | ||
| "concreteType": self.concrete_type, | ||
| "entityId": self.entity_id, | ||
| "query": self.query, | ||
| } | ||
| if self.part_mask is not None: | ||
| result["partMask"] = self.part_mask | ||
| delete_none_keys(result) | ||
| return result | ||
| def fill_from_dict(self, synapse_response: Dict[str, Any]) -> "Self": | ||
| """Fill the request results from Synapse response (QueryResultBundle).""" | ||
| # Use QueryResultBundle's fill_from_dict logic to populate response fields | ||
| bundle = QueryResultBundle.fill_from_dict(synapse_response) | ||
| # Copy all the result fields from the bundle | ||
| self.query_result = bundle.query_result | ||
| self.query_count = bundle.query_count | ||
| self.select_columns = bundle.select_columns | ||
| self.max_rows_per_page = bundle.max_rows_per_page | ||
| self.column_models = bundle.column_models | ||
| self.facets = bundle.facets | ||
| self.sum_file_sizes = bundle.sum_file_sizes | ||
| self.last_updated_on = bundle.last_updated_on | ||
| self.combined_sql = bundle.combined_sql | ||
| self.actions_required = bundle.actions_required | ||
| return self | ||
| class SchemaStorageStrategy(str, Enum): | ||
@@ -650,0 +1577,0 @@ """Enum used to determine how to store the schema of a table in Synapse.""" |
+174
-59
@@ -1,2 +0,1 @@ | ||
| import asyncio | ||
| from dataclasses import dataclass | ||
@@ -8,2 +7,11 @@ from typing import Dict, List, Optional, Union | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import ( | ||
| create_team, | ||
| delete_team, | ||
| get_membership_status, | ||
| get_team, | ||
| get_team_members, | ||
| get_team_open_invitations, | ||
| invite_to_team, | ||
| ) | ||
| from synapseclient.core.async_utils import async_to_sync, otel_trace_method | ||
@@ -54,2 +62,76 @@ from synapseclient.models.protocols.team_protocol import TeamSynchronousProtocol | ||
| @dataclass | ||
| class TeamMembershipStatus: | ||
| """ | ||
| Contains information about a user's membership status in a Team. | ||
| Represents a [Synapse TeamMembershipStatus](<https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/TeamMembershipStatus.html>). | ||
| User definable fields are: | ||
| Attributes: | ||
| team_id: The id of the Team. | ||
| user_id: The principal id of the user. | ||
| is_member: true if and only if the user is a member of the team | ||
| has_open_invitation: true if and only if the user has an open invitation to join the team | ||
| has_open_request: true if and only if the user has an open request to join the team | ||
| can_join: true if and only if the user requesting this status information can join the user to the team | ||
| membership_approval_required: true if and only if team admin approval is required for the user to join the team | ||
| has_unmet_access_requirement: true if and only if there is at least one unmet access requirement for the user on the team | ||
| can_send_email: true if and only if the user can send an email to the team | ||
| """ | ||
| team_id: Optional[str] = None | ||
| """The ID of the team""" | ||
| user_id: Optional[str] = None | ||
| """The ID of the user""" | ||
| is_member: Optional[bool] = None | ||
| """Whether the user is a member of the team""" | ||
| has_open_invitation: Optional[bool] = None | ||
| """Whether the user has an open invitation to join the team""" | ||
| has_open_request: Optional[bool] = None | ||
| """Whether the user has an open request to join the team""" | ||
| can_join: Optional[bool] = None | ||
| """Whether the user can join the team""" | ||
| membership_approval_required: Optional[bool] = None | ||
| """Whether membership approval is required for the team""" | ||
| has_unmet_access_requirement: Optional[bool] = None | ||
| """Whether the user has unmet access requirements""" | ||
| can_send_email: Optional[bool] = None | ||
| """Whether the user can send email to the team""" | ||
| def fill_from_dict( | ||
| self, membership_status_dict: Dict[str, Union[str, bool]] | ||
| ) -> "TeamMembershipStatus": | ||
| """ | ||
| Converts a response from the REST API into this dataclass. | ||
| Arguments: | ||
| membership_status_dict: The response from the REST API. | ||
| Returns: | ||
| The TeamMembershipStatus object. | ||
| """ | ||
| self.team_id = membership_status_dict.get("teamId", None) | ||
| self.user_id = membership_status_dict.get("userId", None) | ||
| self.is_member = membership_status_dict.get("isMember", None) | ||
| self.has_open_invitation = membership_status_dict.get("hasOpenInvitation", None) | ||
| self.has_open_request = membership_status_dict.get("hasOpenRequest", None) | ||
| self.can_join = membership_status_dict.get("canJoin", None) | ||
| self.membership_approval_required = membership_status_dict.get( | ||
| "membershipApprovalRequired", None | ||
| ) | ||
| self.has_unmet_access_requirement = membership_status_dict.get( | ||
| "hasUnmetAccessRequirement", None | ||
| ) | ||
| self.can_send_email = membership_status_dict.get("canSendEmail", None) | ||
| return self | ||
| @dataclass | ||
| @async_to_sync | ||
@@ -156,3 +238,2 @@ class Team(TeamSynchronousProtocol): | ||
| """ | ||
| loop = asyncio.get_event_loop() | ||
| trace.get_current_span().set_attributes( | ||
@@ -164,11 +245,9 @@ { | ||
| ) | ||
| team = await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client(synapse_client=synapse_client).create_team( | ||
| name=self.name, | ||
| description=self.description, | ||
| icon=self.icon, | ||
| can_public_join=self.can_public_join, | ||
| can_request_membership=self.can_request_membership, | ||
| ), | ||
| team = await create_team( | ||
| name=self.name, | ||
| description=self.description, | ||
| icon=self.icon, | ||
| can_public_join=self.can_public_join, | ||
| can_request_membership=self.can_request_membership, | ||
| synapse_client=synapse_client, | ||
| ) | ||
@@ -192,9 +271,3 @@ self.fill_from_dict(synapse_team=team) | ||
| """ | ||
| loop = asyncio.get_event_loop() | ||
| await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client(synapse_client=synapse_client).delete_team( | ||
| id=self.id, | ||
| ), | ||
| ) | ||
| await delete_team(id=self.id, synapse_client=synapse_client) | ||
@@ -221,18 +294,6 @@ @otel_trace_method( | ||
| if self.id: | ||
| loop = asyncio.get_event_loop() | ||
| api_team = await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client(synapse_client=synapse_client).getTeam( | ||
| id=self.id, | ||
| ), | ||
| ) | ||
| api_team = await get_team(id=self.id, synapse_client=synapse_client) | ||
| return self.fill_from_dict(api_team) | ||
| elif self.name: | ||
| loop = asyncio.get_event_loop() | ||
| api_team = await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client(synapse_client=synapse_client).getTeam( | ||
| id=self.name, | ||
| ), | ||
| ) | ||
| api_team = await get_team(id=self.name, synapse_client=synapse_client) | ||
| return self.fill_from_dict(api_team) | ||
@@ -305,8 +366,4 @@ raise ValueError("Team must have either an id or a name") | ||
| """ | ||
| loop = asyncio.get_event_loop() | ||
| team_members = await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client(synapse_client=synapse_client).getTeamMembers( | ||
| team=self | ||
| ), | ||
| team_members = await get_team_members( | ||
| team=self.id, synapse_client=synapse_client | ||
| ) | ||
@@ -324,3 +381,3 @@ team_member_list = [ | ||
| self, | ||
| user: str, | ||
| user: Union[str, int], | ||
| message: str, | ||
@@ -330,8 +387,12 @@ force: bool = True, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> Dict[str, str]: | ||
| ) -> Union[Dict[str, str], None]: | ||
| """Invites a user to a team given the ID field on the Team instance. | ||
| Arguments: | ||
| user: The username of the user to invite. | ||
| user: The username or ID of the user to invite. | ||
| message: The message to send. | ||
| force: If True, will send the invite even if the user is already a member | ||
| or has an open invitation. If False, will not send the invite if the user | ||
| is already a member or has an open invitation. | ||
| Defaults to True. | ||
| synapse_client: If not passed in and caching was not disabled by | ||
@@ -342,14 +403,20 @@ `Synapse.allow_client_caching(False)` this will use the last created | ||
| Returns: | ||
| dict: The invite response. | ||
| The invite response or None if an invite was not sent. | ||
| """ | ||
| loop = asyncio.get_event_loop() | ||
| invite = await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client(synapse_client=synapse_client).invite_to_team( | ||
| team=self, | ||
| user=user, | ||
| message=message, | ||
| force=force, | ||
| ), | ||
| invite = await invite_to_team( | ||
| team=self.id, | ||
| user=user, | ||
| message=message, | ||
| force=force, | ||
| synapse_client=synapse_client, | ||
| ) | ||
| if invite: | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| client.logger.info( | ||
| f"Invited user {invite['inviteeId']} to team {invite['teamId']}" | ||
| ) | ||
| return invite | ||
@@ -373,11 +440,59 @@ | ||
| """ | ||
| loop = asyncio.get_event_loop() | ||
| invitations = await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client( | ||
| synapse_client=synapse_client | ||
| ).get_team_open_invitations( | ||
| team=self, | ||
| ), | ||
| invitations = await get_team_open_invitations( | ||
| team=self.id, synapse_client=synapse_client | ||
| ) | ||
| return list(invitations) | ||
| async def get_user_membership_status_async( | ||
| self, | ||
| user_id: str, | ||
| *, | ||
| synapse_client: Optional[Synapse] = None, | ||
| ) -> TeamMembershipStatus: | ||
| """Retrieve a user's Team Membership Status bundle for this team. | ||
| Arguments: | ||
| user_id: Synapse user ID | ||
| 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: | ||
| TeamMembershipStatus object | ||
| Example: Check if a user is a member of a team | ||
| This example shows how to check a user's membership status in a team. | ||
| ```python | ||
| import asyncio | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Team | ||
| syn = Synapse() | ||
| syn.login() | ||
| async def check_membership(): | ||
| # Get a team by ID | ||
| team = await Team.from_id_async(123456) | ||
| # Check membership status for a specific user | ||
| user_id = "3350396" # Replace with actual user ID | ||
| status = await team.get_user_membership_status_async(user_id) | ||
| print(f"User ID: {status.user_id}") | ||
| print(f"Is member: {status.is_member}") | ||
| print(f"Can join: {status.can_join}") | ||
| print(f"Has open invitation: {status.has_open_invitation}") | ||
| print(f"Has open request: {status.has_open_request}") | ||
| print(f"Membership approval required: {status.membership_approval_required}") | ||
| asyncio.run(check_membership()) | ||
| ``` | ||
| """ | ||
| from synapseclient import Synapse | ||
| client = Synapse.get_client(synapse_client=synapse_client) | ||
| status = await get_membership_status( | ||
| user_id=user_id, team=self.id, synapse_client=client | ||
| ) | ||
| return TeamMembershipStatus().fill_from_dict(status) |
@@ -1,2 +0,1 @@ | ||
| import asyncio | ||
| from dataclasses import dataclass, field | ||
@@ -6,2 +5,7 @@ from typing import Dict, List, Optional, Union | ||
| from synapseclient import Synapse | ||
| from synapseclient.api import ( | ||
| get_user_profile_by_id, | ||
| get_user_profile_by_username, | ||
| is_user_certified, | ||
| ) | ||
| from synapseclient.core.async_utils import async_to_sync, otel_trace_method | ||
@@ -85,3 +89,3 @@ from synapseclient.models.protocols.user_protocol import UserProfileSynchronousProtocol | ||
| concurrent updates. Since the E-Tag changes every time an entity is updated | ||
| it is used to detect when a client's currentrepresentation of an entity is | ||
| it is used to detect when a client's current representation of an entity is | ||
| out-of-date. | ||
@@ -180,4 +184,3 @@ first_name: This person's given name (forename) | ||
| synapse_user_profile: The dictionary to fill the UserProfile object from. | ||
| Typically filled from a | ||
| [Synapse UserProfile](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/UserProfile.html) object. | ||
| Typically filled from a [Synapse UserProfile](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/UserProfile.html) object. | ||
| """ | ||
@@ -248,24 +251,13 @@ self.id = ( | ||
| """ | ||
| loop = asyncio.get_event_loop() | ||
| if self.id: | ||
| synapse_user_profile = await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client( | ||
| synapse_client=synapse_client | ||
| ).get_user_profile_by_id(id=self.id), | ||
| synapse_user_profile = await get_user_profile_by_id( | ||
| id=self.id, synapse_client=synapse_client | ||
| ) | ||
| elif self.username: | ||
| synapse_user_profile = await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client( | ||
| synapse_client=synapse_client | ||
| ).get_user_profile_by_username(username=self.username), | ||
| synapse_user_profile = await get_user_profile_by_username( | ||
| username=self.username, synapse_client=synapse_client | ||
| ) | ||
| else: | ||
| synapse_user_profile = await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client( | ||
| synapse_client=synapse_client | ||
| ).get_user_profile_by_username(), | ||
| synapse_user_profile = await get_user_profile_by_username( | ||
| synapse_client=synapse_client | ||
| ) | ||
@@ -342,10 +334,5 @@ | ||
| """ | ||
| loop = asyncio.get_event_loop() | ||
| if self.id or self.username: | ||
| is_certified = await loop.run_in_executor( | ||
| None, | ||
| lambda: Synapse.get_client(synapse_client=synapse_client).is_certified( | ||
| user=self.id or self.username | ||
| ), | ||
| is_certified = await is_user_certified( | ||
| user=self.id or self.username, synapse_client=synapse_client | ||
| ) | ||
@@ -352,0 +339,0 @@ else: |
@@ -521,2 +521,5 @@ import re | ||
| Raises: | ||
| ValueError: If the defining_sql attribute is not set. | ||
| Example: Create a new virtual table with a defining SQL query. | ||
@@ -557,2 +560,6 @@ | ||
| ) | ||
| else: | ||
| raise ValueError( | ||
| "The defining_sql attribute must be set for a VirtualTable." | ||
| ) | ||
@@ -559,0 +566,0 @@ return await super().store_async( |
+124
-1
@@ -5,10 +5,46 @@ """ | ||
| from synapseclient.core.models.dict_object import DictObject | ||
| from synapseclient.core.utils import deprecated | ||
| @deprecated( | ||
| version="4.9.0", | ||
| reason="To be removed in 5.0.0. " | ||
| "Moved to the `from synapseclient.models import UserProfile` class. " | ||
| "Check the docstring for the replacement function example.", | ||
| ) | ||
| class UserProfile(DictObject): | ||
| """ | ||
| **Deprecated with replacement.** This class will be removed in 5.0.0. | ||
| Use `from synapseclient.models import UserProfile` instead. | ||
| Information about a Synapse user. In practice the constructor is not called directly by the client. | ||
| Example: Migration to new method | ||
| | ||
| ```python | ||
| # Old approach (DEPRECATED) | ||
| # from synapseclient.team import UserProfile | ||
| # New approach (RECOMMENDED) | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import UserProfile | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Get your own profile | ||
| my_profile = UserProfile().get() | ||
| print(f"My profile: {my_profile.username}") | ||
| # Get another user's profile by username | ||
| profile_by_username = UserProfile.from_username(username='synapse-service-dpe-team') | ||
| print(f"Profile by username: {profile_by_username.username}") | ||
| # Get another user's profile by ID | ||
| profile_by_id = UserProfile.from_id(user_id=3485485) | ||
| print(f"Profile by id: {profile_by_id.username}") | ||
| ``` | ||
| Attributes: | ||
@@ -40,7 +76,25 @@ ownerId: A foreign key to the ID of the 'principal' object for the user. | ||
| @deprecated( | ||
| version="4.9.0", | ||
| reason="To be removed in 5.0.0. " | ||
| "Moved to the `from synapseclient.models import UserGroupHeader` class. " | ||
| "Check the docstring for the replacement function example.", | ||
| ) | ||
| class UserGroupHeader(DictObject): | ||
| """ | ||
| **Deprecated with replacement.** This class will be removed in 5.0.0. | ||
| Use `from synapseclient.models import UserGroupHeader` instead. | ||
| Select metadata about a Synapse principal. | ||
| In practice the constructor is not called directly by the client. | ||
| Example: Migration to new method | ||
| ```python | ||
| # Old approach (DEPRECATED) | ||
| # from synapseclient.team import UserGroupHeader | ||
| # New approach (RECOMMENDED) | ||
| from synapseclient.models import UserGroupHeader | ||
| ``` | ||
| Attributes: | ||
@@ -59,7 +113,48 @@ ownerId A foreign key to the ID of the 'principal' object for the user. | ||
| @deprecated( | ||
| version="4.9.0", | ||
| reason="To be removed in 5.0.0. " | ||
| "Moved to the `from synapseclient.models import Team` class. " | ||
| "Check the docstring for the replacement function example.", | ||
| ) | ||
| class Team(DictObject): | ||
| """ | ||
| **Deprecated with replacement.** This class will be removed in 5.0.0. | ||
| Use `from synapseclient.models import Team` instead. | ||
| Represents a [Synapse Team](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/Team.html). | ||
| User definable fields are: | ||
| Example: Migration to new method | ||
| | ||
| ```python | ||
| # Old approach (DEPRECATED) | ||
| # from synapseclient.team import Team | ||
| # New approach (RECOMMENDED) | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Team | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Create a new team | ||
| new_team = Team(name="My Team", description="A sample team") | ||
| created_team = new_team.create() | ||
| print(f"Created team: {created_team.name}") | ||
| # Get a team by ID | ||
| team_by_id = Team.from_id(id=12345) | ||
| print(f"Team by ID: {team_by_id.name}") | ||
| # Get a team by name | ||
| team_by_name = Team.from_name(name="My Team") | ||
| print(f"Team by name: {team_by_name.name}") | ||
| # Get team members | ||
| members = team_by_id.members() | ||
| print(f"Team has {len(members)} members") | ||
| ``` | ||
| Attributes: | ||
@@ -95,7 +190,35 @@ icon: The fileHandleId for icon image of the Team | ||
| @deprecated( | ||
| version="4.9.0", | ||
| reason="To be removed in 5.0.0. " | ||
| "Moved to the `from synapseclient.models import TeamMember` class. " | ||
| "Check the docstring for the replacement function example.", | ||
| ) | ||
| class TeamMember(DictObject): | ||
| """ | ||
| **Deprecated with replacement.** This class will be removed in 5.0.0. | ||
| Use `from synapseclient.models import TeamMember` instead. | ||
| Contains information about a user's membership in a Team. | ||
| In practice the constructor is not called directly by the client. | ||
| Example: Migration to new method | ||
| ```python | ||
| # Old approach (DEPRECATED) | ||
| # from synapseclient.team import TeamMember | ||
| # New approach (RECOMMENDED) | ||
| from synapseclient import Synapse | ||
| from synapseclient.models import Team, TeamMember | ||
| syn = Synapse() | ||
| syn.login() | ||
| # Get team members using the new Team model | ||
| team = Team.from_id(id=12345) | ||
| members = team.members() | ||
| for member in members: | ||
| print(f"Member: {member.member.user_name}") | ||
| ``` | ||
| Attributes: | ||
@@ -102,0 +225,0 @@ teamId: The ID of the team |
@@ -194,4 +194,3 @@ """This module is responsible for holding sync to/from synapse utility functions.""" | ||
| manifest=manifest, | ||
| ), | ||
| syn=syn, | ||
| ) | ||
| ) | ||
@@ -1175,4 +1174,3 @@ | ||
| associate_activity_to_new_version, | ||
| ), | ||
| syn, | ||
| ) | ||
| ) | ||
@@ -1186,4 +1184,3 @@ else: | ||
| associate_activity_to_new_version, | ||
| ), | ||
| syn, | ||
| ) | ||
| ) | ||
@@ -1190,0 +1187,0 @@ progress_bar.update(total_upload_size - progress_bar.n) |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
2835623
21.75%136
4.62%60711
22.52%