Skip to content

Update newest (no deep diff) #449

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 159 additions & 2 deletions src/cript/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import requests
from beartype import beartype

import cript.nodes.primary_nodes as PrimaryNodes
from cript.api.api_config import _API_TIMEOUT
from cript.api.data_schema import DataSchema
from cript.api.exceptions import (
Expand All @@ -30,7 +31,9 @@
)
from cript.api.utils.web_file_downloader import download_file_from_url
from cript.api.valid_search_modes import SearchModes
from cript.nodes.primary_nodes.primary_base_node import PrimaryBaseNode
from cript.nodes.primary_nodes.project import Project
from cript.nodes.util import load_nodes_from_json

# Do not use this directly! That includes devs.
# Use the `_get_global_cached_api for access.
Expand All @@ -47,6 +50,25 @@ def _get_global_cached_api():
return _global_cached_api


class LastModifiedDict(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._order = list(self.keys())

def __setitem__(self, key, value):
super().__setitem__(key, value)
if key in self._order:
self._order.remove(key)
self._order.append(key)

def keys_sorted_by_last_modified(self):
order = []
for key in self._order:
if key in self:
order.append(key)
return order


class API:
"""
## Definition
Expand Down Expand Up @@ -402,7 +424,142 @@ def api_prefix(self):
def api_version(self):
return self._api_version

def save(self, project: Project) -> None:
# _no_condense_uuid is either active or not
def save(self, new_node):
self._internal_save(new_node)
print("GET_ALI_HERE")


def _internal_save(self, new_node: PrimaryBaseNode) -> None:
new_node.validate(force_validation=True)
data = new_node.get_json().json


print("----------\\------------\n")

node_class_name = new_node.node_type.capitalize()
NodeClass = getattr(PrimaryNodes, node_class_name)

old_node_paginator = self.search(node_type=NodeClass, search_mode=SearchModes.UUID, value_to_search=str(new_node.uuid))
old_node_paginator.auto_load_nodes = False
try:
old_node_json = next(old_node_paginator)

except StopIteration: # New Project do POST instead
# Do the POST request call. only on project
# or else its a patch handled by previous node

if new_node.node_type.lower() == "project":

data = new_node.get_json().json


# data = new_node.get_json(condense_to_uuid={}).json
print("---- data 2 -----")
# print(type(data2))
# print(type(json.loads(data2)))
# data = json.loads(data)
# print(type(data))
# print(str(data))
print(data)
# data = str(data)
# data = json.dumps(data)
# data = data.replace('""', "[]")
# print("now data\n", data)
print("---- data end -----")
# if _no_condense_uuid is true do a POST if its false do a patch,
# but wouldn't we then just find the existing node above in the generator?
response = self._capsule_request(url_path="/project/", method="POST", data=data)
if response.status_code in [200, 201]:
print("FINALLY_WORKED!")
return # Return here, since we successfully Posting
else: # debug for now
print("GET HERE ALI")
res = response.json()
raise Exception(f"APIError {res}")

old_project, old_uuid_map = load_nodes_from_json(nodes_json=old_node_json, _use_uuid_cache={})

if new_node.deep_equal(old_project):
return # No save necessary, since nothing changed

delete_uuid = []

patch_map = LastModifiedDict()

# Iterate the new project in DFS
for node in new_node:
try:
old_node = old_uuid_map[node.uuid]
except KeyError:
# This node only exists in the new new project,
# But it has a parent which patches it in, so no action needed
pass

# do we need to delete any children, that existed in the old node, but don't exit in the new node.
node_child_map = {child.uuid: child for child in node.find_children({}, search_depth=1)}
old_child_map = {child.uuid: child for child in old_node.find_children({}, search_depth=1)}
for old_uuid in old_child_map:
if old_uuid not in node_child_map:
if old_uuid not in delete_uuid:
delete_uuid += [old_uuid]

# check if the current new node needs a patch

if not node.shallow_equal(old_node):
patch_map[node.uuid] = node

# now patch and delete

# here its project but really it should be a primary base node

url_path = f"/{new_node.node_type}/{new_node.uuid}"

"""
WIP NOTES CONTINUED:

this is where we would need to make a map of the uids
patch_map[uid_] ? constructed above?

problem right now is ,
we need the uids to be in place for the original POST json
which happens up above
so I'm wondeing how to go about that,

and I think I would need to rewalk the original get_json for the post
switching any {uuid: "uuid"} for {uid:"uid"}

AND I TRY TO DO THAT if you look inside

json.py , you'd find the following below

######## WIP HERE ################
if self.preknown_uid:
element = {"uid": str(uid)}
return element, uid

but we need this uid map and i'm not so sure where to put this ?

"""
for uuid_ in reversed(patch_map.keys_sorted_by_last_modified()):
node = patch_map[uuid_]

# "Doing API PATCH for {node.uuid}"
# either link if found or patch json to parent
data = node.get_json().json
# first level search will also include attributes?
self._capsule_request(url_path=url_path, method="PATCH", data=json.dumps(data))

for uuid_ in delete_uuid:
# do the delete *unlinking here
# actually here we are able to send list of uuids to be deleted - optimize later
# "Doing API Delete for {uuid_}"
unlink_payload = {"uuid": str(uuid_)}
self._capsule_request(url_path=url_path, method="DELETE", data=json.dumps(unlink_payload))

################################################################################

def save_old(self, project: Project) -> None:
"""
This method takes a project node, serializes the class into JSON
and then sends the JSON to be saved to the API.
Expand Down Expand Up @@ -433,7 +590,7 @@ def save(self, project: Project) -> None:
pass
raise exc from exc

def _internal_save(self, node, save_values: Optional[_InternalSaveValues] = None) -> _InternalSaveValues:
def _internal_save_old(self, node, save_values: Optional[_InternalSaveValues] = None) -> _InternalSaveValues:
"""
Internal helper function that handles the saving of different nodes (not just project).

Expand Down
39 changes: 38 additions & 1 deletion src/cript/nodes/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,30 @@ def node_type_snake_case(self):
snake_case = re.sub(r"(?<!^)(?=[A-Z])", "_", camel_case).lower()
return snake_case

################################################################################

# Member function of BaseNode
def shallow_equal(self, other):
self_child_map = {child.uuid: child for child in self.find_children({}, search_depth=1)}
del self_child_map[self.uuid]
other_child_map = {child.uuid: child for child in other.find_children({}, search_depth=1)}
del other_child_map[other.uuid]

self_sorted_json = self.get_json(known_uuid=self_child_map.keys(), sort_keys=True, condense_to_uuid={}).json
other_sorted_json = other.get_json(known_uuid=other_child_map.keys(), sort_keys=True, condense_to_uuid={}).json

return self_sorted_json == other_sorted_json

# Member function of BaseNode
def deep_equal(self, other):
self_sorted_json = self.get_expanded_json(sort_keys=True)
other_sorted_json = other.get_expanded_json(sort_keys=True)
return self_sorted_json == other_sorted_json

################################################################################

################################################################################

# Prevent new attributes being set.
# This might just be temporary, but for now, I don't want to accidentally add new attributes, when I mean to modify one.
def __setattr__(self, key, value):
Expand Down Expand Up @@ -423,7 +447,8 @@ def get_json(
"Project": {"member", "admin"},
"Collection": {"member", "admin"},
},
**kwargs

**kwargs,
):
"""
User facing access to get the JSON of a node.
Expand Down Expand Up @@ -462,6 +487,15 @@ class ReturnTuple:
previous_condense_to_uuid = copy.deepcopy(NodeEncoder.condense_to_uuid)
NodeEncoder.condense_to_uuid = condense_to_uuid


known_uid = set()
for child_node in self:
known_uid.add(child_node.uid)

previous_known_uid = copy.deepcopy(NodeEncoder.known_uid)
NodeEncoder.known_uid = known_uid


try:
tmp_json = json.dumps(self, cls=NodeEncoder, **kwargs)
tmp_dict = json.loads(tmp_json)
Expand All @@ -480,6 +514,9 @@ class ReturnTuple:
NodeEncoder.suppress_attributes = previous_suppress_attributes
NodeEncoder.condense_to_uuid = previous_condense_to_uuid

NodeEncoder.known_uid = previous_known_uid


def find_children(self, search_attr: dict, search_depth: int = -1, handled_nodes: Optional[List] = None) -> List:
"""
Finds all the children in a given tree of nodes (specified by its root),
Expand Down
19 changes: 18 additions & 1 deletion src/cript/nodes/util/json.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
This module contains classes and functions that help with the json serialization and deserialization of nodes.
"""

import dataclasses
import inspect
import json
Expand Down Expand Up @@ -67,6 +68,9 @@ class NodeEncoder(json.JSONEncoder):
condense_to_uuid: Dict[str, Set[str]] = dict()
suppress_attributes: Optional[Dict[str, Set[str]]] = None

known_uid: Optional[Set[str]] = None


def default(self, obj):
"""
Convert CRIPT nodes and other objects to their JSON representation.
Expand Down Expand Up @@ -182,9 +186,22 @@ def strip_to_edge_uuid(element):
except AttributeError:
uid = element["uid"]

element = {"uuid": str(uuid)}

# If this is not the only occurrence of the node, replace it with UID instead of UUID
if self.known_uid and uid in self.known_uid:
element = {"uid": str(uid)}
else:
element = {"uuid": str(uuid)}

return element, uid

#########################
# if self.no_condense_uuid:
# element = ""
# else:
# element = {"uuid": str(uuid)}
# return element, uid

# Processes an attribute based on its type (list or single element)
if isinstance(attribute, List):
processed_elements = []
Expand Down
8 changes: 5 additions & 3 deletions tests/fixtures/primary_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,15 +656,17 @@ def complex_material_node(simple_property_node, simple_process_node, complex_com


@pytest.fixture(scope="function")
def simple_inventory_node(simple_material_node) -> None:
def simple_inventory_node() -> None:
"""
minimal inventory node to use for other tests
"""
# set up inventory node

material_2 = cript.Material(name="material 2 " + str(uuid.uuid4()), bigsmiles="{[][$]COC[$][]}")
# material_2 = cript.Material(name="material 2 " + str(uuid.uuid4()), bigsmiles="{[][$]COC[$][]}")

my_inventory = cript.Inventory(name="my inventory name", material=[simple_material_node, material_2])
my_inventory = cript.Inventory(name="my inventory name", material=[]) # material=[simple_material_node, material_2])

# my_inventory.material = []

# use my_inventory in another test
return my_inventory
Expand Down
5 changes: 4 additions & 1 deletion tests/nodes/primary_nodes/test_computational_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def test_serialize_computational_process_to_json(simple_computational_process_no
assert ref_dict == expected_dict


def test_integration_computational_process(cript_api, simple_project_node, simple_collection_node, simple_experiment_node, simplest_computational_process_node, simple_material_node, simple_data_node) -> None:
def test_integration_computational_process(cript_api, simple_project_node, simple_inventory_node, simple_collection_node, simple_experiment_node, simplest_computational_process_node, simple_material_node, simple_data_node) -> None:
"""
integration test between Python SDK and API Client

Expand All @@ -166,6 +166,9 @@ def test_integration_computational_process(cript_api, simple_project_node, simpl

simple_project_node.material = [simple_material_node]

simple_inventory_node.material = []
simple_project_node.collection[0].inventory = [simple_inventory_node]

simple_project_node.collection = [simple_collection_node]

simple_project_node.collection[0].experiment = [simple_experiment_node]
Expand Down
6 changes: 5 additions & 1 deletion tests/nodes/primary_nodes/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def test_serialize_data_to_json(simple_data_node) -> None:
assert ref_dict == expected_data_dict


def test_integration_data(cript_api, simple_project_node, simple_data_node):
def test_integration_data(cript_api, simple_project_node, simple_inventory_node, simple_data_node):
"""
integration test between Python SDK and API Client

Expand All @@ -182,6 +182,10 @@ def test_integration_data(cript_api, simple_project_node, simple_data_node):
# ========= test create =========
simple_project_node.name = f"test_integration_project_name_{uuid.uuid4().hex}"

# simple_data_node.material something
simple_inventory_node.material = []
simple_project_node.collection[0].inventory = [simple_inventory_node]

simple_project_node.collection[0].experiment[0].data = [simple_data_node]

save_integration_node_helper(cript_api=cript_api, project_node=simple_project_node)
Expand Down
1 change: 1 addition & 0 deletions tests/nodes/primary_nodes/test_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def test_integration_inventory(cript_api, simple_project_node, simple_inventory_
simple_project_node.collection[0].name = f"collection_name_{uuid.uuid4().hex}"
simple_inventory_node.name = f"inventory_name_{uuid.uuid4().hex}"

simple_inventory_node.material = []
simple_project_node.collection[0].inventory = [simple_inventory_node]

save_integration_node_helper(cript_api=cript_api, project_node=simple_project_node)
Expand Down
Loading