New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details
Socket
Book a DemoSign in
Socket

synapseclient

Package Overview
Dependencies
Maintainers
1
Versions
69
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

synapseclient - pypi Package Compare versions

Comparing version
4.9.0
to
4.10.0
+212
synapseclient/api/curation_services.py
"""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 | [![Build Status develop branch](https://github.com/Sage-Bionetworks/synapsePythonClient/workflows/build/badge.svg?branch=develop)](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Adevelop)
master | [![Build Status master branch](https://github.com/Sage-Bionetworks/synapsePythonClient/workflows/build/badge.svg?branch=master)](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Amaster)
develop | [![Build Status develop branch](https://github.com/Sage-Bionetworks/synapsePythonClient/actions/workflows/build.yml/badge.svg?branch=develop)](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Adevelop)
master | [![Build Status master branch](https://github.com/Sage-Bionetworks/synapsePythonClient/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Amaster)
[![Get the synapseclient from PyPI](https://img.shields.io/pypi/v/synapseclient.svg)](https://pypi.python.org/pypi/synapseclient/) [![Supported Python Versions](https://img.shields.io/pypi/pyversions/synapseclient.svg)](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 | [![Build Status develop branch](https://github.com/Sage-Bionetworks/synapsePythonClient/workflows/build/badge.svg?branch=develop)](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Adevelop)
master | [![Build Status master branch](https://github.com/Sage-Bionetworks/synapsePythonClient/workflows/build/badge.svg?branch=master)](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Amaster)
develop | [![Build Status develop branch](https://github.com/Sage-Bionetworks/synapsePythonClient/actions/workflows/build.yml/badge.svg?branch=develop)](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Adevelop)
master | [![Build Status master branch](https://github.com/Sage-Bionetworks/synapsePythonClient/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Amaster)
[![Get the synapseclient from PyPI](https://img.shields.io/pypi/v/synapseclient.svg)](https://pypi.python.org/pypi/synapseclient/) [![Supported Python Versions](https://img.shields.io/pypi/pyversions/synapseclient.svg)](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

@@ -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 | [![Build Status develop branch](https://github.com/Sage-Bionetworks/synapsePythonClient/workflows/build/badge.svg?branch=develop)](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Adevelop)
master | [![Build Status master branch](https://github.com/Sage-Bionetworks/synapsePythonClient/workflows/build/badge.svg?branch=master)](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Amaster)
develop | [![Build Status develop branch](https://github.com/Sage-Bionetworks/synapsePythonClient/actions/workflows/build.yml/badge.svg?branch=develop)](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Adevelop)
master | [![Build Status master branch](https://github.com/Sage-Bionetworks/synapsePythonClient/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/Sage-Bionetworks/synapsePythonClient/actions?query=branch%3Amaster)
[![Get the synapseclient from PyPI](https://img.shields.io/pypi/v/synapseclient.svg)](https://pypi.python.org/pypi/synapseclient/) [![Supported Python Versions](https://img.shields.io/pypi/pyversions/synapseclient.svg)](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 @@ )

@@ -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.
&nbsp;
```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."""

@@ -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 @@ &nbsp;

)
else:
raise ValueError(
"The defining_sql attribute must be set for a VirtualTable."
)

@@ -559,0 +566,0 @@ return await super().store_async(

@@ -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
&nbsp;
```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
&nbsp;
```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