Skip to content

Commit d0f3646

Browse files
authored
[SYNPY-1426] Deprecate getChildren method and extend sync_from_synapse method (#1222)
* Enhance Folder and Project Models to Support Additional Entity Types - Updated Folder and Project models to include support for Tables, EntityViews, SubmissionViews, Datasets, DatasetCollections, MaterializedViews, and VirtualTables. - Modified the StorableContainer mixin to handle the new entity types during synchronization. - Added tests to verify the synchronization of all supported entity types within both Folder and Project models. - Refactored relevant methods to accommodate the new entity types and ensure proper handling during sync operations. - Deprecate the getChildren method call in the Synapse class
1 parent a8e9f31 commit d0f3646

File tree

19 files changed

+1440
-150
lines changed

19 files changed

+1440
-150
lines changed

synapseclient/api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
delete_entity_generated_by,
3232
delete_entity_provenance,
3333
get_activity,
34+
get_children,
3435
get_entities_by_md5,
3536
get_entity,
3637
get_entity_acl,
@@ -136,6 +137,7 @@
136137
"get_activity",
137138
"create_activity",
138139
"update_activity",
140+
"get_children",
139141
# configuration_services
140142
"get_config_file",
141143
"get_config_section_dict",

synapseclient/api/api_client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import sys
23
from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, Optional
34

@@ -46,7 +47,7 @@ async def rest_post_paginated_async(
4647
body["nextPageToken"] = next_page_token
4748
response = await client.rest_post_async(
4849
uri=uri,
49-
body=body,
50+
body=json.dumps(body),
5051
endpoint=endpoint,
5152
headers=headers,
5253
retry_policy=retry_policy,

synapseclient/api/entity_services.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

55
import json
66
from dataclasses import dataclass
7-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
7+
from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, List, Optional, Union
88

99
from async_lru import alru_cache
1010

11+
from synapseclient.api.api_client import rest_post_paginated_async
1112
from synapseclient.core.exceptions import SynapseHTTPError
1213
from synapseclient.core.utils import get_synid_and_version
1314

@@ -867,3 +868,99 @@ async def main():
867868

868869
client = Synapse.get_client(synapse_client=synapse_client)
869870
return await client.rest_get_async(uri=f"/activity/{activity_id}")
871+
872+
873+
async def get_children(
874+
parent: Optional[str] = None,
875+
include_types: List[str] = None,
876+
sort_by: str = "NAME",
877+
sort_direction: str = "ASC",
878+
*,
879+
synapse_client: Optional["Synapse"] = None,
880+
) -> AsyncGenerator[Dict[str, Any], None]:
881+
"""
882+
Retrieve all entities stored within a parent such as folder or project.
883+
884+
Arguments:
885+
parent: The ID of a Synapse container (folder or project) or None to retrieve all projects
886+
include_types: List of entity types to include (e.g., ["folder", "file"]).
887+
Available types can be found at:
888+
https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/EntityType.html
889+
sort_by: How results should be sorted. Can be "NAME" or "CREATED_ON"
890+
sort_direction: The direction of the result sort. Can be "ASC" or "DESC"
891+
synapse_client: If not passed in and caching was not disabled by
892+
`Synapse.allow_client_caching(False)` this will use the last created
893+
instance from the Synapse class constructor.
894+
895+
Yields:
896+
An async generator that yields entity children dictionaries.
897+
898+
Example: Getting children of a folder
899+
Retrieve all children of a folder:
900+
901+
```python
902+
import asyncio
903+
from synapseclient import Synapse
904+
from synapseclient.api import get_children
905+
906+
syn = Synapse()
907+
syn.login()
908+
909+
async def main():
910+
async for child in get_children(parent="syn123456"):
911+
print(f"Child: {child['name']} (ID: {child['id']})")
912+
913+
asyncio.run(main())
914+
```
915+
916+
Example: Getting children with specific types
917+
Retrieve only files and folders:
918+
919+
```python
920+
import asyncio
921+
from synapseclient import Synapse
922+
from synapseclient.api import get_children
923+
924+
syn = Synapse()
925+
syn.login()
926+
927+
async def main():
928+
async for child in get_children(
929+
parent="syn123456",
930+
include_types=["file", "folder"],
931+
sort_by="NAME",
932+
sort_direction="ASC"
933+
):
934+
print(f"Child: {child['name']} (Type: {child['type']})")
935+
936+
asyncio.run(main())
937+
```
938+
"""
939+
if include_types is None:
940+
include_types = [
941+
"folder",
942+
"file",
943+
"table",
944+
"link",
945+
"entityview",
946+
"dockerrepo",
947+
"submissionview",
948+
"dataset",
949+
"materializedview",
950+
]
951+
952+
request_body = {
953+
"parentId": parent,
954+
"includeTypes": include_types,
955+
"sortBy": sort_by,
956+
"sortDirection": sort_direction,
957+
}
958+
959+
response = rest_post_paginated_async(
960+
uri="/entity/children",
961+
body=request_body,
962+
synapse_client=synapse_client,
963+
)
964+
965+
async for child in response:
966+
yield child

synapseclient/api/json_schema_services.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ async def get_invalid_json_schema_validation(
164164
request_body = {"containerId": synapse_id}
165165
response = rest_post_paginated_async(
166166
f"/entity/{synapse_id}/schema/validation/invalid",
167-
body=json.dumps(request_body),
167+
body=request_body,
168168
synapse_client=synapse_client,
169169
)
170170
async for item in response:

synapseclient/client.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3395,6 +3395,13 @@ def set_annotations(self, annotations: Annotations):
33953395
# Querying #
33963396
############################################################
33973397

3398+
@deprecated(
3399+
version="4.9.0",
3400+
reason="To be removed in 5.0.0. "
3401+
"Use the new dataclass models `from synapseclient.models import Project, Folder` "
3402+
"with their `sync_from_synapse` method for most use cases, "
3403+
"or the `synapseclient.api.get_children` function for direct API access with sorting and filtering.",
3404+
)
33983405
def getChildren(
33993406
self,
34003407
parent,
@@ -3427,6 +3434,87 @@ def getChildren(
34273434
Also see:
34283435
34293436
- [synapseutils.walk][]
3437+
3438+
Example: Migrating from this method to new approaches
3439+
 
3440+
3441+
**Legacy approach (deprecated):**
3442+
```python
3443+
# Using the deprecated getChildren method
3444+
for child in syn.getChildren("syn12345", includeTypes=["file", "folder"]):
3445+
print(f"Child: {child['name']} (ID: {child['id']})")
3446+
```
3447+
3448+
**New approach using dataclass models with sync_from_synapse:**
3449+
```python
3450+
import synapseclient
3451+
from synapseclient.models import Project, Folder
3452+
3453+
# Create client and login
3454+
syn = synapseclient.Synapse()
3455+
syn.login()
3456+
3457+
# For projects - get all children automatically (recursive by default)
3458+
project = Project(id="syn12345")
3459+
project = project.sync_from_synapse(download_file=False)
3460+
3461+
# Access different types of children
3462+
print(f"Files: {len(project.files)}")
3463+
for file in project.files:
3464+
print(f" File: {file.name} (ID: {file.id})")
3465+
3466+
print(f"Folders: {len(project.folders)}")
3467+
for folder in project.folders:
3468+
print(f" Folder: {folder.name} (ID: {folder.id})")
3469+
3470+
print(f"Tables: {len(project.tables)}")
3471+
for table in project.tables:
3472+
print(f" Table: {table.name} (ID: {table.id})")
3473+
3474+
# For folders - get all children automatically (recursive by default)
3475+
folder = Folder(id="syn67890")
3476+
folder = folder.sync_from_synapse(download_file=False)
3477+
3478+
# Access children in the same way
3479+
for file in folder.files:
3480+
print(f" File: {file.name} (ID: {file.id})")
3481+
3482+
# For non-recursive behavior (equivalent to single getChildren call)
3483+
folder = Folder(id="syn67890")
3484+
folder = folder.sync_from_synapse(download_file=False, recursive=False)
3485+
3486+
# This will only get immediate children, not subfolders' contents
3487+
for file in folder.files:
3488+
print(f" File: {file.name} (ID: {file.id})")
3489+
for subfolder in folder.folders:
3490+
print(f" Subfolder: {subfolder.name} (ID: {subfolder.id})")
3491+
# Note: subfolder.files and subfolder.folders will be empty
3492+
# because recursive=False
3493+
```
3494+
3495+
**New approach using the API directly (for advanced sorting/filtering):**
3496+
```python
3497+
import asyncio
3498+
import synapseclient
3499+
from synapseclient.api import get_children
3500+
3501+
# Create client and login
3502+
syn = synapseclient.Synapse()
3503+
syn.login()
3504+
3505+
# Using the new async API function directly
3506+
async def get_sorted_children():
3507+
async for child in get_children(
3508+
parent="syn12345",
3509+
include_types=["file", "folder"],
3510+
sort_by="NAME",
3511+
sort_direction="ASC"
3512+
):
3513+
print(f"Child: {child['name']} (ID: {child['id']}, Type: {child['type']})")
3514+
3515+
# Run the async function
3516+
asyncio.run(get_sorted_children())
3517+
```
34303518
"""
34313519
parentId = id_of(parent) if parent is not None else None
34323520

synapseclient/models/dataset.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from synapseclient import Synapse
1111
from synapseclient.api.table_services import ViewEntityType, ViewTypeMask
12-
from synapseclient.core.async_utils import async_to_sync
12+
from synapseclient.core.async_utils import async_to_sync, wrap_async_to_sync
1313
from synapseclient.core.constants import concrete_types
1414
from synapseclient.core.utils import MB, delete_none_keys
1515
from synapseclient.models import Activity, Annotations
@@ -1022,7 +1022,9 @@ def add_item(
10221022
entity_ref=EntityRef(id=item.id, version=item.version_number)
10231023
)
10241024
elif isinstance(item, Folder):
1025-
children = item._retrieve_children(follow_link=True)
1025+
children = wrap_async_to_sync(
1026+
item._retrieve_children(follow_link=True), client
1027+
)
10261028
for child in children:
10271029
if child["type"] == concrete_types.FILE_ENTITY:
10281030
self._append_entity_ref(
@@ -1127,7 +1129,9 @@ def remove_item(
11271129
).get()
11281130
self._remove_entity_ref(EntityRef(id=item.id, version=item.version_number))
11291131
elif isinstance(item, Folder):
1130-
children = item._retrieve_children(follow_link=True)
1132+
children = wrap_async_to_sync(
1133+
item._retrieve_children(follow_link=True), client
1134+
)
11311135
for child in children:
11321136
if child["type"] == concrete_types.FILE_ENTITY:
11331137
self._remove_entity_ref(

synapseclient/models/folder.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,16 @@
2727
from synapseutils import copy
2828

2929
if TYPE_CHECKING:
30-
from synapseclient.models import Project
30+
from synapseclient.models import (
31+
Dataset,
32+
DatasetCollection,
33+
EntityView,
34+
MaterializedView,
35+
Project,
36+
SubmissionView,
37+
Table,
38+
VirtualTable,
39+
)
3140

3241

3342
@dataclass()
@@ -59,6 +68,13 @@ class Folder(
5968
modified_by: (Read Only) The ID of the user that last modified this entity.
6069
files: Files that exist within this folder.
6170
folders: Folders that exist within this folder.
71+
tables: Tables that exist within this folder.
72+
entityviews: Entity views that exist within this folder.
73+
submissionviews: Submission views that exist within this folder.
74+
datasets: Datasets that exist within this folder.
75+
datasetcollections: Dataset collections that exist within this folder.
76+
materializedviews: Materialized views that exist within this folder.
77+
virtualtables: Virtual tables that exist within this folder.
6278
annotations: Additional metadata associated with the folder. The key is the name
6379
of your desired annotations. The value is an object containing a list of
6480
values (use empty list to represent no values for key) and the value type
@@ -116,6 +132,31 @@ class Folder(
116132
folders: List["Folder"] = field(default_factory=list, compare=False)
117133
"""Folders that exist within this folder."""
118134

135+
tables: List["Table"] = field(default_factory=list, compare=False)
136+
"""Tables that exist within this folder."""
137+
138+
entityviews: List["EntityView"] = field(default_factory=list, compare=False)
139+
"""Entity views that exist within this folder."""
140+
141+
submissionviews: List["SubmissionView"] = field(default_factory=list, compare=False)
142+
"""Submission views that exist within this folder."""
143+
144+
datasets: List["Dataset"] = field(default_factory=list, compare=False)
145+
"""Datasets that exist within this folder."""
146+
147+
datasetcollections: List["DatasetCollection"] = field(
148+
default_factory=list, compare=False
149+
)
150+
"""Dataset collections that exist within this folder."""
151+
152+
materializedviews: List["MaterializedView"] = field(
153+
default_factory=list, compare=False
154+
)
155+
"""Materialized views that exist within this folder."""
156+
157+
virtualtables: List["VirtualTable"] = field(default_factory=list, compare=False)
158+
"""Virtual tables that exist within this folder."""
159+
119160
annotations: Optional[
120161
Dict[
121162
str,

0 commit comments

Comments
 (0)