Skip to content
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

Update newest (no deep diff) #449

Open
wants to merge 22 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 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
180 changes: 178 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,161 @@ 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, _no_condense_uuid=True)
print("GET_ALI_HERE")
quit()
self._internal_save(new_node, _no_condense_uuid=False)

def _internal_save(self, new_node: PrimaryBaseNode, preknown_uid: str, _no_condense_uuid: bool) -> None:
"""
NOTE: for Ludwig
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@InnocentBug

Ludwig , this is still Work In Progress, I think I fleshed out most of the skeleton but where to put and use the "uid map" is causing me to scratch my head a bit. I left a pretty detailed note in the docstring here. do you mind taking a a glance at it ? (the note?) and let me know your thoughts?


WIP NOTES:

unfinished WIP, but the idea is we want to send
a preknown_uuid

I'm imagining something like
data = new_node.get_json(preknown_uid="preknown uid into here").json
in the same way we did the _no_condense_uuid

but, then , I see where I would add a preknown uuid
scroll down to

WIP NOTES CONTINUED:

"""

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(_no_condense_uuid=_no_condense_uuid).json
# data = new_node.get_json(sort_keys=False, condense_to_uuid={}, indent=2).json # indent=2

# data = new_node.get_json(_no_condense_uuid=_no_condense_uuid).json

data = new_node.get_json(_no_condense_uuid=_no_condense_uuid).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 wouldnt 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 +609,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
35 changes: 34 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,9 @@ def get_json(
"Project": {"member", "admin"},
"Collection": {"member", "admin"},
},
**kwargs
_no_condense_uuid: bool = False,
preknown_uid: str = "",
**kwargs,
):
"""
User facing access to get the JSON of a node.
Expand Down Expand Up @@ -462,6 +488,10 @@ class ReturnTuple:
previous_condense_to_uuid = copy.deepcopy(NodeEncoder.condense_to_uuid)
NodeEncoder.condense_to_uuid = condense_to_uuid

previous_no_condense_uuid = copy.deepcopy(NodeEncoder.no_condense_uuid)
NodeEncoder.no_condense_uuid = _no_condense_uuid
NodeEncoder.preknown_uid = preknown_uid

try:
tmp_json = json.dumps(self, cls=NodeEncoder, **kwargs)
tmp_dict = json.loads(tmp_json)
Expand All @@ -479,6 +509,9 @@ class ReturnTuple:
NodeEncoder.known_uuid = previous_known_uuid
NodeEncoder.suppress_attributes = previous_suppress_attributes
NodeEncoder.condense_to_uuid = previous_condense_to_uuid
NodeEncoder.no_condense_uuid = previous_no_condense_uuid
# HERE??
# NodeEncoder.preknown_uid = preknown_uid

def find_children(self, search_attr: dict, search_depth: int = -1, handled_nodes: Optional[List] = None) -> List:
"""
Expand Down
14 changes: 13 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 @@ -66,6 +67,8 @@ class NodeEncoder(json.JSONEncoder):
known_uuid: Set[str] = set()
condense_to_uuid: Dict[str, Set[str]] = dict()
suppress_attributes: Optional[Dict[str, Set[str]]] = None
no_condense_uuid: bool = False
preknown_uid: str = ""

def default(self, obj):
"""
Expand Down Expand Up @@ -182,9 +185,18 @@ def strip_to_edge_uuid(element):
except AttributeError:
uid = element["uid"]

element = {"uuid": str(uuid)}
######## WIP HERE ################
if self.preknown_uid:
element = {"uid": str(uid)}
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
Loading