From d995d9e7a63402b75f9a4b5f20a877f4f84b116c Mon Sep 17 00:00:00 2001 From: Nelson Moore Date: Thu, 20 Jul 2023 16:52:05 -0400 Subject: [PATCH] feat(*): add Model object class - Add Model class to objects.py #42 - edit get_label method of Entity class so that it returns the label provided in the object class's mapspec rather than from the object class's name - add test for get_label method to test_002objects.py - bump package version to 0.2.2 - make_model_changelog bugfixes to prevent duplicate relationship creation if terms or value sets are shared by multiple models and to add nanoid to tags before generating cypher so that multiple relationships aren't created from taggable entities to each generated tag - formatting --- python/pyproject.toml | 4 +- python/scripts/make_model_changelog.py | 6 + python/src/bento_meta/entity.py | 36 +++-- python/src/bento_meta/objects.py | 82 +++++++--- python/tests/samples/test_changelog.ini | 2 +- python/tests/test_002objects.py | 202 +++++++++++++----------- 6 files changed, 200 insertions(+), 132 deletions(-) diff --git a/python/pyproject.toml b/python/pyproject.toml index e5953fe..c7be753 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "bento-meta" -version = "0.2.1" +version = "0.2.2" 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.1" +version = "0.2.2" description = "Python drivers for Bento Metamodel Database" authors = [ "Mark A. Jensen ", diff --git a/python/scripts/make_model_changelog.py b/python/scripts/make_model_changelog.py index b6971a3..bd224fe 100644 --- a/python/scripts/make_model_changelog.py +++ b/python/scripts/make_model_changelog.py @@ -10,6 +10,7 @@ import click from bento_mdf.mdf import MDF from bento_meta.entity import Entity +from bento_meta.mdb.mdb import make_nanoid from bento_meta.model import Model from bento_meta.objects import Concept, Term, ValueSet from bento_meta.util.cypher.clauses import Create, Match, Merge, OnCreateSet, Statement @@ -111,6 +112,10 @@ def generate_cypher_to_add_relationship( """Generates cypher statement to create relationship from src to dst entity""" cypher_src = cypherize_entity(src) cypher_dst = cypherize_entity(dst) + # remove _commit attr from Term and VS ents + for cypher_ent in (cypher_src, cypher_dst): + if isinstance(cypher_ent, (Term, ValueSet)) and "_commit" in cypher_ent.props: + cypher_ent.props.pop("_commit") cypher_stmts["add_rels"].append( Statement( Match(cypher_src, cypher_dst), @@ -124,6 +129,7 @@ def process_tags(entity: Entity, cypher_stmts) -> None: if not entity.tags: return for tag in entity.tags.values(): + tag.nanoid = make_nanoid() generate_cypher_to_add_entity(tag, cypher_stmts) generate_cypher_to_add_relationship(entity, "has_tag", tag, cypher_stmts) diff --git a/python/src/bento_meta/entity.py b/python/src/bento_meta/entity.py index 49b14bc..c8d4e4e 100644 --- a/python/src/bento_meta/entity.py +++ b/python/src/bento_meta/entity.py @@ -7,13 +7,15 @@ * the `CollValue` class to manage collection-valued attributes, and * the `ArgError` exception. """ +from collections import UserDict + # from pdb import set_trace from warnings import warn -from collections import UserDict class ArgError(Exception): """Exception for method argument errors""" + pass @@ -32,6 +34,7 @@ class Entity(object): exceptions when attempts are made to access attributes that are not declared. """ + pvt_attr = [ "pvt", "neoid", @@ -41,7 +44,7 @@ class Entity(object): "mapspec", "belongs", ] - defaults = {}, + defaults = ({},) attspec_ = { "_id": "simple", "nanoid": "simple", @@ -57,9 +60,14 @@ class Entity(object): mapspec_ = { "label": None, "key": "_id", - "property": {"_id": "id", "desc": "desc", - "_from": "_from", "_to": "_to", - "_commit": "_commit", "nanoid": "nanoid"}, + "property": { + "_id": "id", + "desc": "desc", + "_from": "_from", + "_to": "_to", + "_commit": "_commit", + "nanoid": "nanoid", + }, "relationship": { "_next": {"rel": ":_next>", "end_cls": set()}, "_prev": {"rel": ":_prev>", "end_cls": set()}, @@ -143,7 +151,6 @@ def default(cls, propname): return cls.defaults[propname] else: return None - # @classmethod def get_by_id(self, id): @@ -151,11 +158,11 @@ def get_by_id(self, id): :param string id: value of id for desired object """ if self.object_map: - print(' > now in entity.get_by_id where self is {}'.format(self)) - print(' > and class is {}'.format(self.__class__)) + print(" > now in entity.get_by_id where self is {}".format(self)) + print(" > and class is {}".format(self.__class__)) return self.object_map.get_by_id(self, id) else: - print(' _NO_ cls.object_map detected') + print(" _NO_ cls.object_map detected") pass @property @@ -508,7 +515,7 @@ def str_for_obj(thing): def get_label(self) -> str: """returns type of entity as label""" - return self.__class__.__name__.lower() + return self.mapspec_["label"] def get_attr_dict(self): """ @@ -519,8 +526,12 @@ def get_attr_dict(self): """ attr_dict = {} for key, val in vars(self).items(): - if (val and val is not None and key != "pvt" and - isinstance(val, (str, int, float, complex, bool))): + if ( + val + and val is not None + and key != "pvt" + and isinstance(val, (str, int, float, complex, bool)) + ): attr_dict[key] = str(val) return attr_dict @@ -537,6 +548,7 @@ class CollValue(UserDict): :param owner: `Entity` object of which this collection is an attribute :param owner_key: the attribute name of this collection on the owner """ + def __init__(self, init=None, *, owner, owner_key): self.__dict__["__owner"] = owner self.__dict__["__owner_key"] = owner_key diff --git a/python/src/bento_meta/objects.py b/python/src/bento_meta/objects.py index b835479..14be94e 100644 --- a/python/src/bento_meta/objects.py +++ b/python/src/bento_meta/objects.py @@ -7,9 +7,12 @@ """ import sys + sys.path.append("..") from copy import deepcopy + from bento_meta.entity import Entity + # from pdb import set_trace @@ -50,7 +53,7 @@ class Node(Entity): "relationship": { "concept": {"rel": ":has_concept>", "end_cls": "Concept"}, "props": {"rel": ":has_property>", "end_cls": "Property"}, - "tags": {"rel": ":has_tag>", "end_cls":"Tag"} + "tags": {"rel": ":has_tag>", "end_cls": "Tag"}, }, } (attspec, _mapspec) = mergespec("Node", attspec_, mapspec_) @@ -61,6 +64,7 @@ def __init__(self, init=None): class Property(Entity): """Subclass that models a property of a node or relationship (edge).""" + pvt_attr = Entity.pvt_attr + ["value_types"] attspec_ = { "handle": "simple", @@ -90,12 +94,12 @@ class Property(Entity): "relationship": { "concept": {"rel": ":has_concept>", "end_cls": "Concept"}, "value_set": {"rel": ":has_value_set>", "end_cls": "ValueSet"}, - "tags": {"rel": ":has_tag>", "end_cls":"Tag"} + "tags": {"rel": ":has_tag>", "end_cls": "Tag"}, }, } (attspec, _mapspec) = mergespec("Property", attspec_, mapspec_) - defaults = { "value_domain":"TBD" } - + defaults = {"value_domain": "TBD"} + def __init__(self, init=None): super().__init__(init=init) self.value_types = [] @@ -121,6 +125,7 @@ def values(self): class Edge(Entity): """Subclass that models a relationship between model nodes.""" + defaults = { "multiplicity": "many_to_many", } @@ -150,11 +155,11 @@ class Edge(Entity): "dst": {"rel": ":has_dst>", "end_cls": "Node"}, "concept": {"rel": ":has_concept>", "end_cls": "Concept"}, "props": {"rel": ":has_property>", "end_cls": "Property"}, - "tags": {"rel": ":has_tag>", "end_cls":"Tag"} + "tags": {"rel": ":has_tag>", "end_cls": "Tag"}, }, } (attspec, _mapspec) = mergespec("Edge", attspec_, mapspec_) - + def __init__(self, init=None): super().__init__(init=init) @@ -166,10 +171,6 @@ def triplet(self): if self.handle and self.src and self.dst: return (self.handle, self.src.handle, self.dst.handle) - def get_label(self) -> str: - """returns type of entity as label""" - return "relationship" - class Term(Entity): """Subclass that models a term from a terminology.""" @@ -204,7 +205,7 @@ class Term(Entity): "relationship": { "concept": {"rel": ":represents>", "end_cls": "Concept"}, "origin": {"rel": ":has_origin>", "end_cls": "Origin"}, - "tags": {"rel": ":has_tag>", "end_cls":"Tag"} + "tags": {"rel": ":has_tag>", "end_cls": "Tag"}, }, } (attspec, _mapspec) = mergespec("Term", attspec_, mapspec_) @@ -220,6 +221,7 @@ class ValueSet(Entity): """Subclass that models an enumerated set of :class:`Property` values. Essentially a container for :class:`Term` instances. """ + attspec_ = { "handle": "simple", "nanoid": "simple", @@ -230,12 +232,16 @@ class ValueSet(Entity): } mapspec_ = { "label": "value_set", - "property": {"handle": "handle", "url": "url", "nanoid": "nanoid",}, + "property": { + "handle": "handle", + "url": "url", + "nanoid": "nanoid", + }, "relationship": { "prop": {"rel": "<:has_value_set", "end_cls": "Property"}, "terms": {"rel": ":has_term>", "end_cls": "Term"}, "origin": {"rel": ":has_origin>", "end_cls": "Origin"}, - "tags": {"rel": ":has_tag>", "end_cls":"Tag"} + "tags": {"rel": ":has_tag>", "end_cls": "Tag"}, }, } (attspec, _mapspec) = mergespec("ValueSet", attspec_, mapspec_) @@ -267,7 +273,7 @@ class Concept(Entity): "label": "concept", "relationship": { "terms": {"rel": "<:represents", "end_cls": "Term"}, - "tags": {"rel": ":has_tag>", "end_cls":"Tag"} + "tags": {"rel": ":has_tag>", "end_cls": "Tag"}, }, } (attspec, _mapspec) = mergespec("Concept", attspec_, mapspec_) @@ -275,12 +281,11 @@ class Concept(Entity): def __init__(self, init=None): super().__init__(init=init) + class Predicate(Entity): """Subclass that models a semantic link between concepts.""" - attspec_ = { - "handle": "simple", - "subject": "object", - "object": "object"} + + attspec_ = {"handle": "simple", "subject": "object", "object": "object"} mapspec_ = { "label": "predicate", "key": "handle", @@ -290,19 +295,24 @@ class Predicate(Entity): "relationship": { "subject": {"rel": ":has_subject>", "end_cls": "Concept"}, "object": {"rel": ":has_object>", "end_cls": "Concept"}, - "tags": {"rel": ":has_tag>", "end_cls":"Tag"} + "tags": {"rel": ":has_tag>", "end_cls": "Tag"}, }, } (attspec, _mapspec) = mergespec("Predicate", attspec_, mapspec_) def __init__(self, init=None): super().__init__(init=init) - + class Origin(Entity): """Subclass that models a :class:`Term` 's authoritative source.""" - attspec_ = {"url": "simple", "is_external": "simple", "name": "simple", "nanoid": "simple",} + attspec_ = { + "url": "simple", + "is_external": "simple", + "name": "simple", + "nanoid": "simple", + } mapspec_ = { "label": "origin", "key": "name", @@ -312,9 +322,7 @@ class Origin(Entity): "is_external": "is_external", "nanoid": "nanoid", }, - "relationship": { - "tags": {"rel": ":has_tag>", "end_cls":"Tag"} - } + "relationship": {"tags": {"rel": ":has_tag>", "end_cls": "Tag"}}, } (attspec, _mapspec) = mergespec("Origin", attspec_, mapspec_) @@ -325,7 +333,7 @@ def __init__(self, init=None): class Tag(Entity): """Subclass that allows simple key-value tagging of a model at arbitrary points.""" - attspec_ = {"key":"simple", "value": "simple"} + attspec_ = {"key": "simple", "value": "simple"} mapspec_ = { "label": "tag", "key": "key", @@ -335,3 +343,27 @@ class Tag(Entity): def __init__(self, init=None): super().__init__(init=init) + + +class Model(Entity): + """Subclass with information regarding data model.""" + + attspec_ = { + "handle": "simple", + "name": "simple", + "repository": "simple", + "nanoid": "simple", + } + mapspec_ = { + "label": "model", + "key": "handle", + "property": { + "handle": "handle", + "name": "name", + "repository": "repository", + }, + } + (attspec, _mapspec) = mergespec("Model", attspec_, mapspec_) + + def __init__(self, init=None): + super().__init__(init=init) diff --git a/python/tests/samples/test_changelog.ini b/python/tests/samples/test_changelog.ini index 9896818..5db2710 100644 --- a/python/tests/samples/test_changelog.ini +++ b/python/tests/samples/test_changelog.ini @@ -1,3 +1,3 @@ [changelog] -changeset_id = 6593 +changeset_id = 6855 diff --git a/python/tests/test_002objects.py b/python/tests/test_002objects.py index 7bb68fa..c2bee65 100644 --- a/python/tests/test_002objects.py +++ b/python/tests/test_002objects.py @@ -1,104 +1,122 @@ -import re import sys -from pdb import set_trace -sys.path.insert(0,'.') -sys.path.insert(0,'..') -import pytest + +sys.path.insert(0, ".") +sys.path.insert(0, "..") from bento_meta.entity import Entity -from bento_meta.objects import Node, Property, Edge, Term, ValueSet, Concept, Predicate, Origin, Tag +from bento_meta.objects import ( + Concept, + Edge, + Node, + Origin, + Predicate, + Property, + Tag, + Term, + ValueSet, +) + def test_create_objects(): - for cls in [Node,Property,Edge,Term,ValueSet,Concept,Origin,Tag]: - n = cls() - assert n - assert isinstance(n, Entity) + for cls in [Node, Property, Edge, Term, ValueSet, Concept, Origin, Tag]: + n = cls() + assert n + assert isinstance(n, Entity) + def test_init_and_link_objects(): - case = Node({"model":"test","handle":"case"}) - assert case - assert case.model == "test" - assert case.handle == "case" - sample = Node({"model":"test","handle":"sample"}) - assert sample - of_sample = Edge({"model":"test","handle":"of_sample"}) - assert of_sample - assert of_sample.model == "test" - assert of_sample.handle == "of_sample" - of_sample.src = sample - of_sample.dst = case - assert of_sample.dst == case - assert of_sample.src == sample - term = Term({"value":"sample"}) - concept = Concept(); - term.concept = concept - other_concept = Concept() - concept.terms["sample"]=term - sample.concept = concept - [o] = [x for x in term.belongs.values()] - assert o == concept - assert of_sample.src.concept.terms["sample"].value == "sample" - pred = Predicate({"subject":concept, "object":other_concept, "handle":"isa"}) - assert type(pred.subject) == Concept - assert type(pred.object) == Concept + case = Node({"model": "test", "handle": "case"}) + assert case + assert case.model == "test" + assert case.handle == "case" + sample = Node({"model": "test", "handle": "sample"}) + assert sample + of_sample = Edge({"model": "test", "handle": "of_sample"}) + assert of_sample + assert of_sample.model == "test" + assert of_sample.handle == "of_sample" + of_sample.src = sample + of_sample.dst = case + assert of_sample.dst == case + assert of_sample.src == sample + term = Term({"value": "sample"}) + concept = Concept() + term.concept = concept + other_concept = Concept() + concept.terms["sample"] = term + sample.concept = concept + [o] = [x for x in term.belongs.values()] + assert o == concept + assert of_sample.src.concept.terms["sample"].value == "sample" + pred = Predicate({"subject": concept, "object": other_concept, "handle": "isa"}) + assert type(pred.subject) == Concept + assert type(pred.object) == Concept + def test_tags_on_objects(): - nodeTag = Tag({"key":"name","value":"Neddy"}) - relnTag = Tag({"key":"name","value":"Robby"}) - conceptTag = Tag({"key":"name","value":"Catty"}) - conceptTag2 = Tag({"key":"aka","value":"Jehoshaphat"}) - termTag = Tag({"key":"name","value":"Termy"}) - propTag = Tag({"key":"name","value":"Puppy"}) - - case = Node({"model":"test","handle":"case"}) - of_sample = Edge({"model":"test","handle":"of_sample"}) - sample = Node({"model":"test","handle":"sample"}) - of_sample.src = sample - of_sample.dst = case - term = Term({"value":"sample"}) - concept = Concept(); - term.concept = concept - concept.terms["sample"]=term - sample.concept = concept - sample.props['this'] = Property({"that":"this"}) + nodeTag = Tag({"key": "name", "value": "Neddy"}) + relnTag = Tag({"key": "name", "value": "Robby"}) + conceptTag = Tag({"key": "name", "value": "Catty"}) + conceptTag2 = Tag({"key": "aka", "value": "Jehoshaphat"}) + termTag = Tag({"key": "name", "value": "Termy"}) + propTag = Tag({"key": "name", "value": "Puppy"}) - case.tags[nodeTag.key] = nodeTag - of_sample.tags[relnTag.key] = relnTag - term.tags[termTag.key] = termTag - concept.tags[conceptTag.key] = conceptTag - concept.tags[conceptTag2.key] = conceptTag2 - sample.props['this'].tags['name'] = propTag + case = Node({"model": "test", "handle": "case"}) + of_sample = Edge({"model": "test", "handle": "of_sample"}) + sample = Node({"model": "test", "handle": "sample"}) + of_sample.src = sample + of_sample.dst = case + term = Term({"value": "sample"}) + concept = Concept() + term.concept = concept + concept.terms["sample"] = term + sample.concept = concept + sample.props["this"] = Property({"that": "this"}) - names = [x.tags['name'].value for x in [case,of_sample,term,concept,sample.props['this']]]; - assert names == ["Neddy","Robby","Termy","Catty","Puppy"] - assert concept.tags['aka'].value == "Jehoshaphat" + case.tags[nodeTag.key] = nodeTag + of_sample.tags[relnTag.key] = relnTag + term.tags[termTag.key] = termTag + concept.tags[conceptTag.key] = conceptTag + concept.tags[conceptTag2.key] = conceptTag2 + sample.props["this"].tags["name"] = propTag + + names = [ + x.tags["name"].value + for x in [case, of_sample, term, concept, sample.props["this"]] + ] + assert names == ["Neddy", "Robby", "Termy", "Catty", "Puppy"] + assert concept.tags["aka"].value == "Jehoshaphat" -def test_some_object_methods(): - p = Property({"handle":"complaint"}) - assert p - t = Term({"value":"halitosis"}) - assert t - u = Term({"value":"ptomaine"}) - assert u - vs = ValueSet({"_id":"1"}) - assert vs - p.value_set = vs - p.value_types.append("glarp") - assert "glarp" in p.value_types - vs.terms['ptomaine'] = u - assert p.terms['ptomaine'].value == 'ptomaine' - p.terms['halitosis'] = t - assert vs.terms['halitosis'].value == 'halitosis' - vals = p.values - assert isinstance(vals,list) - assert 'ptomaine' in vals - assert 'halitosis' in vals - s = Node({'handle':"case"}) - assert s - d = Node({'handle':"cohort"}) - assert d - e = Edge({'handle':"member_of",'src':s,'dst':d}) - assert e - assert e.triplet == ('member_of','case','cohort') - - +def test_some_object_methods(): + p = Property({"handle": "complaint"}) + assert p + t = Term({"value": "halitosis"}) + assert t + u = Term({"value": "ptomaine"}) + assert u + vs = ValueSet({"_id": "1"}) + assert vs + p.value_set = vs + p.value_types.append("glarp") + assert "glarp" in p.value_types + vs.terms["ptomaine"] = u + assert p.terms["ptomaine"].value == "ptomaine" + p.terms["halitosis"] = t + assert vs.terms["halitosis"].value == "halitosis" + vals = p.values + assert isinstance(vals, list) + assert "ptomaine" in vals + assert "halitosis" in vals + s = Node({"handle": "case"}) + assert s + d = Node({"handle": "cohort"}) + assert d + e = Edge({"handle": "member_of", "src": s, "dst": d}) + assert e + assert e.triplet == ("member_of", "case", "cohort") + # test get_label() method + assert p.get_label() == "property" + assert t.get_label() == "term" + assert vs.get_label() == "value_set" + assert s.get_label() == "node" + assert e.get_label() == "relationship"