diff --git a/synapseclient/extensions/curator/file_based_metadata_task.py b/synapseclient/extensions/curator/file_based_metadata_task.py index cd2a39191..1cfc1f2e2 100644 --- a/synapseclient/extensions/curator/file_based_metadata_task.py +++ b/synapseclient/extensions/curator/file_based_metadata_task.py @@ -12,6 +12,7 @@ from synapseclient.core.exceptions import SynapseHTTPError # type: ignore from synapseclient.extensions.curator.utils import project_id_from_entity_id from synapseclient.models import ( # type: ignore + AuthorizationMode, Column, ColumnType, EntityView, @@ -325,6 +326,8 @@ def create_file_based_metadata_task( enable_derived_annotations: bool = False, assignee_principal_id: Optional[Union[str, int]] = None, view_type_mask: Union[int, ViewTypeMask] = ViewTypeMask.FILE, + suggested_authorization_mode: Optional[Union[AuthorizationMode, str]] = None, + collaborator_principal_ids: Optional[list[str]] = None, *, synapse_client: Optional[Synapse] = None, ) -> Tuple[str, str]: @@ -338,7 +341,7 @@ def create_file_based_metadata_task( ```python import synapseclient from synapseclient.extensions.curator import create_file_based_metadata_task - from synapseclient.models import ViewTypeMask + from synapseclient.models import AuthorizationMode, ViewTypeMask syn = synapseclient.Synapse() syn.login() @@ -351,8 +354,9 @@ def create_file_based_metadata_task( attach_wiki=False, entity_view_name="Biospecimen Metadata View", schema_uri="sage.schemas.v2571-amp.Biospecimen.schema-0.0.1", - assignee_principal_id=123456, # Optional: Assign to a user or team (can be str or int) - view_type_mask=ViewTypeMask.FILE | ViewTypeMask.DOCKER, # Optional: include additional entity types in the view + assignee_principal_id=123456, # Optional: Assign to a user or team (can be str or int) + view_type_mask=ViewTypeMask.FILE | ViewTypeMask.DOCKER, # Optional: include additional entity types in the view + suggested_authorization_mode=AuthorizationMode.SOURCE_BENEFACTOR, ) ``` @@ -377,6 +381,13 @@ def create_file_based_metadata_task( ViewTypeMask.FILE. Additional types can be added using bitwise OR (e.g., ViewTypeMask.FILE | ViewTypeMask.DOCKER). Accepts either a ViewTypeMask enum member or its raw integer value. + suggested_authorization_mode: The authorization mode a client should use when + creating a linked grid session for this task. When omitted, clients follow + legacy behavior: find or create a personal, unlinked grid session. + collaborator_principal_ids: The set of principal IDs that should collaborate on + the grid session. Used to set the owner(s) of a linked GridSession when + suggested_authorization_mode is SESSION_OWNER. Reserved for future + multi-owner support; not actively used at this time. 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. @@ -481,6 +492,8 @@ def create_file_based_metadata_task( task_properties=FileBasedMetadataTaskProperties( upload_folder_id=folder_id, file_view_id=entity_view_id, + suggested_authorization_mode=suggested_authorization_mode, + collaborator_principal_ids=collaborator_principal_ids, ), ).store(synapse_client=synapse_client) except Exception as e: diff --git a/synapseclient/extensions/curator/record_based_metadata_task.py b/synapseclient/extensions/curator/record_based_metadata_task.py index c97a49260..68832e0c4 100644 --- a/synapseclient/extensions/curator/record_based_metadata_task.py +++ b/synapseclient/extensions/curator/record_based_metadata_task.py @@ -14,6 +14,7 @@ from synapseclient.core.utils import test_import_pandas from synapseclient.extensions.curator.utils import project_id_from_entity_id from synapseclient.models import ( + AuthorizationMode, CurationTask, Grid, JSONSchema, @@ -111,6 +112,8 @@ def create_record_based_metadata_task( bind_schema_to_record_set: bool = True, enable_derived_annotations: bool = False, assignee_principal_id: Optional[Union[str, int]] = None, + suggested_authorization_mode: Optional[Union[AuthorizationMode, str]] = None, + collaborator_principal_ids: Optional[list[str]] = None, *, synapse_client: Optional[Synapse] = None, project_id: Optional[str] = None, # Deprecated, will be removed in v5.0.0 @@ -136,13 +139,13 @@ def create_record_based_metadata_task( Example: Creating a record-based metadata curation task with a schema URI In this example, we create a RecordSet and CurationTask for biospecimen metadata curation using a schema URI. By default this will also bind the schema to the - RecordSet, however the `bind_schema_to_record_set` parameter can be set to + RecordSet, however the bind_schema_to_record_set parameter can be set to False to skip that step. - ```python import synapseclient from synapseclient.extensions.curator import create_record_based_metadata_task + from synapseclient.models import AuthorizationMode syn = synapseclient.Synapse() syn.login() @@ -157,6 +160,7 @@ def create_record_based_metadata_task( instructions="Please curate this metadata according to the schema requirements", schema_uri="schema-org-schema.name.schema-v1.0.0", assignee_principal_id=123456, # Optional: Assign to a user or team (can be str or int) + suggested_authorization_mode=AuthorizationMode.SOURCE_BENEFACTOR, create_grid=False, # Opt out of deprecated Grid creation ) ``` @@ -182,6 +186,13 @@ def create_record_based_metadata_task( (default), the task will be unassigned. For metadata tasks, this determines the owner of the grid session. Team members can all join grid sessions owned by their team, while user-owned grid sessions are restricted to that user only. + suggested_authorization_mode: The authorization mode a client should use when + creating a linked grid session for this task. When omitted, clients follow + legacy behavior: find or create a personal, unlinked grid session. + collaborator_principal_ids: The set of principal IDs that should collaborate on + the grid session. Used to set the owner(s) of a linked GridSession when + suggested_authorization_mode is SESSION_OWNER. Reserved for future + multi-owner support; not actively used at this time. 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. @@ -286,6 +297,8 @@ def create_record_based_metadata_task( ), task_properties=RecordBasedMetadataTaskProperties( record_set_id=record_set_id, + suggested_authorization_mode=suggested_authorization_mode, + collaborator_principal_ids=collaborator_principal_ids, ), ).store(synapse_client=synapse_client) synapse_client.logger.info( diff --git a/synapseclient/models/__init__.py b/synapseclient/models/__init__.py index 0badee2db..a5324acb9 100644 --- a/synapseclient/models/__init__.py +++ b/synapseclient/models/__init__.py @@ -8,6 +8,7 @@ ) from synapseclient.models.annotations import Annotations from synapseclient.models.curation import ( + AuthorizationMode, CurationTask, FileBasedMetadataTaskProperties, Grid, @@ -94,6 +95,7 @@ "Team", "TeamMember", "TeamMembershipStatus", + "AuthorizationMode", "CurationTask", "FileBasedMetadataTaskProperties", "RecordBasedMetadataTaskProperties", diff --git a/synapseclient/models/curation.py b/synapseclient/models/curation.py index 8eb110c31..b1852f132 100644 --- a/synapseclient/models/curation.py +++ b/synapseclient/models/curation.py @@ -10,7 +10,16 @@ from dataclasses import dataclass, field, replace from datetime import datetime, timezone from enum import Enum -from typing import Any, AsyncGenerator, Dict, Generator, Optional, Protocol, Union +from typing import ( + Any, + AsyncGenerator, + ClassVar, + Dict, + Generator, + Optional, + Protocol, + Union, +) from opentelemetry import trace @@ -52,6 +61,7 @@ merge_dataclass_entities, ) from synapseclient.models.mixins.asynchronous_job import AsynchronousCommunicator +from synapseclient.models.mixins.enum_coercion import EnumCoercionMixin from synapseclient.models.recordset import ValidationSummary from synapseclient.models.table_components import Column, CsvTableDescriptor, Query @@ -76,8 +86,23 @@ class TaskState(str, Enum): """The task has been canceled and is no longer needed.""" +class AuthorizationMode(str, Enum): + """ + The authorization mode a client should use when creating a linked grid session + for a CurationTask. + + See . + """ + + SESSION_OWNER = "SESSION_OWNER" + """The grid session is owned by one or more explicit principals (collaborator_principal_ids).""" + + SOURCE_BENEFACTOR = "SOURCE_BENEFACTOR" + """The grid session inherits permissions from the benefactor of the source entity.""" + + @dataclass -class FileBasedMetadataTaskProperties: +class FileBasedMetadataTaskProperties(EnumCoercionMixin): """ A CurationTaskProperties for file-based data, describing where data is uploaded and a view which contains the annotations. @@ -89,12 +114,28 @@ class FileBasedMetadataTaskProperties: file_view_id: The synId of the FileView that shows all data of this type """ + _ENUM_FIELDS: ClassVar[dict[str, type]] = { + "suggested_authorization_mode": AuthorizationMode + } + upload_folder_id: Optional[str] = None """The synId of the folder where data files of this type are to be uploaded""" file_view_id: Optional[str] = None """The synId of the FileView that shows all data of this type""" + suggested_authorization_mode: Optional[Union[AuthorizationMode, str]] = None + """The authorization mode a client should use when creating a linked grid session for + this task. When omitted, clients follow legacy behavior: find or create a personal, + unlinked grid session. When this field changes, the server automatically clears + activeSessionId from the task status. Accepts either an AuthorizationMode enum + value or its string equivalent (e.g., "SOURCE_BENEFACTOR").""" + + collaborator_principal_ids: Optional[list[str]] = None + """The set of principal IDs that should collaborate on the grid session. Used to set + the owner(s) of a linked GridSession when suggested_authorization_mode is SESSION_OWNER. + Reserved for future multi-owner support; not actively used at this time.""" + def fill_from_dict( self, synapse_response: Union[Dict[str, Any], Any] ) -> "FileBasedMetadataTaskProperties": @@ -109,6 +150,12 @@ def fill_from_dict( """ self.upload_folder_id = synapse_response.get("uploadFolderId", None) self.file_view_id = synapse_response.get("fileViewId", None) + self.suggested_authorization_mode = synapse_response.get( + "suggestedAuthorizationMode", None + ) + self.collaborator_principal_ids = synapse_response.get( + "collaboratorPrincipalIds", None + ) return self def to_synapse_request(self) -> Dict[str, Any]: @@ -118,16 +165,23 @@ def to_synapse_request(self) -> Dict[str, Any]: Returns: A dictionary representation of this object for API requests. """ - request_dict = {"concreteType": FILE_BASED_METADATA_TASK_PROPERTIES} - if self.upload_folder_id is not None: - request_dict["uploadFolderId"] = self.upload_folder_id - if self.file_view_id is not None: - request_dict["fileViewId"] = self.file_view_id + request_dict = { + "concreteType": FILE_BASED_METADATA_TASK_PROPERTIES, + "uploadFolderId": self.upload_folder_id, + "fileViewId": self.file_view_id, + "suggestedAuthorizationMode": ( + self.suggested_authorization_mode.value + if self.suggested_authorization_mode is not None + else None + ), + "collaboratorPrincipalIds": self.collaborator_principal_ids, + } + delete_none_keys(request_dict) return request_dict @dataclass -class RecordBasedMetadataTaskProperties: +class RecordBasedMetadataTaskProperties(EnumCoercionMixin): """ A CurationTaskProperties for record-based metadata. @@ -137,9 +191,25 @@ class RecordBasedMetadataTaskProperties: record_set_id: The synId of the RecordSet that will contain all record-based metadata """ + _ENUM_FIELDS: ClassVar[Dict[str, type]] = { + "suggested_authorization_mode": AuthorizationMode + } + record_set_id: Optional[str] = None """The synId of the RecordSet that will contain all record-based metadata""" + suggested_authorization_mode: Optional[Union[AuthorizationMode, str]] = None + """The authorization mode a client should use when creating a linked grid session for + this task. When omitted, clients follow legacy behavior: find or create a personal, + unlinked grid session. When this field changes, the server automatically clears + activeSessionId from the task status. Accepts either an AuthorizationMode enum + value or its string equivalent (e.g., "SOURCE_BENEFACTOR").""" + + collaborator_principal_ids: Optional[list[str]] = None + """The set of principal IDs that should collaborate on the grid session. Used to set + the owner(s) of a linked GridSession when suggested_authorization_mode is SESSION_OWNER. + Reserved for future multi-owner support; not actively used at this time.""" + def fill_from_dict( self, synapse_response: Union[Dict[str, Any], Any] ) -> "RecordBasedMetadataTaskProperties": @@ -153,6 +223,12 @@ def fill_from_dict( The RecordBasedMetadataTaskProperties object. """ self.record_set_id = synapse_response.get("recordSetId", None) + self.suggested_authorization_mode = synapse_response.get( + "suggestedAuthorizationMode", None + ) + self.collaborator_principal_ids = synapse_response.get( + "collaboratorPrincipalIds", None + ) return self def to_synapse_request(self) -> Dict[str, Any]: @@ -162,9 +238,17 @@ def to_synapse_request(self) -> Dict[str, Any]: Returns: A dictionary representation of this object for API requests. """ - request_dict = {"concreteType": RECORD_BASED_METADATA_TASK_PROPERTIES} - if self.record_set_id is not None: - request_dict["recordSetId"] = self.record_set_id + request_dict = { + "concreteType": RECORD_BASED_METADATA_TASK_PROPERTIES, + "recordSetId": self.record_set_id, + "suggestedAuthorizationMode": ( + self.suggested_authorization_mode.value + if self.suggested_authorization_mode is not None + else None + ), + "collaboratorPrincipalIds": self.collaborator_principal_ids, + } + delete_none_keys(request_dict) return request_dict