From 5ed1f4cff2f9f6fccf9f81fce9228c1537a4462a Mon Sep 17 00:00:00 2001 From: Patrick Latimer <110747402+patricklatimer@users.noreply.github.com> Date: Fri, 14 Jun 2024 15:26:35 -0700 Subject: [PATCH] Generic Client (#10) * Initial add * Update dependencies * refactor: expand SlimsBaseModel and client * feat: add mouse model and fetch method * feat: add user model and fetch method * Feat client unit tests (#11) * docs: set line length to 88 * tests: adds test coverage --------- Co-authored-by: jtyoung84 <104453205+jtyoung84@users.noreply.github.com> --- .flake8 | 1 + .github/workflows/test_and_lint.yml | 2 +- .gitignore | 1 + doc_template/source/conf.py | 7 +- pyproject.toml | 13 +- src/aind_slims_api/__init__.py | 7 + src/aind_slims_api/configuration.py | 16 + src/aind_slims_api/core.py | 135 +++ src/aind_slims_api/mouse.py | 33 + src/aind_slims_api/user.py | 33 + .../example_fetch_mouse_response.json | 960 ++++++++++++++++++ .../example_fetch_unit_response.json | 472 +++++++++ .../example_fetch_user_response.json | 480 +++++++++ tests/test_configuration.py | 47 + tests/test_core.py | 172 ++++ tests/test_example.py | 16 - tests/test_mouse.py | 74 ++ tests/test_user.py | 74 ++ 18 files changed, 2519 insertions(+), 24 deletions(-) create mode 100644 src/aind_slims_api/configuration.py create mode 100644 src/aind_slims_api/core.py create mode 100644 src/aind_slims_api/mouse.py create mode 100644 src/aind_slims_api/user.py create mode 100644 tests/resources/example_fetch_mouse_response.json create mode 100644 tests/resources/example_fetch_unit_response.json create mode 100644 tests/resources/example_fetch_user_response.json create mode 100644 tests/test_configuration.py create mode 100644 tests/test_core.py delete mode 100644 tests/test_example.py create mode 100644 tests/test_mouse.py create mode 100644 tests/test_user.py diff --git a/.flake8 b/.flake8 index 6d5ce4f..b8016c4 100644 --- a/.flake8 +++ b/.flake8 @@ -4,3 +4,4 @@ exclude = __pycache__, build max-complexity = 10 +max-line-length = 88 diff --git a/.github/workflows/test_and_lint.yml b/.github/workflows/test_and_lint.yml index c8d832d..293d035 100644 --- a/.github/workflows/test_and_lint.yml +++ b/.github/workflows/test_and_lint.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ '3.8', '3.9', '3.10' ] + python-version: [ '3.10', '3.11', '3.12' ] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} diff --git a/.gitignore b/.gitignore index 06a56dd..df28b4e 100644 --- a/.gitignore +++ b/.gitignore @@ -109,6 +109,7 @@ venv/ ENV/ env.bak/ venv.bak/ +.conda # Spyder project settings .spyderproject diff --git a/doc_template/source/conf.py b/doc_template/source/conf.py index 1ecef49..820892b 100644 --- a/doc_template/source/conf.py +++ b/doc_template/source/conf.py @@ -1,12 +1,15 @@ """Configuration file for the Sphinx documentation builder.""" + # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +from datetime import date + # -- Path Setup -------------------------------------------------------------- -from os.path import dirname, abspath +from os.path import abspath, dirname from pathlib import Path -from datetime import date + from aind_slims_api import __version__ as package_version INSTITUTE_NAME = "Allen Institute for Neural Dynamics" diff --git a/pyproject.toml b/pyproject.toml index b796ca6..111fbde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "aind-slims-api" description = "Generated from aind-library-template" license = {text = "MIT"} -requires-python = ">=3.7" +requires-python = ">=3.10" authors = [ {name = "Allen Institute for Neural Dynamics"} ] @@ -17,6 +17,9 @@ readme = "README.md" dynamic = ["version"] dependencies = [ + 'slims-python-api', + 'pydantic', + 'pydantic-settings' ] [project.optional-dependencies] @@ -27,7 +30,7 @@ dev = [ 'interrogate', 'isort', 'Sphinx', - 'furo' + 'furo', ] [tool.setuptools.packages.find] @@ -37,8 +40,8 @@ where = ["src"] version = {attr = "aind_slims_api.__version__"} [tool.black] -line-length = 79 -target_version = ['py36'] +line-length = 88 +target_version = ['py310'] exclude = ''' ( @@ -71,7 +74,7 @@ exclude_lines = [ fail_under = 100 [tool.isort] -line_length = 79 +line_length = 88 profile = "black" [tool.interrogate] diff --git a/src/aind_slims_api/__init__.py b/src/aind_slims_api/__init__.py index d0a8547..25ae369 100644 --- a/src/aind_slims_api/__init__.py +++ b/src/aind_slims_api/__init__.py @@ -1,2 +1,9 @@ """Init package""" + __version__ = "0.0.0" + +from aind_slims_api.configuration import AindSlimsApiSettings + +config = AindSlimsApiSettings() + +from aind_slims_api.core import SlimsClient # noqa diff --git a/src/aind_slims_api/configuration.py b/src/aind_slims_api/configuration.py new file mode 100644 index 0000000..6b6d62e --- /dev/null +++ b/src/aind_slims_api/configuration.py @@ -0,0 +1,16 @@ +""" Library Configuration model """ + +from pydantic import SecretStr +from pydantic_settings import BaseSettings + + +class AindSlimsApiSettings(BaseSettings): + """Settings for SLIMS Client + + Per pydantic-settings docs + https://docs.pydantic.dev/latest/concepts/pydantic_settings/ + Loads slims credentials from environment variables if present""" + + slims_url: str = "https://aind-test.us.slims.agilent.com/slimsrest/" + slims_username: str = "" + slims_password: SecretStr = "" diff --git a/src/aind_slims_api/core.py b/src/aind_slims_api/core.py new file mode 100644 index 0000000..d22f4ec --- /dev/null +++ b/src/aind_slims_api/core.py @@ -0,0 +1,135 @@ +"""Contents: + +Utilities for creating pydantic models for SLIMS data: + SlimsBaseModel - to be subclassed for SLIMS pydantic models + UnitSpec - To be included in a type annotation of a Quantity field + +SlimsClient - Basic wrapper around slims-python-api client with convenience + methods and integration with SlimsBaseModel subtypes +""" + +import logging +from functools import lru_cache +from typing import Literal, Optional + +from slims.criteria import Criterion, conjunction, equals +from slims.internal import Record as SlimsRecord +from slims.slims import Slims, _SlimsApiException + +from aind_slims_api import config + +logger = logging.getLogger() + +# List of slims tables manually accessed, there are many more +SLIMSTABLES = Literal[ + "Project", + "Content", + "ContentEvent", + "Unit", + "Result", + "Test", + "User", + "Groups", +] + + +class SlimsClient: + """Wrapper around slims-python-api client with convenience methods""" + + def __init__(self, url=None, username=None, password=None): + """Create object and try to connect to database""" + self.url = url or config.slims_url + self.db: Optional[Slims] = None + + self.connect( + self.url, + username or config.slims_username, + password or config.slims_password.get_secret_value(), + ) + + def connect(self, url: str, username: str, password: str): + """Connect to the database""" + self.db = Slims( + "slims", + url, + username, + password, + ) + + def fetch( + self, + table: SLIMSTABLES, + *args, + sort: Optional[str | list[str]] = None, + start: Optional[int] = None, + end: Optional[int] = None, + **kwargs, + ) -> list[SlimsRecord]: + """Fetch from the SLIMS database + + Args: + table (str): SLIMS table to query + sort (str | list[str], optional): Fields to sort by; e.g. date + start (int, optional): The first row to return + end (int, optional): The last row to return + *args (Slims.criteria.Criterion): Optional criteria to apply + **kwargs (dict[str,str]): "field=value" filters + + Returns: + records (list[SlimsRecord] | None): Matching records, if any + """ + criteria = conjunction() + for arg in args: + if isinstance(arg, Criterion): + criteria.add(arg) + + for k, v in kwargs.items(): + criteria.add(equals(k, v)) + try: + records = self.db.fetch( + table, + criteria, + sort=sort, + start=start, + end=end, + ) + except _SlimsApiException as e: + # TODO: Add better error handling + # Let's just raise error for the time being + raise e + + return records + + @lru_cache(maxsize=None) + def fetch_pk(self, table: SLIMSTABLES, *args, **kwargs) -> int | None: + """SlimsClient.fetch but returns the pk of the first returned record""" + records = self.fetch(table, *args, **kwargs) + if len(records) > 0: + return records[0].pk() + else: + return None + + def fetch_user(self, user_name: str): + """Fetches a user by username""" + return self.fetch("User", user_userName=user_name) + + def add(self, table: SLIMSTABLES, data: dict): + """Add a SLIMS record to a given SLIMS table""" + record = self.db.add(table, data) + logger.info(f"SLIMS Add: {table}/{record.pk()}") + return record + + def update(self, table: SLIMSTABLES, pk: int, data: dict): + """Update a SLIMS record""" + record = self.db.fetch_by_pk(table, pk) + if record is None: + raise ValueError(f'No data in SLIMS "{table}" table for pk "{pk}"') + new_record = record.update(data) + logger.info(f"SLIMS Update: {table}/{pk}") + return new_record + + def rest_link(self, table: SLIMSTABLES, **kwargs): + """Construct a url link to a SLIMS table with arbitrary filters""" + base_url = f"{self.url}/rest/{table}" + queries = [f"?{k}={v}" for k, v in kwargs.items()] + return base_url + "".join(queries) diff --git a/src/aind_slims_api/mouse.py b/src/aind_slims_api/mouse.py new file mode 100644 index 0000000..b9f5aaa --- /dev/null +++ b/src/aind_slims_api/mouse.py @@ -0,0 +1,33 @@ +"""Contains a model for the mouse content, and a method for fetching it""" + +import logging +from typing import Optional + +from aind_slims_api.core import SlimsClient + +logger = logging.getLogger() + + +def fetch_mouse_content( + client: SlimsClient, + mouse_name: str, +) -> Optional[dict]: + """Fetches mouse information for a mouse with labtracks id {mouse_name}""" + mice = client.fetch( + "Content", + cntp_name="Mouse", + cntn_barCode=mouse_name, + ) + + if len(mice) > 0: + mouse_details = mice[0] + if len(mice) > 1: + logger.warning( + f"Warning, Multiple mice in SLIMS with barcode " + f"{mouse_name}, using pk={mouse_details.cntn_pk.value}" + ) + else: + logger.warning("Warning, Mouse not in SLIMS") + mouse_details = None + + return None if mouse_details is None else mouse_details.json_entity diff --git a/src/aind_slims_api/user.py b/src/aind_slims_api/user.py new file mode 100644 index 0000000..1e5ac46 --- /dev/null +++ b/src/aind_slims_api/user.py @@ -0,0 +1,33 @@ +"""Contains a model for a user, and a method for fetching it""" + +import logging +from typing import Optional + +from aind_slims_api.core import SlimsClient + +logger = logging.getLogger() + + +def fetch_user( + client: SlimsClient, + username: str, +) -> Optional[dict]: + """Fetches user information for a user with username {username}""" + users = client.fetch( + "User", + user_userName=username, + ) + + if len(users) > 0: + user_details = users[0] + if len(users) > 1: + logger.warning( + f"Warning, Multiple users in SLIMS with " + f"username {[u.json_entity for u in users]}, " + f"using pk={user_details.pk()}" + ) + else: + logger.warning("Warning, User not in SLIMS") + user_details = None + + return None if user_details is None else user_details.json_entity diff --git a/tests/resources/example_fetch_mouse_response.json b/tests/resources/example_fetch_mouse_response.json new file mode 100644 index 0000000..ff82db9 --- /dev/null +++ b/tests/resources/example_fetch_mouse_response.json @@ -0,0 +1,960 @@ +[ + { + "pk": 3038, + "tableName": "Content", + "columns": [ + { + "datatype": "FOREIGN_KEY", + "name": "cntn_fk_originalContent", + "title": "Original Content", + "position": 0, + "value": null, + "hidden": true, + "editable": true, + "foreignTable": "Content", + "displayValue": null, + "displayField": "cntn_originalContentBarCode", + "foreignDisplayColumn": "cntn_id" + }, + { + "datatype": "ENUM", + "name": "icon", + "title": "Icon", + "position": 1, + "value": "data_icons/mouse.png", + "hidden": false, + "editable": true, + "displayValue": "data_icons/mouse.png" + }, + { + "datatype": "ENUM", + "name": "containerIcon", + "title": "Container icon", + "position": 2, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "FOREIGN_KEY", + "name": "cntn_fk_category", + "title": "Category", + "position": 3, + "value": 45, + "hidden": false, + "editable": true, + "foreignTable": "ContentType", + "displayValue": "Subjects", + "displayField": "category_name", + "foreignDisplayColumn": "cntp_name" + }, + { + "datatype": "FOREIGN_KEY", + "name": "cntn_fk_contentType", + "title": "Type", + "position": 4, + "value": 5, + "hidden": false, + "editable": false, + "foreignTable": "ContentType", + "displayValue": "Mouse", + "displayField": "cntp_name", + "foreignDisplayColumn": "cntp_name" + }, + { + "datatype": "QUANTITY", + "name": "cntn_cf_volume", + "title": "Volume", + "position": 5, + "value": null, + "hidden": false, + "editable": true, + "unit": "ml" + }, + { + "datatype": "STRING", + "name": "cntn_barCode", + "title": "Barcode", + "position": 6, + "value": "123456", + "hidden": false, + "editable": false + }, + { + "datatype": "FOREIGN_KEY", + "name": "cntn_fk_containerContentType", + "title": "Container type", + "position": 7, + "value": null, + "hidden": false, + "editable": true, + "foreignTable": "ContentType", + "displayValue": null, + "displayField": "container_name", + "foreignDisplayColumn": "cntp_name" + }, + { + "datatype": "FOREIGN_KEY", + "name": "cntn_fk_product", + "title": "Product", + "position": 8, + "value": null, + "hidden": true, + "editable": true, + "foreignTable": "Product", + "displayValue": null, + "displayField": "prdc_name", + "foreignDisplayColumn": "prdc_name" + }, + { + "datatype": "QUANTITY", + "name": "cntn_cf_baselineWeight", + "title": "Baseline Weight", + "position": 9, + "value": 25.2, + "hidden": false, + "editable": true, + "unit": "g" + }, + { + "datatype": "QUANTITY", + "name": "cntn_cf_mass", + "title": "Mass", + "position": 10, + "value": null, + "hidden": true, + "editable": true, + "unit": "g" + }, + { + "datatype": "BOOLEAN", + "name": "cntn_cf_waterRestricted", + "title": "Water Restricted", + "position": 11, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "FLOAT", + "name": "cntn_dilutionFactor", + "title": "Dilution Factor", + "position": 12, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "cntn_id", + "title": "ID", + "position": 13, + "value": "123456", + "hidden": false, + "editable": false + }, + { + "datatype": "STRING", + "name": "cntn_cf_contactPerson", + "title": "Contact Person", + "position": 14, + "value": "PersonB", + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "cntn_cf_scientificPointOfContact", + "title": "Scientific Point of Contact", + "position": 15, + "value": "PersonC", + "hidden": false, + "editable": true + }, + { + "datatype": "FOREIGN_KEY", + "name": "cntn_fk_user", + "title": "User", + "position": 16, + "value": null, + "hidden": false, + "editable": true, + "foreignTable": "User", + "displayValue": null, + "displayField": "user_userName", + "foreignDisplayColumn": "user_userName" + }, + { + "datatype": "FLOAT", + "name": "cntn_quantity", + "title": "Quantity", + "position": 17, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "cntn_unit", + "title": "Unit", + "position": 18, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "ENUM", + "name": "cntn_status", + "title": "Status", + "position": 19, + "value": "10", + "hidden": false, + "editable": false, + "displayValue": "Pending" + }, + { + "datatype": "FOREIGN_KEY", + "name": "cntn_fk_status", + "title": "Status", + "position": 20, + "value": 28, + "hidden": false, + "editable": true, + "foreignTable": "Status", + "displayValue": "Pending", + "displayField": "stts_name", + "foreignDisplayColumn": "stts_name" + }, + { + "datatype": "FOREIGN_KEY", + "name": "cntn_fk_location", + "title": "Location", + "position": 21, + "value": null, + "hidden": false, + "editable": true, + "foreignTable": "Location", + "displayValue": null, + "displayField": "lctn_name", + "foreignDisplayColumn": "lctn_name" + }, + { + "datatype": "FOREIGN_KEY", + "name": "cntn_fk_location_recursive", + "title": "Location (including sublocations)", + "position": 22, + "value": null, + "hidden": false, + "editable": true, + "foreignTable": "Location", + "displayValue": null, + "displayField": "lctn_name", + "foreignDisplayColumn": "lctn_name" + }, + { + "datatype": "STRING", + "name": "locationPath", + "title": "Location path", + "position": 23, + "value": null, + "hidden": false, + "editable": false + }, + { + "datatype": "FOREIGN_KEY", + "name": "cntn_fk_disease", + "title": "Disease", + "position": 24, + "value": null, + "hidden": true, + "editable": true, + "foreignTable": "Disease", + "displayValue": null, + "displayField": "diss_name", + "foreignDisplayColumn": "diss_name" + }, + { + "datatype": "FOREIGN_KEY", + "name": "cntn_fk_group", + "title": "Group", + "position": 25, + "value": null, + "hidden": false, + "editable": true, + "foreignTable": "Groups", + "displayValue": null, + "displayField": "grps_groupName", + "foreignDisplayColumn": "grps_groupName" + }, + { + "datatype": "FOREIGN_KEY", + "name": "cntn_fk_provider", + "title": "Provider", + "position": 26, + "value": null, + "hidden": true, + "editable": true, + "foreignTable": "Provider", + "displayValue": null, + "displayField": "prvd_name", + "foreignDisplayColumn": "prvd_name" + }, + { + "datatype": "STRING", + "name": "cntn_position_row", + "title": "Located at row", + "position": 27, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "cntn_position_column", + "title": "Located at column", + "position": 28, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "DATE", + "name": "cntn_cf_dateOfBirth", + "title": "Date of birth", + "position": 29, + "value": 1715774400000, + "hidden": false, + "editable": true, + "dateFormat": "MM/dd/yyyy", + "subType": "date", + "timeZone": "America/Los_Angeles" + }, + { + "datatype": "FOREIGN_KEY", + "name": "cntn_cf_fk_fundingCode", + "title": "Project ID", + "position": 30, + "value": 1385, + "hidden": false, + "editable": true, + "foreignTable": "ReferenceDataRecord", + "displayValue": "Learning mFISH - V1 omFISH", + "displayField": "cntn_cf_fk_fundingCode_display", + "foreignDisplayColumn": "rdrc_name" + }, + { + "datatype": "STRING", + "name": "cntn_cf_genotype", + "title": "Full Genotype", + "position": 31, + "value": "SSt", + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "cntn_cf_iacucProtocol", + "title": "IACUC Protocol", + "position": 32, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "cntn_cf_labtracksGroup", + "title": "LabTracks Group", + "position": 33, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "cntn_cf_labtracksId", + "title": "Labtracks ID", + "position": 34, + "value": "123456", + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "cntn_cf_lightcycle", + "title": "LightCycle", + "position": 35, + "value": "Reverse Light Cycle (9 PM-9 AM light)", + "hidden": false, + "editable": true + }, + { + "datatype": "INTEGER", + "name": "cntn_cf_mouseAge", + "title": "Mouse age (mo)", + "position": 36, + "value": 29, + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "cntn_cf_parentBarcode", + "title": "Parent barcode", + "position": 37, + "value": "N/A - no parent", + "hidden": false, + "editable": true + }, + { + "datatype": "ENUM", + "name": "cntn_cf_sex", + "title": "Sex", + "position": 38, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "FOREIGN_KEY", + "name": "cntn_fk_product_strain", + "title": "Product (filtering without version)", + "position": 39, + "value": null, + "hidden": false, + "editable": true, + "foreignTable": "Product", + "displayValue": null, + "displayField": "prdc_name", + "foreignDisplayColumn": "prdc_name" + }, + { + "datatype": "STRING", + "name": "relationToProband", + "title": "Relation to proband", + "position": 40, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "FOREIGN_KEY", + "name": "father", + "title": "Father", + "position": 41, + "value": null, + "hidden": false, + "editable": true, + "foreignTable": "Content", + "displayValue": null, + "displayField": "father_display", + "foreignDisplayColumn": "cntn_id" + }, + { + "datatype": "FOREIGN_KEY", + "name": "mother", + "title": "Mother", + "position": 42, + "value": null, + "hidden": false, + "editable": true, + "foreignTable": "Content", + "displayValue": null, + "displayField": "mother_display", + "foreignDisplayColumn": "cntn_id" + }, + { + "datatype": "INTEGER", + "name": "derivedCount", + "title": "Derivation count", + "position": 43, + "value": 1, + "hidden": false, + "editable": true + }, + { + "datatype": "INTEGER", + "name": "ingredientCount", + "title": "Ingredient count", + "position": 44, + "value": 0, + "hidden": false, + "editable": true + }, + { + "datatype": "INTEGER", + "name": "mixCount", + "title": "Mix count", + "position": 45, + "value": 0, + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "cntn_createdBy", + "title": "Created by", + "position": 46, + "value": "PersonD", + "hidden": false, + "editable": true + }, + { + "datatype": "DATE", + "name": "cntn_createdOn", + "title": "Created on", + "position": 47, + "value": 1715797617414, + "hidden": false, + "editable": true, + "dateFormat": "MM/dd/yyyy HH:mm:ss", + "subType": "datetime", + "timeZone": "America/Los_Angeles" + }, + { + "datatype": "STRING", + "name": "cntn_modifiedBy", + "title": "Modified by", + "position": 48, + "value": "PersonE", + "hidden": false, + "editable": true + }, + { + "datatype": "DATE", + "name": "cntn_modifiedOn", + "title": "Modified on", + "position": 49, + "value": 1716928135809, + "hidden": false, + "editable": true, + "dateFormat": "MM/dd/yyyy HH:mm:ss", + "subType": "datetime", + "timeZone": "America/Los_Angeles" + }, + { + "datatype": "FOREIGN_KEY", + "name": "flags", + "title": "Flags", + "position": 50, + "value": null, + "hidden": false, + "editable": true, + "foreignTable": "ContentEventType", + "displayValue": null, + "displayField": "cnvt_name", + "foreignDisplayColumn": "cnvt_name" + }, + { + "datatype": "FOREIGN_KEY", + "name": "previousFlags", + "title": "Previous flags", + "position": 51, + "value": null, + "hidden": false, + "editable": true, + "foreignTable": "ContentEventType", + "displayValue": null, + "displayField": "cnvt_name", + "foreignDisplayColumn": "cnvt_name" + }, + { + "datatype": "STRING", + "name": "cntn_externalId", + "title": "External Id", + "position": 52, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "lctn_name", + "title": "Name", + "position": 53, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "isNaFilter", + "title": "Field is N/A", + "position": 54, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "isNotNaFilter", + "title": "Field is not N/A", + "position": 55, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "BOOLEAN", + "name": "cntp_containerType", + "title": "Is a container type", + "position": 56, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "mother_display", + "title": "mother_display", + "position": 57, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "INTEGER", + "name": "xprs_input", + "title": "xprs_input", + "position": 58, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "BOOLEAN", + "name": "cntp_canEnrollInStudy", + "title": "Can enroll in a study", + "position": 59, + "value": true, + "hidden": true, + "editable": true + }, + { + "datatype": "INTEGER", + "name": "cntn_pk", + "title": "cntn_pk", + "position": 60, + "value": 3038, + "hidden": true, + "editable": true + }, + { + "datatype": "INTEGER", + "name": "attachmentCount", + "title": "attachmentCount", + "position": 61, + "value": 0, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "lctn_rows", + "title": "Rows", + "position": 62, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "BOOLEAN", + "name": "cntp_slimsGeneratesBarcode", + "title": "SLIMS generates a barcode for content of this type", + "position": 63, + "value": false, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "prvd_name", + "title": "Name", + "position": 64, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "BOOLEAN", + "name": "lctp_positionLess", + "title": "Don't require positions", + "position": 65, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "BOOLEAN", + "name": "cntp_useBarcodeAsId", + "title": "Use barcode as id", + "position": 66, + "value": true, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "lctn_barCode", + "title": "Barcode", + "position": 67, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "diss_name", + "title": "Name", + "position": 68, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "lctn_columns", + "title": "Columns", + "position": 69, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "grps_groupName", + "title": "Name", + "position": 70, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "stts_name", + "title": "Name", + "position": 71, + "value": "Pending", + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "cntp_name", + "title": "Name", + "position": 72, + "value": "Mouse", + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "user_userName", + "title": "User name", + "position": 73, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "father_display", + "title": "father_display", + "position": 74, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "cntn_originalContentBarCode", + "title": "Original content barcode", + "position": 75, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "sorc_name", + "title": "Name", + "position": 76, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "INTEGER", + "name": "xprs_output", + "title": "xprs_output", + "position": 77, + "value": null, + "hidden": true, + "editable": true + } + ], + "canUpdate": true, + "canDelete": true, + "links": [ + { + "rel": "self", + "href": "http://fake_url/rest/Content/3038" + }, + { + "rel": "cntn_fk_category", + "href": "http://fake_url/rest/ContentType/45" + }, + { + "rel": "cntn_fk_contentType", + "href": "http://fake_url/rest/ContentType/5" + }, + { + "rel": "cntn_fk_status", + "href": "http://fake_url/rest/Status/28" + }, + { + "rel": "cntn_cf_fk_fundingCode", + "href": "http://fake_url/rest/ReferenceDataRecord/1385" + }, + { + "rel": "-prjo_fk_content", + "href": "http://fake_url/rest/PrinterJob?prjo_fk_content=3038" + }, + { + "rel": "-flfm_fk_content", + "href": "http://fake_url/rest/Fulfillment?flfm_fk_content=3038" + }, + { + "rel": "-cnvn_fk_content", + "href": "http://fake_url/rest/ContentEvent?cnvn_fk_content=3038" + }, + { + "rel": "-cnst_fk_content", + "href": "http://fake_url/rest/ContentStudy?cnst_fk_content=3038" + }, + { + "rel": "-rlrn_fk_content", + "href": "http://fake_url/rest/RuleRun?rlrn_fk_content=3038" + }, + { + "rel": "-qtus_fk_content", + "href": "http://fake_url/rest/QuantityUsage?qtus_fk_content=3038" + }, + { + "rel": "-ccpt_fk_content", + "href": "http://fake_url/rest/Occupation?ccpt_fk_content=3038" + }, + { + "rel": "-corl_fk_from", + "href": "http://fake_url/rest/ContentRelation?corl_fk_from=3038" + }, + { + "rel": "-corl_fk_to", + "href": "http://fake_url/rest/ContentRelation?corl_fk_to=3038" + }, + { + "rel": "-ssnc_fk_content", + "href": "http://fake_url/rest/SavedSelectionContent?ssnc_fk_content=3038" + }, + { + "rel": "-rdcn_fk_content", + "href": "http://fake_url/rest/OrderContent?rdcn_fk_content=3038" + }, + { + "rel": "-rqst_fk_content", + "href": "http://fake_url/rest/Request?rqst_fk_content=3038" + }, + { + "rel": "-cntn_pk", + "href": "http://fake_url/rest/Schedule?cntn_pk=3038" + }, + { + "rel": "-qlng_fk_content", + "href": "http://fake_url/rest/QueueElementIngredient?qlng_fk_content=3038" + }, + { + "rel": "-qlag_fk_content", + "href": "http://fake_url/rest/QueueElementAggregator?qlag_fk_content=3038" + }, + { + "rel": "-tpnr_fk_batch", + "href": "http://fake_url/rest/TimepointEnrollment?tpnr_fk_batch=3038" + }, + { + "rel": "-ssnr_fk_content", + "href": "http://fake_url/rest/StabilityStudyEnrollment?ssnr_fk_content=3038" + }, + { + "rel": "-qulm_fk_content", + "href": "http://fake_url/rest/QueueElement?qulm_fk_content=3038" + }, + { + "rel": "-xrsc_fk_content", + "href": "http://fake_url/rest/ExperimentRunStepContent?xrsc_fk_content=3038" + }, + { + "rel": "-kitc_fk_content", + "href": "http://fake_url/rest/KitContent?kitc_fk_content=3038" + }, + { + "rel": "-nrcn_fk_content", + "href": "http://fake_url/rest/InstrumentRunContent?nrcn_fk_content=3038" + }, + { + "rel": "-rslt_fk_content", + "href": "http://fake_url/rest/Result?rslt_fk_content=3038" + }, + { + "rel": "-rslt_cf_fk_primaryAntibody", + "href": "http://fake_url/rest/Result?rslt_cf_fk_primaryAntibody=3038" + }, + { + "rel": "-rslt_cf_fk_secondaryAntibody", + "href": "http://fake_url/rest/Result?rslt_cf_fk_secondaryAntibody=3038" + }, + { + "rel": "-rslt_cf_fk_virus", + "href": "http://fake_url/rest/Result?rslt_cf_fk_virus=3038" + }, + { + "rel": "-rslt_fk_batch", + "href": "http://fake_url/rest/Result?rslt_fk_batch=3038" + }, + { + "rel": "-ordr_cf_fk_dye", + "href": "http://fake_url/rest/Order?ordr_cf_fk_dye=3038" + }, + { + "rel": "-ordr_fk_batch", + "href": "http://fake_url/rest/Order?ordr_fk_batch=3038" + }, + { + "rel": "-cntn_fk_originalContent", + "href": "http://fake_url/rest/Content?cntn_fk_originalContent=3038" + }, + { + "rel": "-cntn_cf_fk_primaryStain", + "href": "http://fake_url/rest/Content?cntn_cf_fk_primaryStain=3038" + }, + { + "rel": "-cntn_cf_fk_secondaryStain", + "href": "http://fake_url/rest/Content?cntn_cf_fk_secondaryStain=3038" + }, + { + "rel": "-father", + "href": "http://fake_url/rest/Content?father=3038" + }, + { + "rel": "-mother", + "href": "http://fake_url/rest/Content?mother=3038" + }, + { + "rel": "-pptp_fk_content", + "href": "http://fake_url/rest/PlatePositionOutput?pptp_fk_content=3038" + }, + { + "rel": "attachments", + "href": "http://fake_url/rest/attachment/Content/3038" + } + ] + } +] \ No newline at end of file diff --git a/tests/resources/example_fetch_unit_response.json b/tests/resources/example_fetch_unit_response.json new file mode 100644 index 0000000..e14783d --- /dev/null +++ b/tests/resources/example_fetch_unit_response.json @@ -0,0 +1,472 @@ +[ + { + "pk": 31, + "tableName": "Unit", + "columns": [ + { + "datatype": "STRING", + "name": "unit_abbreviation", + "title": "Abbreviation", + "position": 0, + "value": "pm^3", + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "unit_name", + "title": "Name", + "position": 1, + "value": "picometer^3", + "hidden": false, + "editable": true + }, + { + "datatype": "FOREIGN_KEY", + "name": "unit_fk_originalUnit", + "title": "Original unit", + "position": 2, + "value": null, + "hidden": false, + "editable": true, + "foreignTable": "Unit", + "displayValue": null, + "displayField": "originalUnitAbbreviation", + "foreignDisplayColumn": "unit_abbreviation" + }, + { + "datatype": "STRING", + "name": "unit_fromOriginalUnitGroovy", + "title": "Value expression to convert from original unit", + "position": 3, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "unit_toOriginalUnitGroovy", + "title": "Value expression to convert to original unit", + "position": 4, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "ENUM", + "name": "unit_prefix", + "title": "Prefix", + "position": 5, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "unit_subDimension", + "title": "Sub dimension", + "position": 6, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "ENUM", + "name": "unit_type", + "title": "Type", + "position": 7, + "value": "COMPOUND", + "hidden": false, + "editable": true, + "displayValue": "Compound" + }, + { + "datatype": "FOREIGN_KEY", + "name": "unit_fk_dimension", + "title": "Dimension", + "position": 8, + "value": 5, + "hidden": false, + "editable": true, + "foreignTable": "Dimension", + "displayValue": "Volume", + "displayField": "dmns_name", + "foreignDisplayColumn": "dmns_name" + }, + { + "datatype": "STRING", + "name": "unit_createdBy", + "title": "Created by", + "position": 9, + "value": "admin", + "hidden": false, + "editable": true + }, + { + "datatype": "DATE", + "name": "unit_createdOn", + "title": "Created on", + "position": 10, + "value": 1668796217413, + "hidden": false, + "editable": true, + "dateFormat": "MM/dd/yyyy HH:mm:ss", + "subType": "datetime", + "timeZone": "America/Los_Angeles" + }, + { + "datatype": "STRING", + "name": "unit_modifiedBy", + "title": "Modified by", + "position": 11, + "value": "PersonA", + "hidden": false, + "editable": true + }, + { + "datatype": "DATE", + "name": "unit_modifiedOn", + "title": "Modified on", + "position": 12, + "value": 1674504031658, + "hidden": false, + "editable": true, + "dateFormat": "MM/dd/yyyy HH:mm:ss", + "subType": "datetime", + "timeZone": "America/Los_Angeles" + }, + { + "datatype": "INTEGER", + "name": "unit_pk", + "title": "unit_pk", + "position": 13, + "value": 31, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "dmns_name", + "title": "Dimension", + "position": 14, + "value": "Volume", + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "unit_seqNo", + "title": "Sequence number", + "position": 15, + "value": "-33.0000000000000000000", + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "unit_subdimension", + "title": "unit_subdimension", + "position": 16, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "BOOLEAN", + "name": "unit_suspicious", + "title": "Suspicious", + "position": 17, + "value": false, + "hidden": true, + "editable": true + } + ], + "canUpdate": true, + "canDelete": true, + "links": [ + { + "rel": "self", + "href": "http://fake_url/rest/Unit/31" + }, + { + "rel": "unit_fk_dimension", + "href": "http://fake_url/rest/Dimension/5" + }, + { + "rel": "-tftr_fk_unit", + "href": "http://fake_url/rest/TableFieldTypeRestriction?tftr_fk_unit=31" + }, + { + "rel": "-tftr_fk_lowerUnit", + "href": "http://fake_url/rest/TableFieldTypeRestriction?tftr_fk_lowerUnit=31" + }, + { + "rel": "-tftr_fk_upperUnit", + "href": "http://fake_url/rest/TableFieldTypeRestriction?tftr_fk_upperUnit=31" + }, + { + "rel": "-tbfl_fk_unit", + "href": "http://fake_url/rest/Field?tbfl_fk_unit=31" + }, + { + "rel": "-tbfl_fk_lowerUnit", + "href": "http://fake_url/rest/Field?tbfl_fk_lowerUnit=31" + }, + { + "rel": "-tbfl_fk_upperUnit", + "href": "http://fake_url/rest/Field?tbfl_fk_upperUnit=31" + }, + { + "rel": "-unit_fk_originalUnit", + "href": "http://fake_url/rest/Unit?unit_fk_originalUnit=31" + }, + { + "rel": "-cmnl_fk_unit", + "href": "http://fake_url/rest/CompoundUnitLink?cmnl_fk_unit=31" + }, + { + "rel": "-cmnl_fk_compoundUnit", + "href": "http://fake_url/rest/CompoundUnitLink?cmnl_fk_compoundUnit=31" + }, + { + "rel": "attachments", + "href": "http://fake_url/rest/attachment/Unit/31" + } + ] + }, + { + "pk": 15, + "tableName": "Unit", + "columns": [ + { + "datatype": "STRING", + "name": "unit_abbreviation", + "title": "Abbreviation", + "position": 0, + "value": "pm^2", + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "unit_name", + "title": "Name", + "position": 1, + "value": "picometer^2", + "hidden": false, + "editable": true + }, + { + "datatype": "FOREIGN_KEY", + "name": "unit_fk_originalUnit", + "title": "Original unit", + "position": 2, + "value": null, + "hidden": false, + "editable": true, + "foreignTable": "Unit", + "displayValue": null, + "displayField": "originalUnitAbbreviation", + "foreignDisplayColumn": "unit_abbreviation" + }, + { + "datatype": "STRING", + "name": "unit_fromOriginalUnitGroovy", + "title": "Value expression to convert from original unit", + "position": 3, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "unit_toOriginalUnitGroovy", + "title": "Value expression to convert to original unit", + "position": 4, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "ENUM", + "name": "unit_prefix", + "title": "Prefix", + "position": 5, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "unit_subDimension", + "title": "Sub dimension", + "position": 6, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "ENUM", + "name": "unit_type", + "title": "Type", + "position": 7, + "value": "COMPOUND", + "hidden": false, + "editable": true, + "displayValue": "Compound" + }, + { + "datatype": "FOREIGN_KEY", + "name": "unit_fk_dimension", + "title": "Dimension", + "position": 8, + "value": 4, + "hidden": false, + "editable": true, + "foreignTable": "Dimension", + "displayValue": "Surface", + "displayField": "dmns_name", + "foreignDisplayColumn": "dmns_name" + }, + { + "datatype": "STRING", + "name": "unit_createdBy", + "title": "Created by", + "position": 9, + "value": "admin", + "hidden": false, + "editable": true + }, + { + "datatype": "DATE", + "name": "unit_createdOn", + "title": "Created on", + "position": 10, + "value": 1668796214042, + "hidden": false, + "editable": true, + "dateFormat": "MM/dd/yyyy HH:mm:ss", + "subType": "datetime", + "timeZone": "America/Los_Angeles" + }, + { + "datatype": "STRING", + "name": "unit_modifiedBy", + "title": "Modified by", + "position": 11, + "value": "PersonA", + "hidden": false, + "editable": true + }, + { + "datatype": "DATE", + "name": "unit_modifiedOn", + "title": "Modified on", + "position": 12, + "value": 1674504029413, + "hidden": false, + "editable": true, + "dateFormat": "MM/dd/yyyy HH:mm:ss", + "subType": "datetime", + "timeZone": "America/Los_Angeles" + }, + { + "datatype": "INTEGER", + "name": "unit_pk", + "title": "unit_pk", + "position": 13, + "value": 15, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "dmns_name", + "title": "Dimension", + "position": 14, + "value": "Surface", + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "unit_seqNo", + "title": "Sequence number", + "position": 15, + "value": "-24.0000000000000000000", + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "unit_subdimension", + "title": "unit_subdimension", + "position": 16, + "value": null, + "hidden": true, + "editable": true + }, + { + "datatype": "BOOLEAN", + "name": "unit_suspicious", + "title": "Suspicious", + "position": 17, + "value": false, + "hidden": true, + "editable": true + } + ], + "canUpdate": true, + "canDelete": true, + "links": [ + { + "rel": "self", + "href": "http://fake_url/rest/Unit/15" + }, + { + "rel": "unit_fk_dimension", + "href": "http://fake_url/rest/Dimension/4" + }, + { + "rel": "-tftr_fk_unit", + "href": "http://fake_url/rest/TableFieldTypeRestriction?tftr_fk_unit=15" + }, + { + "rel": "-tftr_fk_lowerUnit", + "href": "http://fake_url/rest/TableFieldTypeRestriction?tftr_fk_lowerUnit=15" + }, + { + "rel": "-tftr_fk_upperUnit", + "href": "http://fake_url/rest/TableFieldTypeRestriction?tftr_fk_upperUnit=15" + }, + { + "rel": "-tbfl_fk_unit", + "href": "http://fake_url/rest/Field?tbfl_fk_unit=15" + }, + { + "rel": "-tbfl_fk_lowerUnit", + "href": "http://fake_url/rest/Field?tbfl_fk_lowerUnit=15" + }, + { + "rel": "-tbfl_fk_upperUnit", + "href": "http://fake_url/rest/Field?tbfl_fk_upperUnit=15" + }, + { + "rel": "-unit_fk_originalUnit", + "href": "http://fake_url/rest/Unit?unit_fk_originalUnit=15" + }, + { + "rel": "-cmnl_fk_unit", + "href": "http://fake_url/rest/CompoundUnitLink?cmnl_fk_unit=15" + }, + { + "rel": "-cmnl_fk_compoundUnit", + "href": "http://fake_url/rest/CompoundUnitLink?cmnl_fk_compoundUnit=15" + }, + { + "rel": "attachments", + "href": "http://fake_url/rest/attachment/Unit/15" + } + ] + } +] \ No newline at end of file diff --git a/tests/resources/example_fetch_user_response.json b/tests/resources/example_fetch_user_response.json new file mode 100644 index 0000000..09b4a6f --- /dev/null +++ b/tests/resources/example_fetch_user_response.json @@ -0,0 +1,480 @@ +[ + { + "pk": 8, + "tableName": "User", + "columns": [ + { + "datatype": "FOREIGN_KEY", + "name": "user_fk_avatar", + "title": "Avatar", + "position": 0, + "value": null, + "hidden": false, + "editable": true, + "foreignTable": "Avatar", + "displayValue": null, + "displayField": "avat_name", + "foreignDisplayColumn": "avat_name" + }, + { + "datatype": "STRING", + "name": "user_userName", + "title": "User name", + "position": 1, + "value": "PersonA", + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "user_uniqueIdentifier", + "title": "Unique identifier", + "position": 2, + "value": "user_persona", + "hidden": false, + "editable": true + }, + { + "datatype": "ENUM", + "name": "user_authenticationMethod", + "title": "Authentication method", + "position": 3, + "value": "LOCAL", + "hidden": false, + "editable": true, + "displayValue": "Local Authentication" + }, + { + "datatype": "BOOLEAN", + "name": "user_passwordDirty", + "title": "Require password change on next login", + "position": 4, + "value": false, + "hidden": false, + "editable": true + }, + { + "datatype": "FOREIGN_KEY", + "name": "user_fk_role", + "title": "Role", + "position": 5, + "value": 2, + "hidden": false, + "editable": true, + "foreignTable": "Role", + "displayValue": "Administrator", + "displayField": "role_name", + "foreignDisplayColumn": "role_name" + }, + { + "datatype": "ENUM", + "name": "user_license", + "title": "License type", + "position": 6, + "value": "NAMED", + "hidden": false, + "editable": true, + "displayValue": "Named" + }, + { + "datatype": "STRING", + "name": "user_lastName", + "title": "Last name", + "position": 7, + "value": "A", + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "user_firstName", + "title": "First name", + "position": 8, + "value": "Person", + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "user_fullName", + "title": "Full name", + "position": 9, + "value": "Person A", + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "user_email", + "title": "E-mail", + "position": 10, + "value": "person.a@fake_url", + "hidden": false, + "editable": true + }, + { + "datatype": "ENUM", + "name": "user_locale", + "title": "Language", + "position": 11, + "value": "en", + "hidden": false, + "editable": true, + "displayValue": "English" + }, + { + "datatype": "BOOLEAN", + "name": "user_browserNotifications", + "title": "Enable browser notifications", + "position": 12, + "value": true, + "hidden": false, + "editable": true + }, + { + "datatype": "ENUM", + "name": "user_notificationLevel", + "title": "Display notifications with this level or higher", + "position": 13, + "value": "INFO", + "hidden": false, + "editable": true, + "displayValue": "Info" + }, + { + "datatype": "STRING", + "name": "user_ecm3Account", + "title": "ECM Account", + "position": 14, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "user_ecm3Domain", + "title": "ECM Domain", + "position": 15, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "user_ecm3User", + "title": "ECM Username", + "position": 16, + "value": null, + "hidden": false, + "editable": true + }, + { + "datatype": "BOOLEAN", + "name": "user_active", + "title": "Active", + "position": 17, + "value": true, + "hidden": false, + "editable": true + }, + { + "datatype": "BOOLEAN", + "name": "user_locked", + "title": "Locked", + "position": 18, + "value": false, + "hidden": false, + "editable": true + }, + { + "datatype": "STRING", + "name": "user_createdBy", + "title": "Created by", + "position": 19, + "value": "PersonB", + "hidden": false, + "editable": true + }, + { + "datatype": "DATE", + "name": "user_createdOn", + "title": "Created on", + "position": 20, + "value": 1670355126130, + "hidden": false, + "editable": true, + "dateFormat": "MM/dd/yyyy HH:mm:ss", + "subType": "datetime", + "timeZone": "America/Los_Angeles" + }, + { + "datatype": "STRING", + "name": "user_modifiedBy", + "title": "Modified by", + "position": 21, + "value": "PersonA", + "hidden": false, + "editable": true + }, + { + "datatype": "DATE", + "name": "user_modifiedOn", + "title": "Modified on", + "position": 22, + "value": 1717788094709, + "hidden": false, + "editable": true, + "dateFormat": "MM/dd/yyyy HH:mm:ss", + "subType": "datetime", + "timeZone": "America/Los_Angeles" + }, + { + "datatype": "INTEGER", + "name": "user_fk_perspective", + "title": "Layout", + "position": 24, + "value": 42, + "hidden": true, + "editable": true + }, + { + "datatype": "INTEGER", + "name": "attachmentCount", + "title": "attachmentCount", + "position": 25, + "value": 0, + "hidden": true, + "editable": true + }, + { + "datatype": "INTEGER", + "name": "user_pk", + "title": "user_pk", + "position": 26, + "value": 8, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "user_salt", + "title": "Salt", + "position": 27, + "value": "+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=", + "hidden": true, + "editable": true + }, + { + "datatype": "INTEGER", + "name": "user_failedLoginAttempts", + "title": "Failed login attempts", + "position": 28, + "value": 0, + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "role_name", + "title": "Name", + "position": 29, + "value": "Administrator", + "hidden": true, + "editable": true + }, + { + "datatype": "STRING", + "name": "user_timezone", + "title": "Timezone", + "position": 30, + "value": "America/Los_Angeles", + "hidden": true, + "editable": true + } + ], + "canUpdate": true, + "canDelete": true, + "links": [ + { + "rel": "self", + "href": "http://fake_url/rest/User/8" + }, + { + "rel": "user_fk_role", + "href": "http://fake_url/rest/Role/2" + }, + { + "rel": "-wdgt_fk_user", + "href": "http://fake_url/rest/Widget?wdgt_fk_user=8" + }, + { + "rel": "-prdc_fk_user", + "href": "http://fake_url/rest/Product?prdc_fk_user=8" + }, + { + "rel": "-prjo_fk_user", + "href": "http://fake_url/rest/PrinterJob?prjo_fk_user=8" + }, + { + "rel": "-wrfl_fk_user", + "href": "http://fake_url/rest/Workflow?wrfl_fk_user=8" + }, + { + "rel": "-crfr_fk_user", + "href": "http://fake_url/rest/CaseReportForm?crfr_fk_user=8" + }, + { + "rel": "-prjc_fk_user", + "href": "http://fake_url/rest/Project?prjc_fk_user=8" + }, + { + "rel": "-cnvn_fk_user", + "href": "http://fake_url/rest/ContentEvent?cnvn_fk_user=8" + }, + { + "rel": "-note_fk_user", + "href": "http://fake_url/rest/Note?note_fk_user=8" + }, + { + "rel": "-qtus_fk_user", + "href": "http://fake_url/rest/QuantityUsage?qtus_fk_user=8" + }, + { + "rel": "-evnt_fk_user", + "href": "http://fake_url/rest/Event?evnt_fk_user=8" + }, + { + "rel": "-wrkl_fk_user", + "href": "http://fake_url/rest/WorkList?wrkl_fk_user=8" + }, + { + "rel": "-dhbd_fk_user", + "href": "http://fake_url/rest/Dashboard?dhbd_fk_user=8" + }, + { + "rel": "-attm_fk_user", + "href": "http://fake_url/rest/Attachment?attm_fk_user=8" + }, + { + "rel": "-attm_fk_ecm3UploadUser", + "href": "http://fake_url/rest/Attachment?attm_fk_ecm3UploadUser=8" + }, + { + "rel": "-nfsr_fk_user", + "href": "http://fake_url/rest/NotificationUser?nfsr_fk_user=8" + }, + { + "rel": "-rprt_fk_user", + "href": "http://fake_url/rest/Report?rprt_fk_user=8" + }, + { + "rel": "-rqst_fk_assignee", + "href": "http://fake_url/rest/Request?rqst_fk_assignee=8" + }, + { + "rel": "-lnwo_fk_user", + "href": "http://fake_url/rest/ElnWorkedOn?lnwo_fk_user=8" + }, + { + "rel": "-userPk", + "href": "http://fake_url/rest/ELNTree?userPk=8" + }, + { + "rel": "-lctn_fk_user", + "href": "http://fake_url/rest/Location?lctn_fk_user=8" + }, + { + "rel": "-rdrc_fk_user", + "href": "http://fake_url/rest/ReferenceDataRecord?rdrc_fk_user=8" + }, + { + "rel": "-srgr_fk_user", + "href": "http://fake_url/rest/UserGroup?srgr_fk_user=8" + }, + { + "rel": "-rtrg_fk_user", + "href": "http://fake_url/rest/RightsUserGroup?rtrg_fk_user=8" + }, + { + "rel": "-timr_fk_user", + "href": "http://fake_url/rest/TimerRun?timr_fk_user=8" + }, + { + "rel": "-xprn_fk_user", + "href": "http://fake_url/rest/ExperimentRun?xprn_fk_user=8" + }, + { + "rel": "-bgjb_fk_user", + "href": "http://fake_url/rest/BackgroundJob?bgjb_fk_user=8" + }, + { + "rel": "-xprs_fk_user", + "href": "http://fake_url/rest/ExperimentRunStep?xprs_fk_user=8" + }, + { + "rel": "-chlg_fk_user", + "href": "http://fake_url/rest/Changelog?chlg_fk_user=8" + }, + { + "rel": "-stud_fk_user", + "href": "http://fake_url/rest/Study?stud_fk_user=8" + }, + { + "rel": "-stop_fk_user", + "href": "http://fake_url/rest/Sop?stop_fk_user=8" + }, + { + "rel": "-rslt_cf_fk_surgeon", + "href": "http://fake_url/rest/Result?rslt_cf_fk_surgeon=8" + }, + { + "rel": "-rslt_cf_fk_behaviorTrainer", + "href": "http://fake_url/rest/Result?rslt_cf_fk_behaviorTrainer=8" + }, + { + "rel": "-rslt_fk_filler", + "href": "http://fake_url/rest/Result?rslt_fk_filler=8" + }, + { + "rel": "-prsp_fk_user", + "href": "http://fake_url/rest/PerspectiveComponent?prsp_fk_user=8" + }, + { + "rel": "-ordr_fk_user", + "href": "http://fake_url/rest/Order?ordr_fk_user=8" + }, + { + "rel": "-ordr_cf_fk_histologyRequester", + "href": "http://fake_url/rest/Order?ordr_cf_fk_histologyRequester=8" + }, + { + "rel": "-xprm_fk_user", + "href": "http://fake_url/rest/Experiment?xprm_fk_user=8" + }, + { + "rel": "-cntn_fk_user", + "href": "http://fake_url/rest/Content?cntn_fk_user=8" + }, + { + "rel": "-cntn_cf_fk_histologyRequester", + "href": "http://fake_url/rest/Content?cntn_cf_fk_histologyRequester=8" + }, + { + "rel": "-flrn_fk_user", + "href": "http://fake_url/rest/FlowRun?flrn_fk_user=8" + }, + { + "rel": "-prsp_fk_user", + "href": "http://fake_url/rest/Perspective?prsp_fk_user=8" + }, + { + "rel": "-nstr_fk_user", + "href": "http://fake_url/rest/Instrument?nstr_fk_user=8" + }, + { + "rel": "attachments", + "href": "http://fake_url/rest/attachment/User/8" + } + ] + } +] \ No newline at end of file diff --git a/tests/test_configuration.py b/tests/test_configuration.py new file mode 100644 index 0000000..d1c5f92 --- /dev/null +++ b/tests/test_configuration.py @@ -0,0 +1,47 @@ +"""Tests methods in configuration module""" + +import os +import unittest +from unittest.mock import patch + +from aind_slims_api.configuration import AindSlimsApiSettings + + +class TestAindSlimsApiSettings(unittest.TestCase): + """Tests methods in AindSlimsApiSettings class""" + + def test_default_settings(self): + """Tests that the class will be set with defaults""" + default_settings = AindSlimsApiSettings() + + self.assertEqual( + "https://aind-test.us.slims.agilent.com/slimsrest/", + default_settings.slims_url, + ) + self.assertEqual("", default_settings.slims_username) + self.assertEqual("", default_settings.slims_password.get_secret_value()) + + @patch.dict( + os.environ, + { + "SLIMS_URL": "https://aind.us.slims.agilent.com/slimsrest/", + "SLIMS_PASSWORD": "password2", + "SLIMS_USERNAME": "user2", + }, + clear=True, + ) + def test_settings_from_env_vars(self): + """Tests that the class can be set from env vars""" + default_settings = AindSlimsApiSettings() + + self.assertEqual( + "https://aind.us.slims.agilent.com/slimsrest/", default_settings.slims_url + ) + self.assertEqual("user2", default_settings.slims_username) + self.assertEqual( + "password2", default_settings.slims_password.get_secret_value() + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..5445b91 --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,172 @@ +""" Tests methods in core module""" + +import json +import os +import unittest +from copy import deepcopy +from pathlib import Path +from unittest.mock import MagicMock, patch + +from slims.criteria import conjunction, equals +from slims.internal import Record, _SlimsApiException + +from aind_slims_api.core import SlimsClient + +RESOURCES_DIR = Path(os.path.dirname(os.path.realpath(__file__))) / "resources" + + +class TestSlimsClient(unittest.TestCase): + """Tests methods in SlimsClient class""" + + @classmethod + def setUpClass(cls): + """Sets up class by downloading responses""" + example_client = SlimsClient( + url="http://fake_url", username="user", password="pass" + ) + cls.example_client = example_client + + def get_response(attribute_name: str): + """Utility method to download json file with {attribute_name}.json + from resource dir""" + with open( + os.path.join(str(RESOURCES_DIR), f"{attribute_name}.json"), "r" + ) as f: + response = [ + Record(json_entity=r, slims_api=example_client.db.slims_api) + for r in json.load(f) + ] + return response + + cls.example_fetch_unit_response = get_response("example_fetch_unit_response") + cls.example_fetch_mouse_response = get_response("example_fetch_mouse_response") + cls.example_fetch_user_response = get_response("example_fetch_user_response") + + def test_rest_link(self): + """Tests rest_link method with both queries and no queries.""" + + rest_link_no_queries = self.example_client.rest_link(table="Content") + rest_link_with_queries = self.example_client.rest_link( + table="Content", **{"limit": 1, "start": 0} + ) + self.assertEqual("http://fake_url/rest/Content", rest_link_no_queries) + self.assertEqual( + "http://fake_url/rest/Content?limit=1?start=0", rest_link_with_queries + ) + + @patch("slims.slims.Slims.fetch") + def test_fetch(self, mock_slims_fetch: MagicMock): + """Tests fetch method success""" + mock_slims_fetch.return_value = self.example_fetch_unit_response + response = self.example_client.fetch(table="Unit", start=0, end=2) + self.assertEqual(self.example_fetch_unit_response, response) + + @patch("slims.slims.Slims.fetch") + def test_fetch_with_criteria(self, mock_slims_fetch: MagicMock): + """Tests fetch method constructs criteria correctly""" + mock_slims_fetch.return_value = self.example_fetch_mouse_response + response = self.example_client.fetch( + "Content", + equals("cntn_barCode", "123456"), + cntp_name="Mouse", + ) + expected_criteria = ( + conjunction() + .add(equals("cntn_barCode", "123456")) + .add(equals("cntp_name", "Mouse")) + ) + actual_criteria = mock_slims_fetch.mock_calls[0].args[1] + self.assertEqual(expected_criteria.to_dict(), actual_criteria.to_dict()) + self.assertEqual(self.example_fetch_mouse_response, response) + + @patch("slims.slims.Slims.fetch") + def test_fetch_error(self, mock_slims_fetch: MagicMock): + """Tests fetch method when a _SlimsApiException is raised""" + mock_slims_fetch.side_effect = _SlimsApiException("Something went wrong") + with self.assertRaises(_SlimsApiException) as e: + self.example_client.fetch( + "Content", + cntp_name="Mouse", + ) + self.assertEqual("Something went wrong", e.exception.args[0]) + + @patch("slims.slims.Slims.fetch") + def test_fetch_user(self, mock_slims_fetch: MagicMock): + """Tests fetch_user method""" + mock_slims_fetch.return_value = self.example_fetch_user_response + response = self.example_client.fetch_user(user_name="PersonA") + self.assertEqual(self.example_fetch_user_response, response) + + @patch("slims.slims.Slims.fetch") + def test_fetch_pk(self, mock_slims_fetch: MagicMock): + """Tests fetch_pk method when several records are returned""" + # Use this example_client since result is cached + example_client = SlimsClient( + url="http://fake_url", username="user", password="pass" + ) + mock_slims_fetch.return_value = self.example_fetch_unit_response + pk = example_client.fetch_pk(table="Unit") + self.assertEqual(31, pk) + + @patch("slims.slims.Slims.fetch") + def test_fetch_pk_none(self, mock_slims_fetch: MagicMock): + """Tests fetch_pk method when no records are returned""" + # Use this example_client since result is cached + example_client = SlimsClient( + url="http://fake_url", username="user", password="pass" + ) + mock_slims_fetch.return_value = [] + pk = example_client.fetch_pk(table="Content") + self.assertIsNone(pk) + + @patch("logging.Logger.info") + @patch("slims.slims.Slims.add") + def test_add(self, mock_slims_add: MagicMock, mock_log: MagicMock): + """Tests add method""" + mock_slims_add.return_value = self.example_fetch_unit_response[0] + input_data = deepcopy(self.example_fetch_unit_response[0].json_entity) + record = self.example_client.add(table="Unit", data=input_data) + self.assertEqual(self.example_fetch_unit_response[0], record) + mock_log.assert_called_once_with("SLIMS Add: Unit/31") + + @patch("slims.slims.Slims.fetch_by_pk") + @patch("logging.Logger.info") + @patch("slims.internal.Record.update") + def test_update( + self, mock_update: MagicMock, mock_log: MagicMock, mock_fetch_by_pk: MagicMock + ): + """Tests update method success""" + input_data = deepcopy(self.example_fetch_unit_response[0].json_entity) + mock_record = Record( + json_entity=input_data, slims_api=self.example_client.db.slims_api + ) + mock_fetch_by_pk.return_value = mock_record + new_data = deepcopy(input_data) + new_data["columns"][0]["value"] = "PM^3" + mocked_updated_record = Record( + json_entity=new_data, slims_api=self.example_client.db.slims_api + ) + mock_update.return_value = mocked_updated_record + new_record = self.example_client.update(table="Unit", pk=31, data=new_data) + self.assertEqual(mocked_updated_record, new_record) + mock_log.assert_called_once_with("SLIMS Update: Unit/31") + + @patch("slims.slims.Slims.fetch_by_pk") + @patch("logging.Logger.info") + @patch("slims.internal.Record.update") + def test_update_failure( + self, mock_update: MagicMock, mock_log: MagicMock, mock_fetch_by_pk: MagicMock + ): + """Tests update method when a failure occurs""" + mock_fetch_by_pk.return_value = None + with self.assertRaises(ValueError) as e: + self.example_client.update(table="Unit", pk=30000, data={}) + self.assertEqual( + 'No data in SLIMS "Unit" table for pk "30000"', e.exception.args[0] + ) + mock_update.assert_not_called() + mock_log.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_example.py b/tests/test_example.py deleted file mode 100644 index 06e9e0d..0000000 --- a/tests/test_example.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Example test template.""" - -import unittest - - -class ExampleTest(unittest.TestCase): - """Example Test Class""" - - def test_assert_example(self): - """Example of how to test the truth of a statement.""" - - self.assertTrue(1 == 1) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_mouse.py b/tests/test_mouse.py new file mode 100644 index 0000000..9c698fb --- /dev/null +++ b/tests/test_mouse.py @@ -0,0 +1,74 @@ +"""Tests methods in mouse module""" + +import json +import os +import unittest +from pathlib import Path +from unittest.mock import MagicMock, patch + +from slims.internal import Record + +from aind_slims_api.core import SlimsClient +from aind_slims_api.mouse import fetch_mouse_content + +RESOURCES_DIR = Path(os.path.dirname(os.path.realpath(__file__))) / "resources" + + +class TestMouse(unittest.TestCase): + """Tests top level methods in mouse module""" + + @classmethod + def setUpClass(cls): + """Load json files of expected responses from slims""" + example_client = SlimsClient( + url="http://fake_url", username="user", password="pass" + ) + cls.example_client = example_client + with open(RESOURCES_DIR / "example_fetch_mouse_response.json", "r") as f: + response = [ + Record(json_entity=r, slims_api=example_client.db.slims_api) + for r in json.load(f) + ] + cls.example_fetch_mouse_response = response + + @patch("slims.slims.Slims.fetch") + def test_fetch_mouse_content_success(self, mock_fetch: MagicMock): + """Test fetch_mouse_content when successful""" + mock_fetch.return_value = self.example_fetch_mouse_response + mouse_details = fetch_mouse_content(self.example_client, mouse_name="123456") + self.assertEqual( + self.example_fetch_mouse_response[0].json_entity, mouse_details + ) + + @patch("logging.Logger.warning") + @patch("slims.slims.Slims.fetch") + def test_fetch_mouse_content_no_mouse( + self, mock_fetch: MagicMock, mock_log_warn: MagicMock + ): + """Test fetch_mouse_content when no mouse is returned""" + mock_fetch.return_value = [] + mouse_details = fetch_mouse_content(self.example_client, mouse_name="12") + self.assertIsNone(mouse_details) + mock_log_warn.assert_called_with("Warning, Mouse not in SLIMS") + + @patch("logging.Logger.warning") + @patch("slims.slims.Slims.fetch") + def test_fetch_mouse_content_many_mouse( + self, mock_fetch: MagicMock, mock_log_warn: MagicMock + ): + """Test fetch_mouse_content when too many mice are returned""" + mock_fetch.return_value = [ + self.example_fetch_mouse_response[0], + self.example_fetch_mouse_response[0], + ] + mouse_details = fetch_mouse_content(self.example_client, mouse_name="123456") + self.assertEqual( + self.example_fetch_mouse_response[0].json_entity, mouse_details + ) + mock_log_warn.assert_called_with( + "Warning, Multiple mice in SLIMS with barcode 123456, using pk=3038" + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_user.py b/tests/test_user.py new file mode 100644 index 0000000..02a705d --- /dev/null +++ b/tests/test_user.py @@ -0,0 +1,74 @@ +"""Tests methods in user module""" + +import json +import os +import unittest +from pathlib import Path +from unittest.mock import MagicMock, patch + +from slims.internal import Record + +from aind_slims_api.core import SlimsClient +from aind_slims_api.user import fetch_user + +RESOURCES_DIR = Path(os.path.dirname(os.path.realpath(__file__))) / "resources" + + +class TestUser(unittest.TestCase): + """Tests top level methods in user module""" + + @classmethod + def setUpClass(cls): + """Load json files of expected responses from slims""" + example_client = SlimsClient( + url="http://fake_url", username="user", password="pass" + ) + cls.example_client = example_client + with open(RESOURCES_DIR / "example_fetch_user_response.json", "r") as f: + response = [ + Record(json_entity=r, slims_api=example_client.db.slims_api) + for r in json.load(f) + ] + cls.example_fetch_user_response = response + + @patch("slims.slims.Slims.fetch") + def test_fetch_user_content_success(self, mock_fetch: MagicMock): + """Test fetch_user when successful""" + mock_fetch.return_value = self.example_fetch_user_response + user_info = fetch_user(self.example_client, username="PersonA") + self.assertEqual(self.example_fetch_user_response[0].json_entity, user_info) + + @patch("logging.Logger.warning") + @patch("slims.slims.Slims.fetch") + def test_fetch_user_content_no_user( + self, mock_fetch: MagicMock, mock_log_warn: MagicMock + ): + """Test fetch_user when no user is returned""" + mock_fetch.return_value = [] + user_info = fetch_user(self.example_client, username="PersonX") + self.assertIsNone(user_info) + mock_log_warn.assert_called_with("Warning, User not in SLIMS") + + @patch("logging.Logger.warning") + @patch("slims.slims.Slims.fetch") + def test_fetch_user_content_many_users( + self, mock_fetch: MagicMock, mock_log_warn: MagicMock + ): + """Test fetch_user_content when too many users are returned""" + mocked_response = [ + self.example_fetch_user_response[0], + self.example_fetch_user_response[0], + ] + mock_fetch.return_value = mocked_response + user_info = fetch_user(self.example_client, username="PersonA") + self.assertEqual(self.example_fetch_user_response[0].json_entity, user_info) + expected_warning = ( + f"Warning, Multiple users in SLIMS with " + f"username {[u.json_entity for u in mocked_response]}, " + f"using pk={mocked_response[0].pk()}" + ) + mock_log_warn.assert_called_with(expected_warning) + + +if __name__ == "__main__": + unittest.main()