diff --git a/nisystemlink/clients/core/_uplink/_base_client.py b/nisystemlink/clients/core/_uplink/_base_client.py index 171fde0..966a1e3 100644 --- a/nisystemlink/clients/core/_uplink/_base_client.py +++ b/nisystemlink/clients/core/_uplink/_base_client.py @@ -1,5 +1,6 @@ # mypy: disable-error-code = misc +import json from typing import Dict, Optional, Type from nisystemlink.clients import core @@ -41,7 +42,7 @@ def _handle_http_status(response: Response) -> Optional[Response]: @dumps.to_json(JsonModel) def _deserialize_model(model_cls: Type[JsonModel], model_instance: JsonModel) -> Dict: """Turns a :class:`.JsonModel` instance into a dictionary for serialization.""" - return model_instance.dict(by_alias=True, exclude_unset=True) + return json.loads(model_instance.json(by_alias=True, exclude_unset=True)) class BaseClient(Consumer): diff --git a/nisystemlink/clients/core/_uplink/_paged_result.py b/nisystemlink/clients/core/_uplink/_with_paging.py similarity index 87% rename from nisystemlink/clients/core/_uplink/_paged_result.py rename to nisystemlink/clients/core/_uplink/_with_paging.py index 185bb37..db530d4 100644 --- a/nisystemlink/clients/core/_uplink/_paged_result.py +++ b/nisystemlink/clients/core/_uplink/_with_paging.py @@ -3,8 +3,8 @@ from ._json_model import JsonModel -class PagedResult(JsonModel): - continuation_token: Optional[str] +class WithPaging(JsonModel): + continuation_token: Optional[str] = None """A token which allows the user to resume a query at the next item in the matching results. When querying, a token will be returned if a query may be diff --git a/nisystemlink/clients/dataframe/_data_frame_client.py b/nisystemlink/clients/dataframe/_data_frame_client.py index 6c27259..da5f96e 100644 --- a/nisystemlink/clients/dataframe/_data_frame_client.py +++ b/nisystemlink/clients/dataframe/_data_frame_client.py @@ -75,13 +75,26 @@ def create_table(self, table: models.CreateTableRequest) -> str: """Create a new table with the provided metadata and column definitions. Args: - table: The request create the table. + table: The request to create the table. Returns: The ID of the newly created table. """ ... + @json + @post(_BASE_PATH + "/query-tables", args=(Body,)) + def query_tables(self, query: models.QueryTablesRequest) -> models.PagedTables: + """Queries available tables on the SystemLink DataFrame service and returns their metadata. + + Args: + query: The request to query tables. + + Returns: + models.PagedTables: The list of tables with a continuation token. + """ + ... + @get(_BASE_PATH + "/tables/{id}") def get_table_metadata(self, id: str) -> models.TableMetadata: """Retrieves the metadata and column information for a single table identified by its ID. diff --git a/nisystemlink/clients/dataframe/models/__init__.py b/nisystemlink/clients/dataframe/models/__init__.py index ba3b2f7..18a9fc6 100644 --- a/nisystemlink/clients/dataframe/models/__init__.py +++ b/nisystemlink/clients/dataframe/models/__init__.py @@ -5,6 +5,7 @@ from ._data_type import DataType from ._order_by import OrderBy from ._paged_tables import PagedTables +from ._query_tables_request import QueryTablesRequest from ._table_metadata import TableMetadata # flake8: noqa diff --git a/nisystemlink/clients/dataframe/models/_paged_tables.py b/nisystemlink/clients/dataframe/models/_paged_tables.py index 420512d..15d5a85 100644 --- a/nisystemlink/clients/dataframe/models/_paged_tables.py +++ b/nisystemlink/clients/dataframe/models/_paged_tables.py @@ -1,11 +1,11 @@ from typing import List -from nisystemlink.clients.core._uplink._paged_result import PagedResult +from nisystemlink.clients.core._uplink._with_paging import WithPaging from ._table_metadata import TableMetadata -class PagedTables(PagedResult): +class PagedTables(WithPaging): """The response for a table query containing the matched tables.""" tables: List[TableMetadata] diff --git a/nisystemlink/clients/dataframe/models/_query_tables_request.py b/nisystemlink/clients/dataframe/models/_query_tables_request.py new file mode 100644 index 0000000..7f0b135 --- /dev/null +++ b/nisystemlink/clients/dataframe/models/_query_tables_request.py @@ -0,0 +1,78 @@ +from datetime import datetime +from typing import List, Optional, Union + +from nisystemlink.clients.core._uplink._with_paging import WithPaging +from pydantic import StrictBool, StrictInt + +from ._order_by import OrderBy + + +class QueryTablesRequest(WithPaging): + """Request parameters for querying tables.""" + + filter: str + """The table query filter in `Dynamic LINQ`_ format. + + .. _Dynamic LINQ: https://github.com/ni/systemlink-OpenAPI-documents/wiki/Dynamic-Linq-Query-Language + + Allowed properties in the filter are: + + * ``createdAt``: DateTime the table was created + * ``createdWithin``: TimeSpan in which the table was created + * ``id``: String value uniquely identifying the table + * ``name``: String name for the table + * ``metadataModifiedAt``: DateTime the table's metadata was last modified + * ``metadataModifiedWithin``: TimeSpan in which the table's metadata was + last modified + * ``properties``: Dictionary with string keys and values representing table + metadata + * ``rowsModifiedAt``: DateTime rows were last appended to the table + * ``rowsModifiedWithin``: TimeSpan within rows were last appended to the + table + * ``rowCount``: Int32 number of rows in the table + * ``supportsAppend``: Boolean indicating whether or not the table supports + appending additional rows of data + * ``workspace``: String value ID of the workspace the table belongs to + * ``workspaceName``: String value name of the workspace the table belongs to + + Allowed constants in the filter are: + + * ``RelativeTime.CurrentDay``: TimeSpan representing the elapsed time + between now and the start of the current day + * ``RelativeTime.CurrentWeek``: TimeSpan representing the elapsed time + between now and the start of the current week + * ``RelativeTime.CurrentMonth``: TimeSpan representing the elapsed time + between now and the start of the current month + * ``RelativeTime.CurrentYear``: TimeSpan representing the elapsed time + between now and the start of the current year + """ + + substitutions: Optional[List[Union[StrictInt, StrictBool, str, None]]] = None + """Make substitutions in the query filter expression. + + Substitutions for the query expression are indicated by non-negative + integers that are prefixed with the ``@`` symbol. Each substitution in the given + expression will be replaced by the element at the corresponding index + (zero-based) in this list. For example, ``@0`` in the filter expression will be + replaced with the element at the zeroth index of the substitutions list. + """ + + reference_time: Optional[datetime] = None + """The date and time to use as the reference point for `RelativeTime` filters, + including time zone information. Defaults to the time on the server in UTC.""" + + take: Optional[int] = None + """Limits the returned list to the specified number of results.""" + + order_by: Optional[OrderBy] = None + """The sort order of the returned list of tables.""" + + order_by_descending: Optional[bool] = None + """Whether to sort descending instead of ascending. + + The elements in the list are sorted ascending by default. If the + orderByDescending parameter is specified, the elements in the list are + sorted based on it's value. The orderByDescending value must be a boolean + string. The elements in the list are sorted ascending if false and + descending if true. + """ diff --git a/tests/integration/dataframe/test_dataframe.py b/tests/integration/dataframe/test_dataframe.py index 7f1de8f..3097fa0 100644 --- a/tests/integration/dataframe/test_dataframe.py +++ b/tests/integration/dataframe/test_dataframe.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from datetime import datetime +from datetime import datetime, timezone from typing import List import pytest # type: ignore @@ -111,20 +111,19 @@ def test__get_table_invalid_id__raises(self, client: DataFrameClient): def test__list_tables__returns( self, client: DataFrameClient, test_tables: List[str] ): - take = len(test_tables) - 1 first_page = client.list_tables( - take=take, - id=test_tables, + take=2, + id=test_tables[:3], order_by="NAME", order_by_descending=True, ) - assert len(first_page.tables) == take + assert len(first_page.tables) == 2 assert first_page.tables[0].id == test_tables[-1] # Asserts descending order assert first_page.continuation_token is not None second_page = client.list_tables( - id=test_tables, + id=test_tables[:3], order_by="NAME", order_by_descending=True, continuation_token=first_page.continuation_token, @@ -132,3 +131,28 @@ def test__list_tables__returns( assert len(second_page.tables) == 1 assert second_page.continuation_token is None + + def test__query_tables__returns( + self, client: DataFrameClient, test_tables: List[str] + ): + query = models.QueryTablesRequest( + filter="""(id == @0 or id == @1 or id == @2) + and createdWithin <= RelativeTime.CurrentWeek + and supportsAppend == @3 and rowCount < @4""", + substitutions=[test_tables[0], test_tables[1], test_tables[2], True, 1], + reference_time=datetime.now(tz=timezone.utc), + take=2, + order_by="NAME", + order_by_descending=True, + ) + first_page = client.query_tables(query) + + assert len(first_page.tables) == 2 + assert first_page.tables[0].id == test_tables[-1] # Asserts descending order + assert first_page.continuation_token is not None + + query.continuation_token = first_page.continuation_token + + second_page = client.query_tables(query) + assert len(second_page.tables) == 1 + assert second_page.continuation_token is None