From fb804ed6fcd1ed47b5b864e986c1fb055779bd8e Mon Sep 17 00:00:00 2001 From: Ludwig Schneider Date: Mon, 25 Mar 2024 17:50:55 -0500 Subject: [PATCH 1/4] Error to warning (#445) --- src/cript/api/api.py | 3 + src/cript/exceptions.py | 14 +++++ src/cript/nodes/exceptions.py | 32 +++++------ src/cript/nodes/primary_nodes/material.py | 5 +- src/cript/nodes/primary_nodes/project.py | 7 ++- src/cript/nodes/util/core.py | 64 ++++++++++++---------- tests/test_node_util.py | 67 ++++++++++++++--------- 7 files changed, 115 insertions(+), 77 deletions(-) diff --git a/src/cript/api/api.py b/src/cript/api/api.py index 025863a83..dfa69333b 100644 --- a/src/cript/api/api.py +++ b/src/cript/api/api.py @@ -258,6 +258,9 @@ def _init_logger(self, log_level=logging.INFO) -> None: logger.setLevel(log_level) + # Activate Warning handling + logging.captureWarnings(True) + # Create a console handler console_handler = logging.StreamHandler() diff --git a/src/cript/exceptions.py b/src/cript/exceptions.py index 3891bf646..b5d3e1168 100644 --- a/src/cript/exceptions.py +++ b/src/cript/exceptions.py @@ -10,3 +10,17 @@ class CRIPTException(Exception): @abstractmethod def __str__(self) -> str: pass + + +class CRIPTWarning(Warning): + """ + Parent CRIPT warning. + All CRIPT warning inherit this class. + """ + + @abstractmethod + def __str__(self) -> str: + pass + + def __repr__(self): + return str(self) diff --git a/src/cript/nodes/exceptions.py b/src/cript/nodes/exceptions.py index dfda18352..f24c0fec5 100644 --- a/src/cript/nodes/exceptions.py +++ b/src/cript/nodes/exceptions.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from typing import List -from cript.exceptions import CRIPTException +from cript.exceptions import CRIPTException, CRIPTWarning class CRIPTUUIDException(CRIPTException): @@ -67,7 +67,7 @@ def __str__(self) -> str: return error_message -class CRIPTMaterialIdentifierError(CRIPTException): +class CRIPTMaterialIdentifierWarning(CRIPTWarning): """Every material node needs to have at least one identifier set.""" def __init__(self, material_node): @@ -281,14 +281,14 @@ def __str__(self): ) -class CRIPTOrphanedNodesError(CRIPTException, ABC): +class CRIPTOrphanedNodesWarning(CRIPTWarning, ABC): """ ## Definition This error is raised when a child node is not attached to the appropriate parent node. For example, all material nodes used within a project must belong to the project inventory or are explicitly listed as material of that project. If there is a material node that is used within a project but not a part of the - inventory and the validation code finds it then it raises an `CRIPTOrphanedNodeError` + inventory and the validation code finds it then it raises an `CRIPTOrphanedNodeWarning` ## Troubleshooting Fixing this is simple and easy, just take the node that CRIPT Python SDK @@ -307,10 +307,10 @@ def __str__(self): pass -class CRIPTOrphanedMaterialError(CRIPTOrphanedNodesError): +class CRIPTOrphanedMaterialWarning(CRIPTOrphanedNodesWarning): """ ## Definition - CRIPTOrphanedNodesError, but specific for orphaned materials. + CRIPTOrphanedNodesWarning, but specific for orphaned materials. ## Troubleshooting Handle this error by adding the orphaned materials into the parent project or its inventories. @@ -327,10 +327,10 @@ def __str__(self): return ret_string -class CRIPTOrphanedExperimentError(CRIPTOrphanedNodesError): +class CRIPTOrphanedExperimentWarning(CRIPTOrphanedNodesWarning): """ ## Definition - CRIPTOrphanedNodesError, but specific for orphaned nodes that should be listed in one of the experiments. + CRIPTOrphanedNodesWarning, but specific for orphaned nodes that should be listed in one of the experiments. ## Troubleshooting Handle this error by adding the orphaned node into one the parent project's experiments. @@ -348,10 +348,10 @@ def __str__(self) -> str: return ret_string -class CRIPTOrphanedDataError(CRIPTOrphanedExperimentError): +class CRIPTOrphanedDataWarning(CRIPTOrphanedExperimentWarning): """ ## Definition - CRIPTOrphanedExperimentError, but specific for orphaned Data node that should be listed in one of the experiments. + CRIPTOrphanedExperimentWarning, but specific for orphaned Data node that should be listed in one of the experiments. ## Troubleshooting Handle this error by adding the orphaned node into one the parent project's experiments `data` attribute. @@ -361,10 +361,10 @@ def __init__(self, orphaned_node): super().__init__(orphaned_node) -class CRIPTOrphanedProcessError(CRIPTOrphanedExperimentError): +class CRIPTOrphanedProcessWarning(CRIPTOrphanedExperimentWarning): """ ## Definition - CRIPTOrphanedExperimentError, but specific for orphaned Process node that should be + CRIPTOrphanedExperimentWarning, but specific for orphaned Process node that should be listed in one of the experiments. ## Troubleshooting @@ -376,10 +376,10 @@ def __init__(self, orphaned_node): super().__init__(orphaned_node) -class CRIPTOrphanedComputationError(CRIPTOrphanedExperimentError): +class CRIPTOrphanedComputationWarning(CRIPTOrphanedExperimentWarning): """ ## Definition - CRIPTOrphanedExperimentError, but specific for orphaned Computation node that should be + CRIPTOrphanedExperimentWarning, but specific for orphaned Computation node that should be listed in one of the experiments. ## Troubleshooting @@ -391,10 +391,10 @@ def __init__(self, orphaned_node): super().__init__(orphaned_node) -class CRIPTOrphanedComputationalProcessError(CRIPTOrphanedExperimentError): +class CRIPTOrphanedComputationalProcessWarning(CRIPTOrphanedExperimentWarning): """ ## Definition - CRIPTOrphanedExperimentError, but specific for orphaned ComputationalProcess + CRIPTOrphanedExperimentWarning, but specific for orphaned ComputationalProcess node that should be listed in one of the experiments. ## Troubleshooting diff --git a/src/cript/nodes/primary_nodes/material.py b/src/cript/nodes/primary_nodes/material.py index e0ae0968c..2c6c6a728 100644 --- a/src/cript/nodes/primary_nodes/material.py +++ b/src/cript/nodes/primary_nodes/material.py @@ -1,9 +1,10 @@ +import warnings from dataclasses import dataclass, field, replace from typing import Any, List, Optional, Union from beartype import beartype -from cript.nodes.exceptions import CRIPTMaterialIdentifierError +from cript.nodes.exceptions import CRIPTMaterialIdentifierWarning from cript.nodes.primary_nodes.primary_base_node import PrimaryBaseNode from cript.nodes.primary_nodes.process import Process from cript.nodes.util.json import UIDProxy @@ -218,7 +219,7 @@ def validate(self, api=None, is_patch: bool = False, force_validation: bool = Fa and self.smiles is None and self.vendor is None ): - raise CRIPTMaterialIdentifierError(self) + warnings.warn(CRIPTMaterialIdentifierWarning(self)) @property @beartype diff --git a/src/cript/nodes/primary_nodes/project.py b/src/cript/nodes/primary_nodes/project.py index 2be3056b3..44a7ace42 100644 --- a/src/cript/nodes/primary_nodes/project.py +++ b/src/cript/nodes/primary_nodes/project.py @@ -1,3 +1,4 @@ +import warnings from dataclasses import dataclass, field, replace from typing import List, Optional, Union @@ -106,7 +107,7 @@ def __init__(self, name: str, collection: Optional[List[Union[Collection, UIDPro self._update_json_attrs_if_valid(new_json_attrs) def validate(self, api=None, is_patch=False, force_validation: bool = False): - from cript.nodes.exceptions import CRIPTOrphanedMaterialError + from cript.nodes.exceptions import CRIPTOrphanedMaterialWarning from cript.nodes.util.core import get_orphaned_experiment_exception # First validate like other nodes @@ -122,7 +123,7 @@ def validate(self, api=None, is_patch=False, force_validation: bool = False): project_inventory_materials.append(material) for material in project_graph_materials: if material not in self.material and material not in project_inventory_materials: - raise CRIPTOrphanedMaterialError(material) + warnings.warn(CRIPTOrphanedMaterialWarning(material)) # Check graph for orphaned nodes, that should be listed in the experiments project_experiments = self.find_children({"node": ["Experiment"]}) @@ -145,7 +146,7 @@ def validate(self, api=None, is_patch=False, force_validation: bool = False): experiment_nodes.append(ex_node) for node in project_graph_nodes: if node not in experiment_nodes: - raise get_orphaned_experiment_exception(node) + warnings.warn(get_orphaned_experiment_exception(node)) @property @beartype diff --git a/src/cript/nodes/util/core.py b/src/cript/nodes/util/core.py index e1b5facc6..c212dc946 100644 --- a/src/cript/nodes/util/core.py +++ b/src/cript/nodes/util/core.py @@ -1,12 +1,13 @@ import uuid +import warnings from cript.nodes.exceptions import ( - CRIPTOrphanedComputationalProcessError, - CRIPTOrphanedComputationError, - CRIPTOrphanedDataError, - CRIPTOrphanedExperimentError, - CRIPTOrphanedMaterialError, - CRIPTOrphanedProcessError, + CRIPTOrphanedComputationalProcessWarning, + CRIPTOrphanedComputationWarning, + CRIPTOrphanedDataWarning, + CRIPTOrphanedExperimentWarning, + CRIPTOrphanedMaterialWarning, + CRIPTOrphanedProcessWarning, ) @@ -27,25 +28,28 @@ def add_orphaned_nodes_to_project(project, active_experiment, max_iteration: int raise RuntimeError(f"The provided active experiment {active_experiment} is not part of the project graph. Choose an active experiment that is part of a collection of this project.") counter = 0 - while True: - if counter > max_iteration >= 0: - break # Emergency stop - try: - project.validate() - except CRIPTOrphanedMaterialError as exc: - # because calling the setter calls `validate` we have to force add the material. - project._json_attrs.material.append(exc.orphaned_node) - except CRIPTOrphanedDataError as exc: - active_experiment.data += [exc.orphaned_node] - except CRIPTOrphanedProcessError as exc: - active_experiment.process += [exc.orphaned_node] - except CRIPTOrphanedComputationError as exc: - active_experiment.computation += [exc.orphaned_node] - except CRIPTOrphanedComputationalProcessError as exc: - active_experiment.computation_process += [exc.orphaned_node] - else: - break - counter += 1 + # Convert Errors into exceptions, so we can catch and fix them + with warnings.catch_warnings(): + warnings.simplefilter("error") + while True: + if counter > max_iteration >= 0: + break # Emergency stop + try: + project.validate() + except CRIPTOrphanedMaterialWarning as exc: + # because calling the setter calls `validate` we have to force add the material. + project._json_attrs.material.append(exc.orphaned_node) + except CRIPTOrphanedDataWarning as exc: + active_experiment.data += [exc.orphaned_node] + except CRIPTOrphanedProcessWarning as exc: + active_experiment.process += [exc.orphaned_node] + except CRIPTOrphanedComputationWarning as exc: + active_experiment.computation += [exc.orphaned_node] + except CRIPTOrphanedComputationalProcessWarning as exc: + active_experiment.computation_process += [exc.orphaned_node] + else: + break + counter += 1 def get_orphaned_experiment_exception(orphaned_node): @@ -58,15 +62,15 @@ def get_orphaned_experiment_exception(orphaned_node): from cript.nodes.primary_nodes.process import Process if isinstance(orphaned_node, Data): - return CRIPTOrphanedDataError(orphaned_node) + return CRIPTOrphanedDataWarning(orphaned_node) if isinstance(orphaned_node, Process): - return CRIPTOrphanedProcessError(orphaned_node) + return CRIPTOrphanedProcessWarning(orphaned_node) if isinstance(orphaned_node, Computation): - return CRIPTOrphanedComputationError(orphaned_node) + return CRIPTOrphanedComputationWarning(orphaned_node) if isinstance(orphaned_node, ComputationProcess): - return CRIPTOrphanedComputationalProcessError(orphaned_node) + return CRIPTOrphanedComputationalProcessWarning(orphaned_node) # Base case raise the parent exception. TODO add bug warning. - return CRIPTOrphanedExperimentError(orphaned_node) + return CRIPTOrphanedExperimentWarning(orphaned_node) def iterate_leaves(obj): diff --git a/tests/test_node_util.py b/tests/test_node_util.py index a1125ceda..9419fddaa 100644 --- a/tests/test_node_util.py +++ b/tests/test_node_util.py @@ -1,5 +1,6 @@ import copy import json +import warnings from dataclasses import replace import pytest @@ -10,11 +11,11 @@ CRIPTJsonNodeError, CRIPTJsonSerializationError, CRIPTNodeSchemaError, - CRIPTOrphanedComputationalProcessError, - CRIPTOrphanedComputationError, - CRIPTOrphanedDataError, - CRIPTOrphanedMaterialError, - CRIPTOrphanedProcessError, + CRIPTOrphanedComputationalProcessWarning, + CRIPTOrphanedComputationWarning, + CRIPTOrphanedDataWarning, + CRIPTOrphanedMaterialWarning, + CRIPTOrphanedProcessWarning, ) from tests.utils.util import strip_uid_from_dict @@ -245,26 +246,30 @@ def test_invalid_project_graphs(simple_project_node, simple_material_node, simpl # Add the process to the experiment, but not in inventory or materials # Invalid graph project.collection[0].experiment[0].process += [process] - with pytest.raises(CRIPTOrphanedMaterialError): + with pytest.warns(CRIPTOrphanedMaterialWarning): project.validate() # First fix add material to inventory project.collection[0].inventory += [cript.Inventory("test_inventory", material=[material])] - project.validate() + with warnings.catch_warnings(): + warnings.simplefilter("error") + project.validate() # Reverse this fix project.collection[0].inventory = [] - with pytest.raises(CRIPTOrphanedMaterialError): + with pytest.warns(CRIPTOrphanedMaterialWarning): project.validate() # Fix by add to the materials list instead. # Using the util helper function for this. cript.add_orphaned_nodes_to_project(project, active_experiment=None, max_iteration=10) - project.validate() + with warnings.catch_warnings(): + warnings.simplefilter("error") + project.validate() # Now add an orphan process to the graph process2 = copy.deepcopy(simple_process_node) process.prerequisite_process += [process2] - with pytest.raises(CRIPTOrphanedProcessError): + with pytest.warns(CRIPTOrphanedProcessWarning): project.validate() # Wrong fix it helper node @@ -272,48 +277,58 @@ def test_invalid_project_graphs(simple_project_node, simple_material_node, simpl with pytest.raises(RuntimeError): cript.add_orphaned_nodes_to_project(project, dummy_experiment) # Problem still persists - with pytest.raises(CRIPTOrphanedProcessError): + with pytest.warns(CRIPTOrphanedProcessWarning): project.validate() # Fix by using the helper function correctly cript.add_orphaned_nodes_to_project(project, project.collection[0].experiment[0], 10) - project.validate() + with warnings.catch_warnings(): + warnings.simplefilter("error") + project.validate() # We add property to the material, because that adds the opportunity for orphaned data and computation property = copy.deepcopy(simple_property_node) material.property += [property] - project.validate() + with warnings.catch_warnings(): + warnings.simplefilter("error") + project.validate() # Now add an orphan data data = copy.deepcopy(simple_data_node) property.data = [data] - with pytest.raises(CRIPTOrphanedDataError): + with pytest.warns(CRIPTOrphanedDataWarning): project.validate() # Fix with the helper function cript.add_orphaned_nodes_to_project(project, project.collection[0].experiment[0], 10) - project.validate() + with warnings.catch_warnings(): + warnings.simplefilter("error") + project.validate() # Add an orphan Computation computation = copy.deepcopy(simple_computation_node) property.computation += [computation] - with pytest.raises(CRIPTOrphanedComputationError): + with pytest.warns(CRIPTOrphanedComputationWarning): project.validate() # Fix with the helper function cript.add_orphaned_nodes_to_project(project, project.collection[0].experiment[0], 10) - project.validate() + with warnings.catch_warnings(): + warnings.simplefilter("error") + project.validate() # Add orphan computational process comp_proc = copy.deepcopy(simple_computation_process_node) data.computation_process += [comp_proc] - with pytest.raises(CRIPTOrphanedComputationalProcessError): + with pytest.raises(CRIPTOrphanedComputationalProcessWarning): while True: - try: # Do trigger not orphan materials - project.validate() - except CRIPTOrphanedMaterialError as exc: - project._json_attrs.material.append(exc.orphaned_node) - except CRIPTOrphanedProcessError as exc: - project.collection[0].experiment[0]._json_attrs.process.append(exc.orphaned_node) - else: - break + with warnings.catch_warnings(): + warnings.simplefilter("error") + try: # Do trigger not orphan materials + project.validate() + except CRIPTOrphanedMaterialWarning as exc: + project._json_attrs.material.append(exc.orphaned_node) + except CRIPTOrphanedProcessWarning as exc: + project.collection[0].experiment[0]._json_attrs.process.append(exc.orphaned_node) + else: + break cript.add_orphaned_nodes_to_project(project, project.collection[0].experiment[0], 10) project.validate() From dd4004f21786113d177bae133672f9c7d5bb8c8d Mon Sep 17 00:00:00 2001 From: Ludwig Schneider Date: Fri, 17 May 2024 07:28:10 -0500 Subject: [PATCH 2/4] 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", From 78cd694d9e79b5113ed7036b1254bf15041386a9 Mon Sep 17 00:00:00 2001 From: Ludwig Schneider Date: Mon, 20 May 2024 11:13:19 -0500 Subject: [PATCH 3/4] version bump --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index b138a20a3..57d9258e3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = cript -version = 2.3.0 +version = 2.4.0 description = CRIPT Python SDK long_description = file: README.md long_description_content_type = text/markdown From 4693b7c88abf47db9a8fe42b2fd3fe0f6b606f83 Mon Sep 17 00:00:00 2001 From: Ludwig Schneider Date: Mon, 20 May 2024 11:49:08 -0500 Subject: [PATCH 4/4] do changelog --- .trunk/configs/.cspell.json | 5 +- CHANGELOG.md | 96 +++++++++++++++++++++++++++++++++++-- tests/api/test_search.py | 3 +- 3 files changed, 96 insertions(+), 8 deletions(-) diff --git a/.trunk/configs/.cspell.json b/.trunk/configs/.cspell.json index ccdbc4d40..59538e697 100644 --- a/.trunk/configs/.cspell.json +++ b/.trunk/configs/.cspell.json @@ -123,6 +123,9 @@ "Doctests", "linenums", "XLYOFNOQVPJJNP", - "CRIPTUUID" + "CRIPTUUID", + "rootdir", + "startswith", + "pluggy" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index c61573261..304380cac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,94 @@ # CRIPT Python SDK Changelog +## Version 2.4.0 + +### New Features + +- `cript.API` searches work as intended. +- SDK initiated searches can be controlled and interrupted for large queries. + +### Known Issues and Bugs + +- Saving projects is not supported. Temporarily, you can use `get_expanded_json` to store a JSON representation of projects, which can be uploaded into CRIPT at a later time. +- Permission settings in CRIPT do not influence the behavior of the SDK objects. +- Tests that require valid tokens (like saving or searching) are not included in CI/CD tests. +- BigSMILES searches can contain duplicates. + +### Bug Fixes + +- Search is now matched with the API requirements. + +### Breaking Changes + +- The Paginator interface changed to reflect the backend requirements. + +### Health Report + +````shell +=============================================================================================== test session starts =============================================================================================== +platform linux -- Python 3.11.2, pytest-7.4.3, pluggy-1.3.0 +rootdir: /home/ludwig/git/Python-SDK +plugins: cov-4.1.0 +collected 124 items + +tests/test_node_util.py ........... [ 8%] +tests/api/test_api.py ..... [ 12%] +tests/api/test_db_schema.py ..... [ 16%] +tests/api/test_search.py .....F [ 21%] +tests/nodes/test_utils.py .. [ 23%] +tests/nodes/primary_nodes/test_collection.py .....F [ 28%] +tests/nodes/primary_nodes/test_computation.py .....F [ 33%] +tests/nodes/primary_nodes/test_computational_process.py ....F [ 37%] +tests/nodes/primary_nodes/test_data.py ....F [ 41%] +tests/nodes/primary_nodes/test_experiment.py ....F [ 45%] +tests/nodes/primary_nodes/test_inventory.py ..F [ 47%] +tests/nodes/primary_nodes/test_material.py ...F [ 50%] +tests/nodes/primary_nodes/test_process.py .....F [ 55%] +tests/nodes/primary_nodes/test_project.py ...F [ 58%] +tests/nodes/primary_nodes/test_reference.py ......F [ 64%] +tests/nodes/subobjects/test_algorithm.py ..F [ 66%] +tests/nodes/subobjects/test_citation.py ..F [ 69%] +tests/nodes/subobjects/test_computational_forcefield.py ..F [ 71%] +tests/nodes/subobjects/test_condition.py ..F [ 74%] +tests/nodes/subobjects/test_equipment.py ..F [ 76%] +tests/nodes/subobjects/test_ingredient.py ..F [ 79%] +tests/nodes/subobjects/test_parameter.py ..F [ 81%] +tests/nodes/subobjects/test_property.py ..F [ 83%] +tests/nodes/subobjects/test_quantity.py ..F [ 86%] +tests/nodes/subobjects/test_software.py ...F [ 89%] +tests/nodes/subobjects/test_software_configuration.py ..F [ 91%] +tests/nodes/supporting_nodes/test_file.py ..s....F [ 98%] +tests/nodes/supporting_nodes/test_user.py .. + +FAILED tests/nodes/primary_nodes/test_collection.py::test_integration_collection - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/primary_nodes/test_computation.py::test_integration_computation - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/primary_nodes/test_computational_process.py::test_integration_computational_process - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/primary_nodes/test_data.py::test_integration_data - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/primary_nodes/test_experiment.py::test_integration_experiment - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/primary_nodes/test_inventory.py::test_integration_inventory - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/primary_nodes/test_material.py::test_integration_material - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/primary_nodes/test_process.py::test_integration_complex_process - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/primary_nodes/test_project.py::test_integration_project - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/primary_nodes/test_reference.py::test_integration_reference - IndexError: list index out of range +FAILED tests/nodes/subobjects/test_algorithm.py::test_integration_algorithm - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/subobjects/test_citation.py::test_integration_citation - IndexError: list index out of range +FAILED tests/nodes/subobjects/test_computational_forcefield.py::test_integration_computational_forcefield - AttributeError: 'NoneType' object has no attribute 'description' +FAILED tests/nodes/subobjects/test_condition.py::test_integration_process_condition - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/subobjects/test_equipment.py::test_integration_equipment - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/subobjects/test_ingredient.py::test_integration_ingredient - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/subobjects/test_parameter.py::test_integration_parameter - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/subobjects/test_property.py::test_integration_material_property - IndexError: list index out of range +FAILED tests/nodes/subobjects/test_quantity.py::test_integration_quantity - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/subobjects/test_software.py::test_integration_software - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/subobjects/test_software_configuration.py::test_integration_software_configuration - AttributeError: 'list' object has no attribute 'startswith' +FAILED tests/nodes/supporting_nodes/test_file.py::test_integration_file - AttributeError: 'list' object has no attribute 'startswith' +======================================================================== 22 failed, 101 passed, 1 skipped, 1 warning in 1506.18s (0:25:06) ======================================================================== + + + ## Version 2.3.0 -**New Features:** +### New Features - `cript.API` objects now have a `DataSchema` attribute called `schema`, representing the JSON schema for node validation. - This includes the ability to enable and disable node validation. @@ -19,7 +105,7 @@ - Debugging messages show full API requests and responses for debugging. - Automated UUID caching for nodes can now be explicitly circumvented when using `cript.load_nodes_from_json`. Mostly useful for development. -**Known Issues and Bugs:** +### Known Issues and Bugs - Saving projects is not supported. Temporarily, you can use `get_expanded_json` to store a JSON representation of projects, which can be uploaded into CRIPT at a later time. - BigSMILES search patterns are not supported. @@ -27,14 +113,14 @@ - Permission settings in CRIPT do not influence the behavior of the SDK objects. - Tests that require valid tokens (like saving or searching) are not included in CI/CD tests. -**Bugfixes:** +### Bugfixes - `cript.load_nodes_from_json` can now load JSON files that store different nodes in lists or dictionaries. - Not all nodes were correctly validated at all times, especially if instantiated from JSON. All nodes are automatically validated now. - The documentation has been updated to remove certain mistakes. - Users can have only one Python object with the same UUID to avoid mis-updates. This did not work in all cases, but it works in all cases now. -**Breaking Changes:** +### Breaking Changes - `cript.API()` objects no longer have functions related to JSON schema validation. Please use the new `DataSchema` class instead. The `DataSchema` class can be accessed via the `schema` property of the API class. - Indirect logging control via the API is defunct. Please use the direct access to the `logger` attribute of API classes to control logging output. @@ -102,4 +188,4 @@ FAILED tests/nodes/subobjects/test_software.py::test_integration_software - Attr FAILED tests/nodes/subobjects/test_software_configuration.py::test_integration_software_configuration - AttributeError: 'list' object has no attribute 'starts with' FAILED tests/nodes/supporting_nodes/test_file.py::test_integration_file - AttributeError: 'list' object has no attribute 'starts with' ======================================================================== 23 failed, 100 passed, 1 skipped, 1 warning in 905.51s (0:15:05) ========================================================================= -``` +```` diff --git a/tests/api/test_search.py b/tests/api/test_search.py index 4b9909eba..4a674a7ac 100644 --- a/tests/api/test_search.py +++ b/tests/api/test_search.py @@ -62,8 +62,7 @@ def test_api_search_node_type(cript_api: cript.API) -> None: if mat["uuid"] in uuid_list: print(mat["uuid"]) - # TODO enable duplicate test - # assert mat["uuid"] not in uuid_list + assert mat["uuid"] not in uuid_list assert i < 21