From c5e93c6b06481464fadbc02c35a8b2126683845d Mon Sep 17 00:00:00 2001 From: Nelson Moore Date: Thu, 31 Oct 2024 18:10:21 -0400 Subject: [PATCH] feat: add version attrs to entities - bump version to v0.2.10 from v0.2.9 - add `version` attr to Model, Node, Relationship, and Property classes, add `latest_version` attr to Model (entity) class - update test_000map.py::test_put_then_rm_queries() to create new TemporaryNode class rather than writing over Node.mapspec_ which interfered with subsequent tests when using Pytest - formatting --- python/pyproject.toml | 4 +- python/src/bento_meta/model.py | 2 +- python/src/bento_meta/object_map.py | 222 +++++++------ python/src/bento_meta/objects.py | 40 ++- python/tests/test_000map.py | 480 +++++++++++++++++----------- python/tests/test_003model.py | 116 ++++--- python/tests/test_005db.py | 184 +++++------ python/tests/test_006mapmodel.py | 299 +++++++++-------- python/tests/test_007versioning.py | 9 +- 9 files changed, 781 insertions(+), 575 deletions(-) diff --git a/python/pyproject.toml b/python/pyproject.toml index fa62a3f..d02ee4a 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "bento-meta" -version = "0.2.9" +version = "0.2.10" description = "Python drivers for Bento Metamodel Database" authors = [ { name="Mark A. Jensen", email = "mark.jensen@nih.gov"}, @@ -20,7 +20,7 @@ classifiers = [ [tool.poetry] name = "bento-meta" -version = "0.2.9" +version = "0.2.10" description = "Python drivers for Bento Metamodel Database" authors = [ "Mark A. Jensen ", diff --git a/python/src/bento_meta/model.py b/python/src/bento_meta/model.py index 6fb5c21..e3d13c4 100644 --- a/python/src/bento_meta/model.py +++ b/python/src/bento_meta/model.py @@ -35,7 +35,7 @@ class Model: - def __init__(self, handle=None, version=None, uri=None, mdb=None): + def __init__(self, handle=None, version=None, uri=None, mdb=None): """ Model constructor. diff --git a/python/src/bento_meta/object_map.py b/python/src/bento_meta/object_map.py index 538b308..8e2236f 100644 --- a/python/src/bento_meta/object_map.py +++ b/python/src/bento_meta/object_map.py @@ -2,7 +2,7 @@ bento_meta.object_map ===================== -This module contains :class:`ObjectMap`, a class which provides the +This module contains :class:`ObjectMap`, a class which provides the machinery for mapping bento_meta objects to a Bento Metamodel Database in Neo4j. Mostly not for human consumption. The ObjectMap: @@ -12,23 +12,40 @@ enable them to get and put themselves to the database * generates appropriate `Cypher `_ queries to do gets and puts -One ObjectMap instance should be generated for each Entity subclass (see, e.g., +One ObjectMap instance should be generated for each Entity subclass (see, e.g., :class:`bento_meta.model.Model`) """ + import re import sys + sys.path.append("..") -from neo4j import BoltDriver, Neo4jDriver -from bento_meta.entity import * -from bento_meta.objects import * -# from pdb import set_trace +from warnings import warn +from neo4j import BoltDriver, Neo4jDriver -class ObjectMap(object): - """This module contains :class:`ObjectMap`, a class which provides the +from bento_meta.entity import ArgError, CollValue, Entity +from bento_meta.objects import ( + Concept, + Edge, + Node, + Origin, + Predicate, + Property, + Tag, + Term, + ValueSet, +) + + +class ObjectMap: + """ + This module contains :class:`ObjectMap`, a class which provides the machinery for mapping bento_meta objects to a Bento Metamodel Database - in Neo4j. Mostly not for human consumption.""" + in Neo4j. Mostly not for human consumption. + """ + cache = {} def __init__(self, *, cls=None, drv=None): @@ -40,7 +57,7 @@ def __init__(self, *, cls=None, drv=None): self.drv = drv else: raise ArgError( - "drv= arg must be Neo4jDriver or BoltDriver (returned from GraphDatabase.driver())" + "drv= arg must be Neo4jDriver or BoltDriver (returned from GraphDatabase.driver())", ) self.maps = {} @@ -60,7 +77,17 @@ def cls_by_label(cls, lbl): def keys_by_cls_and_reln(cls, qcls, reln): if not hasattr(cls, "_keysxcls"): cls._keysxcls = {} - for o in (Node, Edge, Property, ValueSet, Term, Concept, Predicate, Origin, Tag): + for o in ( + Node, + Edge, + Property, + ValueSet, + Term, + Concept, + Predicate, + Origin, + Tag, + ): for oatt in [x for x in o.attspec if o.attspec[x] == "object"]: r = o.mapspec()["relationship"][oatt]["rel"] r = re.match("[:<>]*([a-zA-Z_]+)[:<>]*", r).group(1) @@ -74,14 +101,13 @@ def keys_by_cls_and_reln(cls, qcls, reln): @classmethod def _quote_val(cls, value, single=None): # double quote unless single is set if value is None: - return + return None if isinstance(value, (int, float)): return value # no quote + elif single: + return f"'{value}'" # quote else: - if single: - return "'{val}'".format(val=value) # quote - else: - return '"{val}"'.format(val=value) # quote + return f'"{value}"' # quote def get_by_id(self, obj, id, refresh=False): """Get an entity given an id attribute value (not the Neo4j id)""" @@ -103,13 +129,13 @@ def get_by_id(self, obj, id, refresh=False): obj.neoid = neoid return self.get(obj, refresh=True) else: - return + return None def get_by_node_nanoid(self, obj, nanoid, refresh=False): - """PROTOTYPE + """ + PROTOTYPE Get an entity given an id attribute value (not the Neo4j id) """ - neo4jid = None if not self.drv: @@ -122,12 +148,12 @@ def get_by_node_nanoid(self, obj, nanoid, refresh=False): ) # should be unique - this call will warn if there are more than one if rec is not None: neo4jid = rec["id(n)"] - + if neo4jid is None: obj.neoid = neo4jid return self.get(obj, refresh=True) else: - return + return None def get(self, obj, refresh=False): """Get the data for an object instance from the db and load the instance with it""" @@ -144,7 +170,7 @@ def get(self, obj, refresh=False): rec = result.single() if not rec: raise RuntimeError( - "object with id {neoid} not found in db".format(neoid=obj.neoid) + f"object with id {obj.neoid} not found in db", ) if obj.neoid not in ObjectMap.cache: @@ -170,8 +196,8 @@ def get(self, obj, refresh=False): if not c: raise RuntimeError( "node labels {lbls} have no associated class in the object model".format( - lbls=rec["a"].labels - ) + lbls=rec["a"].labels, + ), ) o = c(rec["a"]) o.dirty = -1 @@ -181,9 +207,7 @@ def get(self, obj, refresh=False): values[getattr(o, type(o).mapspec()["key"])] = o if self.cls.attspec[att] == "object" and len(values) > 1: warn( - "expected one node for attribute {att} on class {cls}, but got {n}; using first one".format( - att=att, cls=self.cls.__name__, n=len(values) - ) + f"expected one node for attribute {att} on class {self.cls.__name__}, but got {len(values)}; using first one", ) if self.cls.attspec[att] == "object": setattr(obj, att, first_val) @@ -191,9 +215,7 @@ def get(self, obj, refresh=False): setattr(obj, att, values) else: raise RuntimeError( - "attribute '{att}' has unknown attribute type '{atype}'".format( - att=att, atype=self.cls.attspec[att] - ) + f"attribute '{att}' has unknown attribute type '{self.cls.attspec[att]}'", ) obj.clear_removed_entities() @@ -204,7 +226,6 @@ def put(self, obj): """Put the object instance's attributes to the mapped data node in the database""" if not self.drv: raise ArgError("put() requires Neo4j driver instance") - pass with self.drv.session() as session: result = None with session.begin_transaction() as tx: @@ -214,8 +235,8 @@ def put(self, obj): if obj.neoid is None: raise RuntimeError( "no neo4j id retrived on put for obj '{name}'".format( - name=getattr(obj, self.cls.mapspec()["key"]) - ) + name=getattr(obj, self.cls.mapspec()["key"]), + ), ) for att in self.cls.mapspec()["relationship"]: values = getattr(obj, att) @@ -235,8 +256,8 @@ def put(self, obj): if val.neoid is None: raise RuntimeError( "no neo4j id retrived on put for obj '{name}'".format( - name=val[type(val).mapspec()["key"]] - ) + name=val[type(val).mapspec()["key"]], + ), ) val.dirty = 1 ObjectMap.cache[val.neoid] = val @@ -265,7 +286,8 @@ def rm(self, obj, force=False): return s.value() def add(self, obj, att, tgt): - """Create a link between an object instance and a target object in the database. + """ + Create a link between an object instance and a target object in the database. This represents adding an object-valued attribute to the object. """ if not self.drv: @@ -279,14 +301,15 @@ def add(self, obj, att, tgt): return tgt_id def drop(self, obj, att, tgt, tx=None): - """Remove an existing link between an object instance and a target object in the database. + """ + Remove an existing link between an object instance and a target object in the database. This represents dropping an object-valued attribute from the object. """ if not self.drv: raise ArgError("rm() requires Neo4j driver instance") # if the tgt is not in the database, then dropping it is a no-op: if not tgt.neoid: - return + return None if tx: result = None @@ -335,17 +358,18 @@ def get_owners(self, obj): def get_q(self, obj): if not isinstance(obj, self.cls): raise ArgError( - "arg1 must be object of class {cls}".format(cls=self.cls.__name__) + f"arg1 must be object of class {self.cls.__name__}", ) if obj.neoid is None: raise ArgError("object must be mapped (i.e., obj.neoid must be set)") return "MATCH (n:{lbl}) WHERE id(n)={neoid} RETURN n,id(n)".format( - lbl=self.cls.mapspec()["label"], neoid=obj.neoid + lbl=self.cls.mapspec()["label"], + neoid=obj.neoid, ) def get_by_id_q(self): return "MATCH (n:{lbl}) WHERE id(n)=$id and n._to IS NULL RETURN id(n)".format( - lbl=self.cls.mapspec()["label"] + lbl=self.cls.mapspec()["label"], ) def get_by_node_nanoid_q(self): @@ -355,16 +379,14 @@ def get_by_node_nanoid_q(self): def get_attr_q(self, obj, att): if not isinstance(obj, self.cls): raise ArgError( - "arg1 must be object of class {cls}".format(cls=self.cls.__name__) + f"arg1 must be object of class {self.cls.__name__}", ) if obj.neoid is None: return "" label = self.cls.mapspec()["label"] if att in self.cls.mapspec()["property"]: pr = self.cls.mapspec()["property"][att] - return "MATCH (n:{lbl}) WHERE id(n)={neoid} RETURN n.{pr}".format( - lbl=label, neoid=obj.neoid, pr=pr - ) + return f"MATCH (n:{label}) WHERE id(n)={obj.neoid} RETURN n.{pr}" elif att in self.cls.mapspec()["relationship"]: spec = self.cls.mapspec()["relationship"][att] end_cls = spec["end_cls"] @@ -373,43 +395,35 @@ def get_attr_q(self, obj, att): end_lbls = [eval(x).mapspec()["label"] for x in end_cls] rel = re.sub("^([^:]?)(:[a-zA-Z0-9_]+)(.*)$", r"\1-[\2]-\3", spec["rel"]) if len(end_lbls) == 1: - qry = "MATCH (n:{llbl}){rel}(a:{rlbl}) WHERE id(n)={neoid} RETURN a".format( - neoid=obj.neoid, llbl=label, rel=rel, rlbl=end_lbls[0] - ) + qry = f"MATCH (n:{label}){rel}(a:{end_lbls[0]}) WHERE id(n)={obj.neoid} RETURN a" if self.cls.attspec[att] == "object": qry += " LIMIT 1" return qry else: # multiple end classes possible cond = [] for l in end_lbls: - cond.append("'{lbl}' IN labels(a)".format(lbl=l)) + cond.append(f"'{l}' IN labels(a)") cond = " OR ".join(cond) - return "MATCH (n:{lbl}){rel}(a) WHERE id(n)={neoid} AND ({cond}) RETURN a".format( - lbl=label, rel=rel, neoid=obj.neoid, cond=cond - ) + return f"MATCH (n:{label}){rel}(a) WHERE id(n)={obj.neoid} AND ({cond}) RETURN a" else: raise ArgError( - "'{att}' is not a registered attribute for class '{cls}'".format( - att=att, cls=self.cls.__name__ - ) + f"'{att}' is not a registered attribute for class '{self.cls.__name__}'", ) def get_owners_q(self, obj): if not isinstance(obj, self.cls): raise ArgError( - "arg1 must be object of class {cls}".format(cls=self.cls.__name__) + f"arg1 must be object of class {self.cls.__name__}", ) if obj.neoid is None: raise ArgError("object must be mapped (i.e., obj.neoid must be set)") label = self.cls.mapspec()["label"] - return "MATCH (n:{lbl})<-[r]-(a) WHERE id(n)={neoid} RETURN TYPE(r) as reln, a".format( - neoid=obj.neoid, lbl=label - ) + return f"MATCH (n:{label})<-[r]-(a) WHERE id(n)={obj.neoid} RETURN TYPE(r) as reln, a" def put_q(self, obj): if not isinstance(obj, self.cls): raise ArgError( - "arg1 must be object of class {cls}".format(cls=self.cls.__name__) + f"arg1 must be object of class {self.cls.__name__}", ) props = {} null_props = [] @@ -423,7 +437,7 @@ def put_q(self, obj): set_clause = [] for pr in props: set_clause.append( - "n.{pr}={val}".format(pr=pr, val=ObjectMap._quote_val(props[pr])) + f"n.{pr}={ObjectMap._quote_val(props[pr])}", ) set_clause = "SET " + ",".join(set_clause) stmts.append( @@ -431,37 +445,39 @@ def put_q(self, obj): lbl=self.cls.mapspec()["label"], neoid=obj.neoid, set_clause=set_clause, - ) + ), ) for pr in null_props: stmts.append( "MATCH (n:{lbl}) WHERE id(n)={neoid} REMOVE n.{pr} RETURN n,id(n)".format( - lbl=self.cls.mapspec()["label"], neoid=obj.neoid, pr=pr - ) + lbl=self.cls.mapspec()["label"], + neoid=obj.neoid, + pr=pr, + ), ) return stmts else: spec = [] for pr in props: spec.append( - "{pr}:{val}".format(pr=pr, val=ObjectMap._quote_val(props[pr])) + f"{pr}:{ObjectMap._quote_val(props[pr])}", ) spec = ",".join(spec) return [ "CREATE (n:%s {%s}) RETURN n,id(n)" - % (self.cls.mapspec()["label"], spec) + % (self.cls.mapspec()["label"], spec), ] def put_attr_q(self, obj, att, values): if not isinstance(obj, self.cls): raise ArgError( - "arg1 must be object of class {cls}".format(cls=self.cls.__name__) + f"arg1 must be object of class {self.cls.__name__}", ) if obj.neoid is None: raise ArgError("object must be mapped (i.e., obj.neoid must be set)") if not isinstance(values, (Entity, list, CollValue)): raise ArgError( - "'values' must be a list of values suitable for the attribute" + "'values' must be a list of values suitable for the attribute", ) if isinstance(values, CollValue): values = values.values() @@ -478,9 +494,7 @@ def put_attr_q(self, obj, att, values): if not self._check_values_list(att, values): raise ArgError( "'values' must be a list of mapped Entity objects of " - "the appropriate subclass for attribute '{att}'".format( - att=att - ) + f"the appropriate subclass for attribute '{att}'", ) stmts = [] spec = self.cls.mapspec()["relationship"][att] @@ -491,7 +505,7 @@ def put_attr_q(self, obj, att, values): rel = re.sub("^([^:]?)(:[a-zA-Z0-9_]+)(.*)$", r"\1-[\2]-\3", spec["rel"]) cond = [] for l in end_lbls: - cond.append("'{lbl}' IN labels(a)".format(lbl=l)) + cond.append(f"'{l}' IN labels(a)") cond = " OR ".join(cond) for avalue in values: if len(end_lbls) == 1: @@ -503,7 +517,7 @@ def put_attr_q(self, obj, att, values): neoid=obj.neoid, aneoid=avalue.neoid, rel=rel, - ) + ), ) else: stmts.append( @@ -514,35 +528,33 @@ def put_attr_q(self, obj, att, values): neoid=obj.neoid, aneoid=avalue.neoid, rel=rel, - ) + ), ) return stmts else: raise ArgError( - "'{att}' is not a registered attribute for class '{cls}'".format( - att=att, cls=self.cls.__name__ - ) + f"'{att}' is not a registered attribute for class '{self.cls.__name__}'", ) def rm_q(self, obj, detach=False): if not isinstance(obj, self.cls): raise ArgError( - "arg1 must be object of class {cls}".format(cls=self.cls.__name__) + f"arg1 must be object of class {self.cls.__name__}", ) if obj.neoid is None: raise ArgError("object must be mapped (i.e., obj.neoid must be set)") dlt = "DETACH DELETE n" if detach else "DELETE n" qry = "MATCH (n:{lbl}) WHERE id(n)={neoid} ".format( - lbl=self.cls.mapspec()["label"], neoid=obj.neoid + lbl=self.cls.mapspec()["label"], + neoid=obj.neoid, ) return qry + dlt - pass def rm_attr_q(self, obj, att, values=None): if not isinstance(obj, self.cls): raise ArgError( - "arg1 must be object of class {cls}".format(cls=self.cls.__name__) + f"arg1 must be object of class {self.cls.__name__}", ) if obj.neoid is None: raise ArgError("object must be mapped (i.e., obj.neoid must be set)") @@ -550,7 +562,9 @@ def rm_attr_q(self, obj, att, values=None): values = [values] if att in self.cls.mapspec()["property"]: return "MATCH (n:{lbl}) WHERE id(n)={neoid} REMOVE n.{att} RETURN id(n)".format( - lbl=self.cls.mapspec()["label"], neoid=obj.neoid, att=att + lbl=self.cls.mapspec()["label"], + neoid=obj.neoid, + att=att, ) elif att in self.cls.mapspec()["relationship"]: many = self.cls.attspec[att] == "collection" @@ -561,7 +575,7 @@ def rm_attr_q(self, obj, att, values=None): end_lbls = [eval(x).mapspec()["label"] for x in end_cls] cond = [] for l in end_lbls: - cond.append("'{lbl}' IN labels(a)".format(lbl=l)) + cond.append(f"'{l}' IN labels(a)") cond = " OR ".join(cond) rel = re.sub("^([^:]?)(:[a-zA-Z0-9_]+)(.*)$", r"\1-[r\2]-\3", spec["rel"]) if values[0] == ":all": @@ -585,33 +599,37 @@ def rm_attr_q(self, obj, att, values=None): if not self._check_values_list(att, values): raise ArgError( "'values' must be a list of mapped Entity objects of the " - "appropriate subclass for attribute '{att}'".format(att=att) + f"appropriate subclass for attribute '{att}'", ) for val in values: qry = "" if len(end_lbls) == 1: - qry = "MATCH (n:{lbl}){rel}(a:{albl}) WHERE id(n)={neoid} AND id(a)={aneoid} " \ - "DELETE r RETURN id(n),id(a)".format( - lbl=self.cls.mapspec()["label"], - albl=end_lbls[0], - neoid=obj.neoid, - aneoid=val.neoid, - rel=rel) + qry = ( + "MATCH (n:{lbl}){rel}(a:{albl}) WHERE id(n)={neoid} AND id(a)={aneoid} " + "DELETE r RETURN id(n),id(a)".format( + lbl=self.cls.mapspec()["label"], + albl=end_lbls[0], + neoid=obj.neoid, + aneoid=val.neoid, + rel=rel, + ) + ) else: - qry = "MATCH (n:{lbl}){rel}(a) WHERE id(n)={neoid} AND id(a)={aneoid} AND ({cond}) " \ - "DELETE r RETURN id(n),id(a)".format( - lbl=self.cls.mapspec()["label"], - neoid=obj.neoid, - aneoid=val.neoid, - cond=cond, - rel=rel) + qry = ( + "MATCH (n:{lbl}){rel}(a) WHERE id(n)={neoid} AND id(a)={aneoid} AND ({cond}) " + "DELETE r RETURN id(n),id(a)".format( + lbl=self.cls.mapspec()["label"], + neoid=obj.neoid, + aneoid=val.neoid, + cond=cond, + rel=rel, + ) + ) stmts.append(qry) return stmts else: raise ArgError( - "'{att}' is not a registered attribute for class '{cls}'".format( - att=att, cls=self.cls.__name__ - ) + f"'{att}' is not a registered attribute for class '{self.cls.__name__}'", ) def _check_values_list(self, att, values): @@ -625,6 +643,8 @@ def _check_values_list(self, att, values): if isinstance(end_cls, str): end_cls = {end_cls} cls_set = tuple([eval(x) for x in end_cls]) + print(f"{cls_set=}") + print(f"{v=}") chk = [isinstance(x, cls_set) for x in v] if True in chk: return True diff --git a/python/src/bento_meta/objects.py b/python/src/bento_meta/objects.py index 2623892..e78b659 100644 --- a/python/src/bento_meta/objects.py +++ b/python/src/bento_meta/objects.py @@ -2,10 +2,11 @@ bento_meta.objects ================== -This module contains the subclasses of :class:`Entity` which are used +This module contains the subclasses of :class:`Entity` which are used in representing the models contained in the `MDB `_. """ + import sys sys.path.append("..") @@ -13,11 +14,10 @@ from bento_meta.entity import Entity -# from pdb import set_trace - def mergespec(clsname, attspec, mapspec): - """Merge subclass attribute and mapping specification dicts with the + """ + Merge subclass attribute and mapping specification dicts with the base class's. Not for human consumption. """ spec = deepcopy(attspec) @@ -45,11 +45,17 @@ class Node(Entity): "nanoid": "simple", "concept": "object", "props": "collection", + "version": "simple", } mapspec_ = { "label": "node", "key": "handle", - "property": {"handle": "handle", "model": "model", "nanoid": "nanoid"}, + "property": { + "handle": "handle", + "model": "model", + "nanoid": "nanoid", + "version": "version", + }, "relationship": { "concept": {"rel": ":has_concept>", "end_cls": "Concept"}, "props": {"rel": ":has_property>", "end_cls": "Property"}, @@ -82,6 +88,7 @@ class Property(Entity): "concept": "object", "value_set": "object", "_parent_handle": "simple", + "version": "simple", } mapspec_ = { "label": "property", @@ -100,6 +107,7 @@ class Property(Entity): "is_deprecated": "is_deprecated", "is_strict": "is_strict", "_parent_handle": "_parent_handle", + "version": "version", }, "relationship": { "concept": {"rel": ":has_concept>", "end_cls": "Concept"}, @@ -116,8 +124,10 @@ def __init__(self, init=None): @property def terms(self): - """If the `Property` has a ``value_set`` domain, return the `Term` objects - of its `ValueSet`""" + """ + If the `Property` has a ``value_set`` domain, return the `Term` objects + of its `ValueSet` + """ if self.value_set: return self.value_set.terms else: @@ -125,7 +135,8 @@ def terms(self): @property def values(self): - """If the `Property` as a ``value_set`` domain, return its term values as a list of str. + """ + If the `Property` as a ``value_set`` domain, return its term values as a list of str. :return: list of term values :rtype: list """ @@ -149,6 +160,7 @@ class Edge(Entity): "dst": "object", "concept": "object", "props": "collection", + "version": "simple", } mapspec_ = { "label": "relationship", @@ -159,6 +171,7 @@ class Edge(Entity): "nanoid": "nanoid", "multiplicity": "multiplicity", "is_required": "is_required", + "version": "version", }, "relationship": { "src": {"rel": ":has_src>", "end_cls": "Node"}, @@ -175,7 +188,8 @@ def __init__(self, init=None): @property def triplet(self): - """A 3-tuple that fully qualifies the edge: ``(edge.handle, src.handle, dst.handle)`` + """ + A 3-tuple that fully qualifies the edge: ``(edge.handle, src.handle, dst.handle)`` ``src`` and ``dst`` attributes must be set. """ if self.handle and self.src and self.dst: @@ -228,7 +242,8 @@ def __init__(self, init=None): # (from Bento::Meta), signal need to refresh. Engineer so this happens # here (__setattr__ override), not in Entity class ValueSet(Entity): - """Subclass that models an enumerated set of :class:`Property` values. + """ + Subclass that models an enumerated set of :class:`Property` values. Essentially a container for :class:`Term` instances. """ @@ -364,6 +379,8 @@ class Model(Entity): "name": "simple", "repository": "simple", "nanoid": "simple", + "version": "simple", + "latest_version": "simple", } mapspec_ = { "label": "model", @@ -372,9 +389,12 @@ class Model(Entity): "handle": "handle", "name": "name", "repository": "repository", + "version": "version", + "latest_version": "latest_version", }, } (attspec, _mapspec) = mergespec("Model", attspec_, mapspec_) + defaults = {"latest_version": False} def __init__(self, init=None): super().__init__(init=init) diff --git a/python/tests/test_000map.py b/python/tests/test_000map.py index e7c5520..23ed367 100644 --- a/python/tests/test_000map.py +++ b/python/tests/test_000map.py @@ -1,200 +1,318 @@ import re import sys -sys.path.insert(0,'.') -sys.path.insert(0,'..') + +sys.path.insert(0, ".") +sys.path.insert(0, "..") + import pytest -from pdb import set_trace +from bento_meta.entity import ArgError, Entity from bento_meta.object_map import ObjectMap -from bento_meta.entity import Entity, ArgError -from bento_meta.objects import Node,Property,Term,Concept,Origin,ValueSet,Tag,mergespec +from bento_meta.objects import ( + Concept, + Node, + Origin, + Property, + Tag, + Term, + ValueSet, + mergespec, +) + # FakeNode for testing ['relationship']['endcls'] = a _tuple_ class FakeNode(Entity): - attspec = {"handle":"simple","model":"simple", - "category":"simple", "concept":"object", - "props":"collection"} - - mapspec_={"label":"node", - "property": {"handle":"handle","model":"model","category":"category"}, - "relationship": { - "concept": { "rel" : ":has_concept>", - "end_cls" : {"Concept","Term"} }, - "props": { "rel" : ":has_property>", - "end_cls" : "Property" } - }} - (attspec, _mapspec) = mergespec('FakeNode',attspec,mapspec_) - def __init__(self, init=None): - super().__init__(init=init) - + attspec = { + "handle": "simple", + "model": "simple", + "category": "simple", + "concept": "object", + "props": "collection", + } + + mapspec_ = { + "label": "node", + "property": {"handle": "handle", "model": "model", "category": "category"}, + "relationship": { + "concept": {"rel": ":has_concept>", "end_cls": {"Concept", "Term"}}, + "props": {"rel": ":has_property>", "end_cls": "Property"}, + }, + } + (attspec, _mapspec) = mergespec("FakeNode", attspec, mapspec_) + + def __init__(self, init=None): + super().__init__(init=init) + def test_class(): - m = ObjectMap(cls=Node) - assert isinstance(m, ObjectMap) - assert m.cls.mapspec()["label"] == 'node' - assert m.cls.attspec["props"] == 'collection' - assert m.cls.attspec["_next"] == 'object' - for c in (Node,Property,Term,Concept,Origin,Tag): - assert isinstance(ObjectMap(cls=c),ObjectMap) - assert c.mapspec()["label"] == c.__name__.lower() + m = ObjectMap(cls=Node) + assert isinstance(m, ObjectMap) + assert m.cls.mapspec()["label"] == "node" + assert m.cls.attspec["props"] == "collection" + assert m.cls.attspec["_next"] == "object" + for c in (Node, Property, Term, Concept, Origin, Tag): + assert isinstance(ObjectMap(cls=c), ObjectMap) + assert c.mapspec()["label"] == c.__name__.lower() + def test_get_queries(): - m = ObjectMap(cls=Node) - with pytest.raises(ArgError, match='arg1 must be object of class'): - m.get_q(ValueSet()) - with pytest.raises(ArgError, match='object must be mapped'): - m.get_q(Node()) - n = Node({"handle":"test","model":"test"}) - n.neoid=1 - qry = m.get_q(n) - assert qry == "MATCH (n:node) WHERE id(n)=1 RETURN n,id(n)" - with pytest.raises(ArgError,match="'flerb' is not a registered attribute"): - m.get_attr_q(n,'flerb') - qry = m.get_attr_q(n,'model') - assert qry == "MATCH (n:node) WHERE id(n)=1 RETURN n.model" - qry = m.get_attr_q(n,'props') - assert qry == "MATCH (n:node)-[:has_property]->(a:property) WHERE id(n)=1 RETURN a" - qry = m.get_attr_q(n,'concept') - assert qry == "MATCH (n:node)-[:has_concept]->(a:concept) WHERE id(n)=1 RETURN a LIMIT 1" + m = ObjectMap(cls=Node) + with pytest.raises(ArgError, match="arg1 must be object of class"): + m.get_q(ValueSet()) + with pytest.raises(ArgError, match="object must be mapped"): + m.get_q(Node()) + n = Node({"handle": "test", "model": "test"}) + n.neoid = 1 + qry = m.get_q(n) + assert qry == "MATCH (n:node) WHERE id(n)=1 RETURN n,id(n)" + with pytest.raises(ArgError, match="'flerb' is not a registered attribute"): + m.get_attr_q(n, "flerb") + qry = m.get_attr_q(n, "model") + assert qry == "MATCH (n:node) WHERE id(n)=1 RETURN n.model" + qry = m.get_attr_q(n, "props") + assert qry == "MATCH (n:node)-[:has_property]->(a:property) WHERE id(n)=1 RETURN a" + qry = m.get_attr_q(n, "concept") + assert ( + qry + == "MATCH (n:node)-[:has_concept]->(a:concept) WHERE id(n)=1 RETURN a LIMIT 1" + ) + def test_put_queries(): - m = ObjectMap(cls=Node) - n = Node({"handle":"test","model":"test_model","_commit":1}) - qry = m.put_q(n) - assert qry==['CREATE (n:node {_commit:1,handle:"test",model:"test_model"}) RETURN n,id(n)'] - n.neoid=2 - stmts = m.put_q(n) - assert stmts[0]=='MATCH (n:node) WHERE id(n)=2 SET n._commit=1,n.handle="test",n.model="test_model" RETURN n,id(n)' - assert len(stmts[1:]) == len([x for x in Node.attspec if Node.attspec[x] == 'simple'])-3 - for s in stmts[1:]: - assert re.match('^MATCH \\(n:node\\) WHERE id\\(n\\)=2 REMOVE n.[a-z_]+ RETURN n,id\\(n\\)$',s) - n.neoid=None - with pytest.raises(ArgError, match='object must be mapped'): - m.put_attr_q(n,'_commit',2) - n.neoid=1 - c = Concept({"_id":"blarf"}) - with pytest.raises(ArgError, match="'values' must be a list of mapped Entity objects"): - m.put_attr_q(n,'concept',c) - with pytest.raises(ArgError, match="'values' must be a list of mapped Entity objects"): - m.put_attr_q(n,'concept',[c]) - qry = m.put_attr_q(n,'_commit',[3]) - assert qry=="MATCH (n:node) WHERE id(n)=1 SET _commit=3 RETURN id(n)" - c.neoid=2 - stmts = m.put_attr_q(n,'concept',[c]) - assert stmts[0] == "MATCH (n:node),(a:concept) WHERE id(n)=1 AND id(a)=2 MERGE (n)-[:has_concept]->(a) RETURN id(a)" - assert len(stmts) == 1 - prps = [Property(x) for x in ( {"model":"test","handle":"prop1"}, - {"model":"test","handle":"prop2"}, - {"model":"test","handle":"prop3"} )] - i=5 - for p in prps: - p.neoid=i - i+=1 - stmts = m.put_attr_q(n,'props',prps) - assert stmts[0] == "MATCH (n:node),(a:property) WHERE id(n)=1 AND id(a)=5 MERGE (n)-[:has_property]->(a) RETURN id(a)" - assert len(stmts) == 3 - m=ObjectMap(cls=FakeNode) - n=FakeNode({"handle":"test","model":"test_model","category":1}) - n.neoid=1 - t = Term({"value":"boog"}) - t.neoid=6 - stmts = m.put_attr_q(n,"concept",[t]) - assert re.match("MATCH \\(n:node\\),\\(a\\) WHERE id\\(n\\)=1 AND id\\(a\\)=6 AND \\('[a-z]+' IN labels\\(a\\) OR '[a-z]+' IN labels\\(a\\)\\) MERGE \\(n\\)-\\[:has_concept\\]->\\(a\\) RETURN id\\(a\\)",stmts[0]) - qry = m.get_attr_q(n,"concept") - assert re.match("MATCH \\(n:node\\)-\\[:has_concept\\]->\\(a\\) WHERE id\\(n\\)=1 AND \\('[a-z]+' IN labels\\(a\\) OR '[a-z]+' IN labels\\(a\\)\\) RETURN a",qry) - pass + m = ObjectMap(cls=Node) + n = Node({"handle": "test", "model": "test_model", "_commit": 1}) + qry = m.put_q(n) + assert qry == [ + 'CREATE (n:node {_commit:1,handle:"test",model:"test_model"}) RETURN n,id(n)', + ] + n.neoid = 2 + stmts = m.put_q(n) + assert ( + stmts[0] + == 'MATCH (n:node) WHERE id(n)=2 SET n._commit=1,n.handle="test",n.model="test_model" RETURN n,id(n)' + ) + assert ( + len(stmts[1:]) + == len([x for x in Node.attspec if Node.attspec[x] == "simple"]) - 3 + ) + for s in stmts[1:]: + assert re.match( + "^MATCH \\(n:node\\) WHERE id\\(n\\)=2 REMOVE n.[a-z_]+ RETURN n,id\\(n\\)$", + s, + ) + n.neoid = None + with pytest.raises(ArgError, match="object must be mapped"): + m.put_attr_q(n, "_commit", 2) + n.neoid = 1 + c = Concept({"_id": "blarf"}) + with pytest.raises( + ArgError, + match="'values' must be a list of mapped Entity objects", + ): + m.put_attr_q(n, "concept", c) + with pytest.raises( + ArgError, + match="'values' must be a list of mapped Entity objects", + ): + m.put_attr_q(n, "concept", [c]) + qry = m.put_attr_q(n, "_commit", [3]) + assert qry == "MATCH (n:node) WHERE id(n)=1 SET _commit=3 RETURN id(n)" + c.neoid = 2 + stmts = m.put_attr_q(n, "concept", [c]) + assert ( + stmts[0] + == "MATCH (n:node),(a:concept) WHERE id(n)=1 AND id(a)=2 MERGE (n)-[:has_concept]->(a) RETURN id(a)" + ) + assert len(stmts) == 1 + prps = [ + Property(x) + for x in ( + {"model": "test", "handle": "prop1"}, + {"model": "test", "handle": "prop2"}, + {"model": "test", "handle": "prop3"}, + ) + ] + i = 5 + for p in prps: + p.neoid = i + i += 1 + stmts = m.put_attr_q(n, "props", prps) + assert ( + stmts[0] + == "MATCH (n:node),(a:property) WHERE id(n)=1 AND id(a)=5 MERGE (n)-[:has_property]->(a) RETURN id(a)" + ) + assert len(stmts) == 3 + m = ObjectMap(cls=FakeNode) + n = FakeNode({"handle": "test", "model": "test_model", "category": 1}) + n.neoid = 1 + t = Term({"value": "boog"}) + t.neoid = 6 + stmts = m.put_attr_q(n, "concept", [t]) + assert re.match( + "MATCH \\(n:node\\),\\(a\\) WHERE id\\(n\\)=1 AND id\\(a\\)=6 AND \\('[a-z]+' IN labels\\(a\\) OR '[a-z]+' IN labels\\(a\\)\\) MERGE \\(n\\)-\\[:has_concept\\]->\\(a\\) RETURN id\\(a\\)", + stmts[0], + ) + qry = m.get_attr_q(n, "concept") + assert re.match( + "MATCH \\(n:node\\)-\\[:has_concept\\]->\\(a\\) WHERE id\\(n\\)=1 AND \\('[a-z]+' IN labels\\(a\\) OR '[a-z]+' IN labels\\(a\\)\\) RETURN a", + qry, + ) + def test_rm_queries(): - m = ObjectMap(cls=FakeNode) - n = FakeNode({"handle":"test","model":"test_model","category":1}) - assert FakeNode.mapspec()["relationship"]["concept"]["end_cls"] == {"Concept","Term"} - with pytest.raises(ArgError,match="object must be mapped"): - m.rm_q(n) - n.neoid=1 - qry = m.rm_q(n) - assert qry=='MATCH (n:node) WHERE id(n)=1 DELETE n' - qry = m.rm_q(n,detach=True) - assert qry=='MATCH (n:node) WHERE id(n)=1 DETACH DELETE n' - c = Concept({"_id":"blerf"}) - qry = m.rm_attr_q(n,'model') - assert qry=='MATCH (n:node) WHERE id(n)=1 REMOVE n.model RETURN id(n)' - qry = m.rm_attr_q(n,'props',[':all']) - assert qry=='MATCH (n:node)-[r:has_property]->(a:property) WHERE id(n)=1 DELETE r RETURN id(n),id(a)' - qry = m.rm_attr_q(n,'concept',[':all']) - assert re.match("MATCH \\(n:node\\)-\\[r:has_concept\\]->\\(a\\) WHERE id\\(n\\)=1 AND \\('[a-z]+' IN labels\\(a\\) OR '[a-z]+' IN labels\\(a\\)\\) DELETE r",qry) - prps = [Property(x) for x in ( {"model":"test","handle":"prop1"}, - {"model":"test","handle":"prop2"}, - {"model":"test","handle":"prop3"} )] - i=5 - for p in prps: - p.neoid=i - i+=1 - stmts = m.rm_attr_q(n,'props',prps) - assert stmts[0] == "MATCH (n:node)-[r:has_property]->(a:property) WHERE id(n)=1 AND id(a)=5 DELETE r RETURN id(n),id(a)" - assert len(stmts) == 3 + m = ObjectMap(cls=FakeNode) + n = FakeNode({"handle": "test", "model": "test_model", "category": 1}) + assert FakeNode.mapspec()["relationship"]["concept"]["end_cls"] == { + "Concept", + "Term", + } + with pytest.raises(ArgError, match="object must be mapped"): + m.rm_q(n) + n.neoid = 1 + qry = m.rm_q(n) + assert qry == "MATCH (n:node) WHERE id(n)=1 DELETE n" + qry = m.rm_q(n, detach=True) + assert qry == "MATCH (n:node) WHERE id(n)=1 DETACH DELETE n" + c = Concept({"_id": "blerf"}) + qry = m.rm_attr_q(n, "model") + assert qry == "MATCH (n:node) WHERE id(n)=1 REMOVE n.model RETURN id(n)" + qry = m.rm_attr_q(n, "props", [":all"]) + assert ( + qry + == "MATCH (n:node)-[r:has_property]->(a:property) WHERE id(n)=1 DELETE r RETURN id(n),id(a)" + ) + qry = m.rm_attr_q(n, "concept", [":all"]) + assert re.match( + "MATCH \\(n:node\\)-\\[r:has_concept\\]->\\(a\\) WHERE id\\(n\\)=1 AND \\('[a-z]+' IN labels\\(a\\) OR '[a-z]+' IN labels\\(a\\)\\) DELETE r", + qry, + ) + prps = [ + Property(x) + for x in ( + {"model": "test", "handle": "prop1"}, + {"model": "test", "handle": "prop2"}, + {"model": "test", "handle": "prop3"}, + ) + ] + i = 5 + for p in prps: + p.neoid = i + i += 1 + stmts = m.rm_attr_q(n, "props", prps) + assert ( + stmts[0] + == "MATCH (n:node)-[r:has_property]->(a:property) WHERE id(n)=1 AND id(a)=5 DELETE r RETURN id(n),id(a)" + ) + assert len(stmts) == 3 def test_put_then_rm_queries(): - """test adding then removing attr""" - m = ObjectMap(cls=Node) - n = Node({"handle":"test_","model":"test_model_","_commit":1}) - qry = m.put_q(n) - assert qry==['CREATE (n:node {_commit:1,handle:"test_",model:"test_model_"}) RETURN n,id(n)'] - - # manually set neoid - n.neoid=2 - stmts = m.put_q(n) - assert stmts[0]=='MATCH (n:node) WHERE id(n)=2 SET n._commit=1,n.handle="test_",n.model="test_model_" RETURN n,id(n)' - assert len(stmts[1:]) == len([x for x in Node.attspec if Node.attspec[x] == 'simple'])-3 - for s in stmts[1:]: - assert re.match('^MATCH \\(n:node\\) WHERE id\\(n\\)=2 REMOVE n.[a-z_]+ RETURN n,id\\(n\\)$',s) - - n.neoid=None - with pytest.raises(ArgError, match='object must be mapped'): - m.put_attr_q(n,'_commit',2) - - n.neoid=1 - c = Concept({"_id":"blarfblark"}) - with pytest.raises(ArgError, match="'values' must be a list of mapped Entity objects"): - m.put_attr_q(n,'concept',c) - with pytest.raises(ArgError, match="'values' must be a list of mapped Entity objects"): - m.put_attr_q(n,'concept',[c]) - - qry = m.put_attr_q(n,'_commit',[3]) - assert qry=="MATCH (n:node) WHERE id(n)=1 SET _commit=3 RETURN id(n)" - - c.neoid=2 - stmts = m.put_attr_q(n,'concept',[c]) - #assert stmts[0] == "MATCH (n:node),(a:concept) WHERE id(n)=1 AND id(a)=2 MERGE (n)-[:has_concept]->(a) RETURN id(a)" - assert len(stmts) == 1 - - prps = [Property(x) for x in ( {"model":"test_","handle":"prop1"}, - {"model":"test_","handle":"prop2"}, - {"model":"test_","handle":"prop3"} )] - i=5 - for p in prps: - p.neoid=i - i+=1 - stmts = m.put_attr_q(n,'props',prps) - assert stmts[0] == "MATCH (n:node),(a:property) WHERE id(n)=1 AND id(a)=5 MERGE (n)-[:has_property]->(a) RETURN id(a)" - assert len(stmts) == 3 - Node.mapspec_={"label":"node", - "property": {"handle":"handle","model":"model"}, - "relationship": { - "concept": { "rel" : ":has_concept>", - "end_cls" : {"Concept","Term"} }, - "props": { "rel" : ":has_property>", - "end_cls" : "Property" } - }} - (Node.attspec, Node._mapspec) = mergespec('Node',Node.attspec,Node.mapspec_) - assert Node.mapspec()["relationship"]["concept"]["end_cls"] == {"Concept","Term"} - t = Term({"value":"boogblark"}) - t.neoid=6 - stmts = m.put_attr_q(n,"concept",[t]) - assert re.match("MATCH \\(n:node\\),\\(a\\) WHERE id\\(n\\)=1 AND id\\(a\\)=6 AND \\('[a-z]+' IN labels\\(a\\) OR '[a-z]+' IN labels\\(a\\)\\) MERGE \\(n\\)-\\[:has_concept\\]->\\(a\\) RETURN id\\(a\\)",stmts[0]) - qry = m.get_attr_q(n,"concept") - assert re.match("MATCH \\(n:node\\)-\\[:has_concept\\]->\\(a\\) WHERE id\\(n\\)=1 AND \\('[a-z]+' IN labels\\(a\\) OR '[a-z]+' IN labels\\(a\\)\\) RETURN a",qry) - - # now delete the attr I just added.... - qry2 = m.rm_attr_q(n, "concept", [t]) - assert re.match("MATCH \\(n:node\\)-\\[r:has_concept\\]->\\(a\\) WHERE id\\(n\\)=1 AND id\\(a\\)=6 AND \\('[a-z]+' IN labels\\(a\\) OR '[a-z]+' IN labels\\(a\\)\\) DELETE r RETURN id\\(n\\),id\\(a\\)",qry2[0]) + """Test adding then removing attr""" + m = ObjectMap(cls=Node) + n = Node({"handle": "test_", "model": "test_model_", "_commit": 1}) + qry = m.put_q(n) + assert qry == [ + 'CREATE (n:node {_commit:1,handle:"test_",model:"test_model_"}) RETURN n,id(n)', + ] + + # manually set neoid + n.neoid = 2 + stmts = m.put_q(n) + assert ( + stmts[0] + == 'MATCH (n:node) WHERE id(n)=2 SET n._commit=1,n.handle="test_",n.model="test_model_" RETURN n,id(n)' + ) + assert ( + len(stmts[1:]) + == len([x for x in Node.attspec if Node.attspec[x] == "simple"]) - 3 + ) + for s in stmts[1:]: + assert re.match( + "^MATCH \\(n:node\\) WHERE id\\(n\\)=2 REMOVE n.[a-z_]+ RETURN n,id\\(n\\)$", + s, + ) + + n.neoid = None + with pytest.raises(ArgError, match="object must be mapped"): + m.put_attr_q(n, "_commit", 2) + + n.neoid = 1 + c = Concept({"_id": "blarfblark"}) + with pytest.raises( + ArgError, + match="'values' must be a list of mapped Entity objects", + ): + m.put_attr_q(n, "concept", c) + with pytest.raises( + ArgError, + match="'values' must be a list of mapped Entity objects", + ): + m.put_attr_q(n, "concept", [c]) + + qry = m.put_attr_q(n, "_commit", [3]) + assert qry == "MATCH (n:node) WHERE id(n)=1 SET _commit=3 RETURN id(n)" + + c.neoid = 2 + stmts = m.put_attr_q(n, "concept", [c]) + # assert stmts[0] == "MATCH (n:node),(a:concept) WHERE id(n)=1 AND id(a)=2 MERGE (n)-[:has_concept]->(a) RETURN id(a)" + assert len(stmts) == 1 + + prps = [ + Property(x) + for x in ( + {"model": "test_", "handle": "prop1"}, + {"model": "test_", "handle": "prop2"}, + {"model": "test_", "handle": "prop3"}, + ) + ] + i = 5 + for p in prps: + p.neoid = i + i += 1 + stmts = m.put_attr_q(n, "props", prps) + assert ( + stmts[0] + == "MATCH (n:node),(a:property) WHERE id(n)=1 AND id(a)=5 MERGE (n)-[:has_property]->(a) RETURN id(a)" + ) + assert len(stmts) == 3 + + class TempNode(Node): + attspec_ = Node.attspec_ + mapspec_ = { + "label": "node", + "property": {"handle": "handle", "model": "model"}, + "relationship": { + "concept": {"rel": ":has_concept>", "end_cls": {"Concept", "Term"}}, + "props": {"rel": ":has_property>", "end_cls": "Property"}, + }, + } + (attspec, _mapspec) = mergespec("Node", attspec_, mapspec_) + + def __init__(self, init=None): + super().__init__(init=init) + + assert TempNode.mapspec()["relationship"]["concept"]["end_cls"] == { + "Concept", + "Term", + } + tm = ObjectMap(cls=TempNode) + tn = TempNode(n) + print(tn.mapspec()) + t = Term({"value": "boogblark"}) + t.neoid = 6 + stmts = tm.put_attr_q(tn, "concept", [t]) + assert re.match( + "MATCH \\(n:node\\),\\(a\\) WHERE id\\(n\\)=1 AND id\\(a\\)=6 AND \\('[a-z]+' IN labels\\(a\\) OR '[a-z]+' IN labels\\(a\\)\\) MERGE \\(n\\)-\\[:has_concept\\]->\\(a\\) RETURN id\\(a\\)", + stmts[0], + ) + qry = tm.get_attr_q(tn, "concept") + assert re.match( + "MATCH \\(n:node\\)-\\[:has_concept\\]->\\(a\\) WHERE id\\(n\\)=1 AND \\('[a-z]+' IN labels\\(a\\) OR '[a-z]+' IN labels\\(a\\)\\) RETURN a", + qry, + ) + # now delete the attr I just added.... + qry2 = tm.rm_attr_q(tn, "concept", [t]) + assert re.match( + "MATCH \\(n:node\\)-\\[r:has_concept\\]->\\(a\\) WHERE id\\(n\\)=1 AND id\\(a\\)=6 AND \\('[a-z]+' IN labels\\(a\\) OR '[a-z]+' IN labels\\(a\\)\\) DELETE r RETURN id\\(n\\),id\\(a\\)", + qry2[0], + ) diff --git a/python/tests/test_003model.py b/python/tests/test_003model.py index 80a9ab8..de0edde 100644 --- a/python/tests/test_003model.py +++ b/python/tests/test_003model.py @@ -1,58 +1,70 @@ -import re import sys -sys.path.insert(0,'.') -sys.path.insert(0,'..') + +sys.path.insert(0, ".") +sys.path.insert(0, "..") + import pytest -from pdb import set_trace -from bento_meta.model import Model, ArgError -from bento_meta.objects import Node, Property, Edge, Term, ValueSet, Concept, Origin +from bento_meta.model import ArgError, Model +from bento_meta.objects import Edge, Node, Property, Term + def test_init_model(): - with pytest.raises(ArgError, match=".*requires arg 'handle'"): - Model() - m = Model('test') - assert m - assert m.handle == 'test' + with pytest.raises(ArgError, match=".*requires arg 'handle'"): + Model() + m = Model("test") + assert m + assert m.handle == "test" + def test_create_model(): - model = Model('test') - case = Node({"handle":"case"}) -# set_trace() - case.props['days_to_enrollment'] = Property({"handle":'days_to_enrollment'}) - model.add_node(case) - assert isinstance(model.nodes['case'],Node) - assert model.props[('case','days_to_enrollment')] - model.annotate(case, Term({"value":"case", "origin_name":"CTOS"})) - assert model.nodes['case'].concept.terms[('case','CTOS')] - model.add_node({"handle":"sample"}) - assert model.nodes["sample"] - assert isinstance(model.nodes["sample"],Node) - assert model.nodes["sample"].model == 'test' - case_id = Property({"handle":"case_id","value_domain":"string"}) - model.add_prop(case,case_id) - assert model.props[("case","case_id")] - assert model.props[("case","case_id")].value_domain == 'string' - assert 'case_id' in model.nodes['case'].props - sample = model.nodes["sample"] - of_case = Edge({"handle":"of_case","src":sample,"dst":case}) - of_case.props['operator'] = Property({"handle":"operator","value_domain":"boolean"}) - model.annotate(model.props[('case','case_id')], - Term({'value': 'case_id', 'origin_name': 'CTOS'})) - assert case_id.concept.terms[('case_id','CTOS')] - model.add_edge(of_case) - assert model.edges[('of_case','sample','case')] - assert model.contains(of_case.props['operator']) - assert of_case.props['operator'].model == 'test' - assert model.props[('of_case','sample','case','operator')] - assert model.props[('of_case','sample','case','operator')].value_domain == 'boolean' - model.annotate(of_case, Term({"value": "of_case", "origin_name": "CTOS"})) - assert model.edges[('of_case','sample','case')].concept.terms[('of_case', 'CTOS')] - dx = Property({"handle":"diagnosis","value_domain":"value_set"}) - tm = Term({"value":"CRS", "origin_name":"Marilyn"}) - model.add_prop(case, dx) - model.add_terms(dx, tm, 'rockin_pneumonia', 'fungusamongus') - assert {x.value for x in dx.terms.values()} == { 'CRS', 'rockin_pneumonia', 'fungusamongus'} - assert len(model.terms) > 0 - assert ('CRS','Marilyn') in model.terms - assert ('case','CTOS') in model.terms - assert dx.value_set in tm.belongs.values() + model = Model("test") + case = Node({"handle": "case"}) + # set_trace() + case.props["days_to_enrollment"] = Property({"handle": "days_to_enrollment"}) + model.add_node(case) + assert isinstance(model.nodes["case"], Node) + assert model.props[("case", "days_to_enrollment")] + model.annotate(case, Term({"value": "case", "origin_name": "CTOS"})) + assert model.nodes["case"].concept.terms[("case", "CTOS")] + model.add_node({"handle": "sample"}) + assert model.nodes["sample"] + assert isinstance(model.nodes["sample"], Node) + assert model.nodes["sample"].model == "test" + case_id = Property({"handle": "case_id", "value_domain": "string"}) + model.add_prop(case, case_id) + assert model.props[("case", "case_id")] + assert model.props[("case", "case_id")].value_domain == "string" + assert "case_id" in model.nodes["case"].props + sample = model.nodes["sample"] + of_case = Edge({"handle": "of_case", "src": sample, "dst": case}) + of_case.props["operator"] = Property( + {"handle": "operator", "value_domain": "boolean"}, + ) + model.annotate( + model.props[("case", "case_id")], + Term({"value": "case_id", "origin_name": "CTOS"}), + ) + assert case_id.concept.terms[("case_id", "CTOS")] + model.add_edge(of_case) + assert model.edges[("of_case", "sample", "case")] + assert model.contains(of_case.props["operator"]) + assert of_case.props["operator"].model == "test" + assert model.props[("of_case", "sample", "case", "operator")] + assert ( + model.props[("of_case", "sample", "case", "operator")].value_domain == "boolean" + ) + model.annotate(of_case, Term({"value": "of_case", "origin_name": "CTOS"})) + assert model.edges[("of_case", "sample", "case")].concept.terms[("of_case", "CTOS")] + dx = Property({"handle": "diagnosis", "value_domain": "value_set"}) + tm = Term({"value": "CRS", "origin_name": "Marilyn"}) + model.add_prop(case, dx) + model.add_terms(dx, tm, "rockin_pneumonia", "fungusamongus") + assert {x.value for x in dx.terms.values()} == { + "CRS", + "rockin_pneumonia", + "fungusamongus", + } + assert len(model.terms) > 0 + assert ("CRS", "Marilyn") in model.terms + assert ("case", "CTOS") in model.terms + assert dx.value_set in tm.belongs.values() diff --git a/python/tests/test_005db.py b/python/tests/test_005db.py index be56f30..bf104c1 100644 --- a/python/tests/test_005db.py +++ b/python/tests/test_005db.py @@ -1,99 +1,103 @@ -import re import sys -sys.path.insert(0,".") -sys.path.insert(0,"..") + +sys.path.insert(0, ".") +sys.path.insert(0, "..") + +import neo4j.graph import pytest -import pytest_docker +from bento_meta.object_map import ObjectMap +from bento_meta.objects import Concept, Node, Property, Term, ValueSet from neo4j import GraphDatabase -import neo4j.graph from neo4j.exceptions import Neo4jError -from pdb import set_trace -from bento_meta.object_map import ObjectMap -from bento_meta.entity import * -from bento_meta.objects import * -@pytest.mark.docker + +@pytest.mark.docker() def test_get(bento_neo4j): - (b,h)=bento_neo4j - drv = GraphDatabase.driver(b) - assert drv - node_map = ObjectMap(cls=Node,drv=drv) - Node.object_map=node_map - Concept.object_map=ObjectMap(cls=Concept,drv=drv) - Property.object_map=ObjectMap(cls=Property,drv=drv) - n_id=None - with node_map.drv.session() as session: - result = session.run("match (a:node) return id(a) limit 1") - n_id = result.single().value() - assert n_id - node = Node() - node.neoid = n_id - node_map.get(node) - assert node.dirty==0 - assert node.__dict__['concept'].dirty == -1 # before dget() - assert node.concept.dirty == 0 # after dget() - assert node.concept._id == "a5b87a02-1eb3-4ec9-881d-f4479ab917ac" - assert len(node.props) == 3 - assert node.props.data['site_short_name'].dirty == -1 # before dget() - assert node.props['site_short_name'].dirty == 0 # after dget() - assert node.props['site_short_name'].model == 'ICDC' - concept = node.concept - assert concept.belongs[(id(node), 'concept')] == node - owners = node_map.get_owners(node) - assert len(owners) == 1 - cncpt = Concept() - Concept.object_map.get_by_id(cncpt,"a5b87a02-1eb3-4ec9-881d-f4479ab917ac") - assert cncpt.terms[0] == concept.terms[0] - pass + (b, h) = bento_neo4j + drv = GraphDatabase.driver(b) + assert drv + node_map = ObjectMap(cls=Node, drv=drv) + Node.object_map = node_map + Concept.object_map = ObjectMap(cls=Concept, drv=drv) + Property.object_map = ObjectMap(cls=Property, drv=drv) + n_id = None + with node_map.drv.session() as session: + result = session.run("match (a:node) return id(a) limit 1") + n_id = result.single().value() + assert n_id + node = Node() + node.neoid = n_id + node_map.get(node) + assert node.dirty == 0 + assert node.__dict__["concept"].dirty == -1 # before dget() + assert node.concept.dirty == 0 # after dget() + assert node.concept._id == "a5b87a02-1eb3-4ec9-881d-f4479ab917ac" + assert len(node.props) == 3 + assert node.props.data["site_short_name"].dirty == -1 # before dget() + assert node.props["site_short_name"].dirty == 0 # after dget() + assert node.props["site_short_name"].model == "ICDC" + concept = node.concept + assert concept.belongs[(id(node), "concept")] == node + owners = node_map.get_owners(node) + assert len(owners) == 1 + cncpt = Concept() + Concept.object_map.get_by_id(cncpt, "a5b87a02-1eb3-4ec9-881d-f4479ab917ac") + assert cncpt.terms[0] == concept.terms[0] -@pytest.mark.docker -def test_put_rm(bento_neo4j): - (b,h)=bento_neo4j - drv = GraphDatabase.driver(b) - vs_map = ObjectMap(cls=ValueSet,drv=drv) - term_map = ObjectMap(cls=Term,drv=drv) - vs = ValueSet({"_id":"narb"}) - terms = [Term({"value":x}) for x in ['quilm','ferb','narquit']] - vs.terms=terms - assert vs.terms['ferb'].value == 'ferb' - vs_map.put(vs) - rt = [] - with vs_map.drv.session() as session: - result = session.run("match (v:value_set)-[:has_term]->(t:term) where v.id='narb' return t order by t.value") - for rec in result: - rt.append(rec['t']['value']) - assert set(rt) == set(['ferb','narquit','quilm']) - quilm = vs.terms['quilm'] - del vs.terms['quilm'] - assert len(vs.terms)==2 - with pytest.raises(Neo4jError,match='.*Cannot delete'): - term_map.rm(quilm) - t_id=None - with term_map.drv.session() as session: - result = session.run("match (t:term {value:'quilm'}) return id(t)") - t_id = result.single().value() - assert t_id == quilm.neoid - term_map.rm(quilm, 1) - with term_map.drv.session() as session: - result = session.run("match (t:term {value:'quilm'}) return id(t)") - assert result.single() == None +@pytest.mark.docker() +def test_put_rm(bento_neo4j): + (b, h) = bento_neo4j + drv = GraphDatabase.driver(b) + vs_map = ObjectMap(cls=ValueSet, drv=drv) + term_map = ObjectMap(cls=Term, drv=drv) + vs = ValueSet({"_id": "narb"}) + terms = [Term({"value": x}) for x in ["quilm", "ferb", "narquit"]] + vs.terms = terms + assert vs.terms["ferb"].value == "ferb" + vs_map.put(vs) + rt = [] + with vs_map.drv.session() as session: + result = session.run( + "match (v:value_set)-[:has_term]->(t:term) where v.id='narb' return t order by t.value", + ) + for rec in result: + rt.append(rec["t"]["value"]) + assert set(rt) == set(["ferb", "narquit", "quilm"]) + quilm = vs.terms["quilm"] + del vs.terms["quilm"] + assert len(vs.terms) == 2 + with pytest.raises(Neo4jError, match=".*Cannot delete"): + term_map.rm(quilm) + t_id = None + with term_map.drv.session() as session: + result = session.run("match (t:term {value:'quilm'}) return id(t)") + t_id = result.single().value() + assert t_id == quilm.neoid + term_map.rm(quilm, 1) + with term_map.drv.session() as session: + result = session.run("match (t:term {value:'quilm'}) return id(t)") + assert result.single() == None - new_term = Term({"value":"belpit"}) - term_map.put(new_term) - vs_map.add(vs, 'terms', new_term) - assert len(vs.terms) == 2 - vs_map.get(vs,True) - assert len(vs.terms) == 3 - assert vs.terms['belpit'] - old_term = vs.terms['ferb'] - r = None - with term_map.drv.session() as session: - result = session.run("match (t:term {value:'ferb'})<-[r]-(v:value_set) return r") - r = result.single().value() - assert isinstance(r,neo4j.graph.Relationship) - vs_map.drop(vs,'terms',old_term) - with term_map.drv.session() as session: - result = session.run("match (t:term {value:'ferb'})<-[r]-(v:value_set) return r") - assert result.single() == None - old_term = vs.terms['belpit'] + new_term = Term({"value": "belpit"}) + term_map.put(new_term) + vs_map.add(vs, "terms", new_term) + assert len(vs.terms) == 2 + vs_map.get(vs, True) + assert len(vs.terms) == 3 + assert vs.terms["belpit"] + old_term = vs.terms["ferb"] + r = None + with term_map.drv.session() as session: + result = session.run( + "match (t:term {value:'ferb'})<-[r]-(v:value_set) return r", + ) + r = result.single().value() + assert isinstance(r, neo4j.graph.Relationship) + vs_map.drop(vs, "terms", old_term) + with term_map.drv.session() as session: + result = session.run( + "match (t:term {value:'ferb'})<-[r]-(v:value_set) return r", + ) + assert result.single() == None + old_term = vs.terms["belpit"] diff --git a/python/tests/test_006mapmodel.py b/python/tests/test_006mapmodel.py index 25adc64..fb37c79 100644 --- a/python/tests/test_006mapmodel.py +++ b/python/tests/test_006mapmodel.py @@ -1,139 +1,174 @@ -import re import sys -sys.path.insert(0,'.') -sys.path.insert(0,'..') + +sys.path.insert(0, ".") +sys.path.insert(0, "..") + import pytest -import pytest_docker -from neo4j.exceptions import Neo4jError -from pdb import set_trace -from bento_meta.entity import * -from bento_meta.objects import * +from bento_meta.mdb import MDB from bento_meta.model import Model from bento_meta.object_map import ObjectMap -from bento_meta.mdb import MDB +from bento_meta.objects import Term + -@pytest.mark.docker +@pytest.mark.docker() def test_get_model(bento_neo4j): - (b,h)=bento_neo4j - the_mdb = MDB(uri=b) - assert the_mdb - ObjectMap.clear_cache() - m = Model(handle='ICDC',mdb=the_mdb) - m.dget() - - with m.drv.session() as session: - result = session.run('match (n:node) where n.model="ICDC" return count(n)') - assert len(m.nodes) == result.single().value() - result = session.run('match (n:relationship) where n.model="ICDC" return count(n)') - assert len(m.edges) == result.single().value() - result = session.run('match (p:property)<--(n:node) where p.model="ICDC" and n.model="ICDC" return count(p)') - assert len(m.props) == result.single().value() - result = session.run( - 'match (s:node)<-[:has_src]-(e:relationship)-[:has_dst]->(d:node) where e.model="ICDC" return s,e,d') - for rec in result: - (s,e,d) = (rec['s'],rec['e'],rec['d']) - triplet = (e['handle'], s['handle'], d['handle']) - assert m.edges[triplet].handle == e['handle'] - assert m.edges[triplet].src.handle == s['handle'] - assert m.edges[triplet].dst.handle == d['handle'] - - result = session.run( - 'match (n:node)-[:has_property]->(p:property) where (n.model="ICDC") return n, collect(p) as pp') - for rec in result: - for p in rec['pp']: - key = (rec['n']['handle'], p['handle']) - assert m.props[key] - assert m.props[key].neoid == p.id - assert m.nodes[rec['n']['handle']].props[p['handle']].neoid == p.id - - result = session.run( - 'match (t:term)<-[:has_term]-(v:value_set)<-[:has_value_set]-(p:property) where p.model="ICDC" return p, v, collect(t) as tt') - for rec in result: - (p, v, tt) = (rec['p'],rec['v'],rec['tt']) - [op] = [ x for x in m.props.values() if x.handle == p['handle'] ] - vs = op.value_set - assert op - assert set( op.values ) == { t['value'] for t in tt } - -@pytest.mark.docker + (b, h) = bento_neo4j + the_mdb = MDB(uri=b) + assert the_mdb + ObjectMap.clear_cache() + m = Model(handle="ICDC", mdb=the_mdb) + print(f"{m.nodes=}") + m.dget() + + with m.drv.session() as session: + result = session.run('match (n:node) where n.model="ICDC" return count(n)') + assert len(m.nodes) == result.single().value() + result = session.run( + 'match (n:relationship) where n.model="ICDC" return count(n)', + ) + assert len(m.edges) == result.single().value() + result = session.run( + 'match (p:property)<--(n:node) where p.model="ICDC" and n.model="ICDC" return count(p)', + ) + assert len(m.props) == result.single().value() + result = session.run( + 'match (s:node)<-[:has_src]-(e:relationship)-[:has_dst]->(d:node) where e.model="ICDC" return s,e,d', + ) + for rec in result: + (s, e, d) = (rec["s"], rec["e"], rec["d"]) + triplet = (e["handle"], s["handle"], d["handle"]) + assert m.edges[triplet].handle == e["handle"] + assert m.edges[triplet].src.handle == s["handle"] + assert m.edges[triplet].dst.handle == d["handle"] + + result = session.run( + 'match (n:node)-[:has_property]->(p:property) where (n.model="ICDC") return n, collect(p) as pp', + ) + for rec in result: + for p in rec["pp"]: + key = (rec["n"]["handle"], p["handle"]) + assert m.props[key] + assert m.props[key].neoid == p.id + assert m.nodes[rec["n"]["handle"]].props[p["handle"]].neoid == p.id + + result = session.run( + 'match (t:term)<-[:has_term]-(v:value_set)<-[:has_value_set]-(p:property) where p.model="ICDC" return p, v, collect(t) as tt', + ) + for rec in result: + (p, v, tt) = (rec["p"], rec["v"], rec["tt"]) + [op] = [x for x in m.props.values() if x.handle == p["handle"]] + vs = op.value_set + assert op + assert set(op.values) == {t["value"] for t in tt} + + print(f"{m.nodes=}") + + +@pytest.mark.docker() def test_put_model(bento_neo4j): - (b,h)=bento_neo4j - the_mdb = MDB(uri=b) - assert the_mdb - ObjectMap.clear_cache() - m = Model(handle='ICDC',mdb=the_mdb) - m.dget() - prop = m.props[('sample','sample_type')] - sample = m.nodes['sample'] - edge = m.edges[('on_visit','sample', 'visit')] - term = Term({"value":"electric_boogaloo"}) - m.add_terms(prop, term) - node = m.nodes['lab_exam'] - m.dput() - with m.drv.session() as session: - result = session.run('match (v:value_set)-->(t:term {value:"electric_boogaloo"}) return v,t') - rec = result.single() - assert rec['v'].id == prop.value_set.neoid - assert rec['t'].id == term.neoid - assert rec['t']['value'] == term.value - result = session.run('match (n:node {handle:"lab_exam"}) return n') - rec = result.single() - assert rec['n'].id == node.neoid - - term = m.props[('demographic','sex')].terms['M'] - assert term.concept - assert term.concept._id == "337c0e4f-506a-4f4e-95f6-07c3462b81ff" - - concept = term.concept - assert term in concept.belongs.values() - term.concept=None - assert not term in concept.belongs.values() - assert ('concept',concept) in term.removed_entities - m.dput() - with m.drv.session() as session: - result = session.run('match (t:term) where id(t)=$id return t',{"id":term.neoid}) - assert result.single() # term there - result = session.run('match (c:concept) where id(c)=$id return c',{"id":concept.neoid}) - assert result.single() # concept there - result = session.run('match (t:term)-->(c:concept) where id(t)=$id return t',{"id":term.neoid}) - assert not result.single() # but link is gone - - concept._id="heydude" - term.concept = concept - prop.model = None - assert not prop.model - - m.dput() - - with m.drv.session() as session: - result = session.run('match (t:term)--(c:concept) where id(t)=$id return c',{"id":term.neoid}) - s = result.single() - assert s - assert s['c'].id == concept.neoid - assert s['c']['id'] == "heydude" - result = session.run('match (p:property) where id(p)=$id return p',{"id":prop.neoid}) - s = result.single() - assert s - assert s['p'].id == prop.neoid - assert not 'model' in s['p'] - - prop.model = 'ICDC' - at_enrollment = m.edges[('at_enrollment','prior_surgery','enrollment')] - prior_surgery = m.nodes['prior_surgery'] - with m.drv.session() as session: - result = session.run('match (n:node)<-[:has_src]-(r:relationship {handle:"at_enrollment"})-[:has_dst]->(:node {handle:"enrollment"}) where id(n)=$id return r',{"id":prior_surgery.neoid}) - s = result.single() - assert s - - m.rm_edge(at_enrollment) - assert not at_enrollment.src - assert not at_enrollment.dst - assert not at_enrollment in m.edges_out(prior_surgery) - m.dput() - with m.drv.session() as session: - result = session.run('match (n:node)<-[:has_src]-(r:relationship {handle:"at_enrollment"})-[:has_dst]->(:node {handle:"enrollment"}) where id(n)=$id return r',{"id":prior_surgery.neoid}) - s = result.single() - assert not s - result = session.run('match (e:relationship) where id(e)=$id return e',{"id":at_enrollment.neoid}) - s = result.single() - assert s + (b, h) = bento_neo4j + the_mdb = MDB(uri=b) + assert the_mdb + ObjectMap.clear_cache() + m = Model(handle="ICDC", mdb=the_mdb) + m.dget() + prop = m.props[("sample", "sample_type")] + sample = m.nodes["sample"] + edge = m.edges[("on_visit", "sample", "visit")] + term = Term({"value": "electric_boogaloo"}) + m.add_terms(prop, term) + node = m.nodes["lab_exam"] + m.dput() + with m.drv.session() as session: + result = session.run( + 'match (v:value_set)-->(t:term {value:"electric_boogaloo"}) return v,t', + ) + rec = result.single() + assert rec["v"].id == prop.value_set.neoid + assert rec["t"].id == term.neoid + assert rec["t"]["value"] == term.value + result = session.run('match (n:node {handle:"lab_exam"}) return n') + rec = result.single() + assert rec["n"].id == node.neoid + + term = m.props[("demographic", "sex")].terms["M"] + assert term.concept + assert term.concept._id == "337c0e4f-506a-4f4e-95f6-07c3462b81ff" + + concept = term.concept + assert term in concept.belongs.values() + term.concept = None + assert term not in concept.belongs.values() + assert ("concept", concept) in term.removed_entities + m.dput() + with m.drv.session() as session: + result = session.run( + "match (t:term) where id(t)=$id return t", + {"id": term.neoid}, + ) + assert result.single() # term there + result = session.run( + "match (c:concept) where id(c)=$id return c", + {"id": concept.neoid}, + ) + assert result.single() # concept there + result = session.run( + "match (t:term)-->(c:concept) where id(t)=$id return t", + {"id": term.neoid}, + ) + assert not result.single() # but link is gone + + concept._id = "heydude" + term.concept = concept + prop.model = None + assert not prop.model + + m.dput() + + with m.drv.session() as session: + result = session.run( + "match (t:term)--(c:concept) where id(t)=$id return c", + {"id": term.neoid}, + ) + s = result.single() + assert s + assert s["c"].id == concept.neoid + assert s["c"]["id"] == "heydude" + result = session.run( + "match (p:property) where id(p)=$id return p", + {"id": prop.neoid}, + ) + s = result.single() + assert s + assert s["p"].id == prop.neoid + assert "model" not in s["p"] + + prop.model = "ICDC" + at_enrollment = m.edges[("at_enrollment", "prior_surgery", "enrollment")] + prior_surgery = m.nodes["prior_surgery"] + with m.drv.session() as session: + result = session.run( + 'match (n:node)<-[:has_src]-(r:relationship {handle:"at_enrollment"})-[:has_dst]->(:node {handle:"enrollment"}) where id(n)=$id return r', + {"id": prior_surgery.neoid}, + ) + s = result.single() + assert s + + m.rm_edge(at_enrollment) + assert not at_enrollment.src + assert not at_enrollment.dst + assert at_enrollment not in m.edges_out(prior_surgery) + m.dput() + with m.drv.session() as session: + result = session.run( + 'match (n:node)<-[:has_src]-(r:relationship {handle:"at_enrollment"})-[:has_dst]->(:node {handle:"enrollment"}) where id(n)=$id return r', + {"id": prior_surgery.neoid}, + ) + s = result.single() + assert not s + result = session.run( + "match (e:relationship) where id(e)=$id return e", + {"id": at_enrollment.neoid}, + ) + s = result.single() + assert s diff --git a/python/tests/test_007versioning.py b/python/tests/test_007versioning.py index 8a4c67a..fd016ff 100644 --- a/python/tests/test_007versioning.py +++ b/python/tests/test_007versioning.py @@ -1,13 +1,10 @@ -import re import sys sys.path.insert(0, ".") sys.path.insert(0, "..") -from pdb import set_trace -import pytest -from bento_meta.entity import * -from bento_meta.objects import * +from bento_meta.entity import Entity +from bento_meta.objects import Edge, Node, Property def test_object_versioning(): @@ -102,7 +99,7 @@ def test_object_versioning(): p41 = Property({"handle": "p41"}) n2.props["p41"] = p41 assert n2._prev - assert not "p41" in n2._prev.props + assert "p41" not in n2._prev.props assert n2.props["p41"] == p41 Entity.versioning(False) assert not Entity.versioning_on