diff --git a/src/cript/nodes/core.py b/src/cript/nodes/core.py index 584e1b349..0a74fa02d 100644 --- a/src/cript/nodes/core.py +++ b/src/cript/nodes/core.py @@ -4,7 +4,7 @@ import re import uuid from abc import ABC -from dataclasses import asdict, dataclass, replace +from dataclasses import dataclass, replace from typing import Dict, List, Optional, Set from cript.nodes.exceptions import ( @@ -12,6 +12,7 @@ CRIPTExtraJsonAttributes, CRIPTJsonSerializationError, ) +from cript.nodes.node_iterator import NodeIterator tolerated_extra_json = [] @@ -102,7 +103,7 @@ def __str__(self) -> str: str A string representation of the node. """ - return str(asdict(self._json_attrs)) + return str(self._json_attrs) @property def uid(self): @@ -200,6 +201,7 @@ def _from_json(cls, json_dict: dict): attrs = replace(attrs, uid="_:" + attrs.uid) except AttributeError: pass + # But here we force even usually unwritable fields to be set. node._update_json_attrs_if_valid(attrs) @@ -466,7 +468,7 @@ class ReturnTuple: if is_patch: del tmp_dict["uuid"] # patches do not allow UUID is the parent most node - return ReturnTuple(json.dumps(tmp_dict), tmp_dict, NodeEncoder.handled_ids) + return ReturnTuple(json.dumps(tmp_dict, **kwargs), tmp_dict, NodeEncoder.handled_ids) except Exception as exc: # TODO this handling that doesn't tell the user what happened and how they can fix it # this just tells the user that something is wrong @@ -600,40 +602,18 @@ def is_attr_present(node: BaseNode, key, value): if handled_nodes is None: handled_nodes = [] - # Protect against cycles in graph, by handling every instance of a node only once - if self in handled_nodes: - return [] - handled_nodes += [self] - found_children = [] - # In this search we include the calling node itself. - # We check for this node if all specified attributes are present by counting them (AND condition). - found_attr = 0 - for key, value in search_attr.items(): - if is_attr_present(self, key, value): - found_attr += 1 - # If exactly all attributes are found, it matches the search criterion - if found_attr == len(search_attr): - found_children += [self] - - # Recursion according to the recursion depth for all node children. - if search_depth != 0: - # Loop over all attributes, runtime contribution (none, or constant (max number of attributes of a node) - for field in self._json_attrs.__dataclass_fields__: - value = getattr(self._json_attrs, field) - # To save code paths, I convert non-lists into lists with one element. - if not isinstance(value, list): - value = [value] - # Run time contribution: number of elements in the attribute list. - for v in value: - try: # Try every attribute for recursion (duck-typing) - found_children += v.find_children(search_attr, search_depth - 1, handled_nodes=handled_nodes) - except AttributeError: - pass - # Total runtime, of non-recursive call: O(m*h) + O(k) where k is the number of children for this node, - # h being the depth of the search dictionary, m being the number of nodes in the attribute list. - # Total runtime, with recursion: O(n*(k+m*h). A full graph traversal O(n) with a cost per node, that scales with the number of children per node and the search depth of the search dictionary. + node_iterator = NodeIterator(self, search_depth) + for node in node_iterator: + found_attr = 0 + for key, value in search_attr.items(): + if is_attr_present(node, key, value): + found_attr += 1 + # If exactly all attributes are found, it matches the search criterion + if found_attr == len(search_attr): + found_children += [node] + return found_children def remove_child(self, child) -> bool: diff --git a/src/cript/nodes/node_iterator.py b/src/cript/nodes/node_iterator.py new file mode 100644 index 000000000..b99f8e231 --- /dev/null +++ b/src/cript/nodes/node_iterator.py @@ -0,0 +1,68 @@ +from dataclasses import fields +from typing import Any, List, Set + + +class NodeIterator: + def __init__(self, root, max_recursion_depth=-1): + self._iter_position: int = 0 + self._uuid_visited: Set[str] = set() + self._stack: List[Any] = [] + self._recursion_depth = [] + self._max_recursion_depth = max_recursion_depth + self._depth_first(root, 0) + + def _add_node(self, child_node, recursion_depth: int): + if child_node.uuid not in self._uuid_visited: + self._stack.append(child_node) + self._recursion_depth.append(recursion_depth) + self._uuid_visited.add(child_node.uuid) + return True + return False + + def _check_recursion(self, child_node) -> bool: + """Helper function that adds a child to the stack. + + This function can be called for both listed children and regular children attributes + """ + try: + uuid = child_node.uuid + except AttributeError: + return False + + if uuid not in self._uuid_visited: + return True + return False + + def _depth_first(self, node, recursion_depth: int) -> None: + """Helper function that does the traversal in depth first order and stores result in stack""" + node_added = self._add_node(node, recursion_depth) + if not node_added: + return + + if self._max_recursion_depth >= 0 and recursion_depth >= self._max_recursion_depth: + return + + field_names = [field.name for field in fields(node._json_attrs)] + for attr_name in sorted(field_names): + attr = getattr(node._json_attrs, attr_name) + if not isinstance(attr, list): + attr = [attr] + for list_attr in attr: + if self._check_recursion(list_attr): + self._depth_first(list_attr, recursion_depth + 1) + + def __next__(self): + if self._iter_position >= len(self._stack): + raise StopIteration + self._iter_position += 1 + return self._stack[self._iter_position - 1] + + def __iter__(self): + self._iter_position = 0 + return self + + def __len__(self): + return len(self._stack) + + def __getitem__(self, idx: int): + return self._stack[idx] diff --git a/src/cript/nodes/primary_nodes/collection.py b/src/cript/nodes/primary_nodes/collection.py index daa7e6278..6ae9f743c 100644 --- a/src/cript/nodes/primary_nodes/collection.py +++ b/src/cript/nodes/primary_nodes/collection.py @@ -1,10 +1,11 @@ from dataclasses import dataclass, field, replace -from typing import Any, List, Optional +from typing import Any, List, Optional, Union from beartype import beartype from cript.nodes.primary_nodes.primary_base_node import PrimaryBaseNode from cript.nodes.supporting_nodes import User +from cript.nodes.util.json import UIDProxy class Collection(PrimaryBaseNode): @@ -56,17 +57,19 @@ class JsonAttributes(PrimaryBaseNode.JsonAttributes): """ # TODO add proper typing in future, using Any for now to avoid circular import error - member: List[User] = field(default_factory=list) - admin: List[User] = field(default_factory=list) - experiment: List[Any] = field(default_factory=list) - inventory: List[Any] = field(default_factory=list) + member: List[Union[User, UIDProxy]] = field(default_factory=list) + admin: List[Union[User, UIDProxy]] = field(default_factory=list) + experiment: List[Union[Any, UIDProxy]] = field(default_factory=list) + inventory: List[Union[Any, UIDProxy]] = field(default_factory=list) doi: str = "" - citation: List[Any] = field(default_factory=list) + citation: List[Union[Any, UIDProxy]] = field(default_factory=list) _json_attrs: JsonAttributes = JsonAttributes() @beartype - def __init__(self, name: str, experiment: Optional[List[Any]] = None, inventory: Optional[List[Any]] = None, doi: str = "", citation: Optional[List[Any]] = None, notes: str = "", **kwargs) -> None: + def __init__( + self, name: str, experiment: Optional[List[Union[Any, UIDProxy]]] = None, inventory: Optional[List[Union[Any, UIDProxy]]] = None, doi: str = "", citation: Optional[List[Union[Any, UIDProxy]]] = None, notes: str = "", **kwargs + ) -> None: """ create a Collection with a name add list of experiment, inventory, citation, doi, and notes if available. @@ -117,12 +120,12 @@ def __init__(self, name: str, experiment: Optional[List[Any]] = None, inventory: @property @beartype - def member(self) -> List[User]: + def member(self) -> List[Union[User, UIDProxy]]: return self._json_attrs.member.copy() @property @beartype - def admin(self) -> List[User]: + def admin(self) -> List[Union[User, UIDProxy]]: return self._json_attrs.admin @property diff --git a/src/cript/nodes/primary_nodes/computation.py b/src/cript/nodes/primary_nodes/computation.py index 87d9b317a..89b0ac437 100644 --- a/src/cript/nodes/primary_nodes/computation.py +++ b/src/cript/nodes/primary_nodes/computation.py @@ -1,9 +1,10 @@ from dataclasses import dataclass, field, replace -from typing import Any, List, Optional +from typing import Any, List, Optional, Union from beartype import beartype from cript.nodes.primary_nodes.primary_base_node import PrimaryBaseNode +from cript.nodes.util.json import UIDProxy class Computation(PrimaryBaseNode): @@ -64,12 +65,12 @@ class JsonAttributes(PrimaryBaseNode.JsonAttributes): type: str = "" # TODO add proper typing in future, using Any for now to avoid circular import error - input_data: List[Any] = field(default_factory=list) - output_data: List[Any] = field(default_factory=list) - software_configuration: List[Any] = field(default_factory=list) - condition: List[Any] = field(default_factory=list) - prerequisite_computation: Optional["Computation"] = None - citation: List[Any] = field(default_factory=list) + input_data: List[Union[Any, UIDProxy]] = field(default_factory=list) + output_data: List[Union[Any, UIDProxy]] = field(default_factory=list) + software_configuration: List[Union[Any, UIDProxy]] = field(default_factory=list) + condition: List[Union[Any, UIDProxy]] = field(default_factory=list) + prerequisite_computation: Optional[Union["Computation", UIDProxy]] = None + citation: List[Union[Any, UIDProxy]] = field(default_factory=list) _json_attrs: JsonAttributes = JsonAttributes() @@ -78,12 +79,12 @@ def __init__( self, name: str, type: str, - input_data: Optional[List[Any]] = None, - output_data: Optional[List[Any]] = None, - software_configuration: Optional[List[Any]] = None, - condition: Optional[List[Any]] = None, - prerequisite_computation: Optional["Computation"] = None, - citation: Optional[List[Any]] = None, + input_data: Optional[List[Union[Any, UIDProxy]]] = None, + output_data: Optional[List[Union[Any, UIDProxy]]] = None, + software_configuration: Optional[List[Union[Any, UIDProxy]]] = None, + condition: Optional[List[Union[Any, UIDProxy]]] = None, + prerequisite_computation: Optional[Union["Computation", UIDProxy]] = None, + citation: Optional[List[Union[Any, UIDProxy]]] = None, notes: str = "", **kwargs ) -> None: @@ -364,7 +365,7 @@ def condition(self, new_condition_list: List[Any]) -> None: @property @beartype - def prerequisite_computation(self) -> Optional["Computation"]: + def prerequisite_computation(self) -> Optional[Union["Computation", UIDProxy]]: """ prerequisite computation @@ -386,7 +387,7 @@ def prerequisite_computation(self) -> Optional["Computation"]: @prerequisite_computation.setter @beartype - def prerequisite_computation(self, new_prerequisite_computation: Optional["Computation"]) -> None: + def prerequisite_computation(self, new_prerequisite_computation: Optional[Union["Computation", UIDProxy]]) -> None: """ set new prerequisite_computation diff --git a/src/cript/nodes/primary_nodes/computation_process.py b/src/cript/nodes/primary_nodes/computation_process.py index c08f853c6..a514e898f 100644 --- a/src/cript/nodes/primary_nodes/computation_process.py +++ b/src/cript/nodes/primary_nodes/computation_process.py @@ -1,9 +1,10 @@ from dataclasses import dataclass, field, replace -from typing import Any, List, Optional +from typing import Any, List, Optional, Union from beartype import beartype from cript.nodes.primary_nodes.primary_base_node import PrimaryBaseNode +from cript.nodes.util.json import UIDProxy class ComputationProcess(PrimaryBaseNode): @@ -113,13 +114,13 @@ class JsonAttributes(PrimaryBaseNode.JsonAttributes): type: str = "" # TODO add proper typing in future, using Any for now to avoid circular import error - input_data: List[Any] = field(default_factory=list) - output_data: List[Any] = field(default_factory=list) - ingredient: List[Any] = field(default_factory=list) - software_configuration: List[Any] = field(default_factory=list) - condition: List[Any] = field(default_factory=list) - property: List[Any] = field(default_factory=list) - citation: List[Any] = field(default_factory=list) + input_data: List[Union[Any, UIDProxy]] = field(default_factory=list) + output_data: List[Union[Any, UIDProxy]] = field(default_factory=list) + ingredient: List[Union[Any, UIDProxy]] = field(default_factory=list) + software_configuration: List[Union[Any, UIDProxy]] = field(default_factory=list) + condition: List[Union[Any, UIDProxy]] = field(default_factory=list) + property: List[Union[Any, UIDProxy]] = field(default_factory=list) + citation: List[Union[Any, UIDProxy]] = field(default_factory=list) _json_attrs: JsonAttributes = JsonAttributes() @@ -128,13 +129,13 @@ def __init__( self, name: str, type: str, - input_data: List[Any], - ingredient: List[Any], - output_data: Optional[List[Any]] = None, - software_configuration: Optional[List[Any]] = None, - condition: Optional[List[Any]] = None, - property: Optional[List[Any]] = None, - citation: Optional[List[Any]] = None, + input_data: List[Union[Any, UIDProxy]], + ingredient: List[Union[Any, UIDProxy]], + output_data: Optional[List[Union[Any, UIDProxy]]] = None, + software_configuration: Optional[List[Union[Any, UIDProxy]]] = None, + condition: Optional[List[Union[Any, UIDProxy]]] = None, + property: Optional[List[Union[Any, UIDProxy]]] = None, + citation: Optional[List[Union[Any, UIDProxy]]] = None, notes: str = "", **kwargs ): diff --git a/src/cript/nodes/primary_nodes/data.py b/src/cript/nodes/primary_nodes/data.py index e631dd27b..ac59dc870 100644 --- a/src/cript/nodes/primary_nodes/data.py +++ b/src/cript/nodes/primary_nodes/data.py @@ -4,6 +4,7 @@ from beartype import beartype from cript.nodes.primary_nodes.primary_base_node import PrimaryBaseNode +from cript.nodes.util.json import UIDProxy class Data(PrimaryBaseNode): @@ -74,13 +75,13 @@ class JsonAttributes(PrimaryBaseNode.JsonAttributes): type: str = "" # TODO add proper typing in future, using Any for now to avoid circular import error - file: List[Any] = field(default_factory=list) - sample_preparation: Any = field(default_factory=list) - computation: List[Any] = field(default_factory=list) - computation_process: Any = field(default_factory=list) - material: List[Any] = field(default_factory=list) - process: List[Any] = field(default_factory=list) - citation: List[Any] = field(default_factory=list) + file: List[Union[Any, UIDProxy]] = field(default_factory=list) + sample_preparation: Union[Any, UIDProxy] = field(default_factory=list) + computation: List[Union[Any, UIDProxy]] = field(default_factory=list) + computation_process: Union[Any, UIDProxy] = field(default_factory=list) + material: List[Union[Any, UIDProxy]] = field(default_factory=list) + process: List[Union[Any, UIDProxy]] = field(default_factory=list) + citation: List[Union[Any, UIDProxy]] = field(default_factory=list) _json_attrs: JsonAttributes = JsonAttributes() @@ -89,13 +90,13 @@ def __init__( self, name: str, type: str, - file: List[Any], - sample_preparation: Any = None, - computation: Optional[List[Any]] = None, - computation_process: Optional[Any] = None, - material: Optional[List[Any]] = None, - process: Optional[List[Any]] = None, - citation: Optional[List[Any]] = None, + file: List[Union[Any, UIDProxy]], + sample_preparation: Union[Any, UIDProxy] = None, + computation: Optional[List[Union[Any, UIDProxy]]] = None, + computation_process: Optional[Union[Any, UIDProxy]] = None, + material: Optional[List[Union[Any, UIDProxy]]] = None, + process: Optional[List[Union[Any, UIDProxy]]] = None, + citation: Optional[List[Union[Any, UIDProxy]]] = None, notes: str = "", **kwargs ) -> None: diff --git a/src/cript/nodes/primary_nodes/experiment.py b/src/cript/nodes/primary_nodes/experiment.py index 4646f733a..f75ea04f9 100644 --- a/src/cript/nodes/primary_nodes/experiment.py +++ b/src/cript/nodes/primary_nodes/experiment.py @@ -1,9 +1,10 @@ from dataclasses import dataclass, field, replace -from typing import Any, List, Optional +from typing import Any, List, Optional, Union from beartype import beartype from cript.nodes.primary_nodes.primary_base_node import PrimaryBaseNode +from cript.nodes.util.json import UIDProxy class Experiment(PrimaryBaseNode): @@ -65,12 +66,12 @@ class JsonAttributes(PrimaryBaseNode.JsonAttributes): all Collection attributes """ - process: List[Any] = field(default_factory=list) - computation: List[Any] = field(default_factory=list) - computation_process: List[Any] = field(default_factory=list) - data: List[Any] = field(default_factory=list) + process: List[Union[Any, UIDProxy]] = field(default_factory=list) + computation: List[Union[Any, UIDProxy]] = field(default_factory=list) + computation_process: List[Union[Any, UIDProxy]] = field(default_factory=list) + data: List[Union[Any, UIDProxy]] = field(default_factory=list) funding: List[str] = field(default_factory=list) - citation: List[Any] = field(default_factory=list) + citation: List[Union[Any, UIDProxy]] = field(default_factory=list) _json_attrs: JsonAttributes = JsonAttributes() @@ -78,12 +79,12 @@ class JsonAttributes(PrimaryBaseNode.JsonAttributes): def __init__( self, name: str, - process: Optional[List[Any]] = None, - computation: Optional[List[Any]] = None, - computation_process: Optional[List[Any]] = None, - data: Optional[List[Any]] = None, + process: Optional[List[Union[Any, UIDProxy]]] = None, + computation: Optional[List[Union[Any, UIDProxy]]] = None, + computation_process: Optional[List[Union[Any, UIDProxy]]] = None, + data: Optional[List[Union[Any, UIDProxy]]] = None, funding: Optional[List[str]] = None, - citation: Optional[List[Any]] = None, + citation: Optional[List[Union[Any, UIDProxy]]] = None, notes: str = "", **kwargs ): diff --git a/src/cript/nodes/primary_nodes/inventory.py b/src/cript/nodes/primary_nodes/inventory.py index a0509755d..169944272 100644 --- a/src/cript/nodes/primary_nodes/inventory.py +++ b/src/cript/nodes/primary_nodes/inventory.py @@ -1,10 +1,11 @@ from dataclasses import dataclass, field, replace -from typing import List +from typing import List, Union from beartype import beartype from cript.nodes.primary_nodes.material import Material from cript.nodes.primary_nodes.primary_base_node import PrimaryBaseNode +from cript.nodes.util.json import UIDProxy class Inventory(PrimaryBaseNode): @@ -59,12 +60,12 @@ class JsonAttributes(PrimaryBaseNode.JsonAttributes): all Inventory attributes """ - material: List[Material] = field(default_factory=list) + material: List[Union[Material, UIDProxy]] = field(default_factory=list) _json_attrs: JsonAttributes = JsonAttributes() @beartype - def __init__(self, name: str, material: List[Material], notes: str = "", **kwargs) -> None: + def __init__(self, name: str, material: List[Union[Material, UIDProxy]], notes: str = "", **kwargs) -> None: """ Instantiate an inventory node @@ -104,7 +105,7 @@ def __init__(self, name: str, material: List[Material], notes: str = "", **kwarg @property @beartype - def material(self) -> List[Material]: + def material(self) -> List[Union[Material, UIDProxy]]: """ List of [material](../material) in this inventory @@ -131,7 +132,7 @@ def material(self) -> List[Material]: @material.setter @beartype - def material(self, new_material_list: List[Material]): + def material(self, new_material_list: List[Union[Material, UIDProxy]]): """ set the list of material for this inventory node diff --git a/src/cript/nodes/primary_nodes/material.py b/src/cript/nodes/primary_nodes/material.py index 807711d0f..e0ae0968c 100644 --- a/src/cript/nodes/primary_nodes/material.py +++ b/src/cript/nodes/primary_nodes/material.py @@ -6,6 +6,7 @@ from cript.nodes.exceptions import CRIPTMaterialIdentifierError 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 class Material(PrimaryBaseNode): @@ -74,11 +75,11 @@ class JsonAttributes(PrimaryBaseNode.JsonAttributes): """ # TODO add proper typing in future, using Any for now to avoid circular import error - component: List["Material"] = field(default_factory=list) - process: Optional[Process] = None - property: List[Any] = field(default_factory=list) - parent_material: Optional["Material"] = None - computational_forcefield: Optional[Any] = None + component: List[Union["Material", UIDProxy]] = field(default_factory=list) + process: Optional[Union[Process, UIDProxy]] = None + property: List[Union[Any, UIDProxy]] = field(default_factory=list) + parent_material: Optional[Union["Material", UIDProxy]] = None + computational_forcefield: Optional[Union[Any, UIDProxy]] = None keyword: List[str] = field(default_factory=list) amino_acid: Optional[str] = None bigsmiles: Optional[str] = None @@ -99,11 +100,11 @@ class JsonAttributes(PrimaryBaseNode.JsonAttributes): def __init__( self, name: str, - component: Optional[List["Material"]] = None, - process: Optional[Process] = None, - property: Optional[List[Any]] = None, - parent_material: Optional["Material"] = None, - computational_forcefield: Optional[Any] = None, + component: Optional[List[Union["Material", UIDProxy]]] = None, + process: Optional[Union[Process, UIDProxy]] = None, + property: Optional[List[Union[Any, UIDProxy]]] = None, + parent_material: Optional[Union["Material", UIDProxy]] = None, + computational_forcefield: Optional[Union[Any, UIDProxy]] = None, keyword: Optional[List[str]] = None, amino_acid: Optional[str] = None, bigsmiles: Optional[str] = None, @@ -353,7 +354,7 @@ def pubchem_cid(self, new_pubchem_cid: int) -> None: @property @beartype - def component(self) -> List["Material"]: + def component(self) -> List[Union["Material", UIDProxy]]: """ list of components ([material nodes](./)) that make up this material @@ -385,7 +386,7 @@ def component(self) -> List["Material"]: @component.setter @beartype - def component(self, new_component_list: List["Material"]) -> None: + def component(self, new_component_list: List[Union["Material", UIDProxy]]) -> None: """ set the list of component (material nodes) that make up this material @@ -402,7 +403,7 @@ def component(self, new_component_list: List["Material"]) -> None: @property @beartype - def parent_material(self) -> Optional["Material"]: + def parent_material(self) -> Optional[Union["Material", UIDProxy]]: """ List of parent materials @@ -415,7 +416,7 @@ def parent_material(self) -> Optional["Material"]: @parent_material.setter @beartype - def parent_material(self, new_parent_material: Optional["Material"]) -> None: + def parent_material(self, new_parent_material: Optional[Union["Material", UIDProxy]]) -> None: """ set the [parent materials](./) for this material @@ -458,19 +459,19 @@ def computational_forcefield(self) -> Any: @computational_forcefield.setter @beartype - def computational_forcefield(self, new_computational_forcefield_list: Any) -> None: + def computational_forcefield(self, new_computational_forcefield: Any) -> None: """ sets the list of computational forcefields for this material Parameters ---------- - new_computation_forcefield_list: List[ComputationalForcefield] + new_computation_forcefield: ComputationalForcefield Returns ------- None """ - new_attrs = replace(self._json_attrs, computational_forcefield=new_computational_forcefield_list) + new_attrs = replace(self._json_attrs, computational_forcefield=new_computational_forcefield) self._update_json_attrs_if_valid(new_attrs) @property @@ -518,11 +519,11 @@ def keyword(self, new_keyword_list: List[str]) -> None: @property @beartype - def process(self) -> Optional[Process]: + def process(self) -> Optional[Union[Process, UIDProxy]]: return self._json_attrs.process # type: ignore @process.setter - def process(self, new_process: Process) -> None: + def process(self, new_process: Union[Process, UIDProxy]) -> None: new_attrs = replace(self._json_attrs, process=new_process) self._update_json_attrs_if_valid(new_attrs) diff --git a/src/cript/nodes/primary_nodes/process.py b/src/cript/nodes/primary_nodes/process.py index f730a14e2..56c45115a 100644 --- a/src/cript/nodes/primary_nodes/process.py +++ b/src/cript/nodes/primary_nodes/process.py @@ -1,9 +1,10 @@ from dataclasses import dataclass, field, replace -from typing import Any, List, Optional +from typing import Any, List, Optional, Union from beartype import beartype from cript.nodes.primary_nodes.primary_base_node import PrimaryBaseNode +from cript.nodes.util.json import UIDProxy class Process(PrimaryBaseNode): @@ -61,16 +62,16 @@ class JsonAttributes(PrimaryBaseNode.JsonAttributes): type: str = "" # TODO add proper typing in future, using Any for now to avoid circular import error - ingredient: List[Any] = field(default_factory=list) + ingredient: List[Union[Any, UIDProxy]] = field(default_factory=list) description: str = "" - equipment: List[Any] = field(default_factory=list) - product: List[Any] = field(default_factory=list) - waste: List[Any] = field(default_factory=list) - prerequisite_process: List["Process"] = field(default_factory=list) - condition: List[Any] = field(default_factory=list) - property: List[Any] = field(default_factory=list) + equipment: List[Union[Any, UIDProxy]] = field(default_factory=list) + product: List[Union[Any, UIDProxy]] = field(default_factory=list) + waste: List[Union[Any, UIDProxy]] = field(default_factory=list) + prerequisite_process: List[Union["Process", UIDProxy]] = field(default_factory=list) + condition: List[Union[Any, UIDProxy]] = field(default_factory=list) + property: List[Union[Any, UIDProxy]] = field(default_factory=list) keyword: List[str] = field(default_factory=list) - citation: List[Any] = field(default_factory=list) + citation: List[Union[Any, UIDProxy]] = field(default_factory=list) _json_attrs: JsonAttributes = JsonAttributes() @@ -79,16 +80,16 @@ def __init__( self, name: str, type: str, - ingredient: Optional[List[Any]] = None, + ingredient: Optional[List[Union[Any, UIDProxy]]] = None, description: str = "", - equipment: Optional[List[Any]] = None, - product: Optional[List[Any]] = None, - waste: Optional[List[Any]] = None, - prerequisite_process: Optional[List[Any]] = None, - condition: Optional[List[Any]] = None, - property: Optional[List[Any]] = None, + equipment: Optional[List[Union[Any, UIDProxy]]] = None, + product: Optional[List[Union[Any, UIDProxy]]] = None, + waste: Optional[List[Union[Any, UIDProxy]]] = None, + prerequisite_process: Optional[List[Union["Process", UIDProxy]]] = None, + condition: Optional[List[Union[Any, UIDProxy]]] = None, + property: Optional[List[Union[Any, UIDProxy]]] = None, keyword: Optional[List[str]] = None, - citation: Optional[List[Any]] = None, + citation: Optional[List[Union[Any, UIDProxy]]] = None, notes: str = "", **kwargs ) -> None: @@ -416,7 +417,7 @@ def waste(self, new_waste_list: List[Any]) -> None: @property @beartype - def prerequisite_process(self) -> List["Process"]: + def prerequisite_process(self) -> List[Union["Process", UIDProxy]]: """ list of prerequisite process nodes @@ -439,7 +440,7 @@ def prerequisite_process(self) -> List["Process"]: @prerequisite_process.setter @beartype - def prerequisite_process(self, new_prerequisite_process_list: List["Process"]) -> None: + def prerequisite_process(self, new_prerequisite_process_list: List[Union["Process", UIDProxy]]) -> None: """ set the prerequisite_process for the process node diff --git a/src/cript/nodes/primary_nodes/project.py b/src/cript/nodes/primary_nodes/project.py index c644221bc..2be3056b3 100644 --- a/src/cript/nodes/primary_nodes/project.py +++ b/src/cript/nodes/primary_nodes/project.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field, replace -from typing import List, Optional +from typing import List, Optional, Union from beartype import beartype @@ -7,6 +7,7 @@ from cript.nodes.primary_nodes.material import Material from cript.nodes.primary_nodes.primary_base_node import PrimaryBaseNode from cript.nodes.supporting_nodes import User +from cript.nodes.util.json import UIDProxy class Project(PrimaryBaseNode): @@ -59,15 +60,15 @@ class JsonAttributes(PrimaryBaseNode.JsonAttributes): all Project attributes """ - member: List[User] = field(default_factory=list) - admin: List[User] = field(default_factory=list) - collection: List[Collection] = field(default_factory=list) - material: List[Material] = field(default_factory=list) + member: List[Union[User, UIDProxy]] = field(default_factory=list) + admin: List[Union[User, UIDProxy]] = field(default_factory=list) + collection: List[Union[Collection, UIDProxy]] = field(default_factory=list) + material: List[Union[Material, UIDProxy]] = field(default_factory=list) _json_attrs: JsonAttributes = JsonAttributes() @beartype - def __init__(self, name: str, collection: Optional[List[Collection]] = None, material: Optional[List[Material]] = None, notes: str = "", **kwargs): + def __init__(self, name: str, collection: Optional[List[Union[Collection, UIDProxy]]] = None, material: Optional[List[Union[Material, UIDProxy]]] = None, notes: str = "", **kwargs): """ Create a Project node with Project name @@ -148,17 +149,17 @@ def validate(self, api=None, is_patch=False, force_validation: bool = False): @property @beartype - def member(self) -> List[User]: + def member(self) -> List[Union[User, UIDProxy]]: return self._json_attrs.member.copy() @property @beartype - def admin(self) -> List[User]: + def admin(self) -> List[Union[User, UIDProxy]]: return self._json_attrs.admin @property @beartype - def collection(self) -> List[Collection]: + def collection(self) -> List[Union[Collection, UIDProxy]]: """ Collection is a Project node's property that can be set during creation in the constructor or later by setting the project's property @@ -179,7 +180,7 @@ def collection(self) -> List[Collection]: @collection.setter @beartype - def collection(self, new_collection: List[Collection]) -> None: + def collection(self, new_collection: List[Union[Collection, UIDProxy]]) -> None: """ set list of collections for the project node @@ -196,7 +197,7 @@ def collection(self, new_collection: List[Collection]) -> None: @property @beartype - def material(self) -> List[Material]: + def material(self) -> List[Union[Material, UIDProxy]]: """ List of Materials that belong to this Project. @@ -216,7 +217,7 @@ def material(self) -> List[Material]: @material.setter @beartype - def material(self, new_materials: List[Material]) -> None: + def material(self, new_materials: List[Union[Material, UIDProxy]]) -> None: """ set the list of materials for this project diff --git a/src/cript/nodes/subobjects/algorithm.py b/src/cript/nodes/subobjects/algorithm.py index d45f5f894..a643d9b72 100644 --- a/src/cript/nodes/subobjects/algorithm.py +++ b/src/cript/nodes/subobjects/algorithm.py @@ -1,8 +1,9 @@ from dataclasses import dataclass, field, replace -from typing import List, Optional +from typing import List, Optional, Union from cript.nodes.subobjects.citation import Citation from cript.nodes.subobjects.parameter import Parameter +from cript.nodes.util.json import UIDProxy from cript.nodes.uuid_base import UUIDBaseNode @@ -69,12 +70,12 @@ class JsonAttributes(UUIDBaseNode.JsonAttributes): key: str = "" type: str = "" - parameter: List[Parameter] = field(default_factory=list) - citation: List[Citation] = field(default_factory=list) + parameter: List[Union[Parameter, UIDProxy]] = field(default_factory=list) + citation: List[Union[Citation, UIDProxy]] = field(default_factory=list) _json_attrs: JsonAttributes = JsonAttributes() - def __init__(self, key: str, type: str, parameter: Optional[List[Parameter]] = None, citation: Optional[List[Citation]] = None, **kwargs): # ignored + def __init__(self, key: str, type: str, parameter: Optional[List[Union[Parameter, UIDProxy]]] = None, citation: Optional[List[Union[Citation, UIDProxy]]] = None, **kwargs): # ignored """ Create algorithm sub-object @@ -172,7 +173,7 @@ def type(self, new_type: str) -> None: self._update_json_attrs_if_valid(new_attrs) @property - def parameter(self) -> List[Parameter]: + def parameter(self) -> List[Union[Parameter, UIDProxy]]: """ list of [Parameter](../parameter) sub-objects for the algorithm sub-object @@ -194,7 +195,7 @@ def parameter(self) -> List[Parameter]: return self._json_attrs.parameter.copy() @parameter.setter - def parameter(self, new_parameter: List[Parameter]) -> None: + def parameter(self, new_parameter: List[Union[Parameter, UIDProxy]]) -> None: """ set a list of cript.Parameter sub-objects @@ -211,7 +212,7 @@ def parameter(self, new_parameter: List[Parameter]) -> None: self._update_json_attrs_if_valid(new_attrs) @property - def citation(self) -> Citation: + def citation(self) -> List[Union[Citation, UIDProxy]]: """ [citation](../citation) subobject for algorithm subobject @@ -247,7 +248,7 @@ def citation(self) -> Citation: return self._json_attrs.citation.copy() # type: ignore @citation.setter - def citation(self, new_citation: List[Citation]) -> None: + def citation(self, new_citation: List[Union[Citation, UIDProxy]]) -> None: """ set the algorithm citation subobject diff --git a/src/cript/nodes/subobjects/citation.py b/src/cript/nodes/subobjects/citation.py index e4457a251..c22a33a18 100644 --- a/src/cript/nodes/subobjects/citation.py +++ b/src/cript/nodes/subobjects/citation.py @@ -4,6 +4,7 @@ from beartype import beartype from cript.nodes.primary_nodes.reference import Reference +from cript.nodes.util.json import UIDProxy from cript.nodes.uuid_base import UUIDBaseNode @@ -60,12 +61,12 @@ class Citation(UUIDBaseNode): @dataclass(frozen=True) class JsonAttributes(UUIDBaseNode.JsonAttributes): type: str = "" - reference: Optional[Reference] = None + reference: Optional[Union[Reference, UIDProxy]] = None _json_attrs: JsonAttributes = JsonAttributes() @beartype - def __init__(self, type: str, reference: Reference, **kwargs): + def __init__(self, type: str, reference: Union[Reference, UIDProxy], **kwargs): """ create a Citation subobject @@ -164,7 +165,7 @@ def type(self, new_type: str) -> None: @property @beartype - def reference(self) -> Union[Reference, None]: + def reference(self) -> Union[Reference, None, UIDProxy]: """ citation reference node diff --git a/src/cript/nodes/subobjects/computational_forcefield.py b/src/cript/nodes/subobjects/computational_forcefield.py index d77d38b7e..39c2b0e54 100644 --- a/src/cript/nodes/subobjects/computational_forcefield.py +++ b/src/cript/nodes/subobjects/computational_forcefield.py @@ -1,10 +1,11 @@ from dataclasses import dataclass, field, replace -from typing import List, Optional +from typing import List, Optional, Union from beartype import beartype from cript.nodes.primary_nodes.data import Data from cript.nodes.subobjects.citation import Citation +from cript.nodes.util.json import UIDProxy from cript.nodes.uuid_base import UUIDBaseNode @@ -88,13 +89,24 @@ class JsonAttributes(UUIDBaseNode.JsonAttributes): implicit_solvent: str = "" source: str = "" description: str = "" - data: List[Data] = field(default_factory=list) - citation: List[Citation] = field(default_factory=list) + data: List[Union[Data, UIDProxy]] = field(default_factory=list) + citation: List[Union[Citation, UIDProxy]] = field(default_factory=list) _json_attrs: JsonAttributes = JsonAttributes() @beartype - def __init__(self, key: str, building_block: str, coarse_grained_mapping: str = "", implicit_solvent: str = "", source: str = "", description: str = "", data: Optional[List[Data]] = None, citation: Optional[List[Citation]] = None, **kwargs): + def __init__( + self, + key: str, + building_block: str, + coarse_grained_mapping: str = "", + implicit_solvent: str = "", + source: str = "", + description: str = "", + data: Optional[List[Union[Data, UIDProxy]]] = None, + citation: Optional[List[Union[Citation, UIDProxy]]] = None, + **kwargs + ): """ instantiate a computational_forcefield subobject @@ -393,7 +405,7 @@ def description(self, new_description: str) -> None: @property @beartype - def data(self) -> List[Data]: + def data(self) -> List[Union[Data, UIDProxy]]: """ details of mapping schema and forcefield parameters @@ -426,7 +438,7 @@ def data(self) -> List[Data]: @data.setter @beartype - def data(self, new_data: List[Data]) -> None: + def data(self, new_data: List[Union[Data, UIDProxy]]) -> None: """ set the data attribute of this computational_forcefield node @@ -444,7 +456,7 @@ def data(self, new_data: List[Data]) -> None: @property @beartype - def citation(self) -> List[Citation]: + def citation(self) -> List[Union[Citation, UIDProxy]]: """ reference to a book, paper, or scholarly work @@ -483,7 +495,7 @@ def citation(self) -> List[Citation]: @citation.setter @beartype - def citation(self, new_citation: List[Citation]) -> None: + def citation(self, new_citation: List[Union[Citation, UIDProxy]]) -> None: """ set the citation subobject of the computational_forcefield subobject diff --git a/src/cript/nodes/subobjects/condition.py b/src/cript/nodes/subobjects/condition.py index 71903857b..461952f99 100644 --- a/src/cript/nodes/subobjects/condition.py +++ b/src/cript/nodes/subobjects/condition.py @@ -5,6 +5,7 @@ from beartype import beartype from cript.nodes.primary_nodes.data import Data +from cript.nodes.util.json import UIDProxy from cript.nodes.uuid_base import UUIDBaseNode @@ -86,7 +87,7 @@ class JsonAttributes(UUIDBaseNode.JsonAttributes): uncertainty_type: str = "" set_id: Optional[int] = None measurement_id: Optional[int] = None - data: List[Data] = field(default_factory=list) + data: List[Union[Data, UIDProxy]] = field(default_factory=list) _json_attrs: JsonAttributes = JsonAttributes() @@ -102,7 +103,7 @@ def __init__( uncertainty_type: str = "", set_id: Optional[int] = None, measurement_id: Optional[int] = None, - data: Optional[List[Data]] = None, + data: Optional[List[Union[Data, UIDProxy]]] = None, **kwargs ): """ @@ -504,7 +505,7 @@ def measurement_id(self, new_measurement_id: Union[int, None]) -> None: @property @beartype - def data(self) -> List[Data]: + def data(self) -> List[Union[Data, UIDProxy]]: """ detailed data associated with the condition @@ -539,7 +540,7 @@ def data(self) -> List[Data]: @data.setter @beartype - def data(self, new_data: List[Data]) -> None: + def data(self, new_data: List[Union[Data, UIDProxy]]) -> None: """ set the data node for this Condition Subobject diff --git a/src/cript/nodes/subobjects/equipment.py b/src/cript/nodes/subobjects/equipment.py index 99fa03e2c..ecac0af21 100644 --- a/src/cript/nodes/subobjects/equipment.py +++ b/src/cript/nodes/subobjects/equipment.py @@ -1,11 +1,12 @@ from dataclasses import dataclass, field, replace -from typing import List, Union +from typing import List, Optional, Union from beartype import beartype from cript.nodes.subobjects.citation import Citation from cript.nodes.subobjects.condition import Condition from cript.nodes.supporting_nodes.file import File +from cript.nodes.util.json import UIDProxy from cript.nodes.uuid_base import UUIDBaseNode @@ -53,14 +54,14 @@ class Equipment(UUIDBaseNode): class JsonAttributes(UUIDBaseNode.JsonAttributes): key: str = "" description: str = "" - condition: List[Condition] = field(default_factory=list) - file: List[File] = field(default_factory=list) - citation: List[Citation] = field(default_factory=list) + condition: List[Union[Condition, UIDProxy]] = field(default_factory=list) + file: List[Union[File, UIDProxy]] = field(default_factory=list) + citation: List[Union[Citation, UIDProxy]] = field(default_factory=list) _json_attrs: JsonAttributes = JsonAttributes() @beartype - def __init__(self, key: str, description: str = "", condition: Union[List[Condition], None] = None, file: Union[List[File], None] = None, citation: Union[List[Citation], None] = None, **kwargs) -> None: + def __init__(self, key: str, description: str = "", condition: Optional[List[Union[Condition, UIDProxy]]] = None, file: Optional[List[Union[File, UIDProxy]]] = None, citation: Optional[List[Union[Citation, UIDProxy]]] = None, **kwargs) -> None: """ create equipment sub-object @@ -174,7 +175,7 @@ def description(self, new_description: str) -> None: @property @beartype - def condition(self) -> List[Condition]: + def condition(self) -> List[Union[Condition, UIDProxy]]: """ conditions under which the property was measured @@ -199,7 +200,7 @@ def condition(self) -> List[Condition]: @condition.setter @beartype - def condition(self, new_condition: List[Condition]) -> None: + def condition(self, new_condition: List[Union[Condition, UIDProxy]]) -> None: """ set a list of Conditions for the equipment sub-object @@ -217,7 +218,7 @@ def condition(self, new_condition: List[Condition]) -> None: @property @beartype - def file(self) -> List[File]: + def file(self) -> List[Union[File, UIDProxy]]: """ list of file nodes to link to calibration or equipment specification documents @@ -242,7 +243,7 @@ def file(self) -> List[File]: @file.setter @beartype - def file(self, new_file: List[File]) -> None: + def file(self, new_file: List[Union[File, UIDProxy]]) -> None: """ set the file node for the equipment subobject @@ -260,7 +261,7 @@ def file(self, new_file: List[File]) -> None: @property @beartype - def citation(self) -> List[Citation]: + def citation(self) -> List[Union[Citation, UIDProxy]]: """ reference to a book, paper, or scholarly work @@ -296,7 +297,7 @@ def citation(self) -> List[Citation]: @citation.setter @beartype - def citation(self, new_citation: List[Citation]) -> None: + def citation(self, new_citation: List[Union[Citation, UIDProxy]]) -> None: """ set the citation subobject for this equipment subobject diff --git a/src/cript/nodes/subobjects/ingredient.py b/src/cript/nodes/subobjects/ingredient.py index 9c66a304c..7b58b67c5 100644 --- a/src/cript/nodes/subobjects/ingredient.py +++ b/src/cript/nodes/subobjects/ingredient.py @@ -5,6 +5,7 @@ from cript.nodes.primary_nodes.material import Material from cript.nodes.subobjects.quantity import Quantity +from cript.nodes.util.json import UIDProxy from cript.nodes.uuid_base import UUIDBaseNode @@ -65,14 +66,14 @@ class Ingredient(UUIDBaseNode): @dataclass(frozen=True) class JsonAttributes(UUIDBaseNode.JsonAttributes): - material: Optional[Material] = None - quantity: List[Quantity] = field(default_factory=list) + material: Optional[Union[Material, UIDProxy]] = None + quantity: List[Union[Quantity, UIDProxy]] = field(default_factory=list) keyword: List[str] = field(default_factory=list) _json_attrs: JsonAttributes = JsonAttributes() @beartype - def __init__(self, material: Material, quantity: List[Quantity], keyword: Optional[List[str]] = None, **kwargs): + def __init__(self, material: Union[Material, UIDProxy], quantity: List[Union[Quantity, UIDProxy]], keyword: Optional[List[str]] = None, **kwargs): """ create an ingredient sub-object @@ -117,7 +118,7 @@ def _from_json(cls, json_dict: dict): @property @beartype - def material(self) -> Union[Material, None]: + def material(self) -> Union[Material, None, UIDProxy]: """ current material in this ingredient sub-object @@ -130,7 +131,7 @@ def material(self) -> Union[Material, None]: @property @beartype - def quantity(self) -> List[Quantity]: + def quantity(self) -> List[Union[Quantity, UIDProxy]]: """ quantity for the ingredient sub-object @@ -142,7 +143,7 @@ def quantity(self) -> List[Quantity]: return self._json_attrs.quantity.copy() @beartype - def set_material(self, new_material: Material, new_quantity: List[Quantity]) -> None: + def set_material(self, new_material: Union[Material, UIDProxy], new_quantity: List[Union[Quantity, UIDProxy]]) -> None: """ update ingredient sub-object with new material and new list of quantities diff --git a/src/cript/nodes/subobjects/property.py b/src/cript/nodes/subobjects/property.py index 196772865..d6af1a7d2 100644 --- a/src/cript/nodes/subobjects/property.py +++ b/src/cript/nodes/subobjects/property.py @@ -10,6 +10,7 @@ from cript.nodes.primary_nodes.process import Process from cript.nodes.subobjects.citation import Citation from cript.nodes.subobjects.condition import Condition +from cript.nodes.util.json import UIDProxy from cript.nodes.uuid_base import UUIDBaseNode @@ -76,14 +77,14 @@ class JsonAttributes(UUIDBaseNode.JsonAttributes): unit: Optional[str] = "" uncertainty: Optional[Number] = None uncertainty_type: str = "" - component: List[Material] = field(default_factory=list) + component: List[Union[Material, UIDProxy]] = field(default_factory=list) structure: str = "" method: str = "" - sample_preparation: Optional[Process] = None - condition: List[Condition] = field(default_factory=list) - data: List[Data] = field(default_factory=list) - computation: List[Computation] = field(default_factory=list) - citation: List[Citation] = field(default_factory=list) + sample_preparation: Optional[Union[Process, UIDProxy]] = None + condition: List[Union[Condition, UIDProxy]] = field(default_factory=list) + data: List[Union[Data, UIDProxy]] = field(default_factory=list) + computation: List[Union[Computation, UIDProxy]] = field(default_factory=list) + citation: List[Union[Citation, UIDProxy]] = field(default_factory=list) notes: str = "" _json_attrs: JsonAttributes = JsonAttributes() @@ -97,14 +98,14 @@ def __init__( unit: Union[str, None], uncertainty: Optional[Number] = None, uncertainty_type: str = "", - component: Optional[List[Material]] = None, + component: Optional[List[Union[Material, UIDProxy]]] = None, structure: str = "", method: str = "", - sample_preparation: Optional[Process] = None, - condition: Optional[List[Condition]] = None, - data: Optional[List[Data]] = None, - computation: Optional[List[Computation]] = None, - citation: Optional[List[Citation]] = None, + sample_preparation: Optional[Union[Process, UIDProxy]] = None, + condition: Optional[List[Union[Condition, UIDProxy]]] = None, + data: Optional[List[Union[Data, UIDProxy]]] = None, + computation: Optional[List[Union[Computation, UIDProxy]]] = None, + citation: Optional[List[Union[Citation, UIDProxy]]] = None, notes: str = "", **kwargs ): @@ -372,7 +373,7 @@ def uncertainty_type(self) -> str: @property @beartype - def component(self) -> List[Material]: + def component(self) -> List[Union[Material, UIDProxy]]: """ list of Materials that the Property relates to @@ -392,7 +393,7 @@ def component(self) -> List[Material]: @component.setter @beartype - def component(self, new_component: List[Material]) -> None: + def component(self, new_component: List[Union[Material, UIDProxy]]) -> None: """ set the list of Materials as components for the Property subobject @@ -486,7 +487,7 @@ def method(self, new_method: str) -> None: @property @beartype - def sample_preparation(self) -> Union[Process, None]: + def sample_preparation(self) -> Union[Process, None, UIDProxy]: """ sample_preparation @@ -506,7 +507,7 @@ def sample_preparation(self) -> Union[Process, None]: @sample_preparation.setter @beartype - def sample_preparation(self, new_sample_preparation: Union[Process, None]) -> None: + def sample_preparation(self, new_sample_preparation: Union[Process, None, UIDProxy]) -> None: """ set the sample_preparation for the Property subobject @@ -524,7 +525,7 @@ def sample_preparation(self, new_sample_preparation: Union[Process, None]) -> No @property @beartype - def condition(self) -> List[Condition]: + def condition(self) -> List[Union[Condition, UIDProxy]]: """ list of Conditions under which the property was measured @@ -544,7 +545,7 @@ def condition(self) -> List[Condition]: @condition.setter @beartype - def condition(self, new_condition: List[Condition]) -> None: + def condition(self, new_condition: List[Union[Condition, UIDProxy]]) -> None: """ set the list of Conditions for this property subobject @@ -562,7 +563,7 @@ def condition(self, new_condition: List[Condition]) -> None: @property @beartype - def data(self) -> List[Data]: + def data(self) -> List[Union[Data, UIDProxy]]: """ List of Data nodes for this Property subobjects @@ -589,7 +590,7 @@ def data(self) -> List[Data]: @data.setter @beartype - def data(self, new_data: List[Data]) -> None: + def data(self, new_data: List[Union[Data, UIDProxy]]) -> None: """ set the Data node for the Property subobject @@ -607,7 +608,7 @@ def data(self, new_data: List[Data]) -> None: @property @beartype - def computation(self) -> List[Computation]: + def computation(self) -> List[Union[Computation, UIDProxy]]: """ list of Computation nodes that produced this property @@ -627,7 +628,7 @@ def computation(self) -> List[Computation]: @computation.setter @beartype - def computation(self, new_computation: List[Computation]) -> None: + def computation(self, new_computation: List[Union[Computation, UIDProxy]]) -> None: """ set the list of Computation nodes that produced this property @@ -645,7 +646,7 @@ def computation(self, new_computation: List[Computation]) -> None: @property @beartype - def citation(self) -> List[Citation]: + def citation(self) -> List[Union[Citation, UIDProxy]]: """ list of Citation subobjects for this Property subobject @@ -681,7 +682,7 @@ def citation(self) -> List[Citation]: @citation.setter @beartype - def citation(self, new_citation: List[Citation]) -> None: + def citation(self, new_citation: List[Union[Citation, UIDProxy]]) -> None: """ set the list of Citation subobjects for the Property subobject diff --git a/src/cript/nodes/subobjects/software_configuration.py b/src/cript/nodes/subobjects/software_configuration.py index 02dbfa9bd..a7fc4f26e 100644 --- a/src/cript/nodes/subobjects/software_configuration.py +++ b/src/cript/nodes/subobjects/software_configuration.py @@ -6,6 +6,7 @@ from cript.nodes.subobjects.algorithm import Algorithm from cript.nodes.subobjects.citation import Citation from cript.nodes.subobjects.software import Software +from cript.nodes.util.json import UIDProxy from cript.nodes.uuid_base import UUIDBaseNode @@ -57,15 +58,15 @@ class SoftwareConfiguration(UUIDBaseNode): @dataclass(frozen=True) class JsonAttributes(UUIDBaseNode.JsonAttributes): - software: Union[Software, None] = None - algorithm: List[Algorithm] = field(default_factory=list) + software: Optional[Union[Software, UIDProxy]] = None + algorithm: List[Union[Algorithm, UIDProxy]] = field(default_factory=list) notes: str = "" - citation: List[Citation] = field(default_factory=list) + citation: List[Union[Citation, UIDProxy]] = field(default_factory=list) _json_attrs: JsonAttributes = JsonAttributes() @beartype - def __init__(self, software: Software, algorithm: Optional[List[Algorithm]] = None, notes: str = "", citation: Union[List[Citation], None] = None, **kwargs): + def __init__(self, software: Union[Software, UIDProxy], algorithm: Optional[List[Union[Algorithm, UIDProxy]]] = None, notes: str = "", citation: Union[List[Union[Citation, UIDProxy]], None] = None, **kwargs): """ Create Software_Configuration sub-object @@ -102,7 +103,7 @@ def __init__(self, software: Software, algorithm: Optional[List[Algorithm]] = No @property @beartype - def software(self) -> Union[Software, None]: + def software(self) -> Union[Software, None, UIDProxy]: """ Software used @@ -122,7 +123,7 @@ def software(self) -> Union[Software, None]: @software.setter @beartype - def software(self, new_software: Union[Software, None]) -> None: + def software(self, new_software: Union[Software, None, UIDProxy]) -> None: """ set the Software used @@ -140,7 +141,7 @@ def software(self, new_software: Union[Software, None]) -> None: @property @beartype - def algorithm(self) -> List[Algorithm]: + def algorithm(self) -> List[Union[Algorithm, UIDProxy]]: """ list of Algorithms used @@ -161,7 +162,7 @@ def algorithm(self) -> List[Algorithm]: @algorithm.setter @beartype - def algorithm(self, new_algorithm: List[Algorithm]) -> None: + def algorithm(self, new_algorithm: List[Union[Algorithm, UIDProxy]]) -> None: """ set the list of Algorithms @@ -224,7 +225,7 @@ def notes(self, new_notes: str) -> None: @property @beartype - def citation(self) -> List[Citation]: + def citation(self) -> List[Union[Citation, UIDProxy]]: """ list of Citation sub-objects for the Software_Configuration @@ -261,7 +262,7 @@ def citation(self) -> List[Citation]: @citation.setter @beartype - def citation(self, new_citation: List[Citation]) -> None: + def citation(self, new_citation: List[Union[Citation, UIDProxy]]) -> None: """ set the Citation sub-object diff --git a/src/cript/nodes/util/core.py b/src/cript/nodes/util/core.py index 938ec7201..e1b5facc6 100644 --- a/src/cript/nodes/util/core.py +++ b/src/cript/nodes/util/core.py @@ -67,3 +67,16 @@ def get_orphaned_experiment_exception(orphaned_node): return CRIPTOrphanedComputationalProcessError(orphaned_node) # Base case raise the parent exception. TODO add bug warning. return CRIPTOrphanedExperimentError(orphaned_node) + + +def iterate_leaves(obj): + """Helper function that iterates over all leaves of nested dictionaries or lists.""" + + if isinstance(obj, dict): + for value in obj.values(): + yield from iterate_leaves(value) + elif isinstance(obj, list): + for item in obj: + yield from iterate_leaves(item) + else: + yield obj diff --git a/src/cript/nodes/util/json.py b/src/cript/nodes/util/json.py index 274c1df1c..2b923bab5 100644 --- a/src/cript/nodes/util/json.py +++ b/src/cript/nodes/util/json.py @@ -14,9 +14,21 @@ CRIPTJsonDeserializationError, CRIPTJsonNodeError, ) +from cript.nodes.util.core import iterate_leaves from cript.nodes.uuid_base import UUIDBaseNode +@dataclasses.dataclass(frozen=True) +class UIDProxy: + """Helper class that store temporarily unresolved UIDs.""" + + uid: Optional[str] = None + + def __post_init__(self): + if self.uid is None: + raise RuntimeError("UID needs to be initialized") + + class NodeEncoder(json.JSONEncoder): """ Custom JSON encoder for serializing CRIPT nodes to JSON. @@ -93,6 +105,10 @@ def default(self, obj): """ if isinstance(obj, uuid.UUID): return str(obj) + + if isinstance(obj, UIDProxy): + return obj.uid + if isinstance(obj, BaseNode): try: uid = obj.uid @@ -136,21 +152,20 @@ def default(self, obj): def _apply_modifications(self, serialize_dict: Dict): """ - Checks the serialize_dict to see if any other operations are required before it - can be considered done. If other operations are required, then it passes it to the other operations - and at the end returns the fully finished dict. + Checks the serialize_dict to see if any other operations are required before it + can be considered done. If other operations are required, then it passes it to the other operations + and at the end returns the fully finished dict. - This function is essentially a big switch case that checks the node type - and determines what other operations are required for it. + This function is essentially a big switch case that checks the node type + and determines what other operations are required for it. - Parameters - ---------- - <<<<<<< HEAD - serialize_dict: Dict + Parameters + ---------- + serialize_dict: Dict - Returns - ------- - serialize_dict: Dict + Returns + ------- + serialize_dict: Dict """ def process_attribute(attribute): @@ -221,7 +236,11 @@ def __init__(self, uid_cache: Optional[Dict] = None): uid_cache = {} self._uid_cache = uid_cache - def __call__(self, node_str: Union[Dict, str]) -> Dict: + @property + def uid_cache(self): + return self._uid_cache + + def __call__(self, node_str: Union[Dict, str]) -> Union[Dict, UIDProxy]: """ Internal function, used as a hook for json deserialization. @@ -266,9 +285,9 @@ def __call__(self, node_str: Union[Dict, str]) -> Dict: try: return self._uid_cache[node_dict["uid"]] except KeyError: - # TODO if we convince beartype to accept Proxy temporarily, enable return instead of raise - raise CRIPTDeserializationUIDError("Unknown", node_dict["uid"]) - # return _UIDProxy(node_dict["uid"]) + # raise CRIPTDeserializationUIDError("Unknown", node_dict["uid"]) + proxy = UIDProxy(uid=node_dict["uid"]) + return proxy try: node_type_list = node_dict["node"] @@ -294,8 +313,35 @@ def __call__(self, node_str: Union[Dict, str]) -> Dict: # Fall back return node_dict - -def load_nodes_from_json(nodes_json: Union[str, Dict], _use_uuid_cache: Optional[Dict] = None): + def resolve_unresolved_uids(self, node_iter): + def handle_uid_replacement(node, name, attr): + if isinstance(attr, UIDProxy): + unresolved_uid = attr.uid + try: + uid_node = self.uid_cache[unresolved_uid] + except KeyError as exc: + raise CRIPTDeserializationUIDError("Unknown", unresolved_uid) from exc + updated_attrs = dataclasses.replace(node._json_attrs, **{name: uid_node}) + node._update_json_attrs_if_valid(updated_attrs) + + for node_leaves in iterate_leaves(node_iter): + if isinstance(node_leaves, BaseNode): + for node in node_leaves: + field_names = [field.name for field in dataclasses.fields(node._json_attrs)] + for field_name in field_names: + field_attr = getattr(node._json_attrs, field_name) + handle_uid_replacement(node, field_name, field_attr) + if isinstance(field_attr, list): + for i in range(len(field_attr)): + if isinstance(field_attr[i], UIDProxy): + try: + field_attr[i] = self.uid_cache[field_attr[i].uid] + except KeyError as exc: + raise CRIPTDeserializationUIDError("Unknown", field_attr[i].uid) from exc + return node_iter + + +def load_nodes_from_json(nodes_json: Union[str, Dict], api=None, _use_uuid_cache: Optional[Dict] = None, skip_validation: bool = False): """ User facing function, that return a node and all its children from a json string input. @@ -343,6 +389,11 @@ def load_nodes_from_json(nodes_json: Union[str, Dict], _use_uuid_cache: Optional Typically returns a single CRIPT node, but if given a list of nodes, then it will serialize them and return a list of CRIPT nodes """ + from cript.api.api import _get_global_cached_api + + if api is None: + api = _get_global_cached_api() + # Initialize the custom decoder hook for JSON deserialization node_json_hook = _NodeDecoderHook() @@ -357,11 +408,22 @@ def load_nodes_from_json(nodes_json: Union[str, Dict], _use_uuid_cache: Optional if _use_uuid_cache is not None: # If requested use a custom cache. UUIDBaseNode._uuid_cache = _use_uuid_cache + previous_skip_validation = api.schema.skip_validation + # Temporarily disable validation while loading nodes from JSON + api.schema.skip_validation = True try: loaded_nodes = json.loads(nodes_json, object_hook=node_json_hook) + loaded_nodes = node_json_hook.resolve_unresolved_uids(loaded_nodes) finally: # Definitively restore the old cache state UUIDBaseNode._uuid_cache = previous_uuid_cache + api.schema.skip_validation = previous_skip_validation + + # If nodes are actually expected to be checked, do it now + if not previous_skip_validation and not skip_validation: + for node in iterate_leaves(loaded_nodes): + if isinstance(node, BaseNode): + node.validate() if _use_uuid_cache is not None: return loaded_nodes, _use_uuid_cache @@ -399,41 +461,3 @@ def _is_node_field_valid(node_type_list: List) -> bool: return True else: return False - - -# class _UIDProxy: -# """ -# Proxy class for unresolvable UID nodes. -# This is going to be replaced by actual nodes. - -# Report a bug if you find this class in production. -# """ - -# def __init__(self, uid: str): -# self.uid = uid -# print("proxy", uid) - -# TODO: enable this logic to replace proxies, once beartype is OK with that. -# def recursive_proxy_replacement(node, handled_nodes): -# if isinstance(node, _UIDProxy): -# try: -# node = node_json_hook._uid_cache[node.uid] -# except KeyError as exc: -# raise CRIPTDeserializationUIDError(node.node_type, node.uid) -# return node -# handled_nodes.add(node.uid) -# for field in node._json_attrs.__dict__: -# child_node = getattr(node._json_attrs, field) -# if not isinstance(child_node, List): -# if hasattr(cn, "__bases__") and BaseNode in child_node.__bases__: -# child_node = recursive_proxy_replacement(child_node, handled_nodes) -# node._json_attrs = replace(node._json_attrs, field=child_node) -# else: -# for i, cn in enumerate(child_node): -# if hasattr(cn, "__bases__") and BaseNode in cn.__bases__: -# if cn.uid not in handled_nodes: -# child_node[i] = recursive_proxy_replacement(cn, handled_nodes) - -# return node -# handled_nodes = set() -# recursive_proxy_replacement(json_nodes, handled_nodes) diff --git a/src/cript/nodes/uuid_base.py b/src/cript/nodes/uuid_base.py index 87fac9043..ca2e5c319 100644 --- a/src/cript/nodes/uuid_base.py +++ b/src/cript/nodes/uuid_base.py @@ -3,8 +3,11 @@ from dataclasses import dataclass, field, replace from typing import Any, Dict, Optional +from beartype import beartype + from cript.nodes.core import BaseNode from cript.nodes.exceptions import CRIPTUUIDException +from cript.nodes.node_iterator import NodeIterator class UUIDBaseNode(BaseNode, ABC): @@ -30,7 +33,7 @@ class JsonAttributes(BaseNode.JsonAttributes): _json_attrs: JsonAttributes = JsonAttributes() def __new__(cls, *args, **kwargs): - uuid: Optional[str] = kwargs.get("uuid") + uuid: Optional[str] = str(kwargs.get("uuid")) if uuid and uuid in UUIDBaseNode._uuid_cache: existing_node_to_overwrite = UUIDBaseNode._uuid_cache[uuid] if type(existing_node_to_overwrite) is not cls: @@ -51,8 +54,13 @@ def __init__(self, **kwargs): UUIDBaseNode._uuid_cache[uuid] = self @property - def uuid(self) -> uuid.UUID: - return uuid.UUID(self._json_attrs.uuid) + @beartype + def uuid(self) -> str: + if not isinstance(self._json_attrs.uuid, str): + # Some JSON decoding automatically converted this to UUID objects, which we don't want + self._json_attrs = replace(self._json_attrs, uuid=str(self._json_attrs.uuid)) + + return self._json_attrs.uuid @property def url(self): @@ -76,3 +84,7 @@ def updated_at(self): @property def created_at(self): return self._json_attrs.created_at + + def __iter__(self) -> NodeIterator: + """Enables DFS iteration over all children.""" + return NodeIterator(self) diff --git a/tests/fixtures/primary_nodes.py b/tests/fixtures/primary_nodes.py index 65fbf0222..063a3a334 100644 --- a/tests/fixtures/primary_nodes.py +++ b/tests/fixtures/primary_nodes.py @@ -26,15 +26,15 @@ def complex_project_dict(complex_collection_node, simple_material_node, complex_ 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_json(condense_to_uuid={}).json) - project_dict["created_by"] = json.loads(complex_user_node.get_json(condense_to_uuid={}).json) + 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 project_dict["name"] = "my project name" project_dict["notes"] = "my project notes" - project_dict["member"] = [json.loads(complex_user_node.get_json(condense_to_uuid={}).json)] - project_dict["admin"] = [json.loads(complex_user_node.get_json(condense_to_uuid={}).json)] - project_dict["collection"] = [json.loads(complex_collection_node.get_json(condense_to_uuid={}).json)] - project_dict["material"] = [json.loads(copy.deepcopy(simple_material_node).get_json(condense_to_uuid={}).json)] + project_dict["member"] = [json.loads(complex_user_node.get_expanded_json())] + project_dict["admin"] = [json.loads(complex_user_node.get_expanded_json())] + project_dict["collection"] = [json.loads(complex_collection_node.get_expanded_json())] + project_dict["material"] = [json.loads(copy.deepcopy(simple_material_node).get_expanded_json())] return project_dict @@ -47,6 +47,387 @@ def complex_project_node(complex_project_dict) -> cript.Project: return complex_project +@pytest.fixture(scope="function") +def fixed_cyclic_project_node() -> cript.Project: + project_json_string: str = "{\n" + project_json_string += '"node": ["Project"],\n' + project_json_string += '"uid": "_:0e487131-1dbf-4c6c-9ee0-650df148b06e",\n' + project_json_string += '"uuid": "55a41f23-4791-4dc7-bf2c-48fd2b6fc90b",\n' + project_json_string += '"updated_by": {\n' + project_json_string += '"node": ["User"],\n' + project_json_string += '"uid": "_:5838e9cc-f01d-468e-96de-93bfe6fb758f",\n' + project_json_string += '"uuid": "5838e9cc-f01d-468e-96de-93bfe6fb758f",\n' + 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 += '"created_by": {\n' + project_json_string += '"node": ["User"],\n' + project_json_string += '"uid": "_:2bb1fc16-6e72-480d-a3df-b74eac4d32e8",\n' + project_json_string += '"uuid": "ab385d26-6ee5-40d4-9095-2d623616a162",\n' + 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' + project_json_string += '"member": [\n' + project_json_string += "{\n" + project_json_string += '"uid": "_:2bb1fc16-6e72-480d-a3df-b74eac4d32e8"\n' + project_json_string += "}\n" + project_json_string += "],\n" + project_json_string += '"admin": [\n' + project_json_string += "{\n" + project_json_string += '"uid": "_:2bb1fc16-6e72-480d-a3df-b74eac4d32e8"\n' + project_json_string += "}\n" + project_json_string += "],\n" + project_json_string += '"collection": [\n' + project_json_string += "{\n" + project_json_string += '"node": ["Collection"],\n' + project_json_string += '"uid": "_:61a99328-108b-4307-bfcb-fccd12a5dd91",\n' + project_json_string += '"uuid": "61a99328-108b-4307-bfcb-fccd12a5dd91",\n' + project_json_string += '"name": "my complex collection name",\n' + project_json_string += '"experiment": [\n' + project_json_string += "{\n" + project_json_string += '"node": ["Experiment"],\n' + project_json_string += '"uid": "_:d2b3169d-c200-4143-811a-a0d07439dd96",\n' + project_json_string += '"uuid": "d2b3169d-c200-4143-811a-a0d07439dd96",\n' + project_json_string += '"name": "my experiment name",\n' + project_json_string += '"process": [\n' + project_json_string += "{\n" + project_json_string += '"node": ["Process"],\n' + project_json_string += '"uid": "_:5d649b93-8f2c-4509-9aa7-311213df9405",\n' + project_json_string += '"uuid": "5d649b93-8f2c-4509-9aa7-311213df9405",\n' + project_json_string += '"name": "my process name",\n' + project_json_string += '"type": "affinity_pure",\n' + project_json_string += '"ingredient": [\n' + project_json_string += "{\n" + project_json_string += '"node": ["Ingredient"],\n' + project_json_string += '"uid": "_:21601ad5-c225-4626-8baa-3a7c1d64cafa",\n' + project_json_string += '"uuid": "21601ad5-c225-4626-8baa-3a7c1d64cafa",\n' + project_json_string += '"material": {\n' + project_json_string += '"node": ["Material"],\n' + project_json_string += '"uid": "_:ea9ad3e7-84a7-475f-82a8-16f5b9241e37",\n' + project_json_string += '"uuid": "ea9ad3e7-84a7-475f-82a8-16f5b9241e37",\n' + project_json_string += '"name": "my test material 9221be1d-247c-4f67-8a0a-fe1ec657705b",\n' + project_json_string += '"property": [\n' + project_json_string += "{\n" + 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 += '"type": "value",\n' + project_json_string += '"value": 5.0,\n' + project_json_string += '"unit": "GPa",\n' + project_json_string += '"computation": [\n' + project_json_string += "{\n" + project_json_string += '"node": ["Computation"],\n' + project_json_string += '"uid": "_:175708fa-cc29-4442-be7f-adf85e995330",\n' + project_json_string += '"uuid": "175708fa-cc29-4442-be7f-adf85e995330",\n' + project_json_string += '"name": "my computation name",\n' + project_json_string += '"type": "analysis",\n' + project_json_string += '"input_data": [\n' + project_json_string += "{\n" + project_json_string += '"node": ["Data"],\n' + project_json_string += '"uid": "_:dcb516a1-951d-461a-beb6-bdf2aecd0778",\n' + project_json_string += '"uuid": "dcb516a1-951d-461a-beb6-bdf2aecd0778",\n' + project_json_string += '"name": "my data name",\n' + project_json_string += '"type": "afm_amp",\n' + project_json_string += '"file": [\n' + project_json_string += "{\n" + project_json_string += '"node": ["File"],\n' + project_json_string += '"uid": "_:c94aba31-adf2-4eeb-b51e-8c70568c2eb0",\n' + project_json_string += '"uuid": "c94aba31-adf2-4eeb-b51e-8c70568c2eb0",\n' + project_json_string += '"name": "my complex file node fixture",\n' + project_json_string += '"source": "https://criptapp.org",\n' + project_json_string += '"type": "calibration",\n' + project_json_string += '"extension": ".csv",\n' + project_json_string += '"data_dictionary": "my file\'s data dictionary"\n' + project_json_string += "}\n" + project_json_string += "]\n" + project_json_string += "}\n" + project_json_string += "],\n" + project_json_string += '"output_data": [\n' + project_json_string += "{\n" + project_json_string += '"node": ["Data"],\n' + project_json_string += '"uid": "_:6a2e81af-861b-4c66-96fd-b969a38b81b1",\n' + project_json_string += '"uuid": "6a2e81af-861b-4c66-96fd-b969a38b81b1",\n' + project_json_string += '"name": "my data name",\n' + project_json_string += '"type": "afm_amp",\n' + project_json_string += '"file": [\n' + project_json_string += "{\n" + project_json_string += '"node": ["File"],\n' + project_json_string += '"uid": "_:ce01ba93-cfc5-4265-a6d6-8c38397deb43",\n' + project_json_string += '"uuid": "ce01ba93-cfc5-4265-a6d6-8c38397deb43",\n' + project_json_string += '"name": "my complex file node fixture",\n' + project_json_string += '"source": "https://criptapp.org",\n' + project_json_string += '"type": "calibration",\n' + project_json_string += '"extension": ".csv",\n' + project_json_string += '"data_dictionary": "my file\'s data dictionary"\n' + project_json_string += "}\n" + project_json_string += "]\n" + project_json_string += "}\n" + project_json_string += "]\n" + project_json_string += "},\n" + project_json_string += "{\n" + project_json_string += '"node": ["Computation"],\n' + project_json_string += '"uid": "_:1aa11462-b394-4c35-906e-d0e9198be6da",\n' + project_json_string += '"uuid": "1aa11462-b394-4c35-906e-d0e9198be6da",\n' + project_json_string += '"name": "my computation name",\n' + project_json_string += '"type": "analysis",\n' + project_json_string += '"input_data": [\n' + project_json_string += "{\n" + project_json_string += '"uid": "_:dcb516a1-951d-461a-beb6-bdf2aecd0778"\n' + project_json_string += "}\n" + project_json_string += "],\n" + project_json_string += '"output_data": [\n' + project_json_string += "{\n" + project_json_string += '"uid": "_:6a2e81af-861b-4c66-96fd-b969a38b81b1"\n' + project_json_string += "}\n" + project_json_string += "]\n" + project_json_string += "}\n" + project_json_string += "]\n" + project_json_string += "}\n" + project_json_string += "],\n" + project_json_string += '"parent_material": {\n' + project_json_string += '"node": ["Material"],\n' + project_json_string += '"uid": "_:2ee56671-5efb-4f99-a7ea-d659f5b5dd9a",\n' + project_json_string += '"uuid": "2ee56671-5efb-4f99-a7ea-d659f5b5dd9a",\n' + project_json_string += '"name": "my test material 9221be1d-247c-4f67-8a0a-fe1ec657705b",\n' + project_json_string += '"process": {\n' + project_json_string += '"uid": "_:5d649b93-8f2c-4509-9aa7-311213df9405"\n' + project_json_string += "},\n" + project_json_string += '"property": [\n' + project_json_string += "{\n" + 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 += '"type": "value",\n' + project_json_string += '"value": 5.0,\n' + project_json_string += '"unit": "GPa",\n' + project_json_string += '"computation": [\n' + project_json_string += "{\n" + project_json_string += '"node": ["Computation"],\n' + project_json_string += '"uid": "_:2818ed85-2758-45f9-9f30-5c3dfedd3d33",\n' + project_json_string += '"uuid": "2818ed85-2758-45f9-9f30-5c3dfedd3d33",\n' + project_json_string += '"name": "my computation name",\n' + project_json_string += '"type": "analysis",\n' + project_json_string += '"input_data": [\n' + project_json_string += "{\n" + project_json_string += '"node": ["Data"],\n' + project_json_string += '"uid": "_:0a88e09d-488f-45ed-ad9c-14873792b8fd",\n' + project_json_string += '"uuid": "0a88e09d-488f-45ed-ad9c-14873792b8fd",\n' + project_json_string += '"name": "my data name",\n' + project_json_string += '"type": "afm_amp",\n' + project_json_string += '"file": [\n' + project_json_string += "{\n" + project_json_string += '"node": ["File"],\n' + project_json_string += '"uid": "_:1fc95012-2845-46ac-a8e3-7178fe19afcd",\n' + project_json_string += '"uuid": "1fc95012-2845-46ac-a8e3-7178fe19afcd",\n' + project_json_string += '"name": "my complex file node fixture",\n' + project_json_string += '"source": "https://criptapp.org",\n' + project_json_string += '"type": "calibration",\n' + project_json_string += '"extension": ".csv",\n' + project_json_string += '"data_dictionary": "my file\'s data dictionary"\n' + project_json_string += "}\n" + project_json_string += "]\n" + project_json_string += "}\n" + project_json_string += "],\n" + project_json_string += '"output_data": [\n' + project_json_string += "{\n" + project_json_string += '"node": ["Data"],\n' + project_json_string += '"uid": "_:309b0d7b-027c-4422-ab5f-58069fe4adb1",\n' + project_json_string += '"uuid": "309b0d7b-027c-4422-ab5f-58069fe4adb1",\n' + project_json_string += '"name": "my data name",\n' + project_json_string += '"type": "afm_amp",\n' + project_json_string += '"file": [\n' + project_json_string += "{\n" + project_json_string += '"node": ["File"],\n' + project_json_string += '"uid": "_:e78cf3cf-de6c-4364-93c4-4fb3d352bde2",\n' + project_json_string += '"uuid": "e78cf3cf-de6c-4364-93c4-4fb3d352bde2",\n' + project_json_string += '"name": "my complex file node fixture",\n' + project_json_string += '"source": "https://criptapp.org",\n' + project_json_string += '"type": "calibration",\n' + project_json_string += '"extension": ".csv",\n' + project_json_string += '"data_dictionary": "my file\'s data dictionary"\n' + project_json_string += "}\n" + project_json_string += "]\n" + project_json_string += "}\n" + project_json_string += "]\n" + project_json_string += "},\n" + project_json_string += "{\n" + project_json_string += '"node": ["Computation"],\n' + project_json_string += '"uid": "_:09cf72a4-a397-4953-baa6-7cdf5be067c4",\n' + project_json_string += '"uuid": "09cf72a4-a397-4953-baa6-7cdf5be067c4",\n' + project_json_string += '"name": "my computation name",\n' + project_json_string += '"type": "analysis",\n' + project_json_string += '"input_data": [\n' + project_json_string += "{\n" + project_json_string += '"uid": "_:0a88e09d-488f-45ed-ad9c-14873792b8fd"\n' + project_json_string += "}\n" + project_json_string += "],\n" + project_json_string += '"output_data": [\n' + project_json_string += "{\n" + project_json_string += '"uid": "_:309b0d7b-027c-4422-ab5f-58069fe4adb1"\n' + project_json_string += "}\n" + project_json_string += "]\n" + project_json_string += "}\n" + project_json_string += "]\n" + project_json_string += "}\n" + project_json_string += "],\n" + project_json_string += '"bigsmiles": "{[][$]COC[$][]}"\n' + project_json_string += "},\n" + project_json_string += '"bigsmiles": "{[][$]COC[$][]}"\n' + project_json_string += "}\n" + project_json_string += "}\n" + project_json_string += "],\n" + project_json_string += '"product": [\n' + project_json_string += "{\n" + project_json_string += '"uid": "_:ea9ad3e7-84a7-475f-82a8-16f5b9241e37"\n' + project_json_string += "}\n" + project_json_string += "]\n" + project_json_string += "}\n" + project_json_string += "],\n" + project_json_string += '"computation": [\n' + project_json_string += "{\n" + project_json_string += '"uid": "_:2818ed85-2758-45f9-9f30-5c3dfedd3d33"\n' + project_json_string += "},\n" + project_json_string += "{\n" + project_json_string += '"uid": "_:09cf72a4-a397-4953-baa6-7cdf5be067c4"\n' + project_json_string += "},\n" + project_json_string += "{\n" + project_json_string += '"uid": "_:175708fa-cc29-4442-be7f-adf85e995330"\n' + project_json_string += "},\n" + project_json_string += "{\n" + project_json_string += '"uid": "_:1aa11462-b394-4c35-906e-d0e9198be6da"\n' + project_json_string += "}\n" + project_json_string += "],\n" + project_json_string += '"data": [\n' + project_json_string += "{\n" + project_json_string += '"uid": "_:0a88e09d-488f-45ed-ad9c-14873792b8fd"\n' + project_json_string += "},\n" + project_json_string += "{\n" + project_json_string += '"uid": "_:309b0d7b-027c-4422-ab5f-58069fe4adb1"\n' + project_json_string += "},\n" + project_json_string += "{\n" + project_json_string += '"uid": "_:dcb516a1-951d-461a-beb6-bdf2aecd0778"\n' + project_json_string += "},\n" + project_json_string += "{\n" + project_json_string += '"uid": "_:6a2e81af-861b-4c66-96fd-b969a38b81b1"\n' + project_json_string += "}\n" + project_json_string += "]\n" + project_json_string += "}\n" + project_json_string += "],\n" + project_json_string += '"inventory": [\n' + project_json_string += "{\n" + project_json_string += '"node": ["Inventory"],\n' + project_json_string += '"uid": "_:1ff50987-e0a2-4aa3-a1a2-bdcefd54693d",\n' + project_json_string += '"uuid": "1ff50987-e0a2-4aa3-a1a2-bdcefd54693d",\n' + project_json_string += '"name": "my inventory name",\n' + project_json_string += '"material": [\n' + project_json_string += "{\n" + project_json_string += '"uid": "_:ea9ad3e7-84a7-475f-82a8-16f5b9241e37"\n' + project_json_string += "},\n" + project_json_string += "{\n" + project_json_string += '"node": ["Material"],\n' + project_json_string += '"uid": "_:845c90c1-5a93-416f-9c42-1bac1de0bd9a",\n' + project_json_string += '"uuid": "845c90c1-5a93-416f-9c42-1bac1de0bd9a",\n' + project_json_string += '"name": "material 2 730f2483-f018-4583-82d3-beb27947d470",\n' + project_json_string += '"process": {\n' + project_json_string += '"uid": "_:5d649b93-8f2c-4509-9aa7-311213df9405"\n' + project_json_string += "},\n" + project_json_string += '"property": [\n' + project_json_string += "{\n" + project_json_string += '"uid": "_:fc504202-6fdd-43c7-830d-40c7d3f0cb8c"\n' + project_json_string += "}\n" + project_json_string += "],\n" + project_json_string += '"bigsmiles": "{[][$]COC[$][]}"\n' + project_json_string += "}\n" + project_json_string += "]\n" + project_json_string += "}\n" + project_json_string += "],\n" + project_json_string += '"doi": "10.1038/1781168a0",\n' + project_json_string += '"citation": [\n' + project_json_string += "{\n" + project_json_string += '"node": ["Citation"],\n' + project_json_string += '"uid": "_:1232d7be-d870-4357-bf41-f53a09707cca",\n' + project_json_string += '"uuid": "1232d7be-d870-4357-bf41-f53a09707cca",\n' + project_json_string += '"type": "reference",\n' + project_json_string += '"reference": {\n' + project_json_string += '"node": ["Reference"],\n' + project_json_string += '"uid": "_:3fb7801f-7253-4d6b-813b-f1d2d25b6316",\n' + project_json_string += '"uuid": "3fb7801f-7253-4d6b-813b-f1d2d25b6316",\n' + project_json_string += '"type": "journal_article",\n' + project_json_string += '"title": "Multi-architecture Monte-Carlo (MC) simulation of soft coarse-grained polymeric materials: SOft coarse grained Monte-Carlo Acceleration (SOMA)",\n' + project_json_string += '"author": ["Ludwig Schneider", "Marcus M\u00fcller"],\n' + project_json_string += '"journal": "Computer Physics Communications",\n' + project_json_string += '"publisher": "Elsevier",\n' + project_json_string += '"year": 2019,\n' + project_json_string += '"pages": [463, 476],\n' + project_json_string += '"doi": "10.1016/j.cpc.2018.08.011",\n' + project_json_string += '"issn": "0010-4655",\n' + project_json_string += '"website": "https://www.sciencedirect.com/science/article/pii/S0010465518303072"\n' + project_json_string += "}\n" + project_json_string += "}\n" + project_json_string += "]\n" + project_json_string += "}\n" + project_json_string += "],\n" + project_json_string += '"material": [\n' + project_json_string += "{\n" + project_json_string += '"uid": "_:2ee56671-5efb-4f99-a7ea-d659f5b5dd9a"\n' + project_json_string += "}\n" + project_json_string += "]\n" + project_json_string += "}\n" + + return cript.load_nodes_from_json(project_json_string) + + +@pytest.fixture(scope="function") +def fixed_cyclic_project_dfs_uuid_order(): + expected_list = [ + "55a41f23-4791-4dc7-bf2c-48fd2b6fc90b", + "ab385d26-6ee5-40d4-9095-2d623616a162", + "61a99328-108b-4307-bfcb-fccd12a5dd91", + "1232d7be-d870-4357-bf41-f53a09707cca", + "3fb7801f-7253-4d6b-813b-f1d2d25b6316", + "d2b3169d-c200-4143-811a-a0d07439dd96", + "2818ed85-2758-45f9-9f30-5c3dfedd3d33", + "0a88e09d-488f-45ed-ad9c-14873792b8fd", + "1fc95012-2845-46ac-a8e3-7178fe19afcd", + "309b0d7b-027c-4422-ab5f-58069fe4adb1", + "e78cf3cf-de6c-4364-93c4-4fb3d352bde2", + "09cf72a4-a397-4953-baa6-7cdf5be067c4", + "175708fa-cc29-4442-be7f-adf85e995330", + "dcb516a1-951d-461a-beb6-bdf2aecd0778", + "c94aba31-adf2-4eeb-b51e-8c70568c2eb0", + "6a2e81af-861b-4c66-96fd-b969a38b81b1", + "ce01ba93-cfc5-4265-a6d6-8c38397deb43", + "1aa11462-b394-4c35-906e-d0e9198be6da", + "5d649b93-8f2c-4509-9aa7-311213df9405", + "21601ad5-c225-4626-8baa-3a7c1d64cafa", + "ea9ad3e7-84a7-475f-82a8-16f5b9241e37", + "2ee56671-5efb-4f99-a7ea-d659f5b5dd9a", + "fde629f5-8d3a-4546-8cd3-9de63b990187", + "fc504202-6fdd-43c7-830d-40c7d3f0cb8c", + "1ff50987-e0a2-4aa3-a1a2-bdcefd54693d", + "845c90c1-5a93-416f-9c42-1bac1de0bd9a", + "5838e9cc-f01d-468e-96de-93bfe6fb758f", + ] + return expected_list + + @pytest.fixture(scope="function") def simple_collection_node(simple_experiment_node) -> cript.Collection: """ @@ -244,10 +625,10 @@ def complex_material_dict(simple_property_node, simple_process_node, complex_com material_dict = {"node": ["Material"]} material_dict["name"] = "my complex material" - material_dict["property"] = [json.loads(simple_property_node.get_json(condense_to_uuid={}).json)] - material_dict["process"] = json.loads(simple_process_node.get_json(condense_to_uuid={}).json) - material_dict["parent_material"] = json.loads(simple_material_node.get_json(condense_to_uuid={}).json) - material_dict["computational_forcefield"] = json.loads(complex_computational_forcefield_node.get_json(condense_to_uuid={}).json) + material_dict["property"] = [json.loads(simple_property_node.get_expanded_json())] + material_dict["process"] = json.loads(simple_process_node.get_expanded_json()) + material_dict["parent_material"] = json.loads(simple_material_node.get_expanded_json()) + material_dict["computational_forcefield"] = json.loads(complex_computational_forcefield_node.get_expanded_json()) material_dict["bigsmiles"] = "{[][$]CC[$][]}" material_dict["keyword"] = my_material_keyword diff --git a/tests/test_node_util.py b/tests/test_node_util.py index 806d4799b..a1125ceda 100644 --- a/tests/test_node_util.py +++ b/tests/test_node_util.py @@ -187,28 +187,22 @@ def test_local_search(simple_algorithm_node, complex_parameter_node): find_algorithms = a.find_children({"parameter": [{"key": "damping_time"}, {"key": "update_frequency"}, {"foo": "bar"}]}) assert find_algorithms == [] + # Test search depth exclusions + find_algorithms = a.find_children({"node": "Algorithm", "key": "mc_barostat"}, search_depth=0) + assert find_algorithms == [a] + find_parameter = a.find_children({"node": ["Parameter"]}, search_depth=1) + assert find_parameter == [p1, p2] + find_parameter = a.find_children({"node": ["Parameter"]}, search_depth=0) + assert find_parameter == [] + + +def test_cycles(fixed_cyclic_project_node): + new_project = fixed_cyclic_project_node + new_json = new_project.get_expanded_json() -def test_cycles(complex_data_node, simple_computation_node): - # We create a wrong cycle with parameters here. - # TODO replace this with nodes that actually can form a cycle - d = copy.deepcopy(complex_data_node) - c = copy.deepcopy(simple_computation_node) - d.computation += [c] - # Using input and output data guarantees a cycle here. - c.output_data += [d] - c.input_data += [d] - - # # Test the repetition of a citation. - # # Notice that we do not use a deepcopy here, as we want the citation to be the exact same node. - # citation = d.citation[0] - # # c._json_attrs.citation.append(citation) - # c.citation += [citation] - # # print(c.get_json(indent=2).json) - # # c.validate() - - # Generate json with an implicit cycle - c.json - d.json + reloaded_project, cache = cript.load_nodes_from_json(new_json, _use_uuid_cache=dict()) + assert reloaded_project is not new_project + assert reloaded_project.uuid == new_project.uuid def test_uid_serial(simple_inventory_node): @@ -291,6 +285,7 @@ def test_invalid_project_graphs(simple_project_node, simple_material_node, simpl # Now add an orphan data data = copy.deepcopy(simple_data_node) property.data = [data] + with pytest.raises(CRIPTOrphanedDataError): project.validate() # Fix with the helper function @@ -364,3 +359,8 @@ def test_uuid_cache_override(complex_project_node): new_node = cache[key] assert old_node.uuid == new_node.uuid assert old_node is not new_node + + +def test_dfs_order(fixed_cyclic_project_node, fixed_cyclic_project_dfs_uuid_order): + for i, node in enumerate(fixed_cyclic_project_node): + assert node.uuid == fixed_cyclic_project_dfs_uuid_order[i]