diff --git a/CHANGELOG.md b/CHANGELOG.md index 070af08..d8d55d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ Observes [Semantic Versioning](https://semver.org/spec/v2.0.0.html) standard and [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) convention. +## [0.6.4] - 2022-12-07 + +### Added + +- Support for new `antd-table` component. Prior `table` component is deprecated and will be removed in the next major release. PR #128 +- Deprecated warning for `table` PR #128 + ## [0.6.3] - 2022-11-18 ### Added @@ -226,6 +233,7 @@ Observes [Semantic Versioning](https://semver.org/spec/v2.0.0.html) standard and - Support for DataJoint attribute types: `varchar`, `int`, `float`, `datetime`, `date`, `time`, `decimal`, `uuid`. - Check dependency utility to determine child table references. +[0.6.4]: https://github.com/datajoint/pharus/compare/0.6.3...0.6.4 [0.6.3]: https://github.com/datajoint/pharus/compare/0.6.2...0.6.3 [0.6.2]: https://github.com/datajoint/pharus/compare/0.6.1...0.6.2 [0.6.1]: https://github.com/datajoint/pharus/compare/0.6.0...0.6.1 diff --git a/README.rst b/README.rst index 7cc1598..8bf0172 100644 --- a/README.rst +++ b/README.rst @@ -29,13 +29,13 @@ To start the API server, use the command: .. code-block:: bash - PHARUS_VERSION=0.6.3 docker-compose -f docker-compose-deploy.yaml up -d + PHARUS_VERSION=0.6.4 docker-compose -f docker-compose-deploy.yaml up -d To stop the API server, use the command: .. code-block:: bash - PHARUS_VERSION=0.6.3 docker-compose -f docker-compose-deploy.yaml down + PHARUS_VERSION=0.6.4 docker-compose -f docker-compose-deploy.yaml down References ---------- diff --git a/docker-compose-deploy.yaml b/docker-compose-deploy.yaml index ca1b3c3..fe1b312 100644 --- a/docker-compose-deploy.yaml +++ b/docker-compose-deploy.yaml @@ -1,5 +1,5 @@ -# PHARUS_VERSION=0.6.3 docker-compose -f docker-compose-deploy.yaml pull -# PHARUS_VERSION=0.6.3 docker-compose -f docker-compose-deploy.yaml up -d +# PHARUS_VERSION=0.6.4 docker-compose -f docker-compose-deploy.yaml pull +# PHARUS_VERSION=0.6.4 docker-compose -f docker-compose-deploy.yaml up -d # # Intended for production deployment. # Note: You must run both commands above for minimal outage diff --git a/pharus/component_interface.py b/pharus/component_interface.py index ad323d0..649ef10 100644 --- a/pharus/component_interface.py +++ b/pharus/component_interface.py @@ -1,6 +1,6 @@ """This module is a GUI component library of various common interfaces.""" -import json from base64 import b64decode +import json import datajoint as dj import re import inspect @@ -12,6 +12,7 @@ import types import io import numpy as np +from uuid import UUID class NumpyEncoder(json.JSONEncoder): @@ -35,6 +36,10 @@ class NumpyEncoder(json.JSONEncoder): def default(self, o): if type(o) in self.npmap: return self.npmap[type(o)](o) + if type(o) is UUID: + return str(o) + if type(o) is str and o == "NaN": + return None if type(o) in (datetime, date): return o.isoformat() return json.JSONEncoder.default(self, o) @@ -85,7 +90,7 @@ def __init__(self, *args, **kwargs): self.vm_list = [ dj.VirtualModule( s, - s, + s.replace("__", "-"), connection=self.connection, ) for s in inspect.getfullargspec(self.dj_query).args @@ -123,8 +128,17 @@ def dj_query_route(self): query=fetch_metadata["query"] & self.restriction, fetch_args=fetch_metadata["fetch_args"], ) - return dict( - recordHeader=record_header, records=table_records, totalCount=total_count + + return ( + NumpyEncoder.dumps( + dict( + recordHeader=record_header, + records=table_records, + totalCount=total_count, + ) + ), + 200, + {"Content-Type": "application/json"}, ) @@ -282,30 +296,41 @@ def __init__(self, *args, **kwargs): def dj_query_route(self): fetch_metadata = self.fetch_metadata record_header, table_records, total_count = _DJConnector._fetch_records( - query=fetch_metadata["query"] & self.restriction[0], + query=fetch_metadata["query"] & self.restriction, fetch_args=fetch_metadata["fetch_args"], - **{ - k: ( - int(v) - if k in ("limit", "page") - else ( - v.split(",") - if k == "order" - else json.loads(b64decode(v.encode("utf-8")).decode("utf-8")) - ) - ) - for k, v in request.args.items() - }, + limit=int(request.args["limit"]) if "limit" in request.args else 1000, + page=int(request.args["page"]) if "page" in request.args else 1, + order=request.args["order"].split(",") if "order" in request.args else None, + restriction=json.loads(b64decode(request.args["restriction"])) + if "restriction" in request.args + else [], ) - return dict( - recordHeader=record_header, records=table_records, totalCount=total_count + + return ( + NumpyEncoder.dumps( + dict( + recordHeader=record_header, + records=table_records, + totalCount=total_count, + ) + ), + 200, + {"Content-Type": "application/json"}, ) def attributes_route(self): - attributes_meta = _DJConnector._get_attributes(self.fetch_metadata["query"]) - return dict( - attributeHeaders=attributes_meta["attribute_headers"], - attributes=attributes_meta["attributes"], + attributes_meta = _DJConnector._get_attributes( + self.fetch_metadata["query"] & self.restriction, include_unique_values=True + ) + return ( + NumpyEncoder.dumps( + dict( + attributeHeaders=attributes_meta["attribute_headers"], + attributes=attributes_meta["attributes"], + ) + ), + 200, + {"Content-Type": "application/json"}, ) @@ -349,8 +374,16 @@ def dj_query_route(self): query=fetch_metadata["query"] & self.restriction, fetch_args=fetch_metadata["fetch_args"], ) - return dict( - recordHeader=record_header, records=table_records, totalCount=total_count + return ( + NumpyEncoder.dumps( + dict( + recordHeader=record_header, + records=table_records, + totalCount=total_count, + ) + ), + 200, + {"Content-Type": "application/json"}, ) @@ -376,10 +409,14 @@ def __init__(self, *args, **kwargs): def dj_query_route(self): fetch_metadata = self.fetch_metadata - return NumpyEncoder.dumps( - (fetch_metadata["query"] & self.restriction).fetch1( - *fetch_metadata["fetch_args"] - ) + return ( + NumpyEncoder.dumps( + (fetch_metadata["query"] & self.restriction).fetch1( + *fetch_metadata["fetch_args"] + ) + ), + 200, + {"Content-Type": "application/json"}, ) @@ -410,6 +447,7 @@ def dj_query_route(self): "basicquery": FetchComponent, "plot:plotly:stored_json": PlotPlotlyStoredjsonComponent, "table": TableComponent, + "antd-table": TableComponent, "metadata": MetadataComponent, "file:image:attach": FileImageAttachComponent, "slider": FetchComponent, diff --git a/pharus/dynamic_api_gen.py b/pharus/dynamic_api_gen.py index db8d6c4..993590d 100644 --- a/pharus/dynamic_api_gen.py +++ b/pharus/dynamic_api_gen.py @@ -4,7 +4,7 @@ import pkg_resources import json import re - +import warnings from pharus.component_interface import InsertComponent, TableComponent @@ -114,16 +114,34 @@ def {method_name}() -> dict: method_name_type="dj_query_route", ) ) - for comp_name, comp in ( grid["component_templates"] if "component_templates" in grid else grid["components"] ).items(): + if re.match(r"^table.*$", comp["type"]): + # For some reason the warnings package filters out deprecation + # warnings by default so make sure that filter is turned off + warnings.simplefilter("always", DeprecationWarning) + warnings.warn( + "table component to be Deprecated in next major release, " + + "please use antd-table", + DeprecationWarning, + stacklevel=2, + ) if re.match( - r""" - ^(table|metadata|plot|file|slider| - dropdown-query|form|basicquery|external).*$""", + r"""^( + table| + antd-table| + metadata| + plot| + file| + slider| + dropdown-query| + form| + basicquery| + external + ).*$""", comp["type"], flags=re.VERBOSE, ): diff --git a/pharus/interface.py b/pharus/interface.py index a1214cc..bd02bd4 100644 --- a/pharus/interface.py +++ b/pharus/interface.py @@ -207,12 +207,15 @@ def _fetch_records( return list(attributes.keys()), rows, len(query_restricted) @staticmethod - def _get_attributes(query) -> dict: + def _get_attributes(query, include_unique_values=False) -> dict: """ Method to get primary and secondary attributes of a query. :param query: any datajoint object related to QueryExpression :type query: datajoint ``QueryExpression`` or related object + :param include_unique_values: boolean that determines if the unique values are + included as part of the returned attributes + :type include_unique_values: boolean, optional :return: Dict with keys ``attribute_headers`` and ``attributes`` containing ``primary``, ``secondary`` which each contain a ``list`` of ``tuples`` specifying: ``attribute_name``, ``type``, ``nullable``, @@ -230,6 +233,12 @@ def _get_attributes(query) -> dict: attribute_info.nullable, attribute_info.default, attribute_info.autoincrement, + [ + dict({"text": str(v), "value": v}) + for (v,) in (dj.U(attribute_name) & query).fetch() + ] + if include_unique_values + else None, ) ) else: @@ -240,6 +249,12 @@ def _get_attributes(query) -> dict: attribute_info.nullable, attribute_info.default, attribute_info.autoincrement, + [ + dict({"text": str(v), "value": v}) + for (v,) in (dj.U(attribute_name) & query).fetch() + ] + if include_unique_values + else None, ) ) diff --git a/pharus/server.py b/pharus/server.py index 40e8ad6..0332f82 100644 --- a/pharus/server.py +++ b/pharus/server.py @@ -126,7 +126,7 @@ def api_version() -> str: Content-Type: application/json { - "version": "0.6.3" + "version": "0.6.4" } :statuscode 200: No error. diff --git a/pharus/version.py b/pharus/version.py index 3b6d053..3409e04 100644 --- a/pharus/version.py +++ b/pharus/version.py @@ -1,2 +1,2 @@ """Package metadata.""" -__version__ = "0.6.3" +__version__ = "0.6.4" diff --git a/tests/test_api_gen.py b/tests/test_api_gen.py index 32d0e7e..3fbb51c 100644 --- a/tests/test_api_gen.py +++ b/tests/test_api_gen.py @@ -1,4 +1,5 @@ from . import SCHEMA_PREFIX, token, client, connection, schemas_simple +from base64 import b64encode import json @@ -31,18 +32,10 @@ def test_auto_generated_route(token, client, schemas_simple): } ) - assert expected_json == json.dumps( - REST_response1.get_json(force=True), sort_keys=True - ) - assert expected_json == json.dumps( - REST_response2.get_json(force=True), sort_keys=True - ) - assert expected_json == json.dumps( - REST_response3.get_json(force=True), sort_keys=True - ) - assert expected_json == json.dumps( - REST_response4.get_json(force=True), sort_keys=True - ) + assert expected_json == json.dumps(REST_response1.get_json(), sort_keys=True) + assert expected_json == json.dumps(REST_response2.get_json(), sort_keys=True) + assert expected_json == json.dumps(REST_response3.get_json(), sort_keys=True) + assert expected_json == json.dumps(REST_response4.get_json(), sort_keys=True) def test_get_full_plot(token, client, schemas_simple): @@ -63,9 +56,7 @@ def test_get_full_plot(token, client, schemas_simple): ), sort_keys=True, ) - assert expected_json == json.dumps( - REST_response1.get_json(force=True), sort_keys=True - ) + assert expected_json == json.dumps(REST_response1.get_json(), sort_keys=True) def test_get_attributes(token, client, schemas_simple): @@ -77,12 +68,51 @@ def test_get_attributes(token, client, schemas_simple): "attributeHeaders": ["name", "type", "nullable", "default", "autoincrement"], "attributes": { "primary": [ - ["a_id", "int", False, None, False], - ["b_id", "int", False, None, False], + [ + "a_id", + "int", + False, + None, + False, + [{"text": "0", "value": 0}, {"text": "1", "value": 1}], + ], + [ + "b_id", + "int", + False, + None, + False, + [ + {"text": "10", "value": 10}, + {"text": "11", "value": 11}, + {"text": "21", "value": 21}, + ], + ], ], "secondary": [ - ["a_name", "varchar(30)", False, None, False], - ["b_number", "float", False, None, False], + [ + "a_name", + "varchar(30)", + False, + None, + False, + [ + {"text": "Raphael", "value": "Raphael"}, + {"text": "Bernie", "value": "Bernie"}, + ], + ], + [ + "b_number", + "float", + False, + None, + False, + [ + {"text": "22.12", "value": 22.12}, + {"text": "-1.21", "value": -1.21}, + {"text": "7.77", "value": 7.77}, + ], + ], ], }, } @@ -100,6 +130,24 @@ def test_dynamic_restriction(token, client, schemas_simple): "totalCount": 2, } ) - assert expected_json == json.dumps( - REST_response.get_json(force=True), sort_keys=True + assert expected_json == json.dumps(REST_response.get_json(), sort_keys=True) + + +def test_fetch_restriction(token, client, schemas_simple): + restriction = [{"attributeName": "a_id", "operation": "=", "value": 1}] + encoded = b64encode(json.dumps(restriction).encode("utf-8")) + REST_response = client.get( + f"/query1?restriction={encoded.decode()}", + headers=dict(Authorization=f"Bearer {token}"), + ) + # should restrict in the query parameter by a_id=1 + expected_json = json.dumps( + { + "recordHeader": ["a_id", "b_id", "a_name", "b_number"], + "records": [ + [1, 21, "Bernie", 7.77], + ], + "totalCount": 1, + } ) + assert expected_json == json.dumps(REST_response.get_json(), sort_keys=True)