Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement create/get/delete/list table metadata methods #28

Merged
merged 7 commits into from
Nov 18, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@

# Configuration for https://autodoc-pydantic.readthedocs.io
autodoc_pydantic_model_show_config_summary = False
autodoc_pydantic_field_show_alias = False
mure marked this conversation as resolved.
Show resolved Hide resolved
1 change: 0 additions & 1 deletion mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ strict_equality=True

[mypy-tests.*]
disallow_untyped_calls=True
disallow_incomplete_defs=True
mure marked this conversation as resolved.
Show resolved Hide resolved
disallow_untyped_decorators=True

strict_equality=True
Expand Down
18 changes: 15 additions & 3 deletions nisystemlink/clients/core/_uplink/_base_client.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# mypy: disable-error-code = misc

from typing import Optional
from typing import Dict, Optional, Type

from nisystemlink.clients import core
from requests import JSONDecodeError, Response
from uplink import Consumer, response_handler
from uplink import Consumer, dumps, response_handler

from ._json_model import JsonModel


@response_handler
Expand Down Expand Up @@ -36,6 +38,12 @@ def _handle_http_status(response: Response) -> Optional[Response]:
raise core.ApiException(msg, http_status_code=response.status_code)


@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)


class BaseClient(Consumer):
"""Base class for SystemLink clients, built on top of `Uplink <https://github.com/prkumar/uplink>`_."""

Expand All @@ -45,6 +53,10 @@ def __init__(self, configuration: core.HttpConfiguration):
Args:
configuration: Defines the web server to connect to and information about how to connect.
"""
super().__init__(base_url=configuration.server_uri, hooks=[_handle_http_status])
super().__init__(
base_url=configuration.server_uri,
converter=_deserialize_model,
hooks=[_handle_http_status],
)
if configuration.api_keys:
self.session.headers.update(configuration.api_keys)
40 changes: 37 additions & 3 deletions nisystemlink/clients/dataframe/_data_frame_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from nisystemlink.clients import core
from nisystemlink.clients.core._uplink._base_client import BaseClient
from uplink import get, returns
from uplink import Body, delete, get, json, post, returns

from . import models

Expand All @@ -28,8 +28,42 @@ def __init__(self, configuration: Optional[core.HttpConfiguration] = None):

super().__init__(configuration)

@returns.json()
@get(_BASE_PATH)
def api_info(self) -> models.ApiInfo:
"""Returns information about available API operations."""
pass
...

@json
@returns.json(key="id")
@post(_BASE_PATH + "/tables", args=(Body,))
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.

Returns:
The ID of the newly created table.
"""
...

@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.

Args:
id (str): Unique ID of a DataFrame table.

Returns:
models.TableMetadata: The metadata for the table.
"""
...

@delete(_BASE_PATH + "/tables/{id}")
def delete_table(self, id: str) -> None:
"""Deletes a table.

Args:
id (str): Unique ID of a DataFrame table.
"""
...
5 changes: 5 additions & 0 deletions nisystemlink/clients/dataframe/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
from ._api_info import ApiInfo, Operation, OperationsV1
from ._create_table_request import CreateTableRequest
from ._column import Column
from ._column_type import ColumnType
from ._data_type import DataType
from ._table_metadata import TableMetadata

# flake8: noqa
30 changes: 22 additions & 8 deletions nisystemlink/clients/dataframe/models/_api_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,33 @@
class Operation(JsonModel):
"""Represents an operation that can be performed on a data frame."""

available: bool #: Whether or not the operation is available to the caller (e.g. due to permissions).
version: int #: The version of the available operation.
available: bool
"""Whether or not the operation is available to the caller (e.g. due to permissions)."""

version: int
"""The version of the available operation."""


class OperationsV1(JsonModel):
"""The operations available in the routes provided by the v1 HTTP API."""

create_tables: Operation #: The ability to create new DataFrame tables.
delete_tables: Operation #: The ability to delete tables and all of their data.
modify_metadata: Operation #: The ability to modify metadata for tables.
list_tables: Operation #: The ability to locate and read metadata for tables.
read_data: Operation #: The ability to query and read data from tables.
write_data: Operation #: The ability to append rows of data to tables.
create_tables: Operation
"""The ability to create new DataFrame tables."""

delete_tables: Operation
"""The ability to delete tables and all of their data."""

modify_metadata: Operation
"""The ability to modify metadata for tables."""

list_tables: Operation
"""The ability to locate and read metadata for tables."""

read_data: Operation
"""The ability to query and read data from tables."""

write_data: Operation
"""The ability to append rows of data to tables."""


class ApiInfo(JsonModel):
Expand Down
22 changes: 22 additions & 0 deletions nisystemlink/clients/dataframe/models/_column.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from typing import Dict, Optional

from nisystemlink.clients.core._uplink._json_model import JsonModel

from ._column_type import ColumnType
from ._data_type import DataType


class Column(JsonModel):
"""Defines a single column in a table."""

name: str
"""The column name, which must be unique across all columns in the table."""

data_type: DataType
"""The data type of the column."""

column_type: Optional[ColumnType] = ColumnType.Normal
mure marked this conversation as resolved.
Show resolved Hide resolved
"""The column type. Defaults to ColumnType.Normal."""

properties: Optional[Dict[str, str]] = None
"""User-defined properties associated with the column."""
18 changes: 18 additions & 0 deletions nisystemlink/clients/dataframe/models/_column_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from enum import Enum


class ColumnType(str, Enum):
"""Represents the different column types for a table column."""

Normal = "NORMAL"
"""The column has no special properties. This is the default."""

Index = "INDEX"
"""The column provides a unique value per row. Each table must provide
exactly one INDEX column. The column's :class:`.DataType` must be INT32,
INT64, or TIMESTAMP."""

Nullable = "NULLABLE"
"""Rows may contain null values for this column. When appending rows,
NULLABLE columns may be left out entirely, in which case all rows being
appended will use null values for that column."""
22 changes: 22 additions & 0 deletions nisystemlink/clients/dataframe/models/_create_table_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from typing import Dict, List, Optional

from nisystemlink.clients.core._uplink._json_model import JsonModel

from ._column import Column


class CreateTableRequest(JsonModel):
"""Contains information needed to create a table, including its properties and column definitions."""

columns: List[Column]
"""The list of columns in the table. Exactly one column must have a :class:`.ColumnType` of INDEX."""

name: Optional[str] = None
"""The name to associate with the table. When not specified, a name will be
assigned from the table's ID."""

properties: Optional[Dict[str, str]] = None
"""User-defined properties to associate with the table."""

workspace: Optional[str] = None
"""The workspace to create the table in. Uses the default workspace when not specified."""
26 changes: 26 additions & 0 deletions nisystemlink/clients/dataframe/models/_data_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from enum import Enum


class DataType(str, Enum):
"""Represents the different data types for a table column."""

Bool = "BOOL"
"""32-bit IEEE 754 floating-point number."""

Float32 = "FLOAT32"
"""32-bit IEEE 754 floating-point number."""

Float64 = "FLOAT64"
"""64-bit IEEE 754 floating-point number."""

Int32 = "INT32"
"""32-bit signed integers."""

Int64 = "INT64"
"""64-bit signed integers."""

String = "STRING"
"""Arbitrary string data."""

Timestamp = "TIMESTAMP"
"""Date and time represented in UTC with millisecond precision."""
43 changes: 43 additions & 0 deletions nisystemlink/clients/dataframe/models/_table_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from datetime import datetime
from typing import Dict, List

from nisystemlink.clients.core._uplink._json_model import JsonModel

from ._column import Column


class TableMetadata(JsonModel):
"""Contains information about a table, including its properties and column definitions."""

columns: List[Column]
"""The list of columns in the table."""

created_at: datetime
"""The date and time the table was created."""

id: str
"""The table's unique identifier."""

metadata_modified_at: datetime
"""The date and time the table's metadata was last modified."""

metadata_revision: int
"""The table's metadata revision number, incremented each time the metadata is modified."""

name: str
"""The name associated with the table."""

properties: Dict[str, str]
"""User-defined properties associated with the table."""

row_count: int
"""The number of rows in the table."""

rows_modified_at: datetime
"""The date and time the table's data was last modified."""

supports_append: bool
"""Whether the table supports appending additional rows of data."""

workspace: str
"""The workspace the table belongs to."""
50 changes: 48 additions & 2 deletions tests/integration/dataframe/test_dataframe.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,61 @@
# -*- coding: utf-8 -*-
from typing import Iterator

import pytest # type: ignore
from nisystemlink.clients.dataframe import DataFrameClient
from nisystemlink.clients.dataframe import models


@pytest.mark.enterprise
@pytest.mark.integration
class TestDataFrame:
def test__api_info__returns(self, enterprise_config):
client = DataFrameClient(enterprise_config)
@pytest.fixture
def client(self, enterprise_config):
return DataFrameClient(enterprise_config)

@pytest.fixture
def table_id(self, client: DataFrameClient) -> Iterator[str]:
id = client.create_table(
models.CreateTableRequest(
columns=[
models.Column(
name="time",
data_type=models.DataType.Timestamp,
column_type=models.ColumnType.Index,
properties={"cat": "meow"},
),
models.Column(name="value", data_type=models.DataType.Int32),
],
name="Python API test table (delete me)",
properties={"dog": "woof"},
)
)
yield id
client.delete_table(id)

def test__api_info__returns(self, client):
response = client.api_info()

assert len(response.dict()) != 0

def test__create_table__metadata_is_corect(
self, client: DataFrameClient, table_id: str
):
table_metadata = client.get_table_metadata(table_id)

assert table_metadata.name == "Python API test table (delete me)"
assert table_metadata.properties == {"dog": "woof"}
assert table_metadata.columns == [
models.Column(
name="time",
data_type=models.DataType.Timestamp,
column_type=models.ColumnType.Index,
properties={"cat": "meow"},
),
models.Column(
name="value",
data_type=models.DataType.Int32,
column_type=models.ColumnType.Normal,
properties={},
),
]