diff --git a/docs/conf.py b/docs/conf.py index 27feaf6..493077a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,9 +58,9 @@ # built documents. # # The short X.Y version. -version = '0.1' +version = '0.2' # The full version, including alpha/beta/rc tags. -release = '0.1' +release = '0.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/core_concepts.rst b/docs/core_concepts.rst index b0daa14..4d6cb70 100644 --- a/docs/core_concepts.rst +++ b/docs/core_concepts.rst @@ -2,34 +2,36 @@ Core concepts ============= +THIS PAGE IS OUT-DATED. TODO: rewrite it. + Resource -------- -A :class:`~oldman.resource.Resource` object represents a `Web resource `_ +A :class:`~oldman.resource.resource.Resource` object represents a `Web resource `_ identified by a regular `IRI (internationalized URI) `_ or or a `skolem IRI `_ (if it should treated as a `blank node `_). In OldMan, Web resources are described in conformance to the `Resource Description Framework (RDF) `_. -A :class:`~oldman.resource.Resource` object may have some attributes that provide the *predicate* +A :class:`~oldman.resource.resource.Resource` object may have some attributes that provide the *predicate* (also called property) and the *object* terms of RDF triples describing the resource. The resource itself is the *subject* of the triple. Its attributes have arbitrary short names as defined in the JSON-LD context. -A :class:`~oldman.resource.Resource` object access to its attributes through the -:class:`~oldman.model.Model` objects to which it relates (through its :attr:`~oldman.resource.Resource.types`). -Thus, if it has no *type* or its types that are not related to a :class:`~oldman.model.Model` object, -a :class:`~oldman.resource.Resource` object has no "RDF" attribute. +A :class:`~oldman.resource.resource.Resource` object access to its attributes through the +:class:`~oldman.model.model.Model` objects to which it relates (through its :attr:`~oldman.resource.resource.Resource.types`). +Thus, if it has no *type* or its types that are not related to a :class:`~oldman.model.model.Model` object, +a :class:`~oldman.resource.resource.Resource` object has no "RDF" attribute. -In OldMan, the relation between :class:`~oldman.resource.Resource` and :class:`~oldman.model.Model` objects +In OldMan, the relation between :class:`~oldman.resource.resource.Resource` and :class:`~oldman.model.model.Model` objects is *many-to-many*. It differs from traditional ORMs where the relation is *one-to-many* (the resource is usually an instance of the model and the latter is a Python class in these frameworks). -However, we expect that most :class:`~oldman.resource.Resource` objects will relate to one -:class:`~oldman.model.Model` object, but this is not a requirement. +However, we expect that most :class:`~oldman.resource.resource.Resource` objects will relate to one +:class:`~oldman.model.model.Model` object, but this is not a requirement. It is common for a resource in RDF to be instance of multiple RDFS classes so OldMan had to be ok with this practise. -Some inherited Python methods may also be provided by the :class:`~oldman.model.Model` objects. +Some inherited Python methods may also be provided by the :class:`~oldman.model.model.Model` objects. Features @@ -79,17 +81,17 @@ ResourceManager --------------- A :class:`~oldman.management.manager.ResourceManager` object is the central object of OldMan. -It creates :class:`~oldman.model.Model` objects (:func:`~oldman.management.manager.ResourceManager.create_model`) -and retrieves :class:`~oldman.resource.Resource` objects (:func:`~oldman.management.manager.ResourceManager.get`, +It creates :class:`~oldman.model.model.Model` objects (:func:`~oldman.management.manager.ResourceManager.create_model`) +and retrieves :class:`~oldman.resource.resource.Resource` objects (:func:`~oldman.management.manager.ResourceManager.get`, :func:`~oldman.management.manager.ResourceManager.filter` and :func:`~oldman.management.manager.ResourceManager.sparql_filter`). -It accepts Python method declarations if they happen before the creation of :class:`~oldman.model.Model` objects +It accepts Python method declarations if they happen before the creation of :class:`~oldman.model.model.Model` objects (:func:`~oldman.management.manager.ResourceManager.declare_method`). -It also provide helper functions to create new :class:`~oldman.resource.Resource` objects +It also provide helper functions to create new :class:`~oldman.resource.resource.Resource` objects (:func:`~oldman.management.manager.ResourceManager.create` and :func:`~oldman.management.manager.ResourceManager.new`) -but it is usually simpler to use those of a :class:`~oldman.model.Model` object. +but it is usually simpler to use those of a :class:`~oldman.model.model.Model` object. For creating the :class:`~oldman.management.manager.ResourceManager` object, the schema graph and the data store (:class:`~oldman.store.datastore.DataStore`) must be given. @@ -101,25 +103,25 @@ required and what are the constraints. Model ----- -In OldMan, models are not Python classes but :class:`~oldman.model.Model` objects. +In OldMan, models are not Python classes but :class:`~oldman.model.model.Model` objects. However, on the RDF side, they correspond to `RDFS classes `_ (their -:attr:`~oldman.model.Model.class_iri` attributes). +:attr:`~oldman.model.model.Model.class_iri` attributes). -Their main role is to provide attributes and methods to :class:`~oldman.resource.Resource` objects, as explained +Their main role is to provide attributes and methods to :class:`~oldman.resource.resource.Resource` objects, as explained above. -:class:`~oldman.model.Model` objects are created by the :class:`~oldman.management.manager.ResourceManager` object. +:class:`~oldman.model.model.Model` objects are created by the :class:`~oldman.management.manager.ResourceManager` object. A model provide some helpers above the :class:`~oldman.management.manager.ResourceManager` object ( -:func:`~oldman.model.Model.get`, :func:`~oldman.model.Model.filter`, :func:`~oldman.model.Model.new` and -:func:`~oldman.model.Model.create`) that include the :attr:`~oldman.model.Model.class_iri` to the `types` +:func:`~oldman.model.model.Model.get`, :func:`~oldman.model.model.Model.filter`, :func:`~oldman.model.model.Model.new` and +:func:`~oldman.model.model.Model.create`) that include the :attr:`~oldman.model.model.Model.class_iri` to the `types` parameter of these methods. DataStore --------- A :class:`~oldman.store.datastore.DataStore` implements the CRUD operations on Web Resources exposed by the -:class:`~oldman.management.manager.ResourceManager` and :class:`~oldman.model.Model` objects. +:class:`~oldman.management.manager.ResourceManager` and :class:`~oldman.model.model.Model` objects. The vision of OldMan is to include a large choice of data stores. But currently, only SPARQL endpoints are supported. @@ -128,7 +130,7 @@ Non-CRUD operations may also be introduced in the future (in discussion). Any data store accepts a :class:`dogpile.cache.region.CacheRegion` object to enable its :class:`~oldman.store.cache.ResourceCache` object. -By default the latter is disabled so it does not cache the :class:`~oldman.resource.Resource` objects loaded +By default the latter is disabled so it does not cache the :class:`~oldman.resource.resource.Resource` objects loaded from and stored in the data store. SPARQLDataStore diff --git a/docs/examples/dbpedia.rst b/docs/examples/dbpedia.rst index 1421f85..5e24ab5 100644 --- a/docs/examples/dbpedia.rst +++ b/docs/examples/dbpedia.rst @@ -90,6 +90,7 @@ Now we extract the film titles and the names of the actors:: elif name not in film_actors[film_iri]: film_actors[film_iri].append(unicode(name)) break + and display them:: >>> for film_iri in film_titles: @@ -201,7 +202,7 @@ Let's first create two :class:`~oldman.model.Model` objects: `film_model` and `p `context `_ and `schema `_:: - from oldman import ResourceManager, SPARQLDataStore + from oldman import ClientResourceManager, SPARQLDataStore from dogpile.cache import make_region schema_url = "https://raw.githubusercontent.com/oldm/OldMan/master/examples/dbpedia_film_schema.ttl" @@ -209,17 +210,22 @@ and `schema `_ and to subscribe to our mailing list `oldman AT librelist.com`. diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 0000000..7935fe7 --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,7 @@ +oldman +====== + +.. toctree:: + :maxdepth: 4 + + oldman diff --git a/docs/oldman.model.rst b/docs/oldman.model.rst new file mode 100644 index 0000000..2220f1d --- /dev/null +++ b/docs/oldman.model.rst @@ -0,0 +1,78 @@ +oldman.model package +==================== + +Submodules +---------- + +oldman.model.ancestry module +---------------------------- + +.. automodule:: oldman.model.ancestry + :members: + :undoc-members: + :show-inheritance: + +oldman.model.attribute module +----------------------------- + +.. automodule:: oldman.model.attribute + :members: + :undoc-members: + :show-inheritance: + +oldman.model.converter module +----------------------------- + +.. automodule:: oldman.model.converter + :members: + :undoc-members: + :show-inheritance: + +oldman.model.manager module +--------------------------- + +.. automodule:: oldman.model.manager + :members: + :undoc-members: + :show-inheritance: + +oldman.model.model module +------------------------- + +.. automodule:: oldman.model.model + :members: + :undoc-members: + :show-inheritance: + +oldman.model.operation module +----------------------------- + +.. automodule:: oldman.model.operation + :members: + :undoc-members: + :show-inheritance: + +oldman.model.property module +---------------------------- + +.. automodule:: oldman.model.property + :members: + :undoc-members: + :show-inheritance: + +oldman.model.registry module +---------------------------- + +.. automodule:: oldman.model.registry + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: oldman.model + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/oldman.parsing.rst b/docs/oldman.parsing.rst index 0a8a633..79bcf5d 100644 --- a/docs/oldman.parsing.rst +++ b/docs/oldman.parsing.rst @@ -1,10 +1,24 @@ oldman.parsing package ====================== +Subpackages +----------- + .. toctree:: oldman.parsing.schema +Submodules +---------- + +oldman.parsing.operation module +------------------------------- + +.. automodule:: oldman.parsing.operation + :members: + :undoc-members: + :show-inheritance: + oldman.parsing.value module --------------------------- @@ -13,3 +27,11 @@ oldman.parsing.value module :undoc-members: :show-inheritance: + +Module contents +--------------- + +.. automodule:: oldman.parsing + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/oldman.parsing.schema.rst b/docs/oldman.parsing.schema.rst index ca9ca96..56d3e8a 100644 --- a/docs/oldman.parsing.schema.rst +++ b/docs/oldman.parsing.schema.rst @@ -1,6 +1,9 @@ oldman.parsing.schema package ============================= +Submodules +---------- + oldman.parsing.schema.attribute module -------------------------------------- @@ -25,3 +28,11 @@ oldman.parsing.schema.property module :undoc-members: :show-inheritance: + +Module contents +--------------- + +.. automodule:: oldman.parsing.schema + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/oldman.resource.rst b/docs/oldman.resource.rst new file mode 100644 index 0000000..912b22b --- /dev/null +++ b/docs/oldman.resource.rst @@ -0,0 +1,30 @@ +oldman.resource package +======================= + +Submodules +---------- + +oldman.resource.manager module +------------------------------ + +.. automodule:: oldman.resource.manager + :members: + :undoc-members: + :show-inheritance: + +oldman.resource.resource module +------------------------------- + +.. automodule:: oldman.resource.resource + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: oldman.resource + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/oldman.rest.rst b/docs/oldman.rest.rst index e1e0957..642b690 100644 --- a/docs/oldman.rest.rst +++ b/docs/oldman.rest.rst @@ -1,6 +1,17 @@ oldman.rest package =================== +Submodules +---------- + +oldman.rest.controller module +----------------------------- + +.. automodule:: oldman.rest.controller + :members: + :undoc-members: + :show-inheritance: + oldman.rest.crud module ----------------------- @@ -9,3 +20,11 @@ oldman.rest.crud module :undoc-members: :show-inheritance: + +Module contents +--------------- + +.. automodule:: oldman.rest + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/oldman.rst b/docs/oldman.rst index 54b524c..73930d9 100644 --- a/docs/oldman.rst +++ b/docs/oldman.rst @@ -1,12 +1,17 @@ -.. _oldman: - oldman package ============== -oldman.attribute module ------------------------ +Subpackages +----------- + + +Submodules +---------- + +oldman.common module +-------------------- -.. automodule:: oldman.attribute +.. automodule:: oldman.common :members: :undoc-members: :show-inheritance: @@ -27,30 +32,6 @@ oldman.iri module :undoc-members: :show-inheritance: -oldman.model module -------------------- - -.. automodule:: oldman.model - :members: - :undoc-members: - :show-inheritance: - -oldman.property module ----------------------- - -.. automodule:: oldman.property - :members: - :undoc-members: - :show-inheritance: - -oldman.resource module ----------------------- - -.. automodule:: oldman.resource - :members: - :undoc-members: - :show-inheritance: - oldman.vocabulary module ------------------------ @@ -64,9 +45,10 @@ Sub-packages .. toctree:: - oldman.management + oldman.model oldman.parsing + oldman.resource oldman.rest oldman.store oldman.utils - oldman.validation + oldman.validation \ No newline at end of file diff --git a/docs/oldman.store.rst b/docs/oldman.store.rst index 52ebaeb..fbe4a96 100644 --- a/docs/oldman.store.rst +++ b/docs/oldman.store.rst @@ -20,6 +20,22 @@ oldman.store.datastore module :undoc-members: :show-inheritance: +oldman.store.http module +------------------------ + +.. automodule:: oldman.store.http + :members: + :undoc-members: + :show-inheritance: + +oldman.store.selector module +---------------------------- + +.. automodule:: oldman.store.selector + :members: + :undoc-members: + :show-inheritance: + oldman.store.sparql module -------------------------- diff --git a/docs/oldman.utils.rst b/docs/oldman.utils.rst index 9eb7c95..9311c17 100644 --- a/docs/oldman.utils.rst +++ b/docs/oldman.utils.rst @@ -1,6 +1,17 @@ oldman.utils package ==================== +Submodules +---------- + +oldman.utils.crud module +------------------------ + +.. automodule:: oldman.utils.crud + :members: + :undoc-members: + :show-inheritance: + oldman.utils.sparql module -------------------------- @@ -9,3 +20,11 @@ oldman.utils.sparql module :undoc-members: :show-inheritance: + +Module contents +--------------- + +.. automodule:: oldman.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/oldman.validation.rst b/docs/oldman.validation.rst index 67adc06..8180a22 100644 --- a/docs/oldman.validation.rst +++ b/docs/oldman.validation.rst @@ -1,6 +1,9 @@ oldman.validation package ========================= +Submodules +---------- + oldman.validation.value_format module ------------------------------------- @@ -8,3 +11,12 @@ oldman.validation.value_format module :members: :undoc-members: :show-inheritance: + + +Module contents +--------------- + +.. automodule:: oldman.validation + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 4da373f..407d018 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -9,16 +9,13 @@ Model creation First, let's import some functions and classes:: - from rdflib import Graph - from oldman import ResourceManager, parse_graph_safely, SPARQLDataStore + from rdflib import Graph + from oldman import ClientResourceManager, parse_graph_safely, SPARQLDataStore and create the RDF graph `schema_graph` that will contain our schema:: schema_graph = Graph() -The data graph is where we store the data that we generate. -By default, it stores data in memory. - The role of the schema graph is to contain most of the domain logic necessary to build our models. In this example, we load it `from a RDF file `_:: @@ -35,24 +32,21 @@ Here, we just need its IRI:: We now have almost enough domain knowledge to create our models. +TODO: update the comments. Introduce the data store. +It creates :class:`~oldman.model.Model` objects. + But first of all, we have to decide where to store our data. Here we create an in-memory RDF graph and use it as a SPARQL endpoint (:class:`~oldman.store.sparql.SPARQLDataStore`):: data_graph = Graph() - data_store = SPARQLDataStore(data_graph) + data_store = SPARQLDataStore(data_graph, schema_graph=schema_graph) We extract the prefix information from the schema graph:: data_store.extract_prefixes(schema_graph) -Then we instantiate the central object of this framework, -the :class:`~oldman.management.manager.ResourceManager` object. -Basically, it creates :class:`~oldman.model.Model` objects and -offers convenient method to retrieve and create :class:`~oldman.resource.Resource` objects:: - - manager = ResourceManager(schema_graph, data_store) - -Finally, we create our `LocalPerson` :class:`~oldman.model.Model` object. +TODO: update +We create our `LocalPerson` :class:`~oldman.model.Model` object. For that, we need: * The IRI or a JSON-LD term of the RDFS class of the model. Here `"LocalPerson"` is an alias for ``_ defined in the context file ; @@ -62,9 +56,17 @@ For that, we need: * To declare that we want to generate incremental IRIs with short numbers for new :class:`~oldman.resource.Resource` objects. :: - lp_model = manager.create_model("LocalPerson", context_iri, - iri_prefix="http://localhost/persons/", - iri_fragment="me", incremental_iri=True) + data_store.create_model("LocalPerson", context_iri, iri_prefix="http://localhost/persons/", + iri_fragment="me", incremental_iri=True) + + +TODO: update this part. +Then we instantiate the :class:`~oldman.resource.manager.ClientResourceManager` object. +Basically, it offers convenient method to retrieve and create :class:`~oldman.resource.Resource` objects:: + + client_manager = ResourceManager(schema_graph, data_store) + client_manager.use_all_store_models() + lp_model = client_manager.get_model("LocalPerson") Resource editing diff --git a/docs/validation.rst b/docs/validation.rst new file mode 100644 index 0000000..8bd395f --- /dev/null +++ b/docs/validation.rst @@ -0,0 +1,10 @@ +========== +Validation +========== + +Resource-centric validation based on RDF vocabularies: + + - `Hydra`_: `hydra:required`_ , `hydra:readonly`_ and `hydra:writeonly`_; + - Literal validation for common XSD types; + - Literal validation for arbitrary property (e.g. `foaf:mbox `_); + - `JSON-LD collections `_ (set, list and language maps); \ No newline at end of file diff --git a/examples/dbpedia_film.py b/examples/dbpedia_film.py index 268f591..b3c7de9 100644 --- a/examples/dbpedia_film.py +++ b/examples/dbpedia_film.py @@ -6,7 +6,7 @@ from rdflib import Graph from rdflib.plugins.stores.sparqlstore import SPARQLStore -from oldman import ResourceManager, SPARQLDataStore +from oldman import ClientResourceManager, SPARQLDataStore from dogpile.cache import make_region import logging from os import path @@ -51,12 +51,18 @@ def extract_name(person): cache_region = make_region().configure('dogpile.cache.memory_pickle') - # Resource Manager and Models - data_store = SPARQLDataStore(data_graph, cache_region=cache_region) - manager = ResourceManager(schema_graph, data_store) - film_model = manager.create_model("http://dbpedia.org/ontology/Film", context_url) + # Datastore: SPARQL-aware triple store, with two models + data_store = SPARQLDataStore(data_graph, schema_graph=schema_graph, cache_region=cache_region) + data_store.create_model("http://dbpedia.org/ontology/Film", context_url) # JSON-LD terms can be used instead of IRIs - actor_model = manager.create_model("Person", context_url) + data_store.create_model("Person", context_url) + + # Client resource manager + client_manager = ClientResourceManager(data_store) + # Re-uses the models of the data store + client_manager.import_store_models() + film_model = client_manager.get_model("http://dbpedia.org/ontology/Film") + actor_model = client_manager.get_model("Person") print "10 first French films found on DBPedia (with OldMan)" print "----------------------------------------------------" diff --git a/examples/quickstart.py b/examples/quickstart.py index c5bb875..fe43536 100644 --- a/examples/quickstart.py +++ b/examples/quickstart.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from rdflib import Graph -from oldman import ResourceManager, parse_graph_safely, SPARQLDataStore +from oldman import ClientResourceManager, parse_graph_safely, SPARQLDataStore # In-memory store store = "default" @@ -10,7 +10,7 @@ # update_endpoint="http://localhost:3030/test/update") # Graph containing all the schema RDF triples -schema_graph = Graph() +schema_graph = Graph(store) # Load the schema parse_graph_safely(schema_graph, "https://raw.githubusercontent.com/oldm/OldMan/master/examples/quickstart_schema.ttl", @@ -19,20 +19,24 @@ context_iri = "https://raw.githubusercontent.com/oldm/OldMan/master/examples/quickstart_context.jsonld" data_graph = Graph() -data_store = SPARQLDataStore(data_graph) +data_store = SPARQLDataStore(data_graph, schema_graph=schema_graph) # Only for SPARQL data stores data_store.extract_prefixes(schema_graph) -#Resource manager (will generate the model objects) -manager = ResourceManager(schema_graph, data_store) - #LocalPerson model -lp_model = manager.create_model("LocalPerson", context_iri, iri_prefix="http://localhost/persons/", - iri_fragment="me", incremental_iri=True) +data_store.create_model("LocalPerson", context_iri, iri_prefix="http://localhost/persons/", + iri_fragment="me", incremental_iri=True) + +#Client resource manager +client_manager = ClientResourceManager(data_store) +client_manager.import_store_models() + +lp_model = client_manager.get_model("LocalPerson") alice = lp_model.create(name="Alice", emails={"alice@example.org"}, short_bio_en="I am ...") -bob = lp_model.new(name="Bob", blog="http://blog.example.com/", +bob = lp_model.new(name="Bob", + #blog="http://blog.example.com/", short_bio_fr=u"J'ai grandi en ... .") print bob.is_valid() @@ -62,6 +66,7 @@ # First person found named Bob bob = lp_model.get(name="Bob") alice = lp_model.get(id=alice_iri) +print alice.name # Or retrieve her as the unique friend of Bob alice = list(bob.friends)[0] diff --git a/oldman/__init__.py b/oldman/__init__.py index 0a0f3a0..99f3a83 100644 --- a/oldman/__init__.py +++ b/oldman/__init__.py @@ -7,7 +7,7 @@ from rdflib.plugin import register, Parser, Serializer from rdflib import Graph -from .management.manager import ResourceManager +from .resource.manager import ClientResourceManager from .store.sparql import SPARQLDataStore from .store.http import HttpDataStore from .utils.sparql import parse_graph_safely diff --git a/oldman/management/manager.py b/oldman/management/manager.py deleted file mode 100644 index f4790e5..0000000 --- a/oldman/management/manager.py +++ /dev/null @@ -1,278 +0,0 @@ -import json -import logging -from urlparse import urlparse - -from rdflib import Graph - -from oldman.model import Model -from oldman.resource import Resource -from oldman.exception import OMUndeclaredClassNameError, OMExpiredMethodDeclarationTimeSlotError, OMError -from oldman.iri import PrefixedUUIDIriGenerator, IncrementalIriGenerator, BlankNodeIriGenerator -from oldman.parsing.schema.attribute import OMAttributeExtractor -from oldman.parsing.operation import HydraOperationExtractor -from oldman.vocabulary import HYDRA_COLLECTION_IRI, HYDRA_PAGED_COLLECTION_IRI, HTTP_POST -from oldman.operation import append_to_hydra_collection, append_to_hydra_paged_collection -from .registry import ModelRegistry -from .ancestry import ClassAncestry - - -DEFAULT_MODEL_NAME = "Thing" - - -class ResourceManager(object): - """The `resource_manager` is the central object of this OLDM. - - It gives access to the :class:`~oldman.store.datastore.DataStore` object - and creates :class:`~oldman.model.Model` objects. - It also creates, retrieves and caches :class:`~oldman.resource.Resource` objects. - - Internally, it owns a :class:`~oldman.management.registry.ModelRegistry` object. - - :param schema_graph: :class:`rdflib.Graph` object containing all the schema triples. - :param data_store: :class:`~oldman.store.datastore.DataStore` object. Supports CRUD operations on - :class:`~oldman.resource.Resource` objects. - :param attr_extractor: :class:`~oldman.parsing.attribute.OMAttributeExtractor` object that - will extract :class:`~oldman.attribute.OMAttribute` for generating - new :class:`~oldman.model.Model` objects. - Defaults to a new instance of :class:`~oldman.parsing.attribute.OMAttributeExtractor`. - :param oper_extractor: TODO: describe. - :param manager_name: Name of this manager. Defaults to `"default"`. This name must be unique. - :param declare_default_operation_functions: TODO: describe. - """ - _managers = {} - - def __init__(self, schema_graph, data_store, attr_extractor=None, oper_extractor=None, - manager_name="default", declare_default_operation_functions=True): - self._attr_extractor = attr_extractor if attr_extractor is not None else OMAttributeExtractor() - self._operation_extractor = oper_extractor if oper_extractor is not None else HydraOperationExtractor() - self._schema_graph = schema_graph - self._data_store = data_store - self._methods = {} - self._operation_functions = {} - self._registry = ModelRegistry() - self._logger = logging.getLogger(__name__) - self._name = manager_name - if manager_name in self._managers: - raise OMError(u"Manager name \"%s\" is already allocated" % manager_name) - self._managers[manager_name] = self - - self._include_reversed_attributes = False - - # Registered with the "None" key - self._create_model(DEFAULT_MODEL_NAME, {u"@context": {}}, untyped=True, - iri_prefix=u"http://localhost/.well-known/genid/default/", is_default=True) - - # Register it - self._data_store.manager = self - - if declare_default_operation_functions: - self.declare_operation_function(append_to_hydra_collection, HYDRA_COLLECTION_IRI, HTTP_POST) - self.declare_operation_function(append_to_hydra_paged_collection, HYDRA_PAGED_COLLECTION_IRI, HTTP_POST) - - @property - def data_store(self): - """:class:`~oldman.store.datastore.DataStore` object. Supports CRUD operations on - `:class:`~oldman.resource.Resource` objects`. - """ - return self._data_store - - @property - def name(self): - """Name of this manager. - The manager can be retrieved from its name by calling the - class method :func:`~oldman.management.manager.ResourceManager.get_manager`. - """ - return self._name - - @property - def include_reversed_attributes(self): - """Is `True` if at least one of its models use some reversed attributes.""" - return self._include_reversed_attributes - - @classmethod - def get_manager(cls, name): - """Gets a :class:`~oldman.management.manager.ResourceManager` object by its name. - - :param name: manager name. - :return: A :class:`~oldman.management.manager.ResourceManager` object. - """ - return cls._managers.get(name) - - def declare_method(self, method, name, class_iri): - """Attaches a method to the :class:`~oldman.resource.Resource` objects that are instances of a given RDFS class. - - Like in Object-Oriented Programming, this method can be overwritten by attaching a homonymous - method to a class that has a higher inheritance priority (such as a sub-class). - - To benefit from this method (or an overwritten one), :class:`~oldman.resource.Resource` objects - must be associated to a :class:`~oldman.model.Model` that corresponds to the RDFS class or to one of its - subclasses. - - This method can only be used before the creation of any model (except the default one). - - :param method: Python function that takes as first argument a :class:`~oldman.resource.Resource` object. - :param name: Name assigned to this method. - :param class_iri: Targetted RDFS class. If not overwritten, all the instances - (:class:`~oldman.resource.Resource` objects) should inherit this method. - """ - if self._registry.has_specific_models(): - raise OMExpiredMethodDeclarationTimeSlotError(u"Method declaration cannot occur after model creation.") - - if class_iri in self._methods: - if name in self._methods[class_iri]: - self._logger.warn(u"Method %s of %s is overloaded." % (name, class_iri)) - self._methods[class_iri][name] = method - else: - self._methods[class_iri] = {name: method} - - def declare_operation_function(self, func, class_iri, http_method): - """ - TODO: comment - """ - if self._registry.has_specific_models(): - raise OMExpiredMethodDeclarationTimeSlotError(u"Operation declaration cannot occur after model creation.") - - http_method = http_method.upper() - if class_iri in self._operation_functions: - if http_method in self._methods[class_iri]: - self._logger.warn(u"Operation %s of %s is overloaded." % (http_method, class_iri)) - self._operation_functions[class_iri][http_method] = func - else: - self._operation_functions[class_iri] = {http_method: func} - - def create_model(self, class_name_or_iri, context, iri_generator=None, iri_prefix=None, - iri_fragment=None, incremental_iri=False): - """Creates a :class:`~oldman.model.Model` object. - - To create it, they are three elements to consider: - - 1. Its class IRI which can be retrieved from `class_name_or_iri`; - 2. Its JSON-LD context for mapping :class:`~oldman.attribute.OMAttribute` values to RDF triples; - 3. The :class:`~oldman.iri.IriGenerator` object that generates IRIs from new - :class:`~oldman.resource.Resource` objects. - - The :class:`~oldman.iri.IriGenerator` object is either: - - * directly given: `iri_generator`; - * created from the parameters `iri_prefix`, `iri_fragment` and `incremental_iri`. - - :param class_name_or_iri: IRI or JSON-LD term of a RDFS class. - :param context: `dict`, `list` or `IRI` that represents the JSON-LD context . - :param iri_generator: :class:`~oldman.iri.IriGenerator` object. If given, other `iri_*` parameters are - ignored. - :param iri_prefix: Prefix of generated IRIs. Defaults to `None`. - If is `None` and no `iri_generator` is given, a :class:`~oldman.iri.BlankNodeIriGenerator` is created. - :param iri_fragment: IRI fragment that is added at the end of generated IRIs. For instance, `"me"` - adds `"#me"` at the end of the new IRI. Defaults to `None`. Has no effect if `iri_prefix` is not given. - :param incremental_iri: If `True` an :class:`~oldman.iri.IncrementalIriGenerator` is created instead of a - :class:`~oldman.iri.RandomPrefixedIriGenerator`. Defaults to `False`. - Has no effect if `iri_prefix` is not given. - :return: A new :class:`~oldman.model.Model` object. - """ - return self._create_model(class_name_or_iri, context, iri_generator=iri_generator, iri_prefix=iri_prefix, - iri_fragment=iri_fragment, incremental_uri=incremental_iri) - - def _create_model(self, class_name_or_iri, context, iri_prefix=None, iri_fragment=None, - iri_generator=None, untyped=False, incremental_uri=False, is_default=False): - - # Only for the DefaultModel - if untyped: - class_iri = None - ancestry = ClassAncestry(class_iri, self._schema_graph) - om_attributes = {} - else: - class_iri = _extract_class_iri(class_name_or_iri, context) - ancestry = ClassAncestry(class_iri, self._schema_graph) - om_attributes = self._attr_extractor.extract(class_iri, ancestry.bottom_up, context, - self._schema_graph, self) - if iri_generator is not None: - id_generator = iri_generator - elif iri_prefix is not None: - if incremental_uri: - id_generator = IncrementalIriGenerator(iri_prefix, self._data_store, - class_iri, fragment=iri_fragment) - else: - id_generator = PrefixedUUIDIriGenerator(iri_prefix, fragment=iri_fragment) - else: - id_generator = BlankNodeIriGenerator() - - methods = {} - for m_dict in [self._methods.get(t, {}) for t in ancestry.top_down]: - methods.update(m_dict) - - operations = self._operation_extractor.extract(ancestry, self._schema_graph, - self._operation_functions) - - model = Model(self, class_name_or_iri, class_iri, ancestry.bottom_up, context, om_attributes, - id_generator, methods=methods, operations=operations) - self._registry.register(model, is_default=is_default) - - # Reversed attributes awareness - if not self._include_reversed_attributes: - self._include_reversed_attributes = model.has_reversed_attributes - - return model - - def new(self, id=None, types=None, hashless_iri=None, collection_iri=None, **kwargs): - """Creates a new :class:`~oldman.resource.Resource` object **without saving it** in the `data_store`. - - The `kwargs` dict can contains regular attribute key-values that will be assigned to - :class:`~oldman.attribute.OMAttribute` objects. - - :param id: IRI of the new resource. Defaults to `None`. - If not given, the IRI is generated by the IRI generator of the main model. - :param types: IRIs of RDFS classes the resource is instance of. Defaults to `None`. - Note that these IRIs are used to find the models of the resource - (see :func:`~oldman.management.manager.ResourceManager.find_models_and_types` for more details). - :param hashless_iri: hash-less IRI that MAY be considered when generating an IRI for the new resource. - Defaults to `None`. Ignored if `id` is given. Must be `None` if `collection_iri` is given. - :param collection_iri: IRI of the controller to which this resource belongs. This information - is used to generate a new IRI if no `id` is given. The IRI generator may ignore it. - Defaults to `None`. Must be `None` if `hashless_iri` is given. - :return: A new :class:`~oldman.resource.Resource` object. - """ - if (types is None or len(types) == 0) and len(kwargs) == 0: - name = id if id is not None else "" - self._logger.info(u"""New resource %s has no type nor attribute. - As such, nothing is stored in the data graph.""" % name) - return Resource(self, id=id, types=types, hashless_iri=hashless_iri, - collection_iri=collection_iri, **kwargs) - - def create(self, id=None, types=None, hashless_iri=None, collection_iri=None, **kwargs): - """Creates a new resource and save it in the `data_store`. - - See :func:`~oldman.management.manager.ResourceManager.new` for more details. - """ - return self.new(id=id, types=types, hashless_iri=hashless_iri, - collection_iri=collection_iri, **kwargs).save() - - def get(self, id=None, types=None, hashless_iri=None, eager_with_reversed_attributes=True, **kwargs): - """See :func:`oldman.store.datastore.DataStore.get`.""" - return self._data_store.get(id=id, types=types, hashless_iri=hashless_iri, - eager_with_reversed_attributes=eager_with_reversed_attributes, **kwargs) - - def filter(self, types=None, hashless_iri=None, limit=None, eager=False, pre_cache_properties=None, **kwargs): - """See :func:`oldman.store.datastore.DataStore.filter`.""" - return self._data_store.filter(types=types, hashless_iri=hashless_iri, limit=limit, eager=eager, - pre_cache_properties=pre_cache_properties, **kwargs) - - def sparql_filter(self, query): - """See :func:`oldman.store.datastore.DataStore.sparql_filter`.""" - return self._data_store.sparql_filter(query) - - def find_models_and_types(self, type_set): - """See :func:`oldman.management.registry.ModelRegistry.find_models_and_types`.""" - return self._registry.find_models_and_types(type_set) - - -def _extract_class_iri(class_name, context): - """Extracts the class IRI as the type of a blank node.""" - g = Graph().parse(data=json.dumps({u"@type": class_name}), - context=context, format="json-ld") - class_iri = unicode(g.objects().next()) - - # Check the URI - result = urlparse(class_iri) - if result.scheme == u"file": - raise OMUndeclaredClassNameError(u"Deduced URI %s is not a valid HTTP URL" % class_iri) - return class_iri diff --git a/oldman/model/__init__.py b/oldman/model/__init__.py new file mode 100644 index 0000000..7086fc2 --- /dev/null +++ b/oldman/model/__init__.py @@ -0,0 +1 @@ +__author__ = 'benji' diff --git a/oldman/management/ancestry.py b/oldman/model/ancestry.py similarity index 100% rename from oldman/management/ancestry.py rename to oldman/model/ancestry.py diff --git a/oldman/attribute.py b/oldman/model/attribute.py similarity index 77% rename from oldman/attribute.py rename to oldman/model/attribute.py index b7eb529..d34e71a 100644 --- a/oldman/attribute.py +++ b/oldman/model/attribute.py @@ -1,8 +1,10 @@ import logging from collections import namedtuple from weakref import WeakKeyDictionary + from rdflib import Literal -from .exception import OMAttributeTypeCheckError, OMRequiredPropertyError, OMReadOnlyAttributeError, OMEditError + +from oldman.exception import OMAttributeTypeCheckError, OMRequiredPropertyError, OMReadOnlyAttributeError, OMEditError from oldman.parsing.value import AttributeValueExtractor from oldman.validation.value_format import ValueFormatError from oldman.iri import _skolemize @@ -15,6 +17,9 @@ class OMAttribute(object): """An :class:`~oldman.attribute.OMAttribute` object corresponds to a JSON-LD term that refers to a RDF property. + TODO: update the documentation. No direct access to the resource_manager anymore + (indirect through the resource). + Technically, the name of the :class:`~oldman.attribute.OMAttribute` object is a JSON-LD term, namely *"a short-hand string that expands to an IRI or a blank node identifier"* (cf. `the JSON-LD standard `_) which corresponds here to a RDF property @@ -35,7 +40,6 @@ class OMAttribute(object): - An IRI; - A collection (set, list and dict) of these types. - :param manager: :class:`~oldman.management.manager.ResourceManager` object. :param metadata: :class:`~oldman.attribute.OMAttributeMetadata` object. :param value_format: :class:`~oldman.validation.value_format.ValueFormat` object that validates the format of values and converts RDF values @@ -45,24 +49,20 @@ class OMAttribute(object): _CONTAINER_REQUIREMENTS = {'@set': set, '@list': list, '@language': dict, - #'@index': dict, + # '@index': dict, None: object} - def __init__(self, manager, metadata, value_format): - self._manager = manager + def __init__(self, metadata, value_format): self._metadata = metadata self._value_format = value_format - self._data = WeakKeyDictionary() - # Non-saved former values - self._former_values = WeakKeyDictionary() + self._entries = WeakKeyDictionary() self._value_extractor = AttributeValueExtractor(self) # TODO: support "@index" - if not self.container in [None, "@set", "@list", "@language"]: + if self.container not in [None, "@set", "@list", "@language"]: raise NotImplementedError(u"Container %s is not yet supported" % self.container) - @property def is_required(self): """`True` if its property is required.""" @@ -93,11 +93,6 @@ def language(self): """Its language if localized.""" return self._metadata.language - @property - def manager(self): - """Its :class:`~oldman.management.manager.ResourceManager` object.""" - return self._manager - @property def jsonld_type(self): """JSON-LD type (datatype IRI or JSON-LD keyword). May be `None`.""" @@ -152,8 +147,8 @@ def check_validity(self, resource, is_end_user=True): self._check_requirement(resource) def _check_local_constraints(self, resource, is_end_user): - #Read-only constraint - if is_end_user and self.is_read_only and self.has_new_value(resource): + # Read-only constraint + if is_end_user and self.is_read_only and self.has_changed(resource): raise OMReadOnlyAttributeError(u"Attribute %s is not editable by end-users" % self.name) def _check_requirement(self, resource): @@ -171,29 +166,40 @@ def has_value(self, resource): :param resource: :class:`~oldman.resource.Resource` object. :return: `False` if the value is `None`. """ - return self._data.get(resource) is not None + entry = self._entries.get(resource) + return (entry is not None) and (entry.current_value is not None) - def has_new_value(self, resource): + def has_changed(self, resource): """ :param resource: :class:`~oldman.resource.Resource` object. """ - return resource in self._former_values + entry = self._entries.get(resource) + return (entry is not None) and entry.has_changed() - def get_former_value(self, resource): + def diff(self, resource): """Gets out the former value that has been replaced. + TODO: update this comment + :param resource: :class:`~oldman.resource.Resource` object. - :return: its former attribute value or `None`. + :return: The former and new attribute values. """ - return self._former_values.get(resource) + entry = self._entries.get(resource) + if entry is None: + #TODO: throw a more precise exception + raise Exception("No diff available for attribute %s of %s" % (self.name, resource.id)) + return entry.diff() - def delete_former_value(self, resource): + def receive_storage_ack(self, resource): """Clears the former value that has been replaced. + TODO: update this description. + :param resource: :class:`~oldman.resource.Resource` object. """ - if resource in self._former_values: - self._former_values.pop(resource) + entry = self._entries.get(resource) + if entry is not None: + entry.receive_storage_ack() def to_nt(self, resource): """Converts its current attribute value to N-Triples (NT) triples. @@ -203,7 +209,8 @@ def to_nt(self, resource): :param resource: :class:`~oldman.resource.Resource` object. :return: N-Triples serialization of its attribute value. """ - value = self._data.get(resource, None) + entry = self._entries.get(resource, None) + value = entry.current_value if entry is not None else None return self.value_to_nt(value) def value_to_nt(self, value): @@ -225,7 +232,7 @@ def value_to_nt(self, value): lines = "" if self.container == "@list": - #list_value = u"( " + u" ".join(converted_values) + u" )" + # list_value = u"( " + u" ".join(converted_values) + u" )" # List with skolemized nodes first_node = "<%s>" % _skolemize() node = first_node @@ -261,7 +268,7 @@ def update_from_graph(self, resource, sub_graph, initial=False): setattr(resource, self.name, values) if initial: # Clears "None" former value - self.delete_former_value(resource) + self.receive_storage_ack(resource) def _encode_value(self, value, language=None): """Encodes an atomic value into a N-Triples line. @@ -289,8 +296,11 @@ def get(self, resource): :param resource: :class:`~oldman.resource.Resource` object. :return: Atomic value or a generator. """ - value = self._data.get(resource, None) - return value + entry = self._entries.get(resource) + #TODO: should we throw an exception? + if entry is None: + return None + return entry.current_value def get_lightly(self, resource): """Gets the attribute value of a resource in a lightweight manner. @@ -298,7 +308,7 @@ def get_lightly(self, resource): By default, behaves exactly like :func:`~oldman.attribute.OMAttribute.get`. See the latter function for further details. """ - return self.get(resource) + return OMAttribute.get(self, resource) def set(self, resource, value): """Sets the attribute value of a resource. @@ -313,15 +323,12 @@ def set(self, resource, value): if isinstance(value, (list, set, dict)) and len(value) == 0: value = None - # Former value (if not already in cache) - # (robust to multiple changes before saving) - if not resource in self._former_values: - # May be None (trick!) - former_value = self._data.get(resource) - if former_value != value: - self._former_values[resource] = former_value + entry = self._entries.get(resource) + if entry is None: + entry = Entry() + self._entries[resource] = entry - self._data[resource] = value + entry.current_value = value def check_value(self, value): """Checks a new **when assigned**. @@ -348,6 +355,23 @@ def check_value(self, value): except ValueFormatError as e: raise OMAttributeTypeCheckError(unicode(e)) + def get_entry(self, resource): + """TODO: describe. Clearly not for end-users!!! """ + return self._entries.get(resource) + + def set_entry(self, resource, entry): + """TODO: describe. Clearly not for end-users!!! """ + # Validation + if entry.has_changed(): + former_value, new_value = entry.diff() + self.check_value(former_value) + self.check_value(new_value) + else: + self.check_value(entry.current_value) + + self._entries[resource] = entry + + def _check_container(self, value): """Checks that container used is authorized and its items are formatted properly. @@ -379,8 +403,8 @@ class ObjectOMAttribute(OMAttribute): """ - def __init__(self, manager, metadata, value_format): - OMAttribute.__init__(self, manager, metadata, value_format) + def __init__(self, metadata, value_format): + OMAttribute.__init__(self, metadata, value_format) def get(self, resource): """See :func:`~oldman.attribute.OMAttribute.get`. @@ -391,11 +415,11 @@ def get(self, resource): iris = OMAttribute.get(self, resource) if isinstance(iris, (list, set)): # Returns a generator - return (self.manager.get(id=iri) for iri in iris) + return (resource.get_related_resource(id=iri) for iri in iris) elif isinstance(iris, dict): raise NotImplementedError(u"Should we implement it?") elif iris is not None: - return self.manager.get(id=iris) + return resource.get_related_resource(id=iris) else: return None @@ -414,7 +438,7 @@ def set(self, resource, value): Accepts :class:`~oldman.resource.Resource` object(s) or IRI(s). """ - from .resource import Resource + from oldman.resource.resource import Resource f = lambda x: x.id if isinstance(x, Resource) else x if isinstance(value, set): @@ -429,4 +453,60 @@ def set(self, resource, value): u"are not supported for objects.") else: values = f(value) - OMAttribute.set(self, resource, values) \ No newline at end of file + OMAttribute.set(self, resource, values) + + +class Entry(object): + """ Mutable. + + TODO: describe + """ + + def __init__(self, saved_value=None): + self._former_value = saved_value + self._current_value = saved_value + + def clone(self): + new_entry = Entry(self._clone_value(self._former_value)) + new_entry.current_value = self._current_value + return new_entry + + @property + def current_value(self): + return self._clone_value(self._current_value) + + @current_value.setter + def current_value(self, new_value): + self._current_value = self._clone_value(new_value) + + def has_changed(self): + """ True if the value differs from the stored one """ + if self._former_value is None and self._current_value is not None: + return True + return self._former_value != self._current_value + + def diff(self): + """TODO: explain """ + #TODO: find a better exception + if not self.has_changed(): + raise Exception("No diff") + return self._clone_value(self._former_value), self._clone_value(self._current_value) + + def receive_storage_ack(self): + """TODO: explain """ + self._former_value = self._current_value + + @staticmethod + def _clone_value(value): + if isinstance(value, set): + return set(value) + if isinstance(value, list): + return list(value) + if isinstance(value, dict): + return dict(value) + return value + + + + + diff --git a/oldman/model/converter.py b/oldman/model/converter.py new file mode 100644 index 0000000..871ef63 --- /dev/null +++ b/oldman/model/converter.py @@ -0,0 +1,173 @@ +import types +from oldman.resource.resource import ClientResource, StoreResource + + +class ModelConversionManager(object): + """TODO: describe and find a better name.""" + + def __init__(self): + # { (client_model, data_store): store_model } + self._client_to_store_models = {} + # { store_model: client_model + self._store_to_client_models = {} + # { (client_model, store_model): converter} + self._converters = {} + + def register_model_converter(self, client_model, store_model, data_store, model_converter): + self._client_to_store_models[(client_model, data_store)] = store_model + self._store_to_client_models[store_model] = client_model + self._converters[(client_model, store_model)] = model_converter + + def convert_store_to_client_resources(self, store_resources, client_resource_manager): + """TODO: describe """ + if isinstance(store_resources, types.GeneratorType): + return (self.convert_store_to_client_resource(r) + for r in store_resources) + # Otherwise, returns a list + return [self.convert_store_to_client_resource(r, client_resource_manager) + for r in store_resources] + + def convert_store_to_client_resource(self, store_resource, client_resource_manager): + client_former_types, client_new_types = self._extract_types_from_store_resource(store_resource) + + client_model_manager = client_resource_manager.model_manager + + # Mutable + client_resource = ClientResource(client_resource_manager, client_model_manager, store_resource.store, + id=store_resource.id, types=client_new_types, is_new=store_resource.is_new, + former_types=client_former_types) + store = store_resource.store + + # Client models from the most general to the more specific + client_models = list(client_resource.models) + client_models.reverse() + for client_model in client_models: + # Corresponding store model + store_model = self._client_to_store_models.get((client_model, store)) + if store_model is None: + #TODO: find a better exception + raise Exception("No store model associate to %s" % client_model.name) + + converter = self._converters[(client_model, store_model)] + + # Update the client resource according to the model properties + converter.from_store_to_client(store_resource, client_resource) + + return client_resource + + def convert_client_to_store_resource(self, client_resource): + # Same store between the client_resource and the store_resource + store = client_resource.store + store_former_types, store_new_types = self._extract_types_from_client_resource(client_resource) + + #TODO: should we consider late IRI attributions? + store_resource = StoreResource(store.model_manager, store, id=client_resource.id, + types=store_new_types, is_new=client_resource.is_new, + former_types=store_former_types) + + # From the most general to the more specific + store_models = list(store_resource.models) + store_models.reverse() + + for store_model in store_models: + client_model = self._store_to_client_models.get(store_model) + if client_model is None: + #TODO: find a better exception + raise Exception("No client model associate to %s" % store_model.name) + + converter = self._converters[(client_model, store_model)] + + # Update the client resource according to the model properties + converter.from_client_to_store(client_resource, store_resource) + + return store_resource + + def _extract_types_from_store_resource(self, store_resource): + # Non model types + new_types = set(store_resource.non_model_types) + former_types = set(store_resource.former_non_model_types) + + for store_model in store_resource.models: + client_model = self._store_to_client_models.get(store_model) + if client_model is None: + #TODO: See if relevant and find a better name + raise Exception("No client model corresponding to %s" % store_model.name) + + client_model_type = client_model.class_iri + if store_model.class_iri in store_resource.former_types: + former_types.add(client_model_type) + if client_model_type is not None: + new_types.add(client_model_type) + + return former_types, new_types + + def _extract_types_from_client_resource(self, client_resource): + # Non model types + new_types = set(client_resource.non_model_types) + former_types = set(client_resource.former_non_model_types) + + # Types corresponding to models + store = client_resource.store + for client_model in client_resource.models: + store_model = self._client_to_store_models.get((client_model, store)) + if store_model is None: + #TODO: See if relevant and find a better name + raise Exception("No store model corresponding to %s" % client_model.name) + + store_model_type = store_model.class_iri + if client_model.class_iri in client_resource.former_types: + former_types.add(store_model_type) + new_types.add(store_model_type) + + return former_types, new_types + + +class ModelConverter(object): + """TODO: find a better name and explain """ + + def from_client_to_store(self, client_resource, store_resource): + raise NotImplementedError("Should be implemented by a sub-class") + + def from_store_to_client(self, store_resource, client_resource): + raise NotImplementedError("Should be implemented by a sub-class") + + +class DirectMappingModelConverter(ModelConverter): + + def __init__(self, client_to_store_mappings): + """ + + :param client_to_store_mappings: Attribute mapping + :return: + """ + self._client_to_store_mappings = client_to_store_mappings + self._store_to_client_mappings = {v: k for k, v in client_to_store_mappings.items()} + + def from_client_to_store(self, client_resource, store_resource): + self._transfer_values(client_resource, store_resource, self._client_to_store_mappings) + + def from_store_to_client(self, store_resource, client_resource): + self._transfer_values(store_resource, client_resource, self._store_to_client_mappings) + + @staticmethod + def _transfer_values(source_resource, target_resource, mappings): + for source_attr_name, target_attr_name in mappings.items(): + # Attributes + source_attr = source_resource.get_attribute(source_attr_name) + target_attr = target_resource.get_attribute(target_attr_name) + + # Transfers a clone of the source entry + source_entry = source_attr.get_entry(source_resource) + if source_entry is not None: + target_attr.set_entry(target_resource, source_entry.clone()) + + +class EquivalentModelConverter(DirectMappingModelConverter): + """TODO: describe """ + + def __init__(self, client_model, store_model): + mappings = {attr_name: attr_name for attr_name in client_model.om_attributes} + DirectMappingModelConverter.__init__(self, mappings) + #TODO: check that the models are equivalent + + diff --git a/oldman/model/manager.py b/oldman/model/manager.py new file mode 100644 index 0000000..ede9a71 --- /dev/null +++ b/oldman/model/manager.py @@ -0,0 +1,225 @@ +import json +import logging +from urlparse import urlparse + +from rdflib import Graph +from oldman.model.converter import ModelConversionManager, EquivalentModelConverter + +from oldman.model.model import Model, ClientModel +from oldman.exception import OMUndeclaredClassNameError, OMExpiredMethodDeclarationTimeSlotError +from oldman.iri import PrefixedUUIDIriGenerator, IncrementalIriGenerator, BlankNodeIriGenerator +from oldman.parsing.schema.attribute import OMAttributeExtractor +from oldman.parsing.operation import HydraOperationExtractor +from oldman.vocabulary import HYDRA_COLLECTION_IRI, HYDRA_PAGED_COLLECTION_IRI, HTTP_POST +from oldman.model.operation import append_to_hydra_collection, append_to_hydra_paged_collection +from oldman.model.registry import ModelRegistry +from oldman.model.ancestry import ClassAncestry + + +class ModelManager(object): + """ + TODO: update this documentation + + The `model_manager` is the central object of this OLDM. + + It gives access to the :class:`~oldman.store.datastore.DataStore` object + and creates :class:`~oldman.model.Model` objects. + It also creates, retrieves and caches :class:`~oldman.resource.Resource` objects. + + Internally, it owns a :class:`~oldman.resource.registry.ModelRegistry` object. + + TODO: UPDATE DESCRIPTION + + :param schema_graph: :class:`rdflib.Graph` object containing all the schema triples. + :param data_store: :class:`~oldman.store.datastore.DataStore` object. Supports CRUD operations on + :class:`~oldman.resource.Resource` objects. + :param attr_extractor: :class:`~oldman.parsing.attribute.OMAttributeExtractor` object that + will extract :class:`~oldman.attribute.OMAttribute` for generating + new :class:`~oldman.model.Model` objects. + Defaults to a new instance of :class:`~oldman.parsing.attribute.OMAttributeExtractor`. + :param oper_extractor: TODO: describe. + :param declare_default_operation_functions: TODO: describe. + """ + + def __init__(self, schema_graph=None, attr_extractor=None, oper_extractor=None, + declare_default_operation_functions=True): + self._attr_extractor = attr_extractor if attr_extractor is not None else OMAttributeExtractor() + self._operation_extractor = oper_extractor if oper_extractor is not None else HydraOperationExtractor() + self._schema_graph = schema_graph + self._operation_functions = {} + self._registry = ModelRegistry() + self._logger = logging.getLogger(__name__) + + self._include_reversed_attributes = False + + #TODO: examine their relevance + if declare_default_operation_functions: + self.declare_operation_function(append_to_hydra_collection, HYDRA_COLLECTION_IRI, HTTP_POST) + self.declare_operation_function(append_to_hydra_paged_collection, HYDRA_PAGED_COLLECTION_IRI, HTTP_POST) + + @property + def name(self): + """Name of this manager. + The manager can be retrieved from its name by calling the + class method :func:`~oldman.resource.manager.ModelManager.get_manager`. + """ + return self._name + + @property + def include_reversed_attributes(self): + """Is `True` if at least one of its models use some reversed attributes.""" + return self._include_reversed_attributes + + @property + def models(self): + """TODO: describe.""" + return self._registry.models + + @property + def non_default_models(self): + """TODO: describe.""" + return self._registry.non_default_models + + def has_default_model(self): + return self._registry.default_model is not None + + def declare_operation_function(self, func, class_iri, http_method): + """ + TODO: comment + """ + if self._registry.has_specific_models(): + raise OMExpiredMethodDeclarationTimeSlotError(u"Operation declaration cannot occur after model creation.") + + http_method = http_method.upper() + if class_iri in self._operation_functions: + if http_method in self._methods[class_iri]: + self._logger.warn(u"Operation %s of %s is overloaded." % (http_method, class_iri)) + self._operation_functions[class_iri][http_method] = func + else: + self._operation_functions[class_iri] = {http_method: func} + + def find_models_and_types(self, type_set): + """See :func:`oldman.resource.registry.ModelRegistry.find_models_and_types`.""" + return self._registry.find_models_and_types(type_set) + + def find_descendant_models(self, top_ancestor_name_or_iri): + """TODO: explain. Includes the top ancestor. """ + return self._registry.find_descendant_models(top_ancestor_name_or_iri) + + def create_model(self, class_name_or_iri, context, data_store, iri_prefix=None, iri_fragment=None, + iri_generator=None, untyped=False, incremental_iri=False, is_default=False): + """Creates a :class:`~oldman.model.Model` object. + + TODO: remove data_store from the constructor! + + To create it, they are three elements to consider: + + 1. Its class IRI which can be retrieved from `class_name_or_iri`; + 2. Its JSON-LD context for mapping :class:`~oldman.attribute.OMAttribute` values to RDF triples; + 3. The :class:`~oldman.iri.IriGenerator` object that generates IRIs from new + :class:`~oldman.resource.Resource` objects. + + The :class:`~oldman.iri.IriGenerator` object is either: + + * directly given: `iri_generator`; + * created from the parameters `iri_prefix`, `iri_fragment` and `incremental_iri`. + + :param class_name_or_iri: IRI or JSON-LD term of a RDFS class. + :param context: `dict`, `list` or `IRI` that represents the JSON-LD context . + :param iri_generator: :class:`~oldman.iri.IriGenerator` object. If given, other `iri_*` parameters are + ignored. + :param iri_prefix: Prefix of generated IRIs. Defaults to `None`. + If is `None` and no `iri_generator` is given, a :class:`~oldman.iri.BlankNodeIriGenerator` is created. + :param iri_fragment: IRI fragment that is added at the end of generated IRIs. For instance, `"me"` + adds `"#me"` at the end of the new IRI. Defaults to `None`. Has no effect if `iri_prefix` is not given. + :param incremental_iri: If `True` an :class:`~oldman.iri.IncrementalIriGenerator` is created instead of a + :class:`~oldman.iri.RandomPrefixedIriGenerator`. Defaults to `False`. + Has no effect if `iri_prefix` is not given. + """ + + # Only for the DefaultModel + if untyped: + class_iri = None + ancestry = ClassAncestry(class_iri, self._schema_graph) + om_attributes = {} + else: + class_iri = _extract_class_iri(class_name_or_iri, context) + ancestry = ClassAncestry(class_iri, self._schema_graph) + om_attributes = self._attr_extractor.extract(class_iri, ancestry.bottom_up, context, + self._schema_graph) + if iri_generator is not None: + id_generator = iri_generator + elif iri_prefix is not None: + if incremental_iri: + id_generator = IncrementalIriGenerator(iri_prefix, data_store, + class_iri, fragment=iri_fragment) + else: + id_generator = PrefixedUUIDIriGenerator(iri_prefix, fragment=iri_fragment) + else: + id_generator = BlankNodeIriGenerator() + + operations = self._operation_extractor.extract(ancestry, self._schema_graph, + self._operation_functions) + + model = Model(class_name_or_iri, class_iri, ancestry.bottom_up, context, om_attributes, + id_generator, operations=operations) + self._add_model(model, is_default=is_default) + + # Reversed attributes awareness + if not self._include_reversed_attributes: + self._include_reversed_attributes = model.has_reversed_attributes + + return model + + def get_model(self, class_name_or_iri): + return self._registry.get_model(class_name_or_iri) + + def _add_model(self, model, is_default=False): + self._registry.register(model, is_default=is_default) + + +class ClientModelManager(ModelManager): + """TODO: complete the doc """ + + def __init__(self, resource_manager, **kwargs): + ModelManager.__init__(self, **kwargs) + self._resource_manager = resource_manager + self._conversion_manager = ModelConversionManager() + + @property + def resource_manager(self): + return self._resource_manager + + def import_model(self, store_model, data_store, is_default=False): + """TODO: describe """ + if is_default: + # Default model + client_model = self.get_model(None) + else: + client_model = ClientModel.copy_store_model(self._resource_manager, store_model) + # Hierarchy registration + self._registry.register(client_model, is_default=False) + # Converter + converter = EquivalentModelConverter(client_model, store_model) + self._conversion_manager.register_model_converter(client_model, store_model, data_store, converter) + + def convert_store_resources(self, store_resources): + """TODO: describe """ + return self._conversion_manager.convert_store_to_client_resources(store_resources, self._resource_manager) + + def convert_client_resource(self, client_resource): + """TODO: describe """ + return self._conversion_manager.convert_client_to_store_resource(client_resource) + + +def _extract_class_iri(class_name, context): + """Extracts the class IRI as the type of a blank node.""" + g = Graph().parse(data=json.dumps({u"@type": class_name}), + context=context, format="json-ld") + class_iri = unicode(g.objects().next()) + + # Check the URI + result = urlparse(class_iri) + if result.scheme == u"file": + raise OMUndeclaredClassNameError(u"Deduced URI %s is not a valid HTTP URL" % class_iri) + return class_iri diff --git a/oldman/model.py b/oldman/model/model.py similarity index 71% rename from oldman/model.py rename to oldman/model/model.py index 5f0faaa..ee2a964 100644 --- a/oldman/model.py +++ b/oldman/model/model.py @@ -1,9 +1,12 @@ -from .exception import OMReservedAttributeNameError, OMAttributeAccessError +import logging +from oldman.exception import OMReservedAttributeNameError, OMAttributeAccessError class Model(object): """A :class:`~oldman.model.Model` object represents a RDFS class on the Python side. + TODO: update this documentation + It gathers :class:`~oldman.attribute.OMAttribute` objects and Python methods which are made available to :class:`~oldman.resource.Resource` objects that are instances of its RDFS class. @@ -15,12 +18,12 @@ class Model(object): .. admonition:: Model creation :class:`~oldman.model.Model` objects are normally created by a - :class:`~oldman.management.manager.ResourceManager` object. Please use the - :func:`oldman.management.manager.ResourceManager.create_model` method for creating new + :class:`~oldman.resource.manager.ResourceManager` object. Please use the + :func:`oldman.resource.manager.ResourceManager.create_model` method for creating new :class:`~oldman.model.Model` objects. - :param manager: :class:`~oldman.management.manager.ResourceManager` object + :param manager: :class:`~oldman.resource.manager.ResourceManager` object that has created this model. :param name: Model name. Usually corresponds to a JSON-LD term or to a class IRI. :param class_iri: IRI of the RDFS class represented by this :class:`~oldman.model.Model` object. @@ -37,8 +40,8 @@ class Model(object): :param operations: TODO: describe. """ - def __init__(self, manager, name, class_iri, ancestry_iris, context, om_attributes, - id_generator, methods=None, operations=None): + def __init__(self, name, class_iri, ancestry_iris, context, om_attributes, + id_generator, operations=None): reserved_names = ["id", "hashless_iri", "_types", "types"] for field in reserved_names: if field in om_attributes: @@ -49,13 +52,12 @@ def __init__(self, manager, name, class_iri, ancestry_iris, context, om_attribut self._om_attributes = om_attributes self._id_generator = id_generator self._class_types = ancestry_iris - self._manager = manager - self._methods = methods if methods is not None else {} self._operations = operations if operations is not None else {} self._operation_by_name = {op.name: op for op in operations.values() if op.name is not None} self._has_reversed_attributes = True in [a.reversed for a in self._om_attributes.values()] + self._logger = logging.getLogger(__name__) @property def name(self): @@ -72,18 +74,16 @@ def ancestry_iris(self): """IRIs of the ancestry of the attribute `class_iri`.""" return list(self._class_types) + @property + def methods(self): + """Models does not support methods by default.""" + return {} + @property def om_attributes(self): """ `dict` of :class:`~oldman.attribute.OMAttribute` objects. Keys are their names.""" return dict(self._om_attributes) - @property - def methods(self): - """`dict` of Python functions that takes as first argument a - :class:`~oldman.resource.Resource` object. Keys are the method names. - """ - return dict(self._methods) - @property def context(self): """An IRI, a `list` or a `dict` that describes the JSON-LD context. @@ -149,16 +149,66 @@ def reset_counter(self): if hasattr(self._id_generator, "reset_counter"): self._id_generator.reset_counter() + +class ClientModel(Model): + """TODO: describe. + + TODO: further study this specific case + + """ + + @classmethod + def copy_store_model(cls, resource_manager, store_model): + """TODO: describe """ + return ClientModel(resource_manager, store_model.name, store_model.class_iri, + store_model.ancestry_iris, store_model.context, store_model.om_attributes, + store_model._id_generator, operations=store_model._operations) + + def __init__(self, resource_manager, name, class_iri, ancestry_iris, context, om_attributes, + id_generator, operations=None): + Model.__init__(self, name, class_iri, ancestry_iris, context, om_attributes, + id_generator, operations=operations) + self._resource_manager = resource_manager + # {method_name: ancestor_class_iri} + self._method_inheritance = {} + # {method_name: method} + self._methods = {} + + @property + def methods(self): + """`dict` of Python functions that takes as first argument a + :class:`~oldman.resource.Resource` object. Keys are the method names. + """ + return dict(self._methods) + + def declare_method(self, method, name, ancestor_class_iri): + """TODO: describe """ + if name in self._methods: + # Before overriding, compare the positions + previous_ancestor_iri = self._method_inheritance[name] + previous_ancestor_pos = self._class_types.index(previous_ancestor_iri) + new_ancestor_pos = self._class_types.index(ancestor_class_iri) + + if new_ancestor_pos > previous_ancestor_pos: + # Too distant, cannot override + self._logger.warn(u"Method %s of %s is ignored by %s." % (name, ancestor_class_iri, self._class_iri)) + return + + self._logger.warn(u"Method %s of %s is overloaded for %s." % (name, ancestor_class_iri, self._class_iri)) + + self._method_inheritance[name] = ancestor_class_iri + self._methods[name] = method + def new(self, id=None, hashless_iri=None, collection_iri=None, **kwargs): """Creates a new :class:`~oldman.resource.Resource` object without saving it. The `class_iri` attribute is added to the `types`. - See :func:`~oldman.management.manager.ResourceManager.new` for more details. + See :func:`~oldman.resource.manager.ResourceManager.new` for more details. """ types, kwargs = self._update_kwargs_and_types(kwargs, include_ancestry=True) - return self._manager.new(id=id, hashless_iri=hashless_iri, collection_iri=collection_iri, - types=types, **kwargs) + return self._resource_manager.new(id=id, hashless_iri=hashless_iri, collection_iri=collection_iri, + types=types, **kwargs) def create(self, id=None, hashless_iri=None, collection_iri=None, **kwargs): """ Creates a new resource and saves it. @@ -172,10 +222,10 @@ def filter(self, hashless_iri=None, limit=None, eager=False, pre_cache_propertie The `class_iri` attribute is added to the `types`. - See :func:`oldman.management.finder.ResourceFinder.filter` for further details.""" + See :func:`oldman.resource.finder.ResourceFinder.filter` for further details.""" types, kwargs = self._update_kwargs_and_types(kwargs) - return self._manager.filter(types=types, hashless_iri=hashless_iri, limit=limit, eager=eager, - pre_cache_properties=pre_cache_properties, **kwargs) + return self._resource_manager.filter(types=types, hashless_iri=hashless_iri, limit=limit, eager=eager, + pre_cache_properties=pre_cache_properties, **kwargs) def get(self, id=None, hashless_iri=None, **kwargs): """Gets the first :class:`~oldman.resource.Resource` object matching the given criteria. @@ -190,8 +240,8 @@ def get(self, id=None, hashless_iri=None, **kwargs): if eager_with_reversed_attributes is None: eager_with_reversed_attributes = self._has_reversed_attributes - return self._manager.get(id=id, types=types, hashless_iri=hashless_iri, - eager_with_reversed_attributes=eager_with_reversed_attributes, **kwargs) + return self._resource_manager.get(id=id, types=types, hashless_iri=hashless_iri, + eager_with_reversed_attributes=eager_with_reversed_attributes, **kwargs) def all(self, limit=None, eager=False): """Finds every :class:`~oldman.resource.Resource` object that is instance diff --git a/oldman/operation.py b/oldman/model/operation.py similarity index 89% rename from oldman/operation.py rename to oldman/model/operation.py index 2cbf052..40d46b1 100644 --- a/oldman/operation.py +++ b/oldman/model/operation.py @@ -47,17 +47,17 @@ def append_to_hydra_collection(collection_resource, new_resources=None, graph=No def _append_to_hydra_coll_from_graph(collection_resource, graph): collection_iri = collection_resource.id - manager = collection_resource.manager + resource_manager = collection_resource.model_manager.resource_manager # Extracts and classifies subjects bnode_subjects, other_subjects = extract_subjects(graph) #Blank nodes (may obtain a regular IRI) - new_resources = create_blank_nodes(manager, graph, bnode_subjects, collection_iri=collection_iri) + new_resources = create_blank_nodes(resource_manager, graph, bnode_subjects, collection_iri=collection_iri) #Objects with an existing IRI #TODO: ask if it should be accepted - reg_resources, _ = create_regular_resources(manager, graph, other_subjects, collection_iri=collection_iri) + reg_resources, _ = create_regular_resources(resource_manager, graph, other_subjects, collection_iri=collection_iri) new_resources += reg_resources return _append_resources_to_hydra_collection(collection_resource, new_resources) diff --git a/oldman/property.py b/oldman/model/property.py similarity index 95% rename from oldman/property.py rename to oldman/model/property.py index bb01258..26e4c39 100644 --- a/oldman/property.py +++ b/oldman/model/property.py @@ -1,13 +1,16 @@ import logging + from .attribute import OMAttributeMetadata, OMAttribute, ObjectOMAttribute -from .exception import OMAlreadyDeclaredDatatypeError, OMPropertyDefTypeError -from .exception import OMAlreadyGeneratedAttributeError, OMInternalError, OMPropertyDefError +from oldman.exception import OMAlreadyDeclaredDatatypeError, OMPropertyDefTypeError +from oldman.exception import OMAlreadyGeneratedAttributeError, OMInternalError, OMPropertyDefError from oldman.common import DATATYPE_PROPERTY, OBJECT_PROPERTY class OMProperty(object): """An :class:`~oldman.property.OMProperty` object represents the support of a RDF property by a RDFS class. + TODO: check this documentation after the removal of the resource_manager. + It gathers some :class:`~oldman.attribute.OMAttribute` objects (usually one). An :class:`~oldman.property.OMProperty` object is in charge of generating its @@ -21,7 +24,6 @@ class OMProperty(object): Consequently, two :class:`~oldman.property.OMProperty` objects can refer to the same RDF property when one is reversed while the second is not. - :param manager: :class:`~oldman.management.manager.ResourceManager` object. :param property_iri: IRI of the RDF property. :param supporter_class_iri: IRI of the RDFS class that supports the property. :param is_required: If `True` instances of the supporter class must assign a value @@ -37,11 +39,10 @@ class OMProperty(object): :param domains: Set of class IRIs that are declared as the RDFS domain of the property. Defaults to `set()`. :param ranges: Set of class IRIs that are declared as the RDFS range of the property. Defaults to `set()`. """ - def __init__(self, manager, property_iri, supporter_class_iri, is_required=False, read_only=False, + def __init__(self, property_iri, supporter_class_iri, is_required=False, read_only=False, write_only=False, reversed=False, cardinality=None, property_type=None, domains=None, ranges=None): self._logger = logging.getLogger(__name__) - self._manager = manager self._iri = property_iri self._supporter_class_iri = supporter_class_iri self._is_required = is_required @@ -231,7 +232,7 @@ def generate_attributes(self, attr_format_selector): for md in self._tmp_attr_mds: value_format = attr_format_selector.find_value_format(md) attr_cls = ObjectOMAttribute if self._type == OBJECT_PROPERTY else OMAttribute - self._om_attributes.add(attr_cls(self._manager, md, value_format)) + self._om_attributes.add(attr_cls(md, value_format)) # Clears mds self._tmp_attr_mds = [] \ No newline at end of file diff --git a/oldman/management/registry.py b/oldman/model/registry.py similarity index 80% rename from oldman/management/registry.py rename to oldman/model/registry.py index dfd28b8..f180c29 100644 --- a/oldman/management/registry.py +++ b/oldman/model/registry.py @@ -4,12 +4,12 @@ class ModelRegistry(object): - """ A :class:`~oldman.management.registry.ModelRegistry` object registers + """ A :class:`~oldman.resource.registry.ModelRegistry` object registers the :class:`~oldman.model.Model` objects. Its main function is to find and order models from a set of class IRIs (this ordering is crucial when creating new :class:`~oldman.resource.Resource` objects). - See :func:`~oldman.management.registry.ModelRegistry.find_models_and_types` for more details. + See :func:`~oldman.resource.registry.ModelRegistry.find_models_and_types` for more details. """ def __init__(self): @@ -26,10 +26,23 @@ def model_names(self): """Names of the registered models.""" return self._models_by_names.keys() + @property + def models(self): + return self._models_by_names.values() + + @property + def non_default_models(self): + """ Non-default models.""" + return [m for m in self._models_by_names.values() if m.name != self._default_model_name] + def has_specific_models(self): """:return: `True` if contains other models than the default one.""" return len(self._models_by_names) > int(self._default_model_name is not None) + @property + def default_model(self): + return self._models_by_names.get(self._default_model_name) + def register(self, model, is_default=False): """Registers a :class:`~oldman.model.Model` object. @@ -72,13 +85,24 @@ def unregister(self, model): # Clears the cache self._type_set_cache = {} - def get_model(self, class_iri): + def get_model(self, class_name_or_iri): """Gets a :class:`~oldman.model.Model` object. - :param class_iri: IRI of a RDFS class + :param class_name_or_iri: Name or IRI of a RDFS class :return: A :class:`~oldman.model.Model` object or `None` if not found """ - return self._models_by_classes.get(class_iri) + model = self._models_by_classes.get(class_name_or_iri) + if model is None: + model = self._models_by_names.get(class_name_or_iri) + return model + + def find_descendant_models(self, top_ancestor_name_or_iri): + """TODO: explain. Includes the top ancestor. """ + descendant_iris = set(self._model_descendants.get(top_ancestor_name_or_iri, [])) + descendant_iris.add(top_ancestor_name_or_iri) + + models = [self.get_model(class_iri) for class_iri in descendant_iris] + return filter(lambda x: x is not None, models) def find_models_and_types(self, type_set): """Finds the leaf models from a set of class IRIs and orders them. @@ -134,7 +158,10 @@ def _find_leaf_models(self, type_set): leaf_models.append(model) if len(leaf_models) == 0: - return [self._models_by_names[self._default_model_name]] + default_model = self._models_by_names.get(self._default_model_name) + if default_model: + return [default_model] + return [] return self._sort_leaf_models(leaf_models) diff --git a/oldman/parsing/operation.py b/oldman/parsing/operation.py index dad3120..69e01f3 100644 --- a/oldman/parsing/operation.py +++ b/oldman/parsing/operation.py @@ -1,9 +1,10 @@ +import logging + from rdflib import URIRef + from oldman.vocabulary import HYDRA_SUPPORTED_OPERATION, HYDRA_METHOD, HYDRA_EXCEPTS from oldman.vocabulary import HYDRA_RETURNS, OLDM_SHORTNAME -from oldman.resource import Resource -from oldman.operation import Operation -import logging +from oldman.model.operation import Operation def get_operation_function(operation_functions, class_iri, ancestry, method): @@ -46,11 +47,15 @@ def _extract_hydra_operations(self, ancestry, schema_graph, operation_functions): """ Extracts operations supported by Hydra classes. """ + operations = {} + # No schema, nothing to extract + if schema_graph is None: + return operations + # Extracts the IRIs of the operations if needed if self._operation_iris is None: self._extract_operation_iris(schema_graph) - operations = {} # For each in the ancestry for class_iri in ancestry.bottom_up: cls_oper_iris = self._operation_iris.get(class_iri, []) diff --git a/oldman/parsing/schema/attribute.py b/oldman/parsing/schema/attribute.py index e860595..a87b6f8 100644 --- a/oldman/parsing/schema/attribute.py +++ b/oldman/parsing/schema/attribute.py @@ -51,7 +51,7 @@ def add_property_extractor(self, property_extractor): if property_extractor not in self._property_extractors: self._property_extractors.append(property_extractor) - def extract(self, class_iri, type_iris, context_js, schema_graph, manager): + def extract(self, class_iri, type_iris, context_js, schema_graph): """Extracts metadata and generates :class:`~oldman.property.OMProperty` and :class:`~oldman.attribute.OMAttribute` objects. @@ -59,7 +59,6 @@ def extract(self, class_iri, type_iris, context_js, schema_graph, manager): :param type_iris: Ancestry of the RDFS class. :param context_js: the JSON-LD context. :param schema_graph: :class:`rdflib.graph.Graph` object. - :param manager: :class:`~oldman.management.manager.ResourceManager` object. :return: `dict` of :class:`~oldman.attribute.OMAttribute` objects. """ # Supported om_properties @@ -67,7 +66,7 @@ def extract(self, class_iri, type_iris, context_js, schema_graph, manager): # Extracts and updates om_properties for property_extractor in self._property_extractors: - om_properties = property_extractor.update(om_properties, class_iri, type_iris, schema_graph, manager) + om_properties = property_extractor.update(om_properties, class_iri, type_iris, schema_graph) # Updates om_properties with attribute metadata for md_extractor in self._attr_md_extractors: diff --git a/oldman/parsing/schema/property.py b/oldman/parsing/schema/property.py index e176b02..306780c 100644 --- a/oldman/parsing/schema/property.py +++ b/oldman/parsing/schema/property.py @@ -1,5 +1,6 @@ from rdflib import Namespace, URIRef -from oldman.property import OMProperty + +from oldman.model.property import OMProperty class OMPropertyExtractor(object): @@ -9,7 +10,7 @@ class OMPropertyExtractor(object): This class is generic and must derived for supporting various RDF vocabularies. """ - def update(self, om_properties, class_iri, type_iris, schema_graph, manager): + def update(self, om_properties, class_iri, type_iris, schema_graph): """Generates new :class:`~oldman.property.OMProperty` objects or updates them from the schema graph. @@ -54,7 +55,7 @@ class HydraPropertyExtractor(OMPropertyExtractor): """ _ns = {u'hydra': Namespace(u"http://www.w3.org/ns/hydra/core#")} - def update(self, om_properties, class_iri, type_iris, schema_graph, manager): + def update(self, om_properties, class_iri, type_iris, schema_graph): """See :func:`oldman.parsing.schema.property.OMPropertyExtractor.update`.""" prop_params = {} @@ -75,7 +76,7 @@ def update(self, om_properties, class_iri, type_iris, schema_graph, manager): for (property_iri, reversed), (is_required, read_only, write_only) in prop_params.iteritems(): if not (property_iri, reversed) in om_properties: - om_property = OMProperty(manager, property_iri, class_iri, is_required=is_required, + om_property = OMProperty(property_iri, class_iri, is_required=is_required, read_only=read_only, write_only=write_only, reversed=reversed) om_properties[(property_iri, reversed)] = om_property return om_properties \ No newline at end of file diff --git a/oldman/management/__init__.py b/oldman/resource/__init__.py similarity index 100% rename from oldman/management/__init__.py rename to oldman/resource/__init__.py diff --git a/oldman/resource/manager.py b/oldman/resource/manager.py new file mode 100644 index 0000000..4ff24d1 --- /dev/null +++ b/oldman/resource/manager.py @@ -0,0 +1,137 @@ +from oldman.resource.resource import ClientResource +from oldman.store.selector import DataStoreSelector +from oldman.model.manager import ClientModelManager + + +DEFAULT_MODEL_NAME = "Default_Client" + + +class ClientResourceManager: + """ + TODO: describe + """ + + def __init__(self, data_stores, schema_graph=None, attr_extractor=None, oper_extractor=None, + declare_default_operation_functions=True): + self._model_manager = ClientModelManager(self, schema_graph=schema_graph, attr_extractor=attr_extractor, + oper_extractor=oper_extractor, + declare_default_operation_functions=declare_default_operation_functions) + self._store_selector = DataStoreSelector(data_stores) + + # Default model + self._model_manager.create_model(DEFAULT_MODEL_NAME, {u"@context": {}}, self, untyped=True, + iri_prefix=u"http://localhost/.well-known/genid/client/", + is_default=True) + + @property + def model_manager(self): + return self._model_manager + + def declare_method(self, method, name, class_iri): + """Attaches a method to the :class:`~oldman.resource.Resource` objects that are instances of a given RDFS class. + + Like in Object-Oriented Programming, this method can be overwritten by attaching a homonymous + method to a class that has a higher inheritance priority (such as a sub-class). + + To benefit from this method (or an overwritten one), :class:`~oldman.resource.Resource` objects + must be associated to a :class:`~oldman.model.Model` that corresponds to the RDFS class or to one of its + subclasses. + + :param method: Python function that takes as first argument a :class:`~oldman.resource.Resource` object. + :param name: Name assigned to this method. + :param class_iri: Targeted RDFS class. If not overwritten, all the instances + (:class:`~oldman.resource.Resource` objects) should inherit this method. + + """ + + models = self._model_manager.find_descendant_models(class_iri) + for model in models: + if model.class_iri is None: + continue + model.declare_method(method, name, class_iri) + + def new(self, id=None, types=None, hashless_iri=None, collection_iri=None, **kwargs): + """Creates a new :class:`~oldman.resource.Resource` object **without saving it** in the `data_store`. + + The `kwargs` dict can contains regular attribute key-values that will be assigned to + :class:`~oldman.attribute.OMAttribute` objects. + + :param id: IRI of the new resource. Defaults to `None`. + If not given, the IRI is generated by the IRI generator of the main model. + :param types: IRIs of RDFS classes the resource is instance of. Defaults to `None`. + Note that these IRIs are used to find the models of the resource + (see :func:`~oldman.resource.manager.ResourceManager.find_models_and_types` for more details). + :param hashless_iri: hash-less IRI that MAY be considered when generating an IRI for the new resource. + Defaults to `None`. Ignored if `id` is given. Must be `None` if `collection_iri` is given. + :param collection_iri: IRI of the controller to which this resource belongs. This information + is used to generate a new IRI if no `id` is given. The IRI generator may ignore it. + Defaults to `None`. Must be `None` if `hashless_iri` is given. + :return: A new :class:`~oldman.resource.Resource` object. + """ + if (types is None or len(types) == 0) and len(kwargs) == 0: + name = id if id is not None else "" + self._logger.info(u"""New resource %s has no type nor attribute. + As such, nothing is stored in the data graph.""" % name) + + # Store of the resource + store = self._store_selector.select_store(id=id, types=types, hashless_iri=hashless_iri, + collection_iri=collection_iri, **kwargs) + return ClientResource(self, self._model_manager, store, id=id, types=types, hashless_iri=hashless_iri, + collection_iri=collection_iri, **kwargs) + + def create(self, id=None, types=None, hashless_iri=None, collection_iri=None, **kwargs): + """Creates a new resource and save it in the `data_store`. + + See :func:`~oldman.resource.manager.ResourceManager.new` for more details. + """ + return self.new(id=id, types=types, hashless_iri=hashless_iri, + collection_iri=collection_iri, **kwargs).save() + + def get(self, id=None, types=None, hashless_iri=None, eager_with_reversed_attributes=True, **kwargs): + """See :func:`oldman.store.datastore.DataStore.get`.""" + #TODO: consider parallelism + store_resources = [store.get(id=id, types=types, hashless_iri=hashless_iri, + eager_with_reversed_attributes=eager_with_reversed_attributes, **kwargs) + for store in self._store_selector.select_stores(id=id, types=types, + hashless_iri=hashless_iri, **kwargs)] + returned_store_resources = filter(lambda x: x, store_resources) + resources = self._model_manager.convert_store_resources(returned_store_resources) + resource_count = len(resources) + if resource_count == 1: + return resources[0] + elif resource_count == 0: + return None + #TODO: find a better exception and explain better + #TODO: see if relevant + raise Exception("Non unique object") + + def filter(self, types=None, hashless_iri=None, limit=None, eager=False, pre_cache_properties=None, **kwargs): + """See :func:`oldman.store.datastore.DataStore.filter`.""" + #TODO: support again generator. Find a way to aggregate them. + resources = [r for store in self._store_selector.select_stores(types=types, hashless_iri=hashless_iri, + pre_cache_properties=pre_cache_properties, + **kwargs) + for r in store.filter(types=types, hashless_iri=hashless_iri, limit=limit, eager=eager, + pre_cache_properties=pre_cache_properties, **kwargs)] + return self._model_manager.convert_store_resources(resources) + + def sparql_filter(self, query): + """See :func:`oldman.store.datastore.DataStore.sparql_filter`.""" + #TODO: support again generator. Find a way to aggregate them. + resources = [r for store in self._store_selector.select_sparql_stores(query) + for r in store.sparql_filter(query)] + return self._model_manager.convert_store_resources(resources) + + def use_store_model(self, class_iri, data_store=None): + raise NotImplementedError("TODO: implement me here") + + def import_store_models(self): + """TODO: check possible conflicts with local models.""" + for store in self._store_selector.data_stores: + for store_model in store.model_manager.models: + is_default = (store_model.class_iri is None) + self._model_manager.import_model(store_model, store, + is_default=is_default) + + def get_model(self, class_name_or_iri): + return self._model_manager.get_model(class_name_or_iri) diff --git a/oldman/resource.py b/oldman/resource/resource.py similarity index 74% rename from oldman/resource.py rename to oldman/resource/resource.py index 0129e67..6e80a47 100644 --- a/oldman/resource.py +++ b/oldman/resource/resource.py @@ -4,8 +4,8 @@ import json from types import GeneratorType from rdflib import URIRef, Graph, RDF -from .exception import OMUnauthorizedTypeChangeError, OMInternalError, OMUserError -from .exception import OMAttributeAccessError, OMUniquenessError, OMWrongResourceError, OMEditError +from oldman.exception import OMUnauthorizedTypeChangeError, OMInternalError, OMUserError +from oldman.exception import OMAttributeAccessError, OMUniquenessError, OMWrongResourceError, OMEditError from oldman.common import OBJECT_PROPERTY @@ -13,6 +13,8 @@ class Resource(object): """A :class:`~oldman.resource.Resource` object is a subject-centric representation of a Web resource. A set of :class:`~oldman.resource.Resource` objects is equivalent to a RDF graph. + TODO: update (client, store, etc.) + In RDF, a resource is identified by an IRI (globally) or a blank node (locally). Because blank node support is complex and limited (:class:`rdflib.plugins.stores.sparqlstore.SPARQLStore` stores do not support them), **every** :class:`~oldman.resource.Resource` **object has an IRI**. @@ -25,7 +27,7 @@ class Resource(object): A resource is usually instance of some RDFS classes. These classes are grouped in its attribute `types`. :class:`~oldman.model.Model` objects are found from these classes, by calling the method - :func:`oldman.management.manager.ResourceManager.find_models_and_types`. + :func:`oldman.resource.manager.ResourceManager.find_models_and_types`. Models give access to Python methods and to :class:`~oldman.attribute.OMAttribute` objects. Their ordering determines inheritance priorities. The main model is the first one of this list. @@ -38,7 +40,7 @@ class Resource(object): Example:: - >>> alice = Resource(manager, types=["http://schema.org/Person"], name=u"Alice") + >>> alice = Resource(model_manager, types=["http://schema.org/Person"], name=u"Alice") >>> alice.id u'http://localhost/persons/1' >>> alice.name @@ -61,13 +63,13 @@ class Resource(object): :class:`~oldman.resource.Resource` objects are normally created by a :class:`~oldman.model.Model` or a - :class:`~oldman.management.manager.ResourceManager` object. Please use the + :class:`~oldman.resource.manager.ResourceManager` object. Please use the methods :func:`oldman.model.Model.create`, :func:`oldman.model.Model.new`, - :func:`oldman.management.manager.ResourceManager.create` or - :func:`oldman.management.manager.ResourceManager.new` for creating new + :func:`oldman.resource.manager.ResourceManager.create` or + :func:`oldman.resource.manager.ResourceManager.new` for creating new :class:`~oldman.resource.Resource` objects. - :param manager: :class:`~oldman.management.manager.ResourceManager` object. Gives + :param manager: :class:`~oldman.resource.manager.ResourceManager` object. Gives access to the `data_graph` (where the triples are stored), the `union_graph` and the `resource_cache`. :param id: IRI of the resource. If not given, this IRI is generated by the main model. Defaults to `None`. @@ -82,16 +84,22 @@ class Resource(object): :param kwargs: values indexed by their attribute names. """ - _special_attribute_names = ["_models", "_id", "_types", "_is_blank_node", "_manager", - "_former_types", "_logger"] - _pickle_attribute_names = ["_id", '_types'] + _special_attribute_names = ["_models", "_id", "_types", "_is_blank_node", "_model_manager", + "_store", "_former_types", "_logger", "_resource_manager", "_is_new"] + _pickle_attribute_names = ["_id", '_types', '_is_new'] - def __init__(self, manager, id=None, types=None, hashless_iri=None, collection_iri=None, is_new=True, **kwargs): + def __init__(self, model_manager, data_store, id=None, types=None, hashless_iri=None, collection_iri=None, + is_new=True, former_types=None, **kwargs): """Inits but does not save it (in the `data_graph`).""" - self._models, self._types = manager.find_models_and_types(types) - self._former_types = set(self._types) if not is_new else set() + self._models, self._types = model_manager.find_models_and_types(types) + if former_types is not None: + self._former_types = set(former_types) + else: + self._former_types = set(self._types) if not is_new else set() main_model = self._models[0] - self._manager = manager + self._model_manager = model_manager + self._store = data_store + self._is_new = is_new if hashless_iri is not None and collection_iri is not None: raise OMUserError(u"Hashless_iri (%s) and collection_iri (%s) cannot be given in the same time." @@ -100,11 +108,12 @@ def __init__(self, manager, id=None, types=None, hashless_iri=None, collection_i if id is not None: # Anticipated because used in __hash__ self._id = id - if is_new and self._manager.data_store.exists(id): + if is_new and self._store.exists(id): raise OMUniquenessError("Object %s already exist" % self._id) else: self._id = main_model.generate_iri(hashless_iri=hashless_iri, collection_iri=collection_iri) + self._init_non_persistent_attributes(self._id) for k, v in kwargs.iteritems(): @@ -122,6 +131,11 @@ def types(self): """IRI list of the RDFS classes the resource is instance of.""" return list(self._types) + @property + def models(self): + """TODO: describe""" + return list(self._models) + @property def id(self): """IRI that identifies the resource.""" @@ -145,9 +159,41 @@ def context(self): return list(self._models)[0].context @property - def manager(self): + def model_manager(self): """TODO: describe """ - return self._manager + return self._model_manager + + @property + def store(self): + """TODO: describe """ + return self._store + + @property + def is_new(self): + """True if the resource has never been saved.""" + return self._is_new + + @property + def former_types(self): + """Not for end-users""" + return list(self._former_types) + + @property + def non_model_types(self): + """TODO: describe """ + return set(self._types).difference({m.class_iri for m in self._models}) + + @property + def former_non_model_types(self): + """TODO: describe """ + if len(self._former_types) == 0: + return {} + possible_non_model_types = set(self._former_types).difference({m.class_iri + for m in self._models}) + if len(possible_non_model_types) == 0: + return {} + corresponding_models, _ = self._model_manager.find_models_and_types(possible_non_model_types) + return possible_non_model_types.difference({m.class_iri for m in corresponding_models}) def is_valid(self): """Tests if the resource is valid. @@ -194,27 +240,15 @@ def get_operation(self, http_method): def get_lightly(self, attribute_name): """TODO: describe """ + return self.get_attribute(attribute_name).get_lightly(self) + + def get_attribute(self, attribute_name): + """Not for the end-user!""" for model in self._models: if attribute_name in model.om_attributes: - return model.access_attribute(attribute_name).get_lightly(self) + return model.access_attribute(attribute_name) raise AttributeError("%s has no regular attribute %s" % (self, attribute_name)) - @classmethod - def load_from_graph(cls, manager, id, subgraph, is_new=True, collection_iri=None): - """Loads a new :class:`~oldman.resource.Resource` object from a sub-graph. - - :param manager: :class:`~oldman.management.manager.ResourceManager` object. - :param id: IRI of the resource. - :param subgraph: :class:`rdflib.Graph` object containing triples about the resource. - :param is_new: When is `True` and `id` given, checks that the IRI is not already existing in the - `union_graph`. Defaults to `True`. - :return: The :class:`~oldman.resource.Resource` object created. - """ - types = list({unicode(t) for t in subgraph.objects(URIRef(id), RDF.type)}) - instance = cls(manager, id=id, types=types, is_new=is_new, collection_iri=collection_iri) - instance.update_from_graph(subgraph, is_end_user=True, save=False, initial=True) - return instance - def __getattr__(self, name): """Gets: * A declared Python method ; @@ -243,7 +277,8 @@ def __getattr__(self, name): operation = model.get_operation_by_name(name) if operation is not None: return partial(operation, self) - raise AttributeError("%s has not attribute %s" % (self, name)) + + raise AttributeError("%s has no attribute %s" % (self, name)) def __setattr__(self, name, value): """Sets the value of one or multiple :class:`~oldman.attribute.OMAttribute` objects. @@ -266,58 +301,6 @@ def __setattr__(self, name, value): if not found: raise AttributeError("%s has not attribute %s" % (self, name)) - def __getstate__(self): - """Pickles this resource.""" - state = {name: getattr(self, name) for name in self._pickle_attribute_names} - state["manager_name"] = self._manager.name - - # Reversed order so that important models can overwrite values - reversed_models = self._models - reversed_models.reverse() - for model in reversed_models: - for name, attr in model.om_attributes.iteritems(): - value = attr.get_lightly(self) - if isinstance(value, GeneratorType): - if attr.container == "@list": - value = list(value) - else: - value = set(value) - if value is not None: - state[name] = value - return state - - def __setstate__(self, state): - """Unpickles this resource from its serialized `state`.""" - required_fields = self._pickle_attribute_names + ["manager_name"] - for name in required_fields: - if name not in state: - #TODO: find a better exception (due to the cache) - raise OMInternalError(u"Required field %s is missing in the cached state" % name) - - self._id = state["_id"] - self._init_non_persistent_attributes(self._id) - - # Manager - from oldman import ResourceManager - self._manager = ResourceManager.get_manager(state["manager_name"]) - - # Models and types - self._models, self._types = self._manager.find_models_and_types(state["_types"]) - self._former_types = set(self._types) - - # Attributes (Python attributes or OMAttributes) - for name, value in state.iteritems(): - if name in ["manager_name", "_id", "_types"]: - continue - elif name in self._special_attribute_names: - setattr(self, name, value) - # OMAttributes - else: - attribute = self._get_om_attribute(name) - attribute.set(self, value) - # Clears former values (allows modification) - attribute.delete_former_value(self) - def add_type(self, additional_type): """Declares that the resource is instance of another RDFS class. @@ -340,6 +323,15 @@ def check_validity(self): for attr in model.om_attributes.values(): attr.check_validity(self) + def receive_id(self, id): + """TODO: describe. + + Assigned by store. + """ + # TODO: make sure the previous id was a temporary one + self._id = id + self._is_new = False + def save(self, is_end_user=True): """Saves it into the `data_graph` and the `resource_cache`. @@ -350,81 +342,17 @@ def save(self, is_end_user=True): See :func:`~oldman.attribute.OMAttribute.check_validity` for further details. :return: The :class:`~oldman.resource.Resource` object itself. """ - # Checks - attributes = self._extract_attribute_list() - for attr in attributes: - attr.check_validity(self, is_end_user) - - # Find objects to delete - objects_to_delete = [] - for attr in attributes: - if not attr.has_new_value(self): - continue - - # Some former objects may be deleted - if attr.om_property.type == OBJECT_PROPERTY: - former_value = attr.get_former_value(self) - - if isinstance(former_value, dict): - raise NotImplementedError("Object dicts are not yet supported.") - former_value = former_value if isinstance(former_value, (set, list)) else [former_value] - - # Cache invalidation (because of possible reverse properties) - value = attr.get(self) - resources_to_invalidate = set(value) if isinstance(value, (set, list)) else {value} - resources_to_invalidate.update(former_value) - for r in resources_to_invalidate: - if r is not None: - self._manager.data_store.resource_cache.remove_resource_from_id(r) - - objects_to_delete += [self._manager.get(id=v) for v in former_value - if v is not None and is_blank_node(v)] - - # Update literal values - self._manager.data_store.save(self, attributes, self._former_types) - - # Delete the objects - for obj in objects_to_delete: - obj.delete() - - # Clears former values - self._former_types = None - for attr in attributes: - attr.delete_former_value(self) - - return self + raise NotImplementedError("Have to be implemented by sub-classes") def delete(self): """Removes the resource from the `data_graph` and the `resource_cache`. + TODO: update this description. + Cascade deletion is done for related resources satisfying the test :func:`~oldman.resource.should_delete_resource`. """ - attributes = self._extract_attribute_list() - for attr in attributes: - # Delete blank nodes recursively - if attr.om_property.type == OBJECT_PROPERTY: - value = getattr(self, attr.name) - if value is not None: - objs = value if isinstance(value, (list, set, GeneratorType)) else [value] - for obj in objs: - if should_delete_resource(obj): - self._logger.debug(u"%s deleted with %s" % (obj.id, self._id)) - obj.delete() - else: - self._logger.debug(u"%s not deleted with %s" % (obj.id, self._id)) - # Cache invalidation (because of possible reverse properties) - self._manager.data_store.resource_cache.remove_resource(obj) - - setattr(self, attr.name, None) - - #Types - self._change_types(set()) - self._manager.data_store.delete(self, attributes, self._former_types) - - # Clears former values - for attr in attributes: - attr.delete_former_value(self) + raise NotImplementedError("Have to be implemented by sub-classes") def _extract_attribute_list(self): """:return: An ordered list of list of :class:`~oldman.attribute.OMAttribute` objects.""" @@ -536,8 +464,7 @@ def _convert_value(self, value, ignored_iris, remove_none_values, include_differ # Literal return value - def update(self, full_dict, is_end_user=True, allow_new_type=False, allow_type_removal=False, - save=True): + def update(self, full_dict, is_end_user=True, allow_new_type=False, allow_type_removal=False, save=True): """Updates the resource from a flat `dict`. By flat, we mean that sub-resources are only represented by their IRIs: @@ -574,7 +501,7 @@ def update(self, full_dict, is_end_user=True, allow_new_type=False, allow_type_r if key not in attr_names and key not in ["@context", "id", "types"]: raise OMAttributeAccessError(u"%s is not an attribute of %s" % (key, self._id)) - # Type change management + # Type change resource if "types" in full_dict: try: new_types = set(full_dict["types"]) @@ -594,7 +521,7 @@ def update(self, full_dict, is_end_user=True, allow_new_type=False, allow_type_r return self def update_from_graph(self, subgraph, initial=False, is_end_user=True, allow_new_type=False, - allow_type_removal=False, save=True): + allow_type_removal=False, save=True): """Similar to :func:`~oldman.resource.Resource.full_update` but with a RDF graph instead of a Python `dict`. @@ -624,6 +551,15 @@ def update_from_graph(self, subgraph, initial=False, is_end_user=True, allow_new self.save(is_end_user) return self + def get_related_resource(self, id): + """ + TODO: describe. + Not for end-users! + + If cannot get the resource, return its IRI + """ + raise NotImplementedError("To be implemented by a concrete sub-class") + def _check_and_update_types(self, new_types, allow_new_type, allow_type_removal): current_types = set(self._types) if new_types == current_types: @@ -650,12 +586,10 @@ def _check_and_update_types(self, new_types, allow_new_type, allow_type_removal) % (removed_types, self._id)) change = True if change: - self._models, types = self._manager.find_models_and_types(new_types) + self._models, types = self._model_manager.find_models_and_types(new_types) self._change_types(types) def _change_types(self, new_types): - if self._former_types is None: - self._former_types = set(self._types) self._types = new_types def _get_om_attribute(self, name): @@ -666,6 +600,254 @@ def _get_om_attribute(self, name): #self._logger.debug(u"%s" % self._manager._registry.model_names) raise AttributeError(u"%s has not attribute %s" % (self, name)) + def _filter_objects_to_delete(self, ids): + raise NotImplementedError("Implemented by a sub-class") + + +class StoreResource(Resource): + """TODO: describe""" + + @classmethod + def load_from_graph(cls, model_manager, data_store, id, subgraph, is_new=True, collection_iri=None): + """Loads a new :class:`~oldman.resource.StoreResource` object from a sub-graph. + + TODO: update the comments. + + :param manager: :class:`~oldman.resource.manager.ResourceManager` object. + :param id: IRI of the resource. + :param subgraph: :class:`rdflib.Graph` object containing triples about the resource. + :param is_new: When is `True` and `id` given, checks that the IRI is not already existing in the + `union_graph`. Defaults to `True`. + :return: The :class:`~oldman.resource.Resource` object created. + """ + types = list({unicode(t) for t in subgraph.objects(URIRef(id), RDF.type)}) + instance = cls(model_manager, data_store, id=id, types=types, is_new=is_new, collection_iri=collection_iri) + instance.update_from_graph(subgraph, is_end_user=True, save=False, initial=True) + return instance + + def __getstate__(self): + """Pickles this resource.""" + state = {name: getattr(self, name) for name in self._pickle_attribute_names} + state["store_name"] = self._store.name + + # Reversed order so that important models can overwrite values + reversed_models = self._models + reversed_models.reverse() + for model in reversed_models: + for name, attr in model.om_attributes.iteritems(): + value = attr.get_lightly(self) + if isinstance(value, GeneratorType): + if attr.container == "@list": + value = list(value) + else: + value = set(value) + if value is not None: + state[name] = value + return state + + def __setstate__(self, state): + """Unpickles this resource from its serialized `state`.""" + required_fields = self._pickle_attribute_names + ["store_name"] + for name in required_fields: + if name not in state: + #TODO: find a better exception (due to the cache) + raise OMInternalError(u"Required field %s is missing in the cached state" % name) + + self._id = state["_id"] + self._is_new = state["_is_new"] + self._init_non_persistent_attributes(self._id) + + # Store + from oldman.store.datastore import DataStore + self._store = DataStore.get_store(state["store_name"]) + self._model_manager = self._store.model_manager + + # Models and types + self._models, self._types = self._model_manager.find_models_and_types(state["_types"]) + self._former_types = set(self._types) + + # Attributes (Python attributes or OMAttributes) + for name, value in state.iteritems(): + if name in ["store_name", "_id", "_types", "_is_new"]: + continue + elif name in self._special_attribute_names: + setattr(self, name, value) + # OMAttributes + else: + attribute = self._get_om_attribute(name) + attribute.set(self, value) + # Clears former values (allows modification) + attribute.receive_storage_ack(self) + + def get_related_resource(self, id): + """TODO: describe """ + resource = self.store.get(id=id) + if resource is None: + return id + return resource + + def save(self, is_end_user=True): + # Checks + attributes = self._extract_attribute_list() + for attr in attributes: + attr.check_validity(self, is_end_user) + + # Find objects to delete + objects_to_delete = [] + for attr in attributes: + if not attr.has_changed(self): + continue + + # Some former objects may be deleted + if attr.om_property.type == OBJECT_PROPERTY: + former_value, value = attr.diff(self) + + if isinstance(former_value, dict): + raise NotImplementedError("Object dicts are not yet supported.") + former_value = former_value if isinstance(former_value, (set, list)) else [former_value] + + # Cache invalidation (because of possible reverse properties) + resources_to_invalidate = set(value) if isinstance(value, (set, list)) else {value} + resources_to_invalidate.update(former_value) + for r in resources_to_invalidate: + if r is not None: + self._store.resource_cache.remove_resource_from_id(r) + + objects_to_delete += self._filter_objects_to_delete(former_value) + + # Update literal values + self.store.save(self, attributes, self._former_types) + + # Delete the objects + for obj in objects_to_delete: + obj.delete() + + # Clears former values + self._former_types = self._types + for attr in attributes: + attr.receive_storage_ack(self) + + return self + + def delete(self): + attributes = self._extract_attribute_list() + for attr in attributes: + # Delete blank nodes recursively + if attr.om_property.type == OBJECT_PROPERTY: + value = getattr(self, attr.name) + if value is not None: + objs = value if isinstance(value, (list, set, GeneratorType)) else [value] + for obj in objs: + if should_delete_resource(obj): + self._logger.debug(u"%s deleted with %s" % (obj.id, self._id)) + obj.delete() + else: + self._logger.debug(u"%s not deleted with %s" % (obj.id, self._id)) + # Cache invalidation (because of possible reverse properties) + self._store.resource_cache.remove_resource(obj) + + setattr(self, attr.name, None) + + #Types + self._change_types(set()) + self._store.delete(self, attributes, self._former_types) + + # Clears former values + for attr in attributes: + attr.receive_storage_ack(self) + self._is_new = False + + def _filter_objects_to_delete(self, ids): + return [self.store.get(id=id) for id in ids + if id is not None and is_blank_node(id)] + + +class ClientResource(Resource): + """TODO: describe""" + + def __init__(self, resource_manager, model_manager, store, **kwargs): + Resource.__init__(self, model_manager, store, **kwargs) + self._resource_manager = resource_manager + + @classmethod + def load_from_graph(cls, resource_manager, model_manager, data_store, id, subgraph, is_new=True, + collection_iri=None): + """Loads a new :class:`~oldman.resource.ClientResource` object from a sub-graph. + + TODO: update the comments. + + :param manager: :class:`~oldman.resource.manager.ResourceManager` object. + :param id: IRI of the resource. + :param subgraph: :class:`rdflib.Graph` object containing triples about the resource. + :param is_new: When is `True` and `id` given, checks that the IRI is not already existing in the + `union_graph`. Defaults to `True`. + :return: The :class:`~oldman.resource.Resource` object created. + """ + types = list({unicode(t) for t in subgraph.objects(URIRef(id), RDF.type)}) + instance = cls(resource_manager, model_manager, data_store, id=id, types=types, is_new=is_new, + collection_iri=collection_iri) + instance.update_from_graph(subgraph, is_end_user=True, save=False, initial=True) + return instance + + def get_related_resource(self, id): + """TODO: describe """ + resource = self._resource_manager.get(id=id) + if resource is None: + return id + return resource + + def save(self, is_end_user=True): + """TODO: describe.""" + attributes = self._extract_attribute_list() + for attr in attributes: + attr.check_validity(self, is_end_user) + + store_resource = self.model_manager.convert_client_resource(self) + store_resource.save(is_end_user) + + # Clears former values + self._former_types = self._types + # Clears former values + for attr in attributes: + attr.receive_storage_ack(self) + self._is_new = False + # The ID may be updated (if was a temporary IRI before) + self._id = store_resource.id + + return self + + def delete(self): + """TODO: describe.""" + store_resource = self.model_manager.convert_client_resource(self) + store_resource.delete() + + # Clears former values + self._former_types = self._types + # Clears values + for attr in self._extract_attribute_list(): + setattr(self, attr.name, None) + attr.receive_storage_ack(self) + self._is_new = False + + def __getstate__(self): + """Cannot be pickled.""" + #TODO: find the appropriate exception + raise Exception("A ClientResource is not serializable.") + + def __setstate__(self, state): + """Cannot be pickled.""" + #TODO: find the appropriate exception + raise Exception("A ClientResource is not serializable.") + + def _filter_objects_to_delete(self, ids): + """TODO: consider other cases than blank nodes """ + return [self._resource_manager.get(id=id) for id in ids + if id is not None and is_blank_node(id)] + + # @property + # def resource_manager(self): + # return self._resource_manager + def is_blank_node(iri): """Tests if `id` is a locally skolemized IRI. diff --git a/oldman/rest/crud.py b/oldman/rest/crud.py index 7760afb..6d633bb 100644 --- a/oldman/rest/crud.py +++ b/oldman/rest/crud.py @@ -21,7 +21,7 @@ class HashLessCRUDer(object): This class is generic and does not support the Collection pattern (there is no append method). - :param manager: :class:`~oldman.management.manager.ResourceManager` object. + :param manager: :class:`~oldman.resource.manager.ResourceManager` object. Possible improvements: diff --git a/oldman/store/cache.py b/oldman/store/cache.py index 4776439..0e1c345 100644 --- a/oldman/store/cache.py +++ b/oldman/store/cache.py @@ -3,7 +3,7 @@ class ResourceCache(object): - """A :class:`~oldman.management.cache.ResourceCache` object caches + """A :class:`~oldman.resource.cache.ResourceCache` object caches :class:`~oldman.resource.Resource` objects. It interfaces a :class:`dogpile.cache.region.CacheRegion` front-end object. @@ -14,9 +14,9 @@ class ResourceCache(object): by `dogpile.cache `_. When `cache_region` is None, no effective caching is done. - However, methods :func:`~oldman.management.cache.ResourceCache.get_resource`, - :func:`~oldman.management.cache.ResourceCache.set_resource` - and :func:`~oldman.management.cache.ResourceCache.remove_resource` can still safely be + However, methods :func:`~oldman.resource.cache.ResourceCache.get_resource`, + :func:`~oldman.resource.cache.ResourceCache.set_resource` + and :func:`~oldman.resource.cache.ResourceCache.remove_resource` can still safely be called. They just have no effect. :param cache_region: :class:`dogpile.cache.region.CacheRegion` object. @@ -82,7 +82,7 @@ def remove_resource(self, resource): self._logger.debug(u"%s removed from the cache." % resource.id) def remove_resource_from_id(self, id): - """:func:`~oldman.management.cache.ResourceCache.remove_resource` is usually preferred. + """:func:`~oldman.resource.cache.ResourceCache.remove_resource` is usually preferred. Indempotent and does nothing if `cache_region` is `None`. diff --git a/oldman/store/datastore.py b/oldman/store/datastore.py index 6fbe348..bfe4c84 100644 --- a/oldman/store/datastore.py +++ b/oldman/store/datastore.py @@ -1,9 +1,14 @@ import logging +from uuid import uuid4 +from oldman.model.manager import ModelManager from oldman.store.cache import ResourceCache from oldman.exception import UnsupportedDataStorageFeatureException, OMAttributeAccessError from oldman.exception import OMObjectNotFoundError, OMClassInstanceError -from oldman.resource import Resource +from oldman.resource.resource import Resource, StoreResource + + +DEFAULT_MODEL_PREFIX = "Default_" class DataStore(object): @@ -12,42 +17,75 @@ class DataStore(object): In the future, non-CRUD operations may also be supported. - Manages the cache (:class:`~oldman.management.cache.ResourceCache` object) of + Manages the cache (:class:`~oldman.resource.cache.ResourceCache` object) of :class:`~oldman.resource.Resource` object. - A :class:`~oldman.management.manager.ResourceManager` object must be assigned + A :class:`~oldman.resource.manager.ResourceManager` object must be assigned after instantiation of this object. + :param model_manager: TODO: describe!!! :param cache_region: :class:`dogpile.cache.region.CacheRegion` object. This object must already be configured. - Defaults to None (no cache). + Defaults to `None` (no cache). See :class:`~oldman.store.cache.ResourceCache` for further details. + :param accept_iri_generation_configuration: If False, the IRI generator cannot be configured + by the user: it is imposed by the data store. Default to `False`. """ + _stores = {} - def __init__(self, cache_region=None): - self._manager = None + def __init__(self, model_manager, cache_region=None, accept_iri_generation_configuration=True, + support_sparql=False): + self._model_manager = model_manager self._logger = logging.getLogger(__name__) self._resource_cache = ResourceCache(cache_region) + self._name = str(uuid4()) + self._stores[self._name] = self + self._accept_iri_generation_configuration = accept_iri_generation_configuration + self._support_sparql=support_sparql + + if not self._model_manager.has_default_model(): + self._model_manager.create_model(DEFAULT_MODEL_PREFIX + self._name, {u"@context": {}}, self, untyped=True, + iri_prefix=u"http://localhost/.well-known/genid/%s/" % self._name, + is_default=True) @property - def manager(self): - """The :class:`~oldman.management.manager.ResourceManager` object. + def model_manager(self): + """The :class:`~oldman.model.manager.ModelManager` object. + + TODO: update Necessary for creating new :class:`~oldman.resource.Resource` objects and accessing to :class:`~oldman.model.Model` objects. """ - return self._manager - - @manager.setter - def manager(self, resource_manager): - """ Must be called after instantiation. """ - self._manager = resource_manager + return self._model_manager @property def resource_cache(self): - """:class:`~oldman.management.cache.ResourceCache` object.""" + """:class:`~oldman.resource.cache.ResourceCache` object.""" return self._resource_cache + @classmethod + def get_store(cls, name): + """Gets a :class:`~oldman.store.datastore.DataStore` object by its name. + + :param name: store name. + :return: A :class:`~oldman.resource.manager.ModelManager` object. + """ + return cls._stores.get(name) + + @property + def name(self): + """Randomly generated name. Useful for serializing resources.""" + return self._name + + def support_sparql_filtering(self): + """Returns `True` if the datastore supports SPARQL queries (no update). + + Note that in such a case, the :func:`~oldman.store.datastore.DataStore.sparql_filter` method is expected + to be implemented. + """ + return self._support_sparql + def get(self, id=None, types=None, hashless_iri=None, eager_with_reversed_attributes=True, **kwargs): """Gets the first :class:`~oldman.resource.Resource` object matching the given criteria. @@ -135,7 +173,7 @@ def sparql_filter(self, query): :return: A generator of :class:`~oldman.resource.Resource` objects. """ raise UnsupportedDataStorageFeatureException("This datastore %s does not support the SPARQL protocol." - % self.__class__.__name__) + % self.__class__.__name__) def save(self, resource, attributes, former_types): """End-users should not call it directly. Call :func:`oldman.Resource.save()` instead. @@ -144,7 +182,8 @@ def save(self, resource, attributes, former_types): :param attributes: Ordered list of :class:`~oldman.attribute.OMAttribute` objects. :param former_types: List of RDFS class IRIs previously saved. """ - self._save_resource_attributes(resource, attributes, former_types) + id = self._save_resource_attributes(resource, attributes, former_types) + resource.receive_id(id) # Cache self._resource_cache.set_resource(resource) @@ -199,6 +238,21 @@ def check_and_repair_counter(self, class_iri): raise UnsupportedDataStorageFeatureException("This datastore %s does not manage instance counters." % self.__class__.__name__) + def create_model(self, class_name_or_iri, context, iri_generator=None, iri_prefix=None, + iri_fragment=None, incremental_iri=False): + """TODO: comment. Convenience function """ + if not self._accept_iri_generation_configuration: + if iri_generator or iri_prefix or iri_fragment or incremental_iri: + #TODO: find a better exception + raise Exception("The generator is imposed by the datastore, it cannot" + "be configured by the user.") + else: + iri_generator = self._create_iri_generator(class_name_or_iri) + + self._model_manager.create_model(class_name_or_iri, context, self, iri_generator=iri_generator, + iri_prefix=iri_prefix, iri_fragment=iri_fragment, + incremental_iri=incremental_iri) + def _get_first_resource_found(self): raise UnsupportedDataStorageFeatureException("This datastore %s cannot get a resource at random." % self.__class__.__name__) @@ -212,11 +266,17 @@ def _filter(self, type_iris, hashless_iri, limit, eager, pre_cache_properties, * % self.__class__.__name__) def _save_resource_attributes(self, resource, attributes): + """ + TODO: describe + :param resource: + :param attributes: + :return: the ID of resource (useful when the IRI was a temporary one (e.g. a skolemized IRI). + """ raise UnsupportedDataStorageFeatureException("This datastore %s cannot update resources (read-only)." % self.__class__.__name__) def _new_resource_object(self, id, resource_graph): - resource = Resource.load_from_graph(self._manager, id, resource_graph, is_new=(len(resource_graph) == 0)) + resource = StoreResource.load_from_graph(self._model_manager, self, id, resource_graph, is_new=False) self.resource_cache.set_resource(resource) return resource @@ -230,4 +290,8 @@ def _select_resource_from_hashless_iri(self, hashless_iri, resources): # TODO: avoid such arbitrary selection self._logger.warn(u"Multiple resources have the same base_uri: %s\n. " u"The first one is selected." % resources) - return resources[0] \ No newline at end of file + return resources[0] + + def _create_iri_generator(self, class_name_or_iri): + raise UnsupportedDataStorageFeatureException("This datastore %s does create IRI generators." + % self.__class__.__name__) \ No newline at end of file diff --git a/oldman/store/http.py b/oldman/store/http.py index 26c48ed..60c36dc 100644 --- a/oldman/store/http.py +++ b/oldman/store/http.py @@ -4,6 +4,7 @@ from rdflib import Graph from .datastore import DataStore +from oldman.model.manager import ModelManager from oldman.rest.crud import JSON_TYPES @@ -12,11 +13,15 @@ class HttpDataStore(DataStore): Read only. No search feature. """ - def __init__(self, cache_region=None, session=None): - DataStore.__init__(self, cache_region) + def __init__(self, schema_graph=None, cache_region=None, session=None): + DataStore.__init__(self, ModelManager(schema_graph=schema_graph), cache_region) self._session = session if session is not None else requests.session() self._logger = getLogger(__name__) + @property + def session(self): + return self._session + def _get_by_id(self, id): r = self._session.get(id, headers=dict(Accept='text/turtle;q=1.0, ' 'application/rdf+xml;q=1.0, ' diff --git a/oldman/store/selector.py b/oldman/store/selector.py new file mode 100644 index 0000000..00beb12 --- /dev/null +++ b/oldman/store/selector.py @@ -0,0 +1,30 @@ + + +class DataStoreSelector: + """TODO: continue""" + + def __init__(self, data_stores): + if (data_stores is None) or (isinstance(data_stores, (list, set)) and len(data_stores) == 0): + #TODO: find a better type of exception + raise Exception("At least one data store must be given.") + + self._data_stores = list(data_stores) if isinstance(data_stores, (list, set)) else [data_stores] + #TODO: remove + if len(self._data_stores) > 1: + raise NotImplementedError("Multiple data stores are not yet supported.") + + @property + def data_stores(self): + return self._data_stores + + def select_stores(self, id=None, **kwargs): + #TODO: implement seriously + return self._data_stores + + def select_store(self, **kwargs): + """TODO: what is the correct behavior when multiple stores are returned? """ + return self.select_stores(**kwargs)[0] + + def select_sparql_stores(self, query): + #TODO: look at the query for filtering + return filter(lambda s: s.support_sparql_filtering(), self._data_stores) diff --git a/oldman/store/sparql.py b/oldman/store/sparql.py index af2ccd8..fb9447e 100644 --- a/oldman/store/sparql.py +++ b/oldman/store/sparql.py @@ -1,8 +1,11 @@ import logging from threading import Lock + from rdflib import URIRef, Graph, RDF from rdflib.plugins.sparql.parser import ParseException + from oldman.utils.sparql import build_query_part, build_update_query_part +from oldman.model.manager import ModelManager from oldman.exception import OMSPARQLParseError, OMAttributeAccessError, OMSPARQLError from oldman.exception import OMHashIriError from oldman.exception import OMDataStoreError @@ -23,6 +26,8 @@ class SPARQLDataStore(DataStore): This object must already be configured. Defaults to None (no cache). See :class:`~oldman.store.cache.ResourceCache` for further details. + + TODO: explain the choice between schema_graph and resource_manager """ _iri_mutex = Lock() _counter_query_req = u""" @@ -44,8 +49,9 @@ class SPARQLDataStore(DataStore): BIND (?current+1 AS ?next) }""" - def __init__(self, data_graph, union_graph=None, cache_region=None): - DataStore.__init__(self, cache_region) + def __init__(self, data_graph, schema_graph=None, model_manager=None, union_graph=None, cache_region=None): + manager = model_manager if model_manager is not None else ModelManager(schema_graph) + DataStore.__init__(self, manager, cache_region, support_sparql=True) self._logger = logging.getLogger(__name__) self._data_graph = data_graph self._union_graph = union_graph if union_graph is not None else data_graph @@ -152,7 +158,7 @@ def _get_by_id(self, id, eager_with_reversed_attributes=True): resource_graph = Graph() iri = URIRef(id) - eager = eager_with_reversed_attributes and self.manager.include_reversed_attributes + eager = eager_with_reversed_attributes and self.model_manager.include_reversed_attributes if eager: #TODO: look at specific properties and see if it improves the performance triple_query = u"""SELECT ?s ?p ?o @@ -173,10 +179,10 @@ def _get_by_id(self, id, eager_with_reversed_attributes=True): else: resource_graph += self._union_graph.triples((iri, None, None)) - if self.manager.include_reversed_attributes: + if self.model_manager.include_reversed_attributes: #Extracts the types types = {unicode(o) for o in resource_graph.objects(iri, RDF.type)} - models, _ = self.manager.find_models_and_types(types) + models, _ = self.model_manager.find_models_and_types(types) #TODO: improve by looking at specific properties if True in [m.include_reversed_attributes for m in models]: @@ -207,7 +213,7 @@ def _filter(self, type_iris, hashless_iri, limit, eager, pre_cache_properties, * lines = u"?s ?p ?o . \n" else: type_set = set(type_iris) - models, _ = self.manager.find_models_and_types(type_set) + models, _ = self.model_manager.find_models_and_types(type_set) lines = u"" for type_iri in type_iris: @@ -335,10 +341,10 @@ def _save_resource_attributes(self, resource, attributes, former_types): former_lines = u"" new_lines = u"" for attr in attributes: - if not attr.has_new_value(resource): + if not attr.has_changed(resource): continue - former_value = attr.get_former_value(resource) + former_value, _ = attr.diff(resource) former_lines += attr.value_to_nt(former_value) new_lines += attr.to_nt(resource) @@ -364,6 +370,9 @@ def _save_resource_attributes(self, resource, attributes, former_types): except ParseException as e: raise OMSPARQLParseError(u"%s\n %s" % (query, e)) + # Same IRI (no change) + return id + def _find_attribute(models, name): for m in models: diff --git a/oldman/utils/crud.py b/oldman/utils/crud.py index 00104db..b8d3329 100644 --- a/oldman/utils/crud.py +++ b/oldman/utils/crud.py @@ -1,6 +1,7 @@ from rdflib import RDF, URIRef, BNode + from oldman.exception import OMDifferentHashlessIRIError, OMForbiddenSkolemizedIRIError, OMClassInstanceError, OMInternalError -from oldman.resource import Resource, is_blank_node +from oldman.resource.resource import Resource, is_blank_node def extract_subjects(graph): diff --git a/requirements.txt b/requirements.txt index 9cc4a37..0e7051d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,3 @@ python-coveralls -e git+https://github.com/RDFLib/rdflib-jsonld#egg=rdflib-jsonld -e . - -# TODO: port it to Python 3 -negotiator \ No newline at end of file diff --git a/setup.py b/setup.py index ff697e7..b0ca512 100644 --- a/setup.py +++ b/setup.py @@ -3,11 +3,12 @@ author="Benjamin Cogrel", author_email="benjamin.cogrel@bcgl.fr", url="https://github.com/oldm/oldman", - version="0.1", + version="0.2", description="Object Linked Data Mapper", long_description=open('README.rst').read(), packages=['oldman', - 'oldman.management', + 'oldman.model', + 'oldman.resource', 'oldman.parsing', 'oldman.parsing.schema', 'oldman.rest', diff --git a/tests/ancestry_test.py b/tests/ancestry_test.py index 8d75eb0..e6c86a9 100644 --- a/tests/ancestry_test.py +++ b/tests/ancestry_test.py @@ -4,8 +4,10 @@ """ from unittest import TestCase + from rdflib import Graph, RDFS, URIRef, BNode, Literal -from oldman.management.ancestry import ClassAncestry + +from oldman.model.ancestry import ClassAncestry from oldman.vocabulary import MODEL_PRIORITY_IRI, MODEL_HAS_PRIORITY_IRI, MODEL_PRIORITY_CLASS_IRI diff --git a/tests/attr_entry_test.py b/tests/attr_entry_test.py new file mode 100644 index 0000000..5a9d224 --- /dev/null +++ b/tests/attr_entry_test.py @@ -0,0 +1,68 @@ +import unittest +from oldman.model.attribute import Entry + + +class AttributeEntryTest(unittest.TestCase): + + def test_1(self): + entry = Entry() + value1 = 1 + + self.assertNotEquals(entry.current_value, value1) + entry.current_value = value1 + self.assertEquals(entry.current_value, value1) + self.assertTrue(entry.has_changed()) + self.assertEquals(entry.diff(), (None, value1)) + self.assertTrue(entry.has_changed()) + + entry.receive_storage_ack() + self.assertFalse(entry.has_changed()) + + self.assertEquals(entry.current_value, value1) + + #TODO: use a more precise exception + with self.assertRaises(Exception): + entry.diff() + + value2 = 2 + entry.current_value = value2 + self.assertEquals(entry.current_value, value2) + self.assertTrue(entry.has_changed()) + self.assertEquals(entry.diff(), (value1, value2)) + + entry.receive_storage_ack() + self.assertFalse(entry.has_changed()) + self.assertEquals(entry.current_value, value2) + + def test_boolean(self): + entry = Entry() + entry.current_value = False + + self.assertTrue(entry.has_changed()) + self.assertEquals(entry.diff(), (None, False)) + + entry.receive_storage_ack() + self.assertFalse(entry.has_changed()) + + entry.current_value = None + self.assertTrue(entry.has_changed()) + self.assertEquals(entry.diff(), (False, None)) + + def test_clone(self): + value1 = [1] + value2 = {2} + e1 = Entry(value1) + e1.current_value = value2 + self.assertEquals(e1.diff(), (value1, value2)) + + e2 = e1.clone() + self.assertEquals(e1.diff(), e2.diff()) + + value3 = {"f": "3"} + e1.current_value = value3 + self.assertEquals(e1.diff(), (value1, value3)) + self.assertEquals(e2.diff(), (value1, value2)) + + + + diff --git a/tests/cache_test.py b/tests/cache_test.py index e4c0a15..3f74ad6 100644 --- a/tests/cache_test.py +++ b/tests/cache_test.py @@ -12,7 +12,8 @@ def tearDown(self): def test_direct_cache(self): alice1 = lp_model.new(name=alice_name, mboxes={alice_mail}, short_bio_en=alice_bio_en) #For test ONLY. Do not do that yourself - data_store.resource_cache.set_resource(alice1) + alice_store1 = alice1.model_manager.convert_client_resource(alice1) + data_store.resource_cache.set_resource(alice_store1) alice2 = data_store.resource_cache.get_resource(alice1.id) self.assertFalse(alice1 is alice2) self.assertEquals(alice1.name, alice2.name) @@ -25,7 +26,7 @@ def test_direct_cache(self): def test_simple_get(self): alice1 = create_alice() - alice2 = manager.get(id=alice1.id) + alice2 = client_manager.get(id=alice1.id) self.assertFalse(alice1 is alice2) self.assertEquals(alice1.name, alice2.name) self.assertEquals(alice1.id, alice2.id) @@ -38,7 +39,7 @@ def test_get_friend(self): alice1.friends = {bob1} alice1.save() - alice2 = manager.get(id=alice1.id) + alice2 = client_manager.get(id=alice1.id) self.assertEquals(alice1.id, alice2.id) bob2 = list(alice2.friends)[0] @@ -58,7 +59,7 @@ def test_modification(self): new_name = "New Alice" alice1.name = new_name - alice2 = manager.get(id=alice1.id) + alice2 = client_manager.get(id=alice1.id) self.assertFalse(alice1 is alice2) self.assertEquals(alice1.id, alice2.id) self.assertNotEquals(alice1.name, alice2.name) @@ -69,7 +70,7 @@ def test_modification(self): self.assertFalse(bool(data_graph.query(req_name % alice_name))) self.assertTrue(bool(data_graph.query(req_name % new_name))) - alice3 = manager.get(id=alice1.id) + alice3 = client_manager.get(id=alice1.id) self.assertFalse(alice1 is alice3) self.assertEquals(alice1.id, alice3.id) self.assertEquals(alice1.name, alice3.name) @@ -81,7 +82,7 @@ def test_modification(self): self.assertFalse(bool(data_graph.query(req_name % new_name))) self.assertTrue(bool(data_graph.query(req_name % name3))) - alice4 = manager.get(id=alice1.id) + alice4 = client_manager.get(id=alice1.id) self.assertFalse(alice3 is alice4) self.assertEquals(alice3.id, alice4.id) self.assertEquals(alice3.name, alice4.name) @@ -92,16 +93,16 @@ def test_basic_deletion(self): alice_iri = alice1.id alice1.delete() - alice2 = manager.get(id=alice_iri) + alice2 = client_manager.get(id=alice_iri) self.assertEquals(alice2.types, []) def test_delete_from_cache(self): alice1 = create_alice() alice_iri = alice1.id - alice2 = manager.get(id=alice_iri) + alice2 = client_manager.get(id=alice_iri) alice2.delete() - alice3 = manager.get(id=alice_iri) + alice3 = client_manager.get(id=alice_iri) self.assertEquals(alice3.types, []) self.assertFalse(bool(data_graph.query("ASK { <%s> ?p ?o }" % alice_iri))) \ No newline at end of file diff --git a/tests/container_test.py b/tests/container_test.py index 8403d2c..a44b85c 100644 --- a/tests/container_test.py +++ b/tests/container_test.py @@ -9,7 +9,7 @@ from rdflib import ConjunctiveGraph, URIRef import json from copy import copy -from oldman import ResourceManager, parse_graph_safely, SPARQLDataStore +from oldman import ClientResourceManager, parse_graph_safely, SPARQLDataStore from oldman.exception import OMRequiredPropertyError, OMAttributeTypeCheckError default_graph = ConjunctiveGraph() @@ -116,10 +116,11 @@ } } -data_store = SPARQLDataStore(data_graph) -manager = ResourceManager(schema_graph, data_store, manager_name='ct') -# Model class is generated here! -model = manager.create_model("LocalClass", context, iri_prefix="http://localhost/objects/") +data_store = SPARQLDataStore(data_graph, schema_graph=schema_graph) +data_store.create_model("LocalClass", context, iri_prefix="http://localhost/objects/") +client_manager = ClientResourceManager(data_store) +client_manager.import_store_models() +model = client_manager.get_model("LocalClass") default_list_en = ["w1", "w2"] diff --git a/tests/controller_test/controller_test.py b/tests/controller_test/controller_test.py index 71928c2..c89fdb1 100644 --- a/tests/controller_test/controller_test.py +++ b/tests/controller_test/controller_test.py @@ -1,5 +1,5 @@ from rdflib import Graph -from oldman import SPARQLDataStore, ResourceManager, parse_graph_safely +from oldman import SPARQLDataStore, ClientResourceManager, parse_graph_safely from oldman.rest.controller import HTTPController from os import path import unittest @@ -8,21 +8,24 @@ schema_file = path.join(path.dirname(__file__), "controller-schema.ttl") schema_graph = parse_graph_safely(schema_graph, schema_file, format="turtle") -context_file = path.join(path.dirname(__file__), "controller-context.jsonld") +context_file = "file://" + path.join(path.dirname(__file__), "controller-context.jsonld") data_graph = Graph() -data_store = SPARQLDataStore(data_graph) +data_store = SPARQLDataStore(data_graph, schema_graph=schema_graph) -manager = ResourceManager(schema_graph, data_store, manager_name="controller_test") +data_store.create_model("Collection", context_file, iri_prefix="http://localhost/collections/", + incremental_iri=True) +data_store.create_model("Item", context_file, iri_prefix="http://localhost/items/", incremental_iri=True) -collection_model = manager.create_model("Collection", context_file, iri_prefix="http://localhost/collections/", - incremental_iri=True) -item_model = manager.create_model("Item", context_file, iri_prefix="http://localhost/items/", - incremental_iri=True) +client_manager = ClientResourceManager(data_store) +client_manager.import_store_models() + +collection_model = client_manager.get_model("Collection") +item_model = client_manager.get_model("Item") collection1 = collection_model.create() -controller = HTTPController(manager) +controller = HTTPController(client_manager) class ControllerTest(unittest.TestCase): @@ -41,7 +44,7 @@ def test_operation(self): print data_graph.serialize(format="turtle") - item = manager.get(id=item_iri) + item = client_manager.get(id=item_iri) self.assertTrue(item is not None) self.assertEquals(item.title, title) diff --git a/tests/crud_test.py b/tests/crud_test.py index ad39bb6..63de2c2 100644 --- a/tests/crud_test.py +++ b/tests/crud_test.py @@ -1,6 +1,8 @@ import unittest +from rdflib import URIRef, Literal, RDF, XSD from default_model import * -from oldman.exception import OMBadRequestException +from oldman.exception import OMBadRequestException, OMHashIriError, OMObjectNotFoundError, OMDifferentHashlessIRIError, \ + OMForbiddenSkolemizedIRIError class CrudTest(unittest.TestCase): @@ -38,7 +40,7 @@ def test_document_controller_get(self): doc = json.loads(crud_controller.get(doc_iri, "json")[0]) self.assertEquals(doc["id"], doc_iri) - resources = manager.filter(hashless_iri=doc_iri) + resources = client_manager.filter(hashless_iri=doc_iri) self.assertEquals({bob_iri, doc_iri}, {r.id for r in resources}) def test_bob_controller_delete(self): diff --git a/tests/datatype_test.py b/tests/datatype_test.py index 93d2664..dd8a3f2 100644 --- a/tests/datatype_test.py +++ b/tests/datatype_test.py @@ -10,7 +10,7 @@ from decimal import Decimal from copy import copy from datetime import date, datetime, time -from oldman import ResourceManager, parse_graph_safely, SPARQLDataStore +from oldman import ClientResourceManager, parse_graph_safely, SPARQLDataStore from oldman.exception import OMAttributeTypeCheckError default_graph = ConjunctiveGraph() @@ -147,9 +147,12 @@ } } -data_store = SPARQLDataStore(data_graph) -manager = ResourceManager(schema_graph, data_store, manager_name="dt") -lc_model = manager.create_model("LocalClass", context, iri_prefix="http://localhost/objects/") +data_store = SPARQLDataStore(data_graph, schema_graph=schema_graph) +data_store.create_model("LocalClass", context, iri_prefix="http://localhost/objects/") + +client_manager = ClientResourceManager(data_store) +client_manager.import_store_models() +lc_model = client_manager.get_model("LocalClass") default_list_en = ["w1", "w2"] diff --git a/tests/default_model.py b/tests/default_model.py index a607798..0ac1eec 100644 --- a/tests/default_model.py +++ b/tests/default_model.py @@ -1,16 +1,12 @@ from os import path import json import logging.config + from dogpile.cache import make_region -from rdflib import Dataset, Graph, URIRef, Literal, RDF, XSD -from rdflib.plugins.stores.sparqlstore import SPARQLUpdateStore +from rdflib import Dataset, Graph from rdflib.namespace import FOAF -from oldman import ResourceManager, parse_graph_safely, SPARQLDataStore -from oldman.attribute import OMAttributeTypeCheckError, OMRequiredPropertyError -from oldman.exception import OMClassInstanceError, OMAttributeAccessError, OMUniquenessError -from oldman.exception import OMWrongResourceError, OMObjectNotFoundError, OMHashIriError, OMEditError -from oldman.exception import OMDifferentHashlessIRIError, OMForbiddenSkolemizedIRIError, OMUnauthorizedTypeChangeError +from oldman import ClientResourceManager, parse_graph_safely, SPARQLDataStore from oldman.rest.crud import HashLessCRUDer @@ -251,20 +247,25 @@ #cache_region = None cache_region = make_region().configure('dogpile.cache.memory_pickle') -data_store = SPARQLDataStore(data_graph, cache_region=cache_region) +data_store = SPARQLDataStore(data_graph, schema_graph=schema_graph, cache_region=cache_region) # Takes the prefixes from the schema graph data_store.extract_prefixes(schema_graph) -manager = ResourceManager(schema_graph, data_store) -# Model classes are generated here! #lp_name_or_iri = "LocalPerson" lp_name_or_iri = MY_VOC + "LocalPerson" -lp_model = manager.create_model(lp_name_or_iri, context, iri_prefix="http://localhost/persons/", - iri_fragment="me") -rsa_model = manager.create_model("LocalRSAPublicKey", context) -gpg_model = manager.create_model("LocalGPGPublicKey", context) +data_store.create_model(lp_name_or_iri, context, iri_prefix="http://localhost/persons/", iri_fragment="me") +data_store.create_model("LocalRSAPublicKey", context) +data_store.create_model("LocalGPGPublicKey", context) + +client_manager = ClientResourceManager(data_store) +client_manager.import_store_models() + +lp_model = client_manager.get_model(lp_name_or_iri) +rsa_model = client_manager.get_model("LocalRSAPublicKey") +gpg_model = client_manager.get_model("LocalGPGPublicKey") + -crud_controller = HashLessCRUDer(manager) +crud_controller = HashLessCRUDer(client_manager) bob_name = "Bob" bob_blog = "http://blog.example.com/" diff --git a/tests/editing_test.py b/tests/editing_test.py index 87f226e..19de2e7 100644 --- a/tests/editing_test.py +++ b/tests/editing_test.py @@ -1,5 +1,7 @@ import unittest +from rdflib import URIRef from default_model import * +from oldman.exception import OMAttributeTypeCheckError, OMRequiredPropertyError class BasicEditingTest(unittest.TestCase): @@ -240,7 +242,7 @@ def test_children_list(self): data_store.resource_cache.remove_resource(bob) data_store.resource_cache.remove_resource(alice) data_store.resource_cache.remove_resource(john) - bob = manager.get(id=bob_iri) + bob = client_manager.get(id=bob_iri) self.assertEquals([c.id for c in bob.children], bob_children_iris) def test_set_validation(self): @@ -353,7 +355,7 @@ def test_inversed_property_retrieval_single_value(self): # Checks if the datastore still extract reversed attributes # in "lazy" mode data_store.resource_cache.remove_resource(bob) - bob = manager.get(id=bob_iri, eager_with_reversed_attributes=False) + bob = client_manager.get(id=bob_iri, eager_with_reversed_attributes=False) self.assertEquals(alice.id, bob.employer.id) def test_inversed_and_regular_update(self): diff --git a/tests/find_test.py b/tests/find_test.py index b71585a..5cbe110 100644 --- a/tests/find_test.py +++ b/tests/find_test.py @@ -1,5 +1,6 @@ import unittest from default_model import * +from oldman.exception import OMAttributeAccessError class FindTest(unittest.TestCase): @@ -53,31 +54,31 @@ def test_sparql_filter(self): ids = {alice.id, bob.id, john.id} r1 = "SELECT ?s WHERE { ?s a foaf:Person }" - self.assertEquals({r.id for r in manager.sparql_filter(r1)}, ids) + self.assertEquals({r.id for r in client_manager.sparql_filter(r1)}, ids) r2 = """SELECT ?s WHERE { ?s a foaf:Person ; foaf:name "%s"^^xsd:string . }""" % alice_name - self.assertEquals({r.id for r in manager.sparql_filter(r2)}, {alice.id}) + self.assertEquals({r.id for r in client_manager.sparql_filter(r2)}, {alice.id}) def test_no_filter_get(self): - self.assertEquals(manager.get(), None) + self.assertEquals(client_manager.get(), None) alice = create_alice() # Unique object - self.assertEquals(manager.get().id, alice.id) + self.assertEquals(client_manager.get().id, alice.id) def test_empty_filter(self): """ No filtering arguments -> all() """ - self.assertEquals(list(manager.filter()), []) + self.assertEquals(list(client_manager.filter()), []) alice = create_alice() # Unique object - self.assertEquals(list(manager.filter())[0].id, alice.id) + self.assertEquals(list(client_manager.filter())[0].id, alice.id) bob = create_bob() - self.assertEquals({r.id for r in manager.filter()}, {alice.id, bob.id}) + self.assertEquals({r.id for r in client_manager.filter()}, {alice.id, bob.id}) def test_filter_hashless_iri_types_and_names(self): bob = create_bob() @@ -87,31 +88,31 @@ def test_filter_hashless_iri_types_and_names(self): key = gpg_model.create(id=(doc_iri + "#key"), fingerprint=gpg_fingerprint, hex_id=gpg_hex_id) create_john(id=u"http://localhost/john#me") - self.assertEquals({bob.id, alice.id, key.id}, {r.id for r in manager.filter(hashless_iri=doc_iri)}) - self.assertEquals({bob.id, alice.id}, {r.id for r in manager.filter(hashless_iri=doc_iri, + self.assertEquals({bob.id, alice.id, key.id}, {r.id for r in client_manager.filter(hashless_iri=doc_iri)}) + self.assertEquals({bob.id, alice.id}, {r.id for r in client_manager.filter(hashless_iri=doc_iri, types=[MY_VOC + "LocalPerson"])}) # Missing type (name is thus ambiguous) with self.assertRaises(OMAttributeAccessError): - manager.filter(hashless_iri=doc_iri, name=alice_name) + client_manager.filter(hashless_iri=doc_iri, name=alice_name) self.assertEquals({alice.id}, {r.id for r in lp_model.filter(hashless_iri=doc_iri, name=alice_name)}) def test_get_hashless_iri_types_and_names(self): bob = create_bob() doc_iri = bob.hashless_iri key = gpg_model.create(id=(doc_iri + "#key"), fingerprint=gpg_fingerprint, hex_id=gpg_hex_id) - document = manager.create(id=doc_iri, types=[str(FOAF + "Document")]) + document = client_manager.create(id=doc_iri, types=[str(FOAF + "Document")]) - self.assertEquals(document.id, manager.get(hashless_iri=doc_iri).id) - self.assertEquals(bob.id, manager.get(hashless_iri=doc_iri, types=[MY_VOC + "LocalPerson"]).id) - self.assertEquals(key.id, manager.get(hashless_iri=doc_iri, types=[MY_VOC + "LocalGPGPublicKey"]).id) + self.assertEquals(document.id, client_manager.get(hashless_iri=doc_iri).id) + self.assertEquals(bob.id, client_manager.get(hashless_iri=doc_iri, types=[MY_VOC + "LocalPerson"]).id) + self.assertEquals(key.id, client_manager.get(hashless_iri=doc_iri, types=[MY_VOC + "LocalGPGPublicKey"]).id) def test_limit(self): n = 20 for _ in range(20): create_alice() - self.assertEquals(len(list(manager.filter())), n) + self.assertEquals(len(list(client_manager.filter())), n) self.assertEquals(len(list(lp_model.filter())), n) self.assertEquals(len(list(lp_model.all())), n) - self.assertEquals(len(list(manager.filter(limit=10))), 10) + self.assertEquals(len(list(client_manager.filter(limit=10))), 10) self.assertEquals(len(list(lp_model.filter(limit=10))), 10) self.assertEquals(len(list(lp_model.all(limit=10))), 10) \ No newline at end of file diff --git a/tests/http_test/http_store_test.py b/tests/http_test/http_store_test.py index fc2a42c..1f50b37 100644 --- a/tests/http_test/http_store_test.py +++ b/tests/http_test/http_store_test.py @@ -1,7 +1,7 @@ from os import path from unittest import TestCase from rdflib import Graph -from oldman import HttpDataStore, ResourceManager, parse_graph_safely +from oldman import HttpDataStore, ClientResourceManager, parse_graph_safely directory = path.dirname(__file__) schema_graph = parse_graph_safely(Graph(), path.join(directory, 'api_schema.ttl'), format="turtle") @@ -9,10 +9,13 @@ context_uri = path.join(directory, 'api_documentation.json') -data_store = HttpDataStore() -manager = ResourceManager(schema_graph, data_store) +data_store = HttpDataStore(schema_graph=schema_graph) +data_store.create_model('ApiDocumentation', context_uri) -doc_model = manager.create_model('ApiDocumentation', context_uri) +manager = ClientResourceManager(data_store) +manager.import_store_models() + +doc_model = manager.get_model('ApiDocumentation') class HttpStoreTest(TestCase): diff --git a/tests/instance_test.py b/tests/instance_test.py index c483c05..80aa8ab 100644 --- a/tests/instance_test.py +++ b/tests/instance_test.py @@ -5,7 +5,7 @@ from unittest import TestCase from rdflib import ConjunctiveGraph, URIRef -from oldman import ResourceManager, parse_graph_safely, SPARQLDataStore +from oldman import ClientResourceManager, parse_graph_safely, SPARQLDataStore default_graph = ConjunctiveGraph() schema_graph = default_graph.get_context(URIRef("http://localhost/schema")) @@ -85,24 +85,28 @@ def disclaim2(self): return new_disclaim -data_store = SPARQLDataStore(data_graph) -manager = ResourceManager(schema_graph, data_store, manager_name="it") -# Methods -manager.declare_method(square_value, "square_value", EXAMPLE + "GrandParentClass") -manager.declare_method(print_new_value, "print_new_value", EXAMPLE + "ChildClass") -# Method overloading -manager.declare_method(disclaim1, "disclaim", EXAMPLE + "GrandParentClass") -manager.declare_method(disclaim2, "disclaim", EXAMPLE + "ParentClass") - +data_store = SPARQLDataStore(data_graph, schema_graph=schema_graph) # ChildClass is generated before its ancestors!! child_prefix = "http://localhost/children/" uri_fragment = "this" +data_store.create_model("ChildClass", context, iri_prefix=child_prefix, iri_fragment=uri_fragment, incremental_iri=True) +data_store.create_model("GrandParentClass", context, iri_prefix="http://localhost/ancestors/", + iri_fragment=uri_fragment) +data_store.create_model("ParentClass", context, iri_prefix="http://localhost/parents/") + + +client_manager = ClientResourceManager(data_store) +client_manager.import_store_models() +# Methods +client_manager.declare_method(square_value, "square_value", EXAMPLE + "GrandParentClass") +client_manager.declare_method(print_new_value, "print_new_value", EXAMPLE + "ChildClass") +# Method overloading +client_manager.declare_method(disclaim1, "disclaim", EXAMPLE + "GrandParentClass") +client_manager.declare_method(disclaim2, "disclaim", EXAMPLE + "ParentClass") -child_model = manager.create_model("ChildClass", context, iri_prefix=child_prefix, iri_fragment=uri_fragment, - incremental_iri=True) -grand_parent_model = manager.create_model("GrandParentClass", context, iri_prefix="http://localhost/ancestors/", - iri_fragment=uri_fragment) -parent_model = manager.create_model("ParentClass", context, iri_prefix="http://localhost/parents/") +child_model = client_manager.get_model("ChildClass") +grand_parent_model = client_manager.get_model("GrandParentClass") +parent_model = client_manager.get_model("ParentClass") class InstanceTest(TestCase): @@ -241,17 +245,17 @@ def test_gets(self): tom.new_value = tom_new_value tom.save() - tom = manager.get(id=tom_uri) + tom = client_manager.get(id=tom_uri) self.assertEquals(tom.new_value, tom_new_value) self.assertEquals(tom.disclaim(), new_disclaim) self.assertTrue(tom.is_instance_of(child_model)) - jack = manager.get(id=jack_uri) + jack = client_manager.get(id=jack_uri) self.assertEquals(jack.mid_values, jack_mid_values) self.assertTrue(jack.is_instance_of(parent_model)) self.assertFalse(jack.is_instance_of(child_model)) - john = manager.get(id=john_uri) + john = client_manager.get(id=john_uri) self.assertTrue(john.is_instance_of(grand_parent_model)) self.assertFalse(john.is_instance_of(parent_model)) self.assertFalse(john.is_instance_of(child_model)) diff --git a/tests/iri_generation_test.py b/tests/iri_generation_test.py index d6bf528..df7a21b 100644 --- a/tests/iri_generation_test.py +++ b/tests/iri_generation_test.py @@ -2,7 +2,7 @@ from rdflib import ConjunctiveGraph, URIRef, RDF, BNode, Graph -from oldman import ResourceManager, SPARQLDataStore +from oldman import ClientResourceManager, SPARQLDataStore from oldman.iri import UUIDFragmentIriGenerator from oldman.exception import OMRequiredHashlessIRIError from oldman.rest.crud import HashLessCRUDer @@ -27,10 +27,13 @@ } } -data_store = SPARQLDataStore(data_graph) -manager = ResourceManager(schema_graph, data_store, manager_name="igt") -crud_controller = HashLessCRUDer(manager) -model = manager.create_model("MyClass", context, iri_generator=UUIDFragmentIriGenerator()) +data_store = SPARQLDataStore(data_graph, schema_graph=schema_graph) +data_store.create_model("MyClass", context, iri_generator=UUIDFragmentIriGenerator()) + +client_manager = ClientResourceManager(data_store) +client_manager.import_store_models() +crud_controller = HashLessCRUDer(client_manager) +model = client_manager.get_model("MyClass") class DatatypeTest(TestCase): @@ -61,7 +64,7 @@ def test_controller_put(self): g.add((BNode(), RDF.type, URIRef(EXAMPLE + "MyClass"))) crud_controller.update(hashless_iri, g.serialize(format="turtle"), "turtle") - resource = manager.get(hashless_iri=hashless_iri) + resource = client_manager.get(hashless_iri=hashless_iri) self.assertTrue(resource is not None) self.assertTrue(hashless_iri in resource.id) self.assertTrue('#' in resource.id) @@ -77,5 +80,5 @@ def test_relative_iri(self): """ hashless_iri = "http://example.org/doc3" crud_controller.update(hashless_iri, ttl, "turtle", allow_new_type=True) - resource = manager.get(hashless_iri=hashless_iri) + resource = client_manager.get(hashless_iri=hashless_iri) self.assertEquals(resource.id, hashless_iri + "#this") \ No newline at end of file diff --git a/tests/iri_test.py b/tests/iri_test.py index 66a2f36..b38e5cf 100644 --- a/tests/iri_test.py +++ b/tests/iri_test.py @@ -1,5 +1,6 @@ import unittest from default_model import * +from oldman.exception import OMUniquenessError class IriTest(unittest.TestCase): diff --git a/tests/property_test.py b/tests/property_test.py index f3cdf4a..d51da61 100644 --- a/tests/property_test.py +++ b/tests/property_test.py @@ -7,7 +7,7 @@ from os import path from rdflib import ConjunctiveGraph, URIRef, Literal, Graph, XSD import json -from oldman import ResourceManager, parse_graph_safely, SPARQLDataStore +from oldman import ClientResourceManager, parse_graph_safely, SPARQLDataStore from oldman.exception import OMPropertyDefError, OMReadOnlyAttributeError default_graph = ConjunctiveGraph() @@ -87,9 +87,12 @@ } } -data_store = SPARQLDataStore(data_graph) -manager = ResourceManager(schema_graph, data_store, manager_name="pt") -lc_model = manager.create_model("LocalClass", context, iri_prefix="http://localhost/objects/") +data_store = SPARQLDataStore(data_graph, schema_graph=schema_graph) +data_store.create_model("LocalClass", context, iri_prefix="http://localhost/objects/") + +client_manager = ClientResourceManager(data_store) +client_manager.import_store_models() +lc_model = client_manager.get_model("LocalClass") class PropertyTest(TestCase): @@ -100,7 +103,7 @@ def tearDown(self): def test_read_and_write_only(self): with self.assertRaises(OMPropertyDefError): - manager.create_model("BadClass", context, data_graph) + data_store.create_model("BadClass", context, data_graph) def test_write_only(self): obj = lc_model.new() diff --git a/tests/serialization_test.py b/tests/serialization_test.py index f00f666..d26943f 100644 --- a/tests/serialization_test.py +++ b/tests/serialization_test.py @@ -1,4 +1,5 @@ import unittest +from rdflib import URIRef from default_model import * diff --git a/tests/update_delete_test.py b/tests/update_delete_test.py index d887f8a..30dbf4b 100644 --- a/tests/update_delete_test.py +++ b/tests/update_delete_test.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- from unittest import TestCase +from rdflib import URIRef, Literal, RDF, XSD from default_model import * +from oldman.exception import OMClassInstanceError, OMAttributeTypeCheckError, OMWrongResourceError, \ + OMAttributeAccessError, OMUnauthorizedTypeChangeError class UpdateDeleteTest(TestCase):