Skip to content

Commit 5cc2d01

Browse files
authored
feat: Add CSV export capability to DataFrameClient (#45)
1 parent f736f31 commit 5cc2d01

20 files changed

+848
-419
lines changed

docs/api_reference/core.rst

+4
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ nisystemlink.clients.core
77
:members:
88
:inherited-members:
99
:imported-members:
10+
11+
.. automodule:: nisystemlink.clients.core.helpers
12+
:members:
13+
:imported-members:

docs/api_reference/dataframe.rst

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ nisystemlink.clients.dataframe
1919
.. automethod:: get_table_data
2020
.. automethod:: append_table_data
2121
.. automethod:: query_table_data
22+
.. automethod:: export_table_data
2223
.. automethod:: query_decimated_data
2324

2425
.. automodule:: nisystemlink.clients.dataframe.models

docs/getting_started.rst

+13-4
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,13 @@ Subscribe to tag changes
7676
:language: python
7777
:linenos:
7878

79-
Data Frame API
79+
DataFrame API
8080
-------
8181

8282
Overview
8383
~~~~~~~~
8484

85-
The :class:`.DataFrameClient` class is the primary entry point of the Data Frame API.
85+
The :class:`.DataFrameClient` class is the primary entry point of the DataFrame API.
8686

8787
When constructing a :class:`.DataFrameClient`, you can pass an
8888
:class:`.HttpConfiguration` (like one retrieved from the
@@ -91,11 +91,14 @@ default connection. The default connection depends on your environment.
9191

9292
With a :class:`.DataFrameClient` object, you can:
9393

94-
* Create and delete Data Frame Tables.
94+
* Create and delete data tables.
9595

9696
* Modify table metadata and query for tables by their metadata.
9797

98-
* Append rows of data to a table, query for rows of data from a table, and decimate table data.
98+
* Append rows of data to a table, query for rows of data from a table, and
99+
decimate table data.
100+
101+
* Export table data in a comma-separated values (CSV) format.
99102

100103
Examples
101104
~~~~~~~~
@@ -111,3 +114,9 @@ Query and read data from a table
111114
.. literalinclude:: ../examples/dataframe/query_read_data.py
112115
:language: python
113116
:linenos:
117+
118+
Export data from a table
119+
120+
.. literalinclude:: ../examples/dataframe/export_data.py
121+
:language: python
122+
:linenos:

examples/dataframe/export_data.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from shutil import copyfileobj
2+
3+
from nisystemlink.clients.dataframe import DataFrameClient
4+
from nisystemlink.clients.dataframe.models import (
5+
ColumnFilter,
6+
ColumnOrderBy,
7+
ExportFormat,
8+
ExportTableDataRequest,
9+
FilterOperation,
10+
)
11+
12+
client = DataFrameClient()
13+
14+
# List a table
15+
response = client.list_tables(take=1)
16+
table = response.tables[0]
17+
18+
# Export table data with query options
19+
request = ExportTableDataRequest(
20+
columns=["col1"],
21+
order_by=[ColumnOrderBy(column="col2", descending=True)],
22+
filters=[
23+
ColumnFilter(column="col1", operation=FilterOperation.NotEquals, value="0")
24+
],
25+
response_format=ExportFormat.CSV,
26+
)
27+
28+
data = client.export_table_data(id=table.id, query=request)
29+
30+
# Write the export data to a file
31+
with open(f"{table.name}.csv", "wb") as f:
32+
copyfileobj(data, f)
33+
34+
# Alternatively, load the export data into a pandas dataframe
35+
# import pandas as pd
36+
# df = pd.read_csv(data)

nisystemlink/clients/core/_uplink/_methods.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
from typing import Any, Callable, Optional, Sequence, Tuple, TypeVar, Union
44

5-
from uplink import Body, commands, json, returns
5+
from uplink import (
6+
Body,
7+
commands,
8+
json,
9+
response_handler as uplink_response_handler,
10+
returns,
11+
)
612

713
F = TypeVar("F", bound=Callable[..., Any])
814

@@ -50,3 +56,14 @@ def decorator(func: F) -> F:
5056
return commands.delete(path, args=args)(func) # type: ignore
5157

5258
return decorator
59+
60+
61+
def response_handler(
62+
handler: Any, requires_consumer: Optional[bool] = False
63+
) -> Callable[[F], F]:
64+
"""Annotation for creating custom response handlers."""
65+
66+
def decorator(func: F) -> F:
67+
return uplink_response_handler(handler, requires_consumer)(func) # type: ignore
68+
69+
return decorator
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from ._iterator_file_like import IteratorFileLike
2+
3+
# flake8: noqa
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from typing import Any, Iterator
2+
3+
4+
class IteratorFileLike:
5+
"""A file-like object adapter that wraps a python iterator, providing a way to
6+
read from the iterator as if it was a file.
7+
"""
8+
9+
def __init__(self, iterator: Iterator[Any]):
10+
self._iterator = iterator
11+
self._buffer = b""
12+
13+
def read(self, size: int = -1) -> bytes:
14+
"""Read at most `size` bytes from the file-like object. If `size` is not
15+
specified or is negative, read until the iterator is exhausted and
16+
returns all bytes or characters read.
17+
"""
18+
while size < 0 or len(self._buffer) < size:
19+
try:
20+
chunk = next(self._iterator)
21+
self._buffer += chunk
22+
except StopIteration:
23+
break
24+
if size < 0:
25+
data = self._buffer
26+
self._buffer = b""
27+
else:
28+
data = self._buffer[:size]
29+
self._buffer = self._buffer[size:]
30+
return data

nisystemlink/clients/dataframe/_data_frame_client.py

+54-23
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44

55
from nisystemlink.clients import core
66
from nisystemlink.clients.core._uplink._base_client import BaseClient
7-
from nisystemlink.clients.core._uplink._methods import delete, get, patch, post
7+
from nisystemlink.clients.core._uplink._methods import (
8+
delete,
9+
get,
10+
patch,
11+
post,
12+
response_handler,
13+
)
14+
from nisystemlink.clients.core.helpers import IteratorFileLike
15+
from requests.models import Response
816
from uplink import Body, Field, Path, Query
917

1018
from . import models
@@ -21,7 +29,7 @@ def __init__(self, configuration: Optional[core.HttpConfiguration] = None):
2129
is used.
2230
2331
Raises:
24-
ApiException: if unable to communicate with the Data Frame service.
32+
ApiException: if unable to communicate with the DataFrame Service.
2533
"""
2634
if configuration is None:
2735
configuration = core.JupyterHttpConfiguration()
@@ -36,7 +44,7 @@ def api_info(self) -> models.ApiInfo:
3644
Information about available API operations.
3745
3846
Raises:
39-
ApiException: if unable to communicate with the Data Frame service.
47+
ApiException: if unable to communicate with the DataFrame Service.
4048
"""
4149
...
4250

@@ -74,7 +82,7 @@ def list_tables(
7482
The list of tables with a continuation token.
7583
7684
Raises:
77-
ApiException: if unable to communicate with the Data Frame service
85+
ApiException: if unable to communicate with the DataFrame Service
7886
or provided an invalid argument.
7987
"""
8088
...
@@ -90,7 +98,7 @@ def create_table(self, table: models.CreateTableRequest) -> str:
9098
The ID of the newly created table.
9199
92100
Raises:
93-
ApiException: if unable to communicate with the Data Frame service
101+
ApiException: if unable to communicate with the DataFrame Service
94102
or provided an invalid argument.
95103
"""
96104
...
@@ -106,7 +114,7 @@ def query_tables(self, query: models.QueryTablesRequest) -> models.PagedTables:
106114
The list of tables with a continuation token.
107115
108116
Raises:
109-
ApiException: if unable to communicate with the Data Frame service
117+
ApiException: if unable to communicate with the DataFrame Service
110118
or provided an invalid argument.
111119
"""
112120
...
@@ -116,13 +124,13 @@ def get_table_metadata(self, id: str) -> models.TableMetadata:
116124
"""Retrieves the metadata and column information for a single table identified by its ID.
117125
118126
Args:
119-
id (str): Unique ID of a DataFrame table.
127+
id (str): Unique ID of a data table.
120128
121129
Returns:
122130
The metadata for the table.
123131
124132
Raises:
125-
ApiException: if unable to communicate with the Data Frame service
133+
ApiException: if unable to communicate with the DataFrame Service
126134
or provided an invalid argument.
127135
"""
128136
...
@@ -132,11 +140,11 @@ def modify_table(self, id: str, update: models.ModifyTableRequest) -> None:
132140
"""Modify properties of a table or its columns.
133141
134142
Args:
135-
id: Unique ID of a DataFrame table.
143+
id: Unique ID of a data table.
136144
update: The metadata to update.
137145
138146
Raises:
139-
ApiException: if unable to communicate with the Data Frame service
147+
ApiException: if unable to communicate with the DataFrame Service
140148
or provided an invalid argument.
141149
"""
142150
...
@@ -146,10 +154,10 @@ def delete_table(self, id: str) -> None:
146154
"""Deletes a table.
147155
148156
Args:
149-
id (str): Unique ID of a DataFrame table.
157+
id (str): Unique ID of a data table.
150158
151159
Raises:
152-
ApiException: if unable to communicate with the Data Frame service
160+
ApiException: if unable to communicate with the DataFrame Service
153161
or provided an invalid argument.
154162
"""
155163
...
@@ -161,14 +169,14 @@ def delete_tables(
161169
"""Deletes multiple tables.
162170
163171
Args:
164-
ids (List[str]): List of unique IDs of DataFrame tables.
172+
ids (List[str]): List of unique IDs of data tables.
165173
166174
Returns:
167175
A partial success if any tables failed to delete, or None if all
168176
tables were deleted successfully.
169177
170178
Raises:
171-
ApiException: if unable to communicate with the Data Frame service
179+
ApiException: if unable to communicate with the DataFrame Service
172180
or provided an invalid argument.
173181
"""
174182
...
@@ -187,7 +195,7 @@ def modify_tables(
187195
tables were modified successfully.
188196
189197
Raises:
190-
ApiException: if unable to communicate with the Data Frame service
198+
ApiException: if unable to communicate with the DataFrame Service
191199
or provided an invalid argument.
192200
"""
193201
...
@@ -215,7 +223,7 @@ def get_table_data(
215223
"""Reads raw data from the table identified by its ID.
216224
217225
Args:
218-
id: Unique ID of a DataFrame table.
226+
id: Unique ID of a data table.
219227
columns: Columns to include in the response. Data will be returned in the same order as
220228
the columns. If not specified, all columns are returned.
221229
order_by: List of columns to sort by. Multiple columns may be specified to order rows
@@ -230,7 +238,7 @@ def get_table_data(
230238
The table data and total number of rows with a continuation token.
231239
232240
Raises:
233-
ApiException: if unable to communicate with the Data Frame service
241+
ApiException: if unable to communicate with the DataFrame Service
234242
or provided an invalid argument.
235243
"""
236244
...
@@ -240,11 +248,11 @@ def append_table_data(self, id: str, data: models.AppendTableDataRequest) -> Non
240248
"""Appends one or more rows of data to the table identified by its ID.
241249
242250
Args:
243-
id: Unique ID of a DataFrame table.
251+
id: Unique ID of a data table.
244252
data: The rows of data to append and any additional options.
245253
246254
Raises:
247-
ApiException: if unable to communicate with the Data Frame service
255+
ApiException: if unable to communicate with the DataFrame Service
248256
or provided an invalid argument.
249257
"""
250258
...
@@ -256,14 +264,14 @@ def query_table_data(
256264
"""Reads rows of data that match a filter from the table identified by its ID.
257265
258266
Args:
259-
id: Unique ID of a DataFrame table.
267+
id: Unique ID of a data table.
260268
query: The filtering and sorting to apply when reading data.
261269
262270
Returns:
263271
The table data and total number of rows with a continuation token.
264272
265273
Raises:
266-
ApiException: if unable to communicate with the Data Frame service
274+
ApiException: if unable to communicate with the DataFrame Service
267275
or provided an invalid argument.
268276
"""
269277
...
@@ -275,14 +283,37 @@ def query_decimated_data(
275283
"""Reads decimated rows of data from the table identified by its ID.
276284
277285
Args:
278-
id: Unique ID of a DataFrame table.
286+
id: Unique ID of a data table.
279287
query: The filtering and decimation options to apply when reading data.
280288
281289
Returns:
282290
The decimated table data.
283291
284292
Raises:
285-
ApiException: if unable to communicate with the Data Frame service
293+
ApiException: if unable to communicate with the DataFrame Service
294+
or provided an invalid argument.
295+
"""
296+
...
297+
298+
def _iter_content_filelike_wrapper(response: Response) -> IteratorFileLike:
299+
return IteratorFileLike(response.iter_content(chunk_size=4096))
300+
301+
@response_handler(_iter_content_filelike_wrapper)
302+
@post("tables/{id}/export-data", args=[Path, Body])
303+
def export_table_data(
304+
self, id: str, query: models.ExportTableDataRequest
305+
) -> IteratorFileLike:
306+
"""Exports rows of data that match a filter from the table identified by its ID.
307+
308+
Args:
309+
id: Unique ID of a data table.
310+
query: The filtering, sorting, and export format to apply when exporting data.
311+
312+
Returns:
313+
A file-like object for reading the exported data.
314+
315+
Raises:
316+
ApiException: if unable to communicate with the DataFrame Service
286317
or provided an invalid argument.
287318
"""
288319
...

0 commit comments

Comments
 (0)