From dd4004f21786113d177bae133672f9c7d5bb8c8d Mon Sep 17 00:00:00 2001 From: Ludwig Schneider Date: Fri, 17 May 2024 07:28:10 -0500 Subject: [PATCH] Update search, so it works with the new `after` syntax. (#456) * Update to 1.0.1 * update python version * run new version on 3.10 * revamp search * remove explicit model version --- .github/workflows/tests.yml | 2 +- README.md | 4 +- setup.cfg | 4 +- src/cript/api/api.py | 13 +- src/cript/api/paginator.py | 168 ++++++++++-------- src/cript/nodes/core.py | 4 + .../primary_nodes/computation_process.py | 2 +- src/cript/nodes/primary_nodes/material.py | 2 +- src/cript/nodes/primary_nodes/process.py | 2 +- src/cript/nodes/subobjects/property.py | 34 ++-- tests/api/test_search.py | 77 +++++--- tests/fixtures/api_fixtures.py | 3 +- tests/fixtures/primary_nodes.py | 8 +- tests/fixtures/subobjects.py | 8 +- tests/fixtures/supporting_nodes.py | 1 - tests/nodes/primary_nodes/test_material.py | 2 +- tests/nodes/subobjects/test_property.py | 4 +- tests/nodes/test_utils.py | 2 +- tests/test_node_util.py | 12 +- 19 files changed, 194 insertions(+), 158 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ba088676b..0055e7b3f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,7 +35,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest] - python-version: [3.8, 3.12] + python-version: ["3.10", 3.12] env: CRIPT_HOST: https://lb-stage.mycriptapp.org/ diff --git a/README.md b/README.md index ef855d800..6805c6ace 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![License](./CRIPT_full_logo_colored_transparent.png)](https://github.com/C-Accel-CRIPT/Python-SDK/blob/develop/LICENSE.md) [![License](https://img.shields.io/github/license/C-Accel-CRIPT/cript?style=flat-square)](https://github.com/C-Accel-CRIPT/Python-SDK/blob/develop/LICENSE.md) -[![Python](https://img.shields.io/badge/Language-Python%203.8+-blue?style=flat-square&logo=python)](https://www.python.org/) +[![Python](https://img.shields.io/badge/Language-Python%203.10+-blue?style=flat-square&logo=python)](https://www.python.org/) [![Code style is black](https://img.shields.io/badge/Code%20Style-black-000000.svg?style=flat-square&logo=python)](https://github.com/psf/black) [![Link to CRIPT website](https://img.shields.io/badge/platform-criptapp.org-blueviolet?style=flat-square)](https://criptapp.org/) [![Using Pytest](https://img.shields.io/badge/Dependencies-pytest-green?style=flat-square&logo=Pytest)](https://docs.pytest.org/en/7.2.x/) @@ -36,7 +36,7 @@ The CRIPT Python SDK allows programmatic access to the [CRIPT platform](https:// ## Installation -CRIPT Python SDK requires Python 3.8+ +CRIPT Python SDK requires Python 3.10+ The latest released of CRIPT Python SDK is available on [Python Package Index (PyPI)](https://pypi.org/project/cript/) diff --git a/setup.cfg b/setup.cfg index e653060fe..b138a20a3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,13 +14,13 @@ classifiers = Topic :: Scientific/Engineering Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.10 [options] package_dir = =src packages = find: -python_requires = >=3.8 +python_requires = >=3.10 include_package_data = True install_requires = requests==2.31.0 diff --git a/src/cript/api/api.py b/src/cript/api/api.py index dfa69333b..cca2e13a6 100644 --- a/src/cript/api/api.py +++ b/src/cript/api/api.py @@ -787,35 +787,32 @@ def search( node_type = node_type.node_type_snake_case api_endpoint: str = "" - page_number: Union[int, None] = None - + limit_node_fetches: Optional[int] = None if search_mode == SearchModes.NODE_TYPE: api_endpoint = f"/search/{node_type}" - page_number = 0 + value_to_search = "" elif search_mode == SearchModes.CONTAINS_NAME: api_endpoint = f"/search/{node_type}" - page_number = 0 elif search_mode == SearchModes.EXACT_NAME: api_endpoint = f"/search/exact/{node_type}" - page_number = None + limit_node_fetches = 1 elif search_mode == SearchModes.UUID: api_endpoint = f"/{node_type}/{value_to_search}" # putting the value_to_search in the URL instead of a query value_to_search = "" - page_number = None + limit_node_fetches = 1 elif search_mode == SearchModes.BIGSMILES: api_endpoint = "/search/bigsmiles/" - page_number = 0 # error handling if none of the API endpoints got hit else: raise RuntimeError("Internal Error: Failed to recognize any search modes. Please report this bug on https://github.com/C-Accel-CRIPT/Python-SDK/issues.") - return Paginator(api=self, url_path=api_endpoint, page_number=page_number, query=value_to_search) + return Paginator(api=self, url_path=api_endpoint, query=value_to_search, limit_node_fetches=limit_node_fetches) def delete(self, node) -> None: """ diff --git a/src/cript/api/paginator.py b/src/cript/api/paginator.py index 23e419ed0..9756b9b9f 100644 --- a/src/cript/api/paginator.py +++ b/src/cript/api/paginator.py @@ -1,5 +1,5 @@ import json -from typing import Dict, Union +from typing import Dict, Optional, Tuple from urllib.parse import quote import requests @@ -9,6 +9,16 @@ from cript.nodes.util import load_nodes_from_json +def _get_uuid_score_from_json(node_dict: Dict) -> Tuple[str, Optional[float]]: + """ + Get the UUID string and search score from a JSON node representation if available. + """ + node_uuid: str = node_dict["uuid"] + node_score: Optional[float] = node_dict.get("score", None) + + return node_uuid, node_score + + class Paginator: """ Paginator is used to flip through different pages of data that the API returns when searching. @@ -29,22 +39,17 @@ class Paginator: _url_path: str _query: str - _initial_page_number: Union[int, None] _current_position: int _fetched_nodes: list + _uuid_search_score_map: Dict _number_fetched_pages: int = 0 - _limit_page_fetches: Union[int, None] = None - _num_skip_pages: int = 0 + _limit_node_fetches: Optional[int] = None + _start_after_uuid: Optional[str] = None + _start_after_score: Optional[float] = None auto_load_nodes: bool = True @beartype - def __init__( - self, - api, - url_path: str, - page_number: Union[int, None], - query: str, - ): + def __init__(self, api, url_path: str, query: str, limit_node_fetches: Optional[int] = None): """ create a paginator @@ -53,15 +58,14 @@ def __init__( Parameters ---------- - http_headers: dict - get already created http headers from API and just use them in paginator - api_endpoint: str - api endpoint to send the search requests to - it already contains what node the user is looking for - current_page_number: int - page number to start from. Keep track of current page for user to flip back and forth between pages of data + api: cript.API + Object through which the API call is routed. + url_path: str + query URL used. query: str the value the user is searching for + limit_node_fetches: Optional[int] = None + limits the number of nodes fetches through this call. Returns ------- @@ -69,18 +73,18 @@ def __init__( instantiate a paginator """ self._api = api - self._initial_page_number = page_number - self._number_fetched_pages = 0 self._fetched_nodes = [] self._current_position = 0 + self._limit_node_fetches = limit_node_fetches + self._uuid_search_score_map = {} # check if it is a string and not None to avoid AttributeError try: - self._url_path = quote(url_path.rstrip("/").strip()) + self._url_path = url_path.rstrip("/").strip() except Exception as exc: raise RuntimeError(f"Invalid type for api_endpoint {self._url_path} for a paginator.") from exc - self._query = quote(query) + self._query = query @beartype def _fetch_next_page(self) -> None: @@ -105,16 +109,36 @@ def _fetch_next_page(self) -> None: None """ - # Check if we are supposed to fetch more pages - if self._limit_page_fetches and self._number_fetched_pages >= self._limit_page_fetches: - raise StopIteration - # Composition of the query URL - temp_url_path: str = self._url_path - temp_url_path += f"/?q={self._query}" - if self._initial_page_number is not None: - temp_url_path += f"&page={self.page_number}" - self._number_fetched_pages += 1 + temp_url_path: str = self._url_path + "/" + + query_list = [] + + if len(self._query) > 0: + query_list += [f"q={self._query}"] + + if self._limit_node_fetches is None or self._limit_node_fetches > 1: # This limits these parameters + if self._start_after_uuid is not None: + query_list += [f"after={self._start_after_uuid}"] + if self._start_after_score is not None: # Always None for none BigSMILES searches + query_list += [f"score={self._start_after_score}"] + + # Reset to allow normal search to continue + self._start_after_uuid = None + self._start_after_score = None + + elif len(self._fetched_nodes) > 0: # Use known last element + node_uuid, node_score = _get_uuid_score_from_json(self._fetched_nodes[-1]) + query_list += [f"after={node_uuid}"] + if node_score is not None: + query_list += [f"score={node_score}"] + + for i, query in enumerate(query_list): + if i == 0: + temp_url_path += "?" + else: + temp_url_path += "&" + temp_url_path += quote(query, safe="/=&?") response: requests.Response = self._api._capsule_request(url_path=temp_url_path, method="GET") @@ -153,18 +177,18 @@ def _fetch_next_page(self) -> None: self._fetched_nodes += json_list def __next__(self): + if self._limit_node_fetches and self._current_position >= self._limit_node_fetches: + raise StopIteration + if self._current_position >= len(self._fetched_nodes): - # Without a page number argument, we can only fetch once. - if self._initial_page_number is None and self._number_fetched_pages > 0: - raise StopIteration self._fetch_next_page() try: next_node_json = self._fetched_nodes[self._current_position - 1] - except IndexError: # This is not a random access iteration. + except IndexError as exc: # This is not a random access iteration. # So if fetching a next page wasn't enough to get the index inbound, # The iteration stops - raise StopIteration + raise StopIteration from exc if self.auto_load_nodes: return_data = load_nodes_from_json(next_node_json) @@ -181,24 +205,8 @@ def __iter__(self): self._current_position = 0 return self - @property - def page_number(self) -> Union[int, None]: - """Obtain the current page number the paginator is fetching next. - - Returns - ------- - int - positive number of the next page this paginator is fetching. - None - if no page number is associated with the pagination - """ - page_number = self._num_skip_pages + self._number_fetched_pages - if self._initial_page_number is not None: - page_number += self._initial_page_number - return page_number - @beartype - def limit_page_fetches(self, max_num_pages: Union[int, None]) -> None: + def limit_node_fetches(self, max_num_nodes: Optional[int]) -> None: """Limit pagination to a maximum number of pages. This can be used for very large searches with the paginator, so the search can be split into @@ -206,40 +214,44 @@ def limit_page_fetches(self, max_num_pages: Union[int, None]) -> None: Parameters ---------- - max_num_pages: Union[int, None], + max_num_nodes: Optional[int], positive integer with maximum number of page fetches. or None, indicating unlimited number of page fetches are permitted. """ - self._limit_page_fetches = max_num_pages + self._limit_node_fetches = max_num_nodes - def skip_pages(self, skip_pages: int) -> int: - """Skip pages in the pagination. - - Warning this function is advanced usage and may not produce the results you expect. - In particular, every search is different, even if we search for the same values there is - no guarantee that the results are in the same order. (And results can change if data is - added or removed from CRIPT.) So if you break up your search with `limit_page_fetches` and - `skip_pages` there is no guarantee that it is the same as one continuous search. - If the paginator associated search does not accept pages, there is no effect. + @beartype + def start_after_uuid(self, start_after_uuid: str, start_after_score: Optional[float] = None): + """ + This can be used to continue a search from a last known node. Parameters ---------- - skip_pages:int - Number of pages that the paginator skips now before fetching the next page. - The parameter is added to the internal state, so repeated calls skip more pages. + start_after_uuid: str + UUID string of the last node from a previous search + start_after_score: float + required for BigSMILES searches, the last score from a BigSMILES search. + Must be None if not a BigSMILES search. Returns ------- - int - The number this paginator is skipping. Internal skip count. + None + """ + self._start_after_uuid = start_after_uuid + self._start_after_score = start_after_score - Raises - ------ - RuntimeError - If the total number of skipped pages is negative. + @beartype + def get_bigsmiles_search_score(self, uuid: str): """ - num_skip_pages = self._num_skip_pages + skip_pages - if self._num_skip_pages < 0: - RuntimeError(f"Invalid number of skipped pages. The total number of pages skipped is negative {num_skip_pages}, requested to skip {skip_pages}.") - self._num_skip_pages = num_skip_pages - return self._num_skip_pages + Get the ranking score for nodes from the BigSMILES search. + Will return None if not a BigSMILES search or raise an Exception. + """ + if uuid not in self._uuid_search_score_map.keys(): + start = len(self._uuid_search_score_map.keys()) + for node_json in self._fetched_nodes[start:]: + node_uuid, node_score = _get_uuid_score_from_json(node_json) + self._uuid_search_score_map[node_uuid] = node_score + try: + return self._uuid_search_score_map[uuid] + except KeyError as exc: + raise RuntimeError(f"The requested UUID {uuid} is not know from the search. Search scores are limited only to current search.") from exc diff --git a/src/cript/nodes/core.py b/src/cript/nodes/core.py index 0a74fa02d..3cc7398ab 100644 --- a/src/cript/nodes/core.py +++ b/src/cript/nodes/core.py @@ -174,6 +174,10 @@ def _from_json(cls, json_dict: dict): pass else: arguments[field] = json_dict[field] + try: # TODO remove this hack to work with compatible model versions + del arguments["model_version"] + except KeyError: + pass # add omitted fields from default (necessary if they are required) for field_name in [field.name for field in dataclasses.fields(default_dataclass)]: diff --git a/src/cript/nodes/primary_nodes/computation_process.py b/src/cript/nodes/primary_nodes/computation_process.py index a514e898f..91e1ceeca 100644 --- a/src/cript/nodes/primary_nodes/computation_process.py +++ b/src/cript/nodes/primary_nodes/computation_process.py @@ -521,7 +521,7 @@ def property(self) -> List[Any]: Examples -------- >>> import cript - >>> my_property = cript.Property(key="modulus_shear", type="min", value=1.23, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.23, unit="J") >>> my_computation_process.property = [my_property] # doctest: +SKIP Returns diff --git a/src/cript/nodes/primary_nodes/material.py b/src/cript/nodes/primary_nodes/material.py index 2c6c6a728..e59f0207a 100644 --- a/src/cript/nodes/primary_nodes/material.py +++ b/src/cript/nodes/primary_nodes/material.py @@ -540,7 +540,7 @@ def property(self) -> List[Any]: ... name="my component material 1", ... smiles = "component 1 smiles", ... ) - >>> my_property = cript.Property(key="modulus_shear", type="min", value=1.23, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.23, unit="J") >>> my_material.property = [my_property] Returns diff --git a/src/cript/nodes/primary_nodes/process.py b/src/cript/nodes/primary_nodes/process.py index 56c45115a..c20d9f572 100644 --- a/src/cript/nodes/primary_nodes/process.py +++ b/src/cript/nodes/primary_nodes/process.py @@ -580,7 +580,7 @@ def property(self) -> List[Any]: -------- >>> import cript >>> my_process = cript.Process(name="my process name", type="affinity_pure") - >>> my_property = cript.Property(key="modulus_shear", type="min", value=1.23, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.23, unit="J") >>> my_process.property = [my_property] Returns diff --git a/src/cript/nodes/subobjects/property.py b/src/cript/nodes/subobjects/property.py index d6af1a7d2..bc40de981 100644 --- a/src/cript/nodes/subobjects/property.py +++ b/src/cript/nodes/subobjects/property.py @@ -38,7 +38,7 @@ class Property(UUIDBaseNode): | attribute | type | example | description | required | vocab | |--------------------|-------------------|-----------------------------------------|------------------------------------------------------------------------------|----------|-------| - | key | str | modulus_shear | type of property | True | True | + | key | str | enthalpy | type of property | True | True | | type | str | min | type of value stored | True | True | | value | Any | 1.23 | value or quantity | True | | | unit | str | gram | unit for value | True | | @@ -58,10 +58,10 @@ class Property(UUIDBaseNode): ## JSON Representation ```json { - "key":"modulus_shear", + "key":"enthalpy", "node":["Property"], "type":"value", - "unit":"GPa", + "unit":"J", "value":5.0 "uid":"_:bc3abb68-25b5-4144-aa1b-85d82b7c77e1", "uuid":"bc3abb68-25b5-4144-aa1b-85d82b7c77e1", @@ -149,7 +149,7 @@ def __init__( Examples -------- >>> import cript - >>> my_property = cript.Property(key="air_flow", type="min", value=1.00, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.00, unit="J") Returns ------- @@ -197,7 +197,7 @@ def key(self) -> str: Examples -------- >>> import cript - >>> my_property = cript.Property(key="air_flow", type="min", value=1.00, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.00, unit="J") >>> my_property.key = "angle_rdist" Returns @@ -236,7 +236,7 @@ def type(self) -> str: Examples --------- >>> import cript - >>> my_property = cript.Property(key="air_flow", type="min", value=1.00, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.00, unit="J") >>> my_property.type = "max" Returns @@ -285,7 +285,7 @@ def set_value(self, new_value: Union[Number, str, None], new_unit: str) -> None: Examples --------- >>> import cript - >>> my_property = cript.Property(key="air_flow", type="min", value=1.00, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.00, unit="J") >>> my_property.set_value(new_value=1, new_unit="gram") Parameters @@ -345,7 +345,7 @@ def set_uncertainty(self, new_uncertainty: Optional[Number], new_uncertainty_typ Examples -------- >>> import cript - >>> my_property = cript.Property(key="air_flow", type="min", value=1.00, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.00, unit="J") >>> my_property.set_uncertainty(new_uncertainty=2, new_uncertainty_type="fwhm") Returns @@ -380,7 +380,7 @@ def component(self) -> List[Union[Material, UIDProxy]]: Examples --------- >>> import cript - >>> my_property = cript.Property(key="air_flow", type="min", value=1.00, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.00, unit="J") >>> my_material = cript.Material(name="my material", bigsmiles = "123456") >>> my_property.component = [my_material] @@ -418,7 +418,7 @@ def structure(self) -> str: Examples -------- >>> import cript - >>> my_property = cript.Property(key="air_flow", type="min", value=1.00, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.00, unit="J") >>> my_property.structure = "{[][$][C:1][C:1][$],[$][C:2][C:2]([C:2])[$][]}" Returns @@ -457,7 +457,7 @@ def method(self) -> str: Examples -------- >>> import cript - >>> my_property = cript.Property(key="air_flow", type="min", value=1.00, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.00, unit="J") >>> my_property.method = "ASTM_D3574_Test_A" Returns @@ -494,7 +494,7 @@ def sample_preparation(self) -> Union[Process, None, UIDProxy]: Examples -------- >>> import cript - >>> my_property = cript.Property(key="air_flow", type="min", value=1.00, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.00, unit="J") >>> my_process = cript.Process(name="my process name", type="affinity_pure") >>> my_property.sample_preparation = my_process @@ -532,7 +532,7 @@ def condition(self) -> List[Union[Condition, UIDProxy]]: Examples -------- >>> import cript - >>> my_property = cript.Property(key="air_flow", type="min", value=1.00, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.00, unit="J") >>> my_condition = cript.Condition(key="atm", type="max", value=1) >>> my_property.condition = [my_condition] @@ -570,7 +570,7 @@ def data(self) -> List[Union[Data, UIDProxy]]: Examples -------- >>> import cript - >>> my_property = cript.Property(key="air_flow", type="min", value=1.00, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.00, unit="J") >>> my_file = cript.File( ... name="my file node name", ... source="https://criptapp.org", @@ -615,7 +615,7 @@ def computation(self) -> List[Union[Computation, UIDProxy]]: Examples -------- >>> import cript - >>> my_property = cript.Property(key="air_flow", type="min", value=1.00, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.00, unit="J") >>> my_computation = cript.Computation(name="my computation name", type="analysis") >>> my_property.computation = [my_computation] @@ -653,7 +653,7 @@ def citation(self) -> List[Union[Citation, UIDProxy]]: Examples -------- >>> import cript - >>> my_property = cript.Property(key="air_flow", type="min", value=1.00, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.00, unit="J") >>> title = ( ... "Multi-architecture Monte-Carlo (MC) simulation of soft coarse-grained polymeric materials: " ... "Soft coarse grained Monte-Carlo Acceleration (SOMA)" @@ -707,7 +707,7 @@ def notes(self) -> str: Examples -------- >>> import cript - >>> my_property = cript.Property(key="air_flow", type="min", value=1.00, unit="gram") + >>> my_property = cript.Property(key="enthalpy", type="min", value=1.00, unit="J") >>> my_property.notes = "these are my notes" Returns diff --git a/tests/api/test_search.py b/tests/api/test_search.py index 0b88da769..4b9909eba 100644 --- a/tests/api/test_search.py +++ b/tests/api/test_search.py @@ -24,33 +24,47 @@ def test_api_search_node_type(cript_api: cript.API) -> None: # test search results assert isinstance(materials_paginator, Paginator) - materials_paginator.skip_pages(3) - materials_paginator.limit_page_fetches(3) + materials_paginator.limit_node_fetches(15) materials_list = [] while True: try: try: material_node = next(materials_paginator) - except cript.CRIPTException as exc: + materials_list += [material_node] + except cript.CRIPTException: materials_paginator.auto_load_nodes = False material_json = next(materials_paginator) - print(exc, material_json) - else: - materials_list += [material_node] + materials_list.append(material_json) finally: materials_paginator.auto_load_nodes = True except StopIteration: break - # We don't need to search for a million pages here. - if materials_paginator._number_fetched_pages > 6: - break # Assure that we paginated more then one page - assert materials_paginator._number_fetched_pages > 0 - assert len(materials_list) > 5 - first_page_first_result = materials_list[0].name - # just checking that the word has a few characters in it - assert len(first_page_first_result) > 3 + assert len(materials_list) == 15 + first_page_first_result = materials_list[0]["name"] + assert first_page_first_result + + materials_paginator2 = cript_api.search(node_type=cript.Material, search_mode=cript.SearchModes.NODE_TYPE) + materials_paginator2.limit_node_fetches(21) + uuid_list = [] + for mat in materials_list: + try: + uuid = mat.uuid + except AttributeError: + uuid = mat["uuid"] + uuid_list.append(uuid) + + materials_paginator2.start_after_uuid(uuid_list[-1]) + materials_paginator2.auto_load_nodes = False + + for i, mat in enumerate(materials_paginator2): + if mat["uuid"] in uuid_list: + print(mat["uuid"]) + + # TODO enable duplicate test + # assert mat["uuid"] not in uuid_list + assert i < 21 @pytest.mark.skipif(not HAS_INTEGRATION_TESTS_ENABLED, reason="requires a real cript_api_token") @@ -59,17 +73,17 @@ def test_api_search_contains_name(cript_api: cript.API) -> None: tests that it can correctly search with contains name mode searches for a material that contains the name "polystyrene" """ - contains_name_paginator = cript_api.search(node_type=cript.Material, search_mode=cript.SearchModes.CONTAINS_NAME, value_to_search="polystyrene") + query = "act" + contains_name_paginator = cript_api.search(node_type=cript.Material, search_mode=cript.SearchModes.CONTAINS_NAME, value_to_search=query) + contains_name_paginator.auto_load_nodes = False assert isinstance(contains_name_paginator, Paginator) - contains_name_list = list(contains_name_paginator) + contains_name_list = [mat["name"] for mat in contains_name_paginator] # Assure that we paginated more then one page - assert len(contains_name_list) > 2 - - contains_name_first_result = contains_name_list[0].name + assert len(contains_name_list) > 50 - # just checking that the result has a few characters in it - assert len(contains_name_first_result) > 3 + for name in contains_name_list: + assert query.upper() in name.upper() @pytest.mark.skipif(not HAS_INTEGRATION_TESTS_ENABLED, reason="requires a real cript_api_token") @@ -81,9 +95,10 @@ def test_api_search_exact_name(cript_api: cript.API) -> None: exact_name_paginator = cript_api.search(node_type=cript.Material, search_mode=cript.SearchModes.EXACT_NAME, value_to_search="Sodium polystyrene sulfonate") assert isinstance(exact_name_paginator, Paginator) + exact_name_paginator.auto_load_nodes = False exact_name_list = list(exact_name_paginator) assert len(exact_name_list) == 1 - assert exact_name_list[0].name == "Sodium polystyrene sulfonate" + assert exact_name_list[0]["name"] == "Sodium polystyrene sulfonate" @pytest.mark.skipif(not HAS_INTEGRATION_TESTS_ENABLED, reason="requires a real cript_api_token") @@ -99,11 +114,12 @@ def test_api_search_uuid(cript_api: cript.API, dynamic_material_data) -> None: """ uuid_paginator = cript_api.search(node_type=cript.Material, search_mode=cript.SearchModes.UUID, value_to_search=dynamic_material_data["uuid"]) + uuid_paginator.auto_load_nodes = False assert isinstance(uuid_paginator, Paginator) uuid_list = list(uuid_paginator) assert len(uuid_list) == 1 - assert uuid_list[0].name == dynamic_material_data["name"] - assert str(uuid_list[0].uuid) == dynamic_material_data["uuid"] + assert uuid_list[0]["name"] == dynamic_material_data["name"] + assert uuid_list[0]["uuid"] == dynamic_material_data["uuid"] @pytest.mark.skipif(not HAS_INTEGRATION_TESTS_ENABLED, reason="requires a real cript_api_token") @@ -138,6 +154,17 @@ def test_api_search_bigsmiles(cript_api: cript.API) -> None: bigsmiles_paginator = cript_api.search(node_type=cript.Material, search_mode=cript.SearchModes.BIGSMILES, value_to_search=bigsmiles_search_value) + bigsmiles_paginator.limit_node_fetches(15) + + bigsmiles_paginator.auto_load_nodes = False assert isinstance(bigsmiles_paginator, Paginator) bigsmiles_list = list(bigsmiles_paginator) - assert len(bigsmiles_list) >= 1 + assert len(bigsmiles_list) == 15 + uuid_list = [mat["uuid"] for mat in bigsmiles_list] + # Check that we don't have duplicates + + for uuid in uuid_list: + print(uuid, bigsmiles_paginator.get_bigsmiles_search_score(uuid), uuid_list.count(uuid)) + + # Enable duplicate test + # assert len(set(uuid_list)) == len(uuid_list) diff --git a/tests/fixtures/api_fixtures.py b/tests/fixtures/api_fixtures.py index 062c19f75..8b9ddfab7 100644 --- a/tests/fixtures/api_fixtures.py +++ b/tests/fixtures/api_fixtures.py @@ -26,7 +26,8 @@ def dynamic_material_data(cript_api: cript.API) -> Dict[str, str]: exact_name_paginator = cript_api.search(node_type=cript.Material, search_mode=cript.SearchModes.EXACT_NAME, value_to_search=material_name) + exact_name_paginator.auto_load_nodes = False material = next(exact_name_paginator) - material_uuid: str = str(material.uuid) + material_uuid: str = material["uuid"] return {"name": material_name, "uuid": material_uuid} diff --git a/tests/fixtures/primary_nodes.py b/tests/fixtures/primary_nodes.py index 063a3a334..24c932da3 100644 --- a/tests/fixtures/primary_nodes.py +++ b/tests/fixtures/primary_nodes.py @@ -25,7 +25,6 @@ def simple_project_node(simple_collection_node) -> cript.Project: def complex_project_dict(complex_collection_node, simple_material_node, complex_user_node) -> dict: project_dict = {"node": ["Project"]} project_dict["locked"] = True - project_dict["model_version"] = "1.0.0" project_dict["updated_by"] = json.loads(copy.deepcopy(complex_user_node).get_expanded_json()) project_dict["created_by"] = json.loads(complex_user_node.get_expanded_json()) project_dict["public"] = True @@ -60,7 +59,6 @@ def fixed_cyclic_project_node() -> cript.Project: project_json_string += '"created_at": "2024-03-12 15:58:12.486673",\n' project_json_string += '"updated_at": "2024-03-12 15:58:12.486681",\n' project_json_string += '"email": "test@emai.com",\n' - project_json_string += '"model_version": "1.0.0",\n' project_json_string += '"orcid": "0000-0002-0000-0000",\n' project_json_string += '"picture": "/my/picture/path",\n' project_json_string += '"username": "testuser"\n' @@ -72,13 +70,11 @@ def fixed_cyclic_project_node() -> cript.Project: project_json_string += '"created_at": "2024-03-12 15:58:12.486673",\n' project_json_string += '"updated_at": "2024-03-12 15:58:12.486681",\n' project_json_string += '"email": "test@emai.com",\n' - project_json_string += '"model_version": "1.0.0",\n' project_json_string += '"orcid": "0000-0002-0000-0000",\n' project_json_string += '"picture": "/my/picture/path",\n' project_json_string += '"username": "testuser"\n' project_json_string += "},\n" project_json_string += '"locked": true,\n' - project_json_string += '"model_version": "1.0.0",\n' project_json_string += '"public": true,\n' project_json_string += '"name": "my project name",\n' project_json_string += '"notes": "my project notes",\n' @@ -126,7 +122,7 @@ def fixed_cyclic_project_node() -> cript.Project: project_json_string += '"node": ["Property"],\n' project_json_string += '"uid": "_:fc504202-6fdd-43c7-830d-40c7d3f0cb8c",\n' project_json_string += '"uuid": "fc504202-6fdd-43c7-830d-40c7d3f0cb8c",\n' - project_json_string += '"key": "modulus_shear",\n' + project_json_string += '"key": "enthalpy",\n' project_json_string += '"type": "value",\n' project_json_string += '"value": 5.0,\n' project_json_string += '"unit": "GPa",\n' @@ -213,7 +209,7 @@ def fixed_cyclic_project_node() -> cript.Project: project_json_string += '"node": ["Property"],\n' project_json_string += '"uid": "_:fde629f5-8d3a-4546-8cd3-9de63b990187",\n' project_json_string += '"uuid": "fde629f5-8d3a-4546-8cd3-9de63b990187",\n' - project_json_string += '"key": "modulus_shear",\n' + project_json_string += '"key": "enthalpy",\n' project_json_string += '"type": "value",\n' project_json_string += '"value": 5.0,\n' project_json_string += '"unit": "GPa",\n' diff --git a/tests/fixtures/subobjects.py b/tests/fixtures/subobjects.py index 46d95ef1e..b2af7f004 100644 --- a/tests/fixtures/subobjects.py +++ b/tests/fixtures/subobjects.py @@ -122,7 +122,7 @@ def complex_property_node(complex_material_node, complex_condition_node, complex a maximal property sub-object with all possible fields filled """ my_complex_property = cript.Property( - key="modulus_shear", + key="enthalpy", type="value", value=5.0, unit="GPa", @@ -144,7 +144,7 @@ def complex_property_node(complex_material_node, complex_condition_node, complex def complex_property_dict(complex_material_node, complex_condition_dict, complex_citation_dict, complex_data_node, simple_process_node, simple_computation_node) -> dict: ret_dict = { "node": ["Property"], - "key": "modulus_shear", + "key": "enthalpy", "type": "value", "value": 5.0, "unit": "GPa", @@ -165,7 +165,7 @@ def complex_property_dict(complex_material_node, complex_condition_dict, complex @pytest.fixture(scope="function") def simple_property_node() -> cript.Property: my_property = cript.Property( - key="modulus_shear", + key="enthalpy", type="value", value=5.0, unit="GPa", @@ -177,7 +177,7 @@ def simple_property_node() -> cript.Property: def simple_property_dict() -> dict: ret_dict = { "node": ["Property"], - "key": "modulus_shear", + "key": "enthalpy", "type": "value", "value": 5.0, "unit": "GPa", diff --git a/tests/fixtures/supporting_nodes.py b/tests/fixtures/supporting_nodes.py index 2ad7fe25d..6ea4d733c 100644 --- a/tests/fixtures/supporting_nodes.py +++ b/tests/fixtures/supporting_nodes.py @@ -63,7 +63,6 @@ def complex_local_file_node(tmp_path_factory) -> cript.File: def complex_user_dict() -> dict: user_dict = {"node": ["User"]} user_dict["created_at"] = str(datetime.datetime.now()) - user_dict["model_version"] = "1.0.0" user_dict["picture"] = "/my/picture/path" user_dict["updated_at"] = str(datetime.datetime.now()) user_dict["username"] = "testuser" diff --git a/tests/nodes/primary_nodes/test_material.py b/tests/nodes/primary_nodes/test_material.py index 9a858db66..d03c2a9b8 100644 --- a/tests/nodes/primary_nodes/test_material.py +++ b/tests/nodes/primary_nodes/test_material.py @@ -34,7 +34,7 @@ def test_create_complex_material(cript_api, simple_material_node, simple_computa component = [simple_material_node] forcefield = simple_computational_forcefield_node - my_property = [cript.Property(key="modulus_shear", type="min", value=1.23, unit="gram")] + my_property = [cript.Property(key="rho_z", type="min", value=1.23, unit="gram")] my_material = cript.Material( name=material_name, diff --git a/tests/nodes/subobjects/test_property.py b/tests/nodes/subobjects/test_property.py index bd1d0eeb2..65f3c41c3 100644 --- a/tests/nodes/subobjects/test_property.py +++ b/tests/nodes/subobjects/test_property.py @@ -20,8 +20,8 @@ def test_json(complex_property_node, complex_property_dict): def test_setter_getter(complex_property_node, simple_material_node, simple_process_node, complex_condition_node, simple_data_node, simple_computation_node, complex_citation_node): - complex_property_node.key = "modulus_loss" - assert complex_property_node.key == "modulus_loss" + complex_property_node.key = "rho_z" + assert complex_property_node.key == "rho_z" complex_property_node.type = "min" assert complex_property_node.type == "min" diff --git a/tests/nodes/test_utils.py b/tests/nodes/test_utils.py index 81e403701..67faf5b20 100644 --- a/tests/nodes/test_utils.py +++ b/tests/nodes/test_utils.py @@ -35,7 +35,7 @@ def test_load_node_from_json_dict_argument() -> None: "uuid": material_uuid, "name": material_name, "notes": material_notes, - "property": [{"node": ["Property"], "uid": "_:aedce614-7acb-49d2-a2f6-47463f15b707", "uuid": "aedce614-7acb-49d2-a2f6-47463f15b707", "key": "modulus_shear", "type": "value", "value": 5.0, "unit": "GPa"}], + "property": [{"node": ["Property"], "uid": "_:aedce614-7acb-49d2-a2f6-47463f15b707", "uuid": "aedce614-7acb-49d2-a2f6-47463f15b707", "key": "enthalpy", "type": "value", "value": 5.0, "unit": "GPa"}], "computational_forcefield": {"node": ["ComputationalForcefield"], "uid": "_:059952a3-20f2-4739-96bd-a5ea43068065", "uuid": "059952a3-20f2-4739-96bd-a5ea43068065", "key": "amber", "building_block": "atom"}, "keyword": ["acetylene"], "bigsmiles": material_bigsmiles, diff --git a/tests/test_node_util.py b/tests/test_node_util.py index 9419fddaa..6f858318e 100644 --- a/tests/test_node_util.py +++ b/tests/test_node_util.py @@ -33,8 +33,8 @@ def test_uid_deserialization(simple_algorithm_node, complex_parameter_node, simp material = cript.Material(name="my material", bigsmiles="{[][$]CC[$][]}") computation = cript.Computation(name="my computation name", type="analysis") - property1 = cript.Property("modulus_shear", "value", 5.0, "GPa", computation=[computation]) - property2 = cript.Property("modulus_loss", "value", 5.0, "GPa", computation=[computation]) + property1 = cript.Property("enthalpy", "value", 5.0, "GPa", computation=[computation]) + property2 = cript.Property("rho_z", "value", 5.0, "GPa", computation=[computation]) material.property = [property1, property2] material2 = cript.load_nodes_from_json(material.json) @@ -50,7 +50,7 @@ def test_uid_deserialization(simple_algorithm_node, complex_parameter_node, simp "node": ["Property"], "uid": "_:82e7270e-9f35-4b35-80a2-faa6e7f670be", "uuid": "82e7270e-9f35-4b35-80a2-faa6e7f670be", - "key": "modulus_shear", + "key": "enthalpy", "type": "value", "value": 5.0, "unit": "GPa", @@ -60,7 +60,7 @@ def test_uid_deserialization(simple_algorithm_node, complex_parameter_node, simp "node": ["Property"], "uid": "_:fc4dfa5e-742c-4d0b-bb66-2185461f4582", "uuid": "fc4dfa5e-742c-4d0b-bb66-2185461f4582", - "key": "modulus_loss", + "key": "rho_z", "type": "value", "value": 5.0, "unit": "GPa", @@ -92,7 +92,7 @@ def test_uid_deserialization(simple_algorithm_node, complex_parameter_node, simp # ], # "uid": "_:82e7270e-9f35-4b35-80a2-faa6e7f670be", # "uuid": "82e7270e-9f35-4b35-80a2-faa6e7f670be", - # "key": "modulus_shear", + # "key": "enthalpy", # "type": "value", # "value": 5.0, # "unit": "GPa", @@ -111,7 +111,7 @@ def test_uid_deserialization(simple_algorithm_node, complex_parameter_node, simp # ], # "uid": "_:fc4dfa5e-742c-4d0b-bb66-2185461f4582", # "uuid": "fc4dfa5e-742c-4d0b-bb66-2185461f4582", - # "key": "modulus_loss", + # "key": "rho_z", # "type": "value", # "value": 5.0, # "unit": "GPa",