Skip to content

Commit 019528d

Browse files
committed
used claude with claude markdown docs
1 parent eb74b2b commit 019528d

5 files changed

Lines changed: 468 additions & 0 deletions

File tree

synapseclient/core/constants/concrete_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
GRID_RECORD_SET_EXPORT_REQUEST = (
123123
"org.sagebionetworks.repo.model.grid.GridRecordSetExportRequest"
124124
)
125+
GRID_CSV_IMPORT_REQUEST = "org.sagebionetworks.repo.model.grid.GridCsvImportRequest"
125126
LIST_GRID_SESSIONS_REQUEST = (
126127
"org.sagebionetworks.repo.model.grid.ListGridSessionsRequest"
127128
)

synapseclient/models/curation.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@
2828
from synapseclient.core.constants.concrete_types import (
2929
CREATE_GRID_REQUEST,
3030
FILE_BASED_METADATA_TASK_PROPERTIES,
31+
GRID_CSV_IMPORT_REQUEST,
3132
GRID_RECORD_SET_EXPORT_REQUEST,
3233
LIST_GRID_SESSIONS_REQUEST,
3334
LIST_GRID_SESSIONS_RESPONSE,
3435
RECORD_BASED_METADATA_TASK_PROPERTIES,
3536
)
37+
from synapseclient.core.upload.multipart_upload_async import multipart_upload_file_async
3638
from synapseclient.core.utils import delete_none_keys, merge_dataclass_entities
3739
from synapseclient.models.mixins.asynchronous_job import AsynchronousCommunicator
3840
from synapseclient.models.recordset import ValidationSummary
@@ -1078,6 +1080,58 @@ def to_synapse_request(self) -> Dict[str, Any]:
10781080
return request_dict
10791081

10801082

1083+
@dataclass
1084+
class GridCsvImportRequest(AsynchronousCommunicator):
1085+
"""
1086+
A request to import a CSV file into an active grid session.
1087+
1088+
Represents a [Synapse GridCsvImportRequest](https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/grid/GridCsvImportRequest.html).
1089+
1090+
Attributes:
1091+
concrete_type: The concrete type for the request
1092+
session_id: The grid session ID to import the CSV into
1093+
file_handle_id: The file handle ID of the CSV to import
1094+
"""
1095+
1096+
concrete_type: str = GRID_CSV_IMPORT_REQUEST
1097+
"""The concrete type for the request"""
1098+
1099+
session_id: Optional[str] = None
1100+
"""The grid session ID to import the CSV into"""
1101+
1102+
file_handle_id: Optional[str] = None
1103+
"""The file handle ID of the CSV to import"""
1104+
1105+
def fill_from_dict(
1106+
self, synapse_response: Union[Dict[str, Any], Any]
1107+
) -> "GridCsvImportRequest":
1108+
"""
1109+
Converts a response from the REST API into this dataclass.
1110+
1111+
Arguments:
1112+
synapse_response: The response from the REST API.
1113+
1114+
Returns:
1115+
The GridCsvImportRequest object.
1116+
"""
1117+
self.session_id = synapse_response.get("sessionId", None)
1118+
return self
1119+
1120+
def to_synapse_request(self) -> Dict[str, Any]:
1121+
"""
1122+
Converts this dataclass to a dictionary suitable for a Synapse REST API request.
1123+
1124+
Returns:
1125+
A dictionary representation of this object for API requests.
1126+
"""
1127+
request_dict = {"concreteType": self.concrete_type}
1128+
if self.session_id is not None:
1129+
request_dict["sessionId"] = self.session_id
1130+
if self.file_handle_id is not None:
1131+
request_dict["fileHandleId"] = self.file_handle_id
1132+
return request_dict
1133+
1134+
10811135
@dataclass
10821136
class GridSession:
10831137
"""
@@ -1373,6 +1427,70 @@ def delete(self, *, synapse_client: Optional[Synapse] = None) -> None:
13731427
"""
13741428
return None
13751429

1430+
def import_csv(
1431+
self,
1432+
file_handle_id: Optional[str] = None,
1433+
path: Optional[str] = None,
1434+
*,
1435+
timeout: int = 120,
1436+
synapse_client: Optional[Synapse] = None,
1437+
) -> "Grid":
1438+
"""
1439+
Import a CSV file into the active grid session.
1440+
1441+
Either `file_handle_id` or `path` must be provided. If `path` is provided,
1442+
the file will be uploaded via multipart upload and the resulting file handle
1443+
ID will be used.
1444+
1445+
Arguments:
1446+
file_handle_id: The file handle ID of the CSV to import.
1447+
Mutually exclusive with `path`.
1448+
path: Local path to a CSV file to upload and import.
1449+
Mutually exclusive with `file_handle_id`.
1450+
timeout: The number of seconds to wait for the job to complete or
1451+
progress before raising a SynapseTimeoutError. Defaults to 120.
1452+
synapse_client: If not passed in and caching was not disabled by
1453+
`Synapse.allow_client_caching(False)` this will use the last created
1454+
instance from the Synapse class constructor.
1455+
1456+
Returns:
1457+
Grid: This Grid instance.
1458+
1459+
Raises:
1460+
ValueError: If `session_id` is not set.
1461+
ValueError: If neither `file_handle_id` nor `path` is provided.
1462+
ValueError: If both `file_handle_id` and `path` are provided.
1463+
1464+
Example: Import a CSV by file handle ID
1465+
 
1466+
1467+
```python
1468+
from synapseclient import Synapse
1469+
from synapseclient.models import Grid
1470+
1471+
syn = Synapse()
1472+
syn.login()
1473+
1474+
grid = Grid(session_id="abc-123-def")
1475+
grid = grid.import_csv(file_handle_id="123456")
1476+
```
1477+
1478+
Example: Import a CSV from a local path
1479+
 
1480+
1481+
```python
1482+
from synapseclient import Synapse
1483+
from synapseclient.models import Grid
1484+
1485+
syn = Synapse()
1486+
syn.login()
1487+
1488+
grid = Grid(session_id="abc-123-def")
1489+
grid = grid.import_csv(path="/path/to/data.csv")
1490+
```
1491+
"""
1492+
return self
1493+
13761494
@classmethod
13771495
def list(
13781496
cls,
@@ -1838,3 +1956,108 @@ async def main():
18381956
await delete_grid_session(
18391957
session_id=self.session_id, synapse_client=synapse_client
18401958
)
1959+
1960+
async def import_csv_async(
1961+
self,
1962+
file_handle_id: Optional[str] = None,
1963+
path: Optional[str] = None,
1964+
*,
1965+
timeout: int = 120,
1966+
synapse_client: Optional[Synapse] = None,
1967+
) -> "Grid":
1968+
"""
1969+
Import a CSV file into the active grid session.
1970+
1971+
Either `file_handle_id` or `path` must be provided. If `path` is provided,
1972+
the file will be uploaded via multipart upload and the resulting file handle
1973+
ID will be used.
1974+
1975+
Arguments:
1976+
file_handle_id: The file handle ID of the CSV to import.
1977+
Mutually exclusive with `path`.
1978+
path: Local path to a CSV file to upload and import.
1979+
Mutually exclusive with `file_handle_id`.
1980+
timeout: The number of seconds to wait for the job to complete or
1981+
progress before raising a SynapseTimeoutError. Defaults to 120.
1982+
synapse_client: If not passed in and caching was not disabled by
1983+
`Synapse.allow_client_caching(False)` this will use the last created
1984+
instance from the Synapse class constructor.
1985+
1986+
Returns:
1987+
Grid: This Grid instance.
1988+
1989+
Raises:
1990+
ValueError: If `session_id` is not set.
1991+
ValueError: If neither `file_handle_id` nor `path` is provided.
1992+
ValueError: If both `file_handle_id` and `path` are provided.
1993+
1994+
Example: Import a CSV by file handle ID asynchronously
1995+
 
1996+
1997+
```python
1998+
import asyncio
1999+
from synapseclient import Synapse
2000+
from synapseclient.models import Grid
2001+
2002+
syn = Synapse()
2003+
syn.login()
2004+
2005+
async def main():
2006+
grid = Grid(session_id="abc-123-def")
2007+
grid = await grid.import_csv_async(file_handle_id="123456")
2008+
2009+
asyncio.run(main())
2010+
```
2011+
2012+
Example: Import a CSV from a local path asynchronously
2013+
 
2014+
2015+
```python
2016+
import asyncio
2017+
from synapseclient import Synapse
2018+
from synapseclient.models import Grid
2019+
2020+
syn = Synapse()
2021+
syn.login()
2022+
2023+
async def main():
2024+
grid = Grid(session_id="abc-123-def")
2025+
grid = await grid.import_csv_async(path="/path/to/data.csv")
2026+
2027+
asyncio.run(main())
2028+
```
2029+
"""
2030+
if not self.session_id:
2031+
raise ValueError("session_id is required to import a CSV into a Grid")
2032+
if file_handle_id is None and path is None:
2033+
raise ValueError(
2034+
"Either file_handle_id or path must be provided to import a CSV"
2035+
)
2036+
if file_handle_id is not None and path is not None:
2037+
raise ValueError(
2038+
"Only one of file_handle_id or path may be provided, not both"
2039+
)
2040+
2041+
trace.get_current_span().set_attributes(
2042+
{
2043+
"synapse.session_id": self.session_id or "",
2044+
}
2045+
)
2046+
2047+
if path is not None:
2048+
client = Synapse.get_client(synapse_client=synapse_client)
2049+
file_handle_id = await multipart_upload_file_async(
2050+
syn=client,
2051+
file_path=path,
2052+
content_type="text/csv",
2053+
)
2054+
2055+
import_request = GridCsvImportRequest(
2056+
session_id=self.session_id,
2057+
file_handle_id=file_handle_id,
2058+
)
2059+
await import_request.send_job_and_wait_async(
2060+
timeout=timeout, synapse_client=synapse_client
2061+
)
2062+
2063+
return self

synapseclient/models/mixins/asynchronous_job.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
CREATE_GRID_REQUEST,
1616
CREATE_SCHEMA_REQUEST,
1717
GET_VALIDATION_SCHEMA_REQUEST,
18+
GRID_CSV_IMPORT_REQUEST,
1819
GRID_RECORD_SET_EXPORT_REQUEST,
1920
QUERY_BUNDLE_REQUEST,
2021
QUERY_TABLE_CSV_REQUEST,
@@ -29,6 +30,7 @@
2930
ASYNC_JOB_URIS = {
3031
AGENT_CHAT_REQUEST: "/agent/chat/async",
3132
CREATE_GRID_REQUEST: "/grid/session/async",
33+
GRID_CSV_IMPORT_REQUEST: "/grid/import/csv/async",
3234
GRID_RECORD_SET_EXPORT_REQUEST: "/grid/export/recordset/async",
3335
TABLE_UPDATE_TRANSACTION_REQUEST: "/entity/{entityId}/table/transaction/async",
3436
GET_VALIDATION_SCHEMA_REQUEST: "/schema/type/validation/async",

tests/integration/synapseclient/models/async/test_grid_async.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,90 @@ async def test_delete_grid_session_validation_error_async(self) -> None:
183183
match="session_id is required to delete a GridSession",
184184
):
185185
await grid.delete_async(synapse_client=self.syn)
186+
187+
async def test_import_csv_with_file_handle_id_async(
188+
self, record_set_fixture: RecordSet
189+
) -> None:
190+
# GIVEN: A grid session and a file handle from an existing record set
191+
grid = Grid(record_set_id=record_set_fixture.id)
192+
created_grid = await grid.create_async(
193+
timeout=ASYNC_JOB_TIMEOUT_SEC, synapse_client=self.syn
194+
)
195+
self.schedule_for_cleanup(
196+
lambda: self.syn.restDELETE(f"/grid/session/{created_grid.session_id}")
197+
)
198+
199+
# WHEN: Importing a CSV using the file handle ID from the record set
200+
result = await created_grid.import_csv_async(
201+
file_handle_id=record_set_fixture.data_file_handle_id,
202+
timeout=ASYNC_JOB_TIMEOUT_SEC,
203+
synapse_client=self.syn,
204+
)
205+
206+
# THEN: The import should succeed and return the same Grid instance
207+
assert result is created_grid
208+
assert result.session_id == created_grid.session_id
209+
210+
async def test_import_csv_with_path_async(
211+
self, record_set_fixture: RecordSet
212+
) -> None:
213+
# GIVEN: A grid session and a local CSV file
214+
grid = Grid(record_set_id=record_set_fixture.id)
215+
created_grid = await grid.create_async(
216+
timeout=ASYNC_JOB_TIMEOUT_SEC, synapse_client=self.syn
217+
)
218+
self.schedule_for_cleanup(
219+
lambda: self.syn.restDELETE(f"/grid/session/{created_grid.session_id}")
220+
)
221+
222+
# Create a temporary CSV file with the same schema as the record set
223+
test_data = pd.DataFrame(
224+
{
225+
"id": [6, 7],
226+
"name": ["Zeta", "Eta"],
227+
"value": [60.1, 70.2],
228+
"category": ["A", "B"],
229+
"active": [True, False],
230+
}
231+
)
232+
temp_fd, filename = tempfile.mkstemp(suffix=".csv")
233+
try:
234+
os.close(temp_fd)
235+
test_data.to_csv(filename, index=False)
236+
self.schedule_for_cleanup(filename)
237+
238+
# WHEN: Importing the CSV from a local path
239+
result = await created_grid.import_csv_async(
240+
path=filename,
241+
timeout=ASYNC_JOB_TIMEOUT_SEC,
242+
synapse_client=self.syn,
243+
)
244+
245+
# THEN: The import should succeed and return the same Grid instance
246+
assert result is created_grid
247+
assert result.session_id == created_grid.session_id
248+
except Exception:
249+
if os.path.exists(filename):
250+
os.unlink(filename)
251+
raise
252+
253+
async def test_import_csv_validation_errors_async(self) -> None:
254+
# GIVEN: A Grid instance with no session_id
255+
grid = Grid()
256+
257+
# WHEN/THEN: Calling import_csv_async without session_id raises ValueError
258+
with pytest.raises(
259+
ValueError,
260+
match="session_id is required",
261+
):
262+
await grid.import_csv_async(file_handle_id="123", synapse_client=self.syn)
263+
264+
# GIVEN: A Grid with session_id but no file source
265+
grid_with_session = Grid(session_id="some-session-id")
266+
267+
# WHEN/THEN: Calling without file_handle_id or path raises ValueError
268+
with pytest.raises(
269+
ValueError,
270+
match="Either file_handle_id or path",
271+
):
272+
await grid_with_session.import_csv_async(synapse_client=self.syn)

0 commit comments

Comments
 (0)