Skip to content

Commit c277ef5

Browse files
authored
Merge pull request #1321 from Sage-Bionetworks/synpy-1759
[SYNPY-1759] Allow task to be assigned using assigneePrincipalId
2 parents 4a847d7 + d128f0b commit c277ef5

7 files changed

Lines changed: 249 additions & 11 deletions

File tree

docs/guides/extensions/curator/metadata_curation.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ By following this guide, you will:
1818
- Python environment with synapseclient and the `curator` extension installed (ie. `pip install --upgrade "synapseclient[curator]"`)
1919
- An existing Synapse project and folder where you want to manage metadata
2020
- A JSON Schema registered in Synapse (many schemas are already available for Sage-affiliated projects, or you can register your own by following the [JSON Schema tutorial](../../../tutorials/python/json_schema.md))
21+
- (Optional) An existing Synapse team if you want multiple users to collaborate on the same Grid session. Pass the team's ID as `assignee_principal_id` when creating the curation task.
2122

2223
## Step 1: Authenticate and import required functions
2324

@@ -80,7 +81,8 @@ record_set, curation_task, data_grid = create_record_based_metadata_task(
8081
upsert_keys=["StudyKey"], # Fields that uniquely identify records
8182
instructions="Complete all required fields according to the schema. Use StudyKey to link records to your data files.",
8283
schema_uri=schema_uri, # Schema found in Step 2
83-
bind_schema_to_record_set=True
84+
bind_schema_to_record_set=True,
85+
assignee_principal_id="123456" # Optional: Assign to a user or team
8486
)
8587

8688
print(f"Created RecordSet: {record_set.id}")
@@ -106,7 +108,8 @@ entity_view_id, task_id = create_file_based_metadata_task(
106108
instructions="Annotate each file with metadata according to the schema requirements.",
107109
attach_wiki=False, # Creates a wiki in the folder with the entity view (Defaults to False)
108110
entity_view_name="Animal Study Files View",
109-
schema_uri=schema_uri # Schema found in Step 2
111+
schema_uri=schema_uri, # Schema found in Step 2
112+
assignee_principal_id="123456" # Optional: Assign to a user or team
110113
)
111114

112115
print(f"Created EntityView: {entity_view_id}")
@@ -156,7 +159,8 @@ record_set, curation_task, data_grid = create_record_based_metadata_task(
156159
upsert_keys=["StudyKey"],
157160
instructions="Complete metadata for all study animals using StudyKey to link records to data files.",
158161
schema_uri=schema_uri,
159-
bind_schema_to_record_set=True
162+
bind_schema_to_record_set=True,
163+
assignee_principal_id="123456" # Optional: Assign to a user or team
160164
)
161165

162166
print(f"Record-based workflow created:")
@@ -171,7 +175,8 @@ entity_view_id, task_id = create_file_based_metadata_task(
171175
instructions="Annotate each file with complete metadata according to schema.",
172176
attach_wiki=True,
173177
entity_view_name="Animal Study Files View",
174-
schema_uri=schema_uri
178+
schema_uri=schema_uri,
179+
assignee_principal_id="123456" # Optional: Assign to a user or team
175180
)
176181

177182
print(f"File-based workflow created:")

synapseclient/extensions/curator/file_based_metadata_task.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
in Synapse, including EntityView creation, CurationTask setup, and Wiki attachment.
66
"""
77

8-
from typing import Any, Optional, Tuple
8+
from typing import Any, Optional, Tuple, Union
99

1010
from synapseclient import Synapse # type: ignore
1111
from synapseclient import Wiki # type: ignore
@@ -298,6 +298,7 @@ def create_file_based_metadata_task(
298298
entity_view_name: str = "JSON Schema view",
299299
schema_uri: Optional[str] = None,
300300
enable_derived_annotations: bool = False,
301+
assignee_principal_id: Optional[Union[str, int]] = None,
301302
*,
302303
synapse_client: Optional[Synapse] = None,
303304
) -> Tuple[str, str]:
@@ -322,7 +323,8 @@ def create_file_based_metadata_task(
322323
instructions="Please curate this metadata according to the schema requirements",
323324
attach_wiki=False,
324325
entity_view_name="Biospecimen Metadata View",
325-
schema_uri="sage.schemas.v2571-amp.Biospecimen.schema-0.0.1"
326+
schema_uri="sage.schemas.v2571-amp.Biospecimen.schema-0.0.1",
327+
assignee_principal_id=123456 # Optional: Assign to a user or team (can be str or int)
326328
)
327329
```
328330
@@ -338,6 +340,11 @@ def create_file_based_metadata_task(
338340
the schema will be bound to the folder before creating the entity view.
339341
(e.g., 'sage.schemas.v2571-amp.Biospecimen.schema-0.0.1')
340342
enable_derived_annotations: If true, enable derived annotations. Defaults to False.
343+
assignee_principal_id: The principal ID of the user or team to assign to this
344+
curation task. Can be provided as either a string or an integer. If None
345+
(default), the task will be unassigned. For metadata tasks, this determines
346+
the owner of the grid session. Team members can all join grid sessions owned
347+
by their team, while user-owned grid sessions are restricted to that user only.
341348
synapse_client: If not passed in and caching was not disabled by
342349
`Synapse.allow_client_caching(False)` this will use the last created
343350
instance from the Synapse class constructor.
@@ -445,6 +452,11 @@ def create_file_based_metadata_task(
445452
data_type=task_datatype,
446453
project_id=project.id,
447454
instructions=instructions,
455+
assignee_principal_id=(
456+
str(assignee_principal_id)
457+
if assignee_principal_id is not None
458+
else None
459+
),
448460
task_properties=FileBasedMetadataTaskProperties(
449461
upload_folder_id=folder_id,
450462
file_view_id=entity_view_id,

synapseclient/extensions/curator/record_based_metadata_task.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
in Synapse, including RecordSet creation, CurationTask setup, and Grid view initialization.
77
"""
88
import tempfile
9-
from typing import Any, Dict, List, Optional, Tuple
9+
from typing import Any, Dict, List, Optional, Tuple, Union
1010

1111
from synapseclient import Synapse
1212
from synapseclient.core.typing_utils import DataFrame as DATA_FRAME_TYPE
@@ -109,6 +109,7 @@ def create_record_based_metadata_task(
109109
schema_uri: str,
110110
bind_schema_to_record_set: bool = True,
111111
enable_derived_annotations: bool = False,
112+
assignee_principal_id: Optional[Union[str, int]] = None,
112113
*,
113114
synapse_client: Optional[Synapse] = None,
114115
) -> Tuple[RecordSet, CurationTask, Grid]:
@@ -148,7 +149,8 @@ def create_record_based_metadata_task(
148149
curation_task_name="BiospecimenMetadataTemplate",
149150
upsert_keys=["specimenID"],
150151
instructions="Please curate this metadata according to the schema requirements",
151-
schema_uri="schema-org-schema.name.schema-v1.0.0"
152+
schema_uri="schema-org-schema.name.schema-v1.0.0",
153+
assignee_principal_id=123456 # Optional: Assign to a user or team (can be str or int)
152154
)
153155
```
154156
@@ -167,6 +169,11 @@ def create_record_based_metadata_task(
167169
bind_schema_to_record_set: Whether to bind the given schema to the RecordSet
168170
(default: True).
169171
enable_derived_annotations: If true, enable derived annotations. Defaults to False.
172+
assignee_principal_id: The principal ID of the user or team to assign to this
173+
curation task. Can be provided as either a string or an integer. If None
174+
(default), the task will be unassigned. For metadata tasks, this determines
175+
the owner of the grid session. Team members can all join grid sessions owned
176+
by their team, while user-owned grid sessions are restricted to that user only.
170177
synapse_client: If not passed in and caching was not disabled by
171178
`Synapse.allow_client_caching(False)` this will use the last created
172179
instance from the Synapse class constructor.
@@ -244,6 +251,11 @@ def create_record_based_metadata_task(
244251
data_type=curation_task_name,
245252
project_id=project_id,
246253
instructions=instructions,
254+
assignee_principal_id=(
255+
str(assignee_principal_id)
256+
if assignee_principal_id is not None
257+
else None
258+
),
247259
task_properties=RecordBasedMetadataTaskProperties(
248260
record_set_id=record_set_id,
249261
),

synapseclient/models/curation.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,11 @@ class CurationTask(CurationTaskSynchronousProtocol):
466466
modified_by: Optional[str] = None
467467
"""(Read Only) The ID of the user that last modified this task"""
468468

469+
assignee_principal_id: Optional[str] = None
470+
"""The principal ID of the user or team assigned to this task. Null if unassigned. For metadata
471+
tasks, determines the owner of the grid session. Team members can all join grid sessions
472+
owned by their team, while user-owned grid sessions are restricted to that user only."""
473+
469474
_last_persistent_instance: Optional["CurationTask"] = field(
470475
default=None, repr=False, compare=False
471476
)
@@ -510,6 +515,7 @@ def fill_from_dict(
510515
self.modified_on = synapse_response.get("modifiedOn", None)
511516
self.created_by = synapse_response.get("createdBy", None)
512517
self.modified_by = synapse_response.get("modifiedBy", None)
518+
self.assignee_principal_id = synapse_response.get("assigneePrincipalId", None)
513519

514520
task_properties_dict = synapse_response.get("taskProperties", None)
515521
if task_properties_dict:
@@ -536,6 +542,7 @@ def to_synapse_request(self) -> Dict[str, Any]:
536542
request_dict["modifiedOn"] = self.modified_on
537543
request_dict["createdBy"] = self.created_by
538544
request_dict["modifiedBy"] = self.modified_by
545+
request_dict["assigneePrincipalId"] = self.assignee_principal_id
539546

540547
if self.task_properties is not None:
541548
request_dict["taskProperties"] = self.task_properties.to_synapse_request()

tests/integration/conftest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,10 @@ async def _cleanup(syn: Synapse, items):
200200
else:
201201
print("Error cleaning up entity: " + str(ex))
202202
else:
203-
sys.stderr.write("Don't know how to clean: %s" % str(item))
203+
sys.stderr.write(
204+
"Don't know how to clean: %s (type: %s)"
205+
% (str(item), type(item).__name__)
206+
)
204207

205208

206209
active_span_processors = []

tests/integration/synapseclient/models/synchronous/test_curation.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
Project,
2121
RecordBasedMetadataTaskProperties,
2222
RecordSet,
23+
Team,
2324
ViewTypeMask,
2425
)
2526

@@ -147,6 +148,12 @@ def init(self, syn: Synapse, schedule_for_cleanup: Callable[..., None]) -> None:
147148
self.syn = syn
148149
self.schedule_for_cleanup = schedule_for_cleanup
149150

151+
@pytest.fixture(scope="function")
152+
def team(self) -> Team:
153+
team = Team(name=f"test_team_{uuid.uuid4()}").create(synapse_client=self.syn)
154+
self.schedule_for_cleanup(team)
155+
return team
156+
150157
@pytest.fixture(scope="function")
151158
def folder_with_view(self, project_model: Project) -> tuple[Folder, EntityView]:
152159
"""Create a folder with an associated EntityView for file-based testing."""
@@ -239,7 +246,7 @@ def record_set(self, project_model: Project) -> RecordSet:
239246
raise
240247

241248
def test_store_file_based_curation_task(
242-
self, project_model: Project, folder_with_view: tuple[Folder, EntityView]
249+
self, team, project_model: Project, folder_with_view: tuple[Folder, EntityView]
243250
) -> None:
244251
# GIVEN a project, folder, and entity view
245252
folder, entity_view = folder_with_view
@@ -257,6 +264,7 @@ def test_store_file_based_curation_task(
257264
project_id=project_model.id,
258265
instructions="Please curate this test data.",
259266
task_properties=task_properties,
267+
assignee_principal_id=str(team.id),
260268
)
261269

262270
# WHEN I store the curation task
@@ -273,9 +281,10 @@ def test_store_file_based_curation_task(
273281
assert stored_task.etag is not None
274282
assert stored_task.created_on is not None
275283
assert stored_task.created_by is not None
284+
assert stored_task.assignee_principal_id == str(team.id)
276285

277286
def test_store_record_based_curation_task(
278-
self, project_model: Project, record_set: RecordSet
287+
self, project_model: Project, record_set: RecordSet, team: Team
279288
) -> None:
280289
# GIVEN a project and record set
281290
# AND a RecordBasedMetadataTaskProperties
@@ -290,6 +299,7 @@ def test_store_record_based_curation_task(
290299
project_id=project_model.id,
291300
instructions="Please curate this record-based test data.",
292301
task_properties=task_properties,
302+
assignee_principal_id=str(team.id),
293303
)
294304

295305
# WHEN I store the curation task
@@ -307,6 +317,7 @@ def test_store_record_based_curation_task(
307317
assert stored_task.etag is not None
308318
assert stored_task.created_on is not None
309319
assert stored_task.created_by is not None
320+
assert stored_task.assignee_principal_id == str(team.id)
310321

311322
def test_store_update_existing_curation_task(
312323
self, project_model: Project, record_set: RecordSet

0 commit comments

Comments
 (0)