Source code for yarom.dataObject

import rdflib as R
import logging
import hashlib
import six
import random

from .mapper import (
    MappedPropertyClass,
    MappedClass,
    Resolver,
    get_most_specific_rdf_type,
    oid)
from .dataUser import DataUser
from .configure import BadConf
from .simpleProperty import (SimpleProperty, DatatypeProperty, ObjectProperty)
from .rdfUtils import triples_to_bgp
from .graphObject import (
    GraphObject,
    GraphObjectQuerier,
    ComponentTripler,
    HeroTripler,
    ReferenceTripler,
    DescendantTripler,
    IdentifierMissingException)

"""
.. autoclass:: DataObject
"""

# NOTE: This module (and all modules containing DataObject sub-types) may
# be reloaded. Objects should not be created in the top-level of this
# module.

L = logging.getLogger(__name__)


def _bnode_to_var(x):
    return "?" + x


def get_hash_function(method_name):
    if method_name == "sha224":
        return hashlib.sha224
    elif method_name == "md5":
        return hashlib.md5
    elif method_name in hashlib.algorithms_available:
        return (lambda data: hashlib.new(method_name, data))


[docs]class DataObject(six.with_metaclass(MappedClass, GraphObject, DataUser)): """ An object backed by the database Attributes ----------- rdf_type : rdflib.term.URIRef The RDF type URI for objects of this type rdf_namespace : rdflib.namespace.Namespace The rdflib namespace (prefix for URIs) for objects from this class properties : list of Property Properties belonging to this object owner_properties : list of Property Properties belonging to parents of this object """ _openSet = set() _closedSet = set() configuration_variables = { "rdf.namespace": { "description": "Namespaces for DataObject sub-classes will, by " "default, be based off of this. For example, a subclass named A " "would have a namespace '[rdf.namespace]A/'", "type": R.Namespace, "directly_configureable": True}, "dataObject.identifier_hash": { "description": "The hash method used for object identifiers. " "Defaults to md5.", "type": "sha224, md5, or one of the types accepted by" "hashlib.new()", "directly_configureable": True}, } @classmethod
[docs] def open_set(self): """ The open set contains items that must be saved directly in order for their data to be written out """ return self._openSet
[docs] def __init__( self, ident=False, var=False, key=False, generate_key=False, **kwargs): """A subclass of DataObject cannot have any required positional arguments. Parameters ---------- ident : rdflib.term.URIRef or str The identifier for this DataObject var : str In lieu of `ident`, sets the variable for this object key : str or object In lieu of `ident` or `var`, sets the identifier for this DataObject using the key value. For a namespace `ex:` and key `a`, the identifier would be `ex:a`. generate_key : bool If true generates a random key value kwargs : dict Values to set for named properties """ try: super(DataObject, self).__init__() except BadConf as e: six.raise_from( Exception( "You may need to connect to a database before continuing."), e) self._id = False if ident: if isinstance(ident, R.URIRef): self._id = ident else: self._id = R.URIRef(ident) elif var: self._id_variable = R.Variable(var) # TODO: Support a key function that generates the key based on live # values of the object (e.g., property values) elif key: self.setKey(key) elif generate_key: self.setKey(random.random()) else: # Randomly generate an identifier if the derived class can't # come up with one from the start. Ensures we always have something # that functions as an identifier v = (random.random(), random.random()) cname = self.__class__.__name__ self._id_variable = R.Variable(cname + "_" + hashlib.md5( str(v).encode()).hexdigest()) for x in self.__class__.dataObjectProperties: self.attachProperty(x) existing_property_names = [x.linkName for x in self.properties] for propName in kwargs.keys(): if propName not in existing_property_names: raise ValueError( "No such argument {} to {}::__init__".format( propName, self.__class__.__name__)) for x in self.properties: if x.linkName in kwargs: self.relate(x.linkName, kwargs[x.linkName]) if isinstance(self, PropertyDataObject): self.relate( 'rdf_type_property', RDFProperty.getInstance(), RDFTypeProperty) elif isinstance(self, TypeDataObject): self.relate( 'rdf_type_property', RDFSClass.getInstance(), RDFTypeProperty) elif isinstance(self, RDFProperty): self.relate( 'rdf_type_property', RDFSClass.getInstance(), RDFTypeProperty) elif isinstance(self, RDFSClass): self.relate('rdf_type_property', self, RDFTypeProperty) else: self.relate( 'rdf_type_property', self.rdf_type_object, RDFTypeProperty)
@classmethod def identifier_hash_method(self, o): return get_hash_function( self.conf.get( 'dataObject.identifier_hash', 'md5'))(o) def make_identifier_from_properties(self, *properties): if len(properties) == 0: raise Exception("No properties provided to make identifier") data = "" for propName in properties: for value in getattr(self, propName).values: data += value.idl.n3() if len(data) == 0: raise Exception("No properties to make identifier") return self.make_identifier(data) @property def defined(self): """Returns `True` if this object has an identifier To define a custom identifier, override :meth:`defined_augment` to return True when your custom identifier would be defined. You must also override :meth:`identifier_augment` """ # TODO: Add a check for circular definition of "defined" status by # defined_augment like: # # a is defined if b is defined # b is defined if a is defined. # # such definitions are legal and if one or the other has its # ident set explicitly, then there would be done unbounded # recursion. if self._id: return True else: return self.defined_augment()
[docs] def defined_augment(self): """ This fuction must return False if :meth:`identifier_augment` would raise an :exc:`~yarom.graphObject.IdentifierMissingException`. Override it when defining a non-standard identifier for subclasses of DataObjects. """ return False
[docs] def variable(self): """ Returns the variable to be usedin queries with this DataObject Raises ------ IdentifierMissingException """ if self._id_variable is not None: return self._id_variable else: raise IdentifierMissingException(self)
def setKey(self, key): if isinstance(key, str): self._id = self.make_identifier_direct(key) else: self._id = self.make_identifier(key) def relate(self, linkName, other, prop=False): existing_property_names = [x.linkName for x in self.properties] if linkName in existing_property_names: if prop and not isinstance(getattr(self, linkName), prop): L.warning( "A property class, {}, was provided to relate, but it" " will be ignored since there's already a property, {}," " with the given linkName '{}' and it has a different" " class".format(prop, getattr(self, linkName), linkName)) p = getattr(self, linkName) else: if not prop: # Make up a property class property_type = None if isinstance(other, DataObject): property_type = ObjectProperty else: property_type = DatatypeProperty self._create_related_property(linkName, property_type) elif not hasattr(prop, 'linkName'): prop.linkName = linkName p = self.attachProperty(prop) return p.set(other) def _create_related_property(self, linkName, property_type): property_type = None link = type(self).rdf_namespace[linkName] return MappedPropertyClass( linkName, (property_type,), dict( link=link, linkName=linkName, multiple=True)) def attachProperty(self, prop): if not hasattr(prop, 'linkName'): raise Exception("The given property class cannot be attached" " because it has no `linkName` attribute") p = prop(resolver=Resolver.get_instance(), owner=self) if hasattr(self, prop.linkName): raise Exception( "Cannot attach property '{}'. A property must have a different \ name from any attributes in DataObject".format( prop.linkName)) self.properties.append(p) setattr(self, p.linkName, p) return p def get_defined_component(self): g = ComponentTripler(self)() if not isinstance(g, R.Graph): h = R.Graph() for t in g: h.add(t) g = h g.namespace_manager = self.namespace_manager return g def __eq__(self, other): return ( isinstance( other, DataObject) and ( self.idl == other.idl)) or ( isinstance( other, R.URIRef) and self.idl == other) def __hash__(self): return hash(self.idl) def __str__(self): return self.namespace_manager.normalizeUri(self.idl) def __repr__(self): return self.__str__() @classmethod def add_to_open_set(cls, o): cls._openSet.add(o) @classmethod def remove_from_open_set(cls, o): if o not in cls._closedSet: cls._openSet.remove(o) cls._closedSet.add(o) @classmethod def extract_unique_part(cls, uri): if uri.startswith(cls.rdf_namespace): return uri[:len(cls.rdf_namespace)] else: raise Exception( "This URI ({}) doesn't start with the appropriate namespace ({})".format( uri, cls.rdf_namespace)) @classmethod def make_identifier(cls, data): # NOTE: The "a" prefix allows all identifiers to nicely reduce to # abbreviated form in n3 return R.URIRef( cls.rdf_namespace[ "a" + cls.identifier_hash_method( str(data).encode()).hexdigest()]) @classmethod def make_identifier_direct(cls, string): if not isinstance(string, str): raise Exception("make_identifier_direct only accepts strings") from six.moves import urllib return R.URIRef(cls.rdf_namespace[urllib.parse.quote(string)])
[docs] def identifier(self): """ The identifier for this object in the rdf graph. This identifier may be randomly generated, but an identifier returned from the graph can be used to retrieve the specific object that it refers to. If it is desireable to customize the identifier, a subclass of DataObject should override :meth:`identifier_augment` rather than this method. Returns ------- :class:`rdflib.term.URIRef` """ if self._id: return self._id else: return self.identifier_augment()
[docs] def identifier_augment(self): """ Override this method to define an identifier in lieu of one explicity set. One must also override :meth:`defined_augment` to return True whenever this method could return a valid identifier. :exc:`~yarom.graphObject.IdentifierMissingException` should be raised if an identifier cannot be generated by this method. Raises ------ IdentifierMissingException """ raise IdentifierMissingException(self)
[docs] def triples(self): """ Returns 3-tuples of the connected component of the object graph starting from this object. Returns -------- An iterable of triples """ return self.get_defined_component()
[docs] def graph_pattern(self, shorten=False): """ Get the graph pattern for this object. It should be as simple as converting the result of triples() into a BGP Parameters ---------- query : bool Indicates whether or not the graph_pattern is to be used for querying (as in a SPARQL query) or for storage shorten : bool Indicates whether to shorten the URLs with the namespace manager attached to the ``self`` """ nm = None if shorten: nm = self.namespace_manager return triples_to_bgp( self.get_defined_component(), namespace_manager=nm)
def load(self): for ident in GraphObjectQuerier(self, self.rdf)(): types = set() for rdf_type in self.rdf.objects(ident, R.RDF['type']): types.add(rdf_type) the_type = get_most_specific_rdf_type(types) yield oid(ident, the_type)
[docs] def save(self): """ Write in-memory data to the database. Derived classes should call this to update the store. Dual to retract. """ self.add_statements(self.get_defined_component())
[docs] def retract(self): """ Remove this object from the data store. Retract removes an object and everything it points to, transitively, and everything which points to it. Dual to save. """ self.retract_statements(self.get_defined_component())
[docs] def save_object(self): """ Write in-memory data to the database. Derived classes should call this to update the store. Dual to retract_object. """ self.add_statements(DescendantTripler(self)())
[docs] def retract_object(self): """ Remove this object from the data store. Retract removes an object and everything it points to, transitively, and everything which points to it. Dual to save_object. """ self.retract_statements(HeroTripler(self)())
[docs] def retract_objectG(self): """ Remove this object from the data store. Retract removes an object and everything it points to, transitively, and everything which points to it. Dual to save_objectG. """ g = HeroTripler(self, self.rdf)() self.retract_statements(g)
[docs] def retract_references(self): """ Remove all references directly to or made by this object """ self.retract_statements(ReferenceTripler(self)())
[docs] def retract_referencesG(self): """ Remove all references directly to or made by this object """ self.retract_statements(ReferenceTripler(self, self.rdf)())
def __getitem__(self, x): try: return self.conf[x] except KeyError: raise Exception( "You attempted to get the value `%s' from `%s'. It isn't here. Perhaps you misspelled the name of a Property?" % (x, self))
[docs] def get_owners(self, property_name): """ Return the owners along a property pointing to this object """ res = [] for x in self.owner_properties: if isinstance(x, SimpleProperty): if str(x.linkName) == str(property_name): res.append(x.owner) return res
[docs]def validateG(do_type): """ Given a DataObject type, call validate() on all objects of that type in the Python object graph """
[docs]def validate(do_type): """ Given a DataObject type, call validate() on all objects of that type in the RDF object graph """
class TypeDataObject(DataObject): pass class DataObjectSingleton(DataObject): instance = None def __init__(self, *args, **kwargs): if type(self)._gettingInstance: DataObject.__init__(self, *args, **kwargs) else: raise Exception( "You must call getInstance to get " + type(self).__name__) @classmethod def getInstance(cls): if cls.instance is None: cls._gettingInstance = True cls.instance = cls() cls._gettingInstance = False return cls.instance
[docs]class RDFSClass(DataObjectSingleton): # This maybe becomes a DataObject later """ The DataObject corresponding to rdfs:Class """ # XXX: This class may be changed from a singleton later to facilitate dumping # and reloading the object graph rdf_type = R.RDFS['Class'] def __init__(self): super(RDFSClass, self).__init__(R.RDFS["Class"])
[docs]class RDFProperty(DataObjectSingleton): """ The DataObject corresponding to rdf:Property """ rdf_type = R.RDF['Property'] def __init__(self): super(RDFProperty, self).__init__(R.RDF["Property"])
class RDFTypeProperty(ObjectProperty): link = R.RDF['type'] linkName = "rdf_type_property" owner_type = DataObject value_type = RDFSClass multiple = True class RDFSSubClassOfProperty(ObjectProperty): link = R.RDFS['subClassOf'] linkName = "rdfs_subClassOf" owner_type = RDFSClass value_type = RDFSClass multiple = True
[docs]class PropertyDataObject(DataObject): """ A PropertyDataObject represents the property-as-object. Try not to confuse this with the Property class """
class RDFSDomainProperty(ObjectProperty): link = R.RDFS['domain'] linkName = "rdfs_domain" owner_type = RDFProperty value_type = RDFSClass multiple = True class RDFSRangeProperty(ObjectProperty): link = R.RDFS['range'] linkName = "rdfs_range" owner_type = RDFProperty value_type = RDFSClass multiple = True
[docs]class ObjectCollection(DataObject): """ A convenience class for working with a collection of objects Example:: v = ObjectCollection('unc-13 neurons and muscles') n = P.Neuron() m = P.Muscle() n.receptor('UNC-13') m.receptor('UNC-13') for x in n.load(): v.value(x) for x in m.load(): v.value(x) # Save the group for later use v.save() ... # get the list back u = ObjectCollection('unc-13 neurons and muscles') nm = list(u.value()) Parameters ---------- group_name : string A name of the group of objects Attributes ---------- name : DatatypeProperty The name of the group of objects group_name : DataObject an alias for ``name`` member : ObjectProperty An object in the group add : ObjectProperty an alias for ``value`` """ _ = ['member'] datatypeProperties = [{'name': 'name', 'multiple': False}] def __init__(self, group_name=False, **kwargs): super(ObjectCollection, self).__init__(key=group_name, **kwargs) self.add = self.member self.group_name = self.name self.name(group_name) def identifier(self, query=False): return self.make_identifier(self.group_name)