Skip to content

Commit 7be03be

Browse files
authored
feat: Add modify_tables method (#35)
1 parent 51bbdf5 commit 7be03be

File tree

6 files changed

+164
-74
lines changed

6 files changed

+164
-74
lines changed

nisystemlink/clients/core/_uplink/_base_client.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,15 @@ def decoder(response: Response) -> Any:
7272
class BaseClient(Consumer):
7373
"""Base class for SystemLink clients, built on top of `Uplink <https://github.com/prkumar/uplink>`_."""
7474

75-
def __init__(self, configuration: core.HttpConfiguration):
75+
def __init__(self, configuration: core.HttpConfiguration, base_path: str = ""):
7676
"""Initialize an instance.
7777
7878
Args:
7979
configuration: Defines the web server to connect to and information about how to connect.
80+
base_path: The base path for all API calls.
8081
"""
8182
super().__init__(
82-
base_url=configuration.server_uri,
83+
base_url=configuration.server_uri + base_path,
8384
converter=_JsonModelConverter(),
8485
hooks=[_handle_http_status],
8586
)

nisystemlink/clients/dataframe/_data_frame_client.py

+28-11
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111

1212

1313
class DataFrameClient(BaseClient):
14-
_BASE_PATH = "/nidataframe/v1"
15-
1614
def __init__(self, configuration: Optional[core.HttpConfiguration] = None):
1715
"""Initialize an instance.
1816
@@ -24,15 +22,15 @@ def __init__(self, configuration: Optional[core.HttpConfiguration] = None):
2422
if configuration is None:
2523
configuration = core.JupyterHttpConfiguration()
2624

27-
super().__init__(configuration)
25+
super().__init__(configuration, "/nidataframe/v1/")
2826

29-
@get(_BASE_PATH)
27+
@get("")
3028
def api_info(self) -> models.ApiInfo:
3129
"""Returns information about available API operations."""
3230
...
3331

3432
@get(
35-
_BASE_PATH + "/tables",
33+
"tables",
3634
args=(
3735
Query("take"),
3836
Query("id"),
@@ -66,7 +64,7 @@ def list_tables(
6664
"""
6765
...
6866

69-
@post(_BASE_PATH + "/tables", return_key="id")
67+
@post("tables", return_key="id")
7068
def create_table(self, table: models.CreateTableRequest) -> str:
7169
"""Create a new table with the provided metadata and column definitions.
7270
@@ -78,7 +76,7 @@ def create_table(self, table: models.CreateTableRequest) -> str:
7876
"""
7977
...
8078

81-
@post(_BASE_PATH + "/query-tables")
79+
@post("query-tables")
8280
def query_tables(self, query: models.QueryTablesRequest) -> models.PagedTables:
8381
"""Queries available tables on the SystemLink DataFrame service and returns their metadata.
8482
@@ -90,7 +88,7 @@ def query_tables(self, query: models.QueryTablesRequest) -> models.PagedTables:
9088
"""
9189
...
9290

93-
@get(_BASE_PATH + "/tables/{id}")
91+
@get("tables/{id}")
9492
def get_table_metadata(self, id: str) -> models.TableMetadata:
9593
"""Retrieves the metadata and column information for a single table identified by its ID.
9694
@@ -102,7 +100,7 @@ def get_table_metadata(self, id: str) -> models.TableMetadata:
102100
"""
103101
...
104102

105-
@patch(_BASE_PATH + "/tables/{id}", args=[Path, Body])
103+
@patch("tables/{id}", args=[Path, Body])
106104
def modify_table(self, id: str, update: models.ModifyTableRequest) -> None:
107105
"""Modify properties of a table or its columns.
108106
@@ -112,7 +110,7 @@ def modify_table(self, id: str, update: models.ModifyTableRequest) -> None:
112110
"""
113111
...
114112

115-
@delete(_BASE_PATH + "/tables/{id}")
113+
@delete("tables/{id}")
116114
def delete_table(self, id: str) -> None:
117115
"""Deletes a table.
118116
@@ -121,13 +119,32 @@ def delete_table(self, id: str) -> None:
121119
"""
122120
...
123121

124-
@post(_BASE_PATH + "/delete-tables", args=[Field("ids")])
122+
@post("delete-tables", args=[Field("ids")])
125123
def delete_tables(
126124
self, ids: List[str]
127125
) -> Optional[models.DeleteTablesPartialSuccess]:
128126
"""Deletes multiple tables.
129127
130128
Args:
131129
ids (List[str]): List of unique IDs of DataFrame tables.
130+
131+
Returns:
132+
A partial success if any tables failed to delete, or None if all
133+
tables were deleted successfully.
134+
"""
135+
...
136+
137+
@post("modify-tables")
138+
def modify_tables(
139+
self, updates: models.ModifyTablesRequest
140+
) -> Optional[models.ModifyTablesPartialSuccess]:
141+
"""Modify the properties associated with the tables identified by their IDs.
142+
143+
Args:
144+
updates: The table modifications to apply.
145+
146+
Returns:
147+
A partial success if any tables failed to be modified, or None if all
148+
tables were modified successfully.
132149
"""
133150
...

nisystemlink/clients/dataframe/models/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
from ._column_type import ColumnType
55
from ._data_type import DataType
66
from ._delete_tables_partial_success import DeleteTablesPartialSuccess
7+
from ._modify_tables_partial_success import ModifyTablesPartialSuccess
78
from ._modify_table_request import ColumnMetadataPatch, ModifyTableRequest
9+
from ._modify_tables_request import ModifyTablesRequest, TableMetdataModification
810
from ._order_by import OrderBy
911
from ._paged_tables import PagedTables
1012
from ._query_tables_request import QueryTablesRequest
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from typing import List
2+
3+
from nisystemlink.clients.core import ApiError
4+
from nisystemlink.clients.core._uplink._json_model import JsonModel
5+
6+
from ._modify_tables_request import TableMetdataModification
7+
8+
9+
class ModifyTablesPartialSuccess(JsonModel):
10+
"""The result of modifying multiple tables when one or more tables could not be modified."""
11+
12+
modified_table_ids: List[str]
13+
"""The IDs of the tables that were successfully modified."""
14+
15+
failed_modifications: List[TableMetdataModification]
16+
"""The requested modifications that could not be applied."""
17+
18+
error: ApiError
19+
"""The error that occurred when modifying the tables."""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from typing import Dict, List, Optional
2+
3+
from nisystemlink.clients.core._uplink._json_model import JsonModel
4+
5+
6+
class TableMetdataModification(JsonModel):
7+
"""Contains the metadata properties to modify. Values not included in the
8+
request or included with a ``None`` value will remain unchanged.
9+
"""
10+
11+
id: str
12+
"""The ID of the table to modify."""
13+
14+
metadata_revision: Optional[int] = None
15+
"""When specified, this is an integer that must match the last known
16+
revision number of the table, incremented by one. If it doesn't match the
17+
current ``metadataRevision`` incremented by one at the time of execution, the
18+
modify request will be rejected with a conflict error. This is used to
19+
ensure that changes to this table's metadata are based on a known, previous
20+
state."""
21+
22+
name: Optional[str] = None
23+
"""The new name of the table."""
24+
25+
workspace: Optional[str] = None
26+
"""The new workspace for the table. Changing the workspace requires
27+
permission to delete the table in its current workspace and permission to
28+
create the table in its new workspace."""
29+
30+
properties: Optional[Dict[str, Optional[str]]] = None
31+
"""The properties to modify. A map of key value properties containing the
32+
metadata to be added or modified. Setting a property value to ``None`` will
33+
delete the property. Existing properties not included in the map are
34+
unaffected unless replace is true in the top-level request object."""
35+
36+
37+
class ModifyTablesRequest(JsonModel):
38+
"""Contains one or more table modifications to apply."""
39+
40+
tables: List[TableMetdataModification]
41+
"""The table modifications to apply. Each table may only appear once in the list."""
42+
43+
replace: Optional[bool] = None
44+
"""When true, existing properties are replaced instead of merged."""

tests/integration/dataframe/test_dataframe.py

+68-61
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
# -*- coding: utf-8 -*-
22
from datetime import datetime, timezone
3-
from typing import List
3+
from typing import List, Optional
44

55
import pytest # type: ignore
66
from nisystemlink.clients.core import ApiException
77
from nisystemlink.clients.dataframe import DataFrameClient
88
from nisystemlink.clients.dataframe import models
99

10+
basic_table_model = models.CreateTableRequest(
11+
columns=[
12+
models.Column(
13+
name="index",
14+
data_type=models.DataType.Int32,
15+
column_type=models.ColumnType.Index,
16+
)
17+
]
18+
)
19+
1020

1121
@pytest.fixture(scope="class")
1222
def client(enterprise_config):
@@ -19,8 +29,8 @@ def create_table(client: DataFrameClient):
1929
"""Fixture to return a factory that creates tables."""
2030
tables = []
2131

22-
def _create_table(table: models.CreateTableRequest) -> str:
23-
id = client.create_table(table)
32+
def _create_table(table: Optional[models.CreateTableRequest] = None) -> str:
33+
id = client.create_table(table or basic_table_model)
2434
tables.append(id)
2535
return id
2636

@@ -85,17 +95,7 @@ def test__create_table__metadata_is_corect(
8595
]
8696

8797
def test__get_table__correct_timestamp(self, client: DataFrameClient, create_table):
88-
id = create_table(
89-
models.CreateTableRequest(
90-
columns=[
91-
models.Column(
92-
name="index",
93-
data_type=models.DataType.Int32,
94-
column_type=models.ColumnType.Index,
95-
)
96-
]
97-
)
98-
)
98+
id = create_table(basic_table_model)
9999
table = client.get_table_metadata(id)
100100

101101
now = datetime.now().timestamp()
@@ -156,17 +156,7 @@ def test__query_tables__returns(
156156
assert second_page.continuation_token is None
157157

158158
def test__modify_table__returns(self, client: DataFrameClient, create_table):
159-
id = create_table(
160-
models.CreateTableRequest(
161-
columns=[
162-
models.Column(
163-
name="index",
164-
data_type=models.DataType.Int32,
165-
column_type=models.ColumnType.Index,
166-
)
167-
]
168-
)
169-
)
159+
id = create_table(basic_table_model)
170160

171161
client.modify_table(
172162
id,
@@ -213,59 +203,76 @@ def test__modify_table__returns(self, client: DataFrameClient, create_table):
213203
assert table.columns[0].properties == {}
214204

215205
def test__delete_table__deletes(self, client: DataFrameClient):
216-
id = client.create_table( # Don't use fixture to avoid deleting the table twice
217-
models.CreateTableRequest(
218-
columns=[
219-
models.Column(
220-
name="index",
221-
data_type=models.DataType.Int32,
222-
column_type=models.ColumnType.Index,
223-
)
224-
]
225-
)
226-
)
206+
id = client.create_table(
207+
basic_table_model
208+
) # Don't use fixture to avoid deleting the table twice
227209

228210
assert client.delete_table(id) is None
229211

230212
with pytest.raises(ApiException, match="404 Not Found"):
231213
client.get_table_metadata(id)
232214

233215
def test__delete_tables__deletes(self, client: DataFrameClient):
234-
ids = [
235-
client.create_table(
236-
models.CreateTableRequest(
237-
columns=[
238-
models.Column(
239-
name="index",
240-
data_type=models.DataType.Int32,
241-
column_type=models.ColumnType.Index,
242-
)
243-
]
244-
)
245-
)
246-
for _ in range(3)
247-
]
216+
ids = [client.create_table(basic_table_model) for _ in range(3)]
248217

249218
assert client.delete_tables(ids) is None
250219

251220
assert client.list_tables(id=ids).tables == []
252221

253222
def test__delete_tables__returns_partial_success(self, client: DataFrameClient):
254-
id = client.create_table(
255-
models.CreateTableRequest(
256-
columns=[
257-
models.Column(
258-
name="index",
259-
data_type=models.DataType.Int32,
260-
column_type=models.ColumnType.Index,
261-
)
262-
]
263-
)
264-
)
223+
id = client.create_table(basic_table_model)
265224

266225
response = client.delete_tables([id, "invalid_id"])
267226

268227
assert response is not None
269228
assert response.deleted_table_ids == [id]
270229
assert response.failed_table_ids == ["invalid_id"]
271230
assert len(response.error.inner_errors) == 1
231+
232+
def test__modify_tables__modifies_tables(
233+
self, client: DataFrameClient, create_table
234+
):
235+
ids = [create_table(basic_table_model) for _ in range(3)]
236+
237+
updates = [
238+
models.TableMetdataModification(
239+
id=id, name="Modified table", properties={"duck": "quack"}
240+
)
241+
for id in ids
242+
]
243+
244+
assert client.modify_tables(models.ModifyTablesRequest(tables=updates)) is None
245+
246+
for table in client.list_tables(id=ids).tables:
247+
assert table.name == "Modified table"
248+
assert table.properties == {"duck": "quack"}
249+
250+
updates = [
251+
models.TableMetdataModification(id=id, properties={"pig": "oink"})
252+
for id in ids
253+
]
254+
255+
assert (
256+
client.modify_tables(
257+
models.ModifyTablesRequest(tables=updates, replace=True)
258+
)
259+
is None
260+
)
261+
262+
for table in client.list_tables(id=ids).tables:
263+
assert table.properties == {"pig": "oink"}
264+
265+
def test__modify_tables__returns_partial_success(self, client: DataFrameClient):
266+
id = client.create_table(basic_table_model)
267+
268+
updates = [
269+
models.TableMetdataModification(id=id, name="Modified table")
270+
for id in [id, "invalid_id"]
271+
]
272+
273+
response = client.modify_tables(models.ModifyTablesRequest(tables=updates))
274+
275+
assert response is not None
276+
assert response.modified_table_ids == [id]
277+
assert response.failed_modifications == [updates[1]]
278+
assert len(response.error.inner_errors) == 1

0 commit comments

Comments
 (0)