From c2c1f193d4531917f6939e2f8aaf75d8a7979d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= <1929830+PonteIneptique@users.noreply.github.com> Date: Thu, 8 Mar 2018 12:33:15 +0100 Subject: [PATCH 01/89] Enhance modularization of CtsCapitainsLocalResolver and the Graph - Issue #153 : Dispatch is now working after the texts and work have been analyzed - Issue #154 : Modularized the way texts are built by using a dictionary of classes. Allows for better inheritance modularity - Issue #158 : Moved namespace generation at the end of the XML building according - Modifications done to XmlCTS module to allow subclassing from staticmethod... - Used children instead of texts (which was CTS specific) for WorkClass in CtsCapitainsLocalResolver - Simplified inner working of Citation to avoid multiple private value tracking : only the refsDecl is kept in memory - Normalizing CTS specific name access to .children instead of Private Value in the Resolver - Removing unused lines that was overwriting TYPE_URI for no reasons in CTS collections - Gen Graph as a helper function for tests mostly - Cut Resolver parse into 6 distinct function to allow multiprocessing or individual replacement of inside routine --- MyCapytain/__init__.py | 2 +- MyCapytain/common/constants.py | 29 +- MyCapytain/common/reference.py | 61 ++-- MyCapytain/resolvers/cts/local.py | 332 +++++++++++++----- MyCapytain/resources/collections/cts.py | 99 ++++-- .../resources/prototypes/cts/inventory.py | 22 +- tests/common/test_reference.py | 1 + tests/resolvers/cts/test_local.py | 44 +++ 8 files changed, 418 insertions(+), 172 deletions(-) diff --git a/MyCapytain/__init__.py b/MyCapytain/__init__.py index 36f472a8..c9ba04de 100644 --- a/MyCapytain/__init__.py +++ b/MyCapytain/__init__.py @@ -9,4 +9,4 @@ """ -__version__ = "2.0.6" +__version__ = "2.1.0dev0" diff --git a/MyCapytain/common/constants.py b/MyCapytain/common/constants.py index c3f63d4a..44ecede8 100644 --- a/MyCapytain/common/constants.py +++ b/MyCapytain/common/constants.py @@ -99,13 +99,30 @@ class MyCapytain: PLAINTEXT = "text/plain" +GRAPH_BINDINGS = { + "": RDF_NAMESPACES.CTS, + "dts": RDF_NAMESPACES.DTS, + "tei": RDF_NAMESPACES.TEI, + "skos": SKOS, + "cpt": RDF_NAMESPACES.CAPITAINS +} + + +def bind_graph(graph=None): + """ Bind a graph with generic MyCapytain prefixes + + :param graph: Graph (Optional) + :return: Bound graph + """ + if graph is None: + graph = Graph() + for prefix, ns in GRAPH_BINDINGS.items(): + graph.bind(prefix, ns, True) + return graph + + global __MYCAPYTAIN_TRIPLE_GRAPH__ -__MYCAPYTAIN_TRIPLE_GRAPH__ = Graph() -__MYCAPYTAIN_TRIPLE_GRAPH__.bind("", RDF_NAMESPACES.CTS) -__MYCAPYTAIN_TRIPLE_GRAPH__.bind("dts", RDF_NAMESPACES.DTS) -__MYCAPYTAIN_TRIPLE_GRAPH__.bind("tei", RDF_NAMESPACES.TEI) -__MYCAPYTAIN_TRIPLE_GRAPH__.bind("skos", SKOS) -__MYCAPYTAIN_TRIPLE_GRAPH__.bind("cpt", RDF_NAMESPACES.CAPITAINS) +__MYCAPYTAIN_TRIPLE_GRAPH__ = bind_graph() def set_graph(graph): diff --git a/MyCapytain/common/reference.py b/MyCapytain/common/reference.py index 2059a6bd..ee495b7f 100644 --- a/MyCapytain/common/reference.py +++ b/MyCapytain/common/reference.py @@ -665,15 +665,14 @@ def __init__(self, name=None, xpath=None, scope=None, refsDecl=None, child=None) """ Initialize a XmlCtsCitation object """ self.__name = None - self.__xpath = None - self.__scope = None self.__refsDecl = None self.__child = None self.name = name - self.scope = scope - self.xpath = xpath - self.refsDecl = refsDecl + if scope and xpath: + self._fromScopeXpathToRefsDecl(scope, xpath) + else: + self.refsDecl = refsDecl if child is not None: self.child = child @@ -698,13 +697,13 @@ def xpath(self): :type: basestring :Example: //tei:l[@n="?"] """ - return self.__xpath + return self._parseXpathScope()[1] @xpath.setter - def xpath(self, val): - if val is not None: - self.__xpath = val - self.__upRefsDecl() + def xpath(self, new_xpath): + if new_xpath is not None and self.refsDecl: + current_scope, current_xpath = self._parseXpathScope() + self._fromScopeXpathToRefsDecl(current_scope, new_xpath) @property def scope(self): @@ -713,13 +712,13 @@ def scope(self): :type: basestring :Example: /tei:TEI/tei:text/tei:body/tei:div """ - return self.__scope + return self._parseXpathScope()[0] @scope.setter - def scope(self, val): - if val is not None: - self.__scope = val - self.__upRefsDecl() + def scope(self, new_scope): + if new_scope is not None and self.refsDecl: + current_scope, current_xpath = self._parseXpathScope() + self._fromScopeXpathToRefsDecl(new_scope, current_xpath) @property def refsDecl(self): @@ -734,7 +733,6 @@ def refsDecl(self): def refsDecl(self, val): if val is not None: self.__refsDecl = val - self.__upXpathScope() @property def child(self): @@ -760,28 +758,28 @@ def attribute(self): ) return refs[-1] - def __upXpathScope(self): + def _parseXpathScope(self): """ Update xpath and scope property when refsDecl is updated - + + :returns: Scope, Xpath """ - rd = self.__refsDecl + rd = self.refsDecl matches = REFSDECL_SPLITTER.findall(rd) - self.__scope = REFSDECL_REPLACER.sub("?", "".join(matches[0:-1])) - self.__xpath = REFSDECL_REPLACER.sub("?", matches[-1]) + return REFSDECL_REPLACER.sub("?", "".join(matches[0:-1])), REFSDECL_REPLACER.sub("?", matches[-1]) - def __upRefsDecl(self): + def _fromScopeXpathToRefsDecl(self, scope, xpath): """ Update xpath and scope property when refsDecl is updated """ - if self.__scope is not None and self.__xpath is not None: - xpath = self.__scope + self.__xpath - i = xpath.find("?") + if scope is not None and xpath is not None: + _xpath = scope + xpath + i = _xpath.find("?") ii = 1 while i >= 0: - xpath = xpath[:i] + "$" + str(ii) + xpath[i+1:] - i = xpath.find("?") + _xpath = _xpath[:i] + "$" + str(ii) + _xpath[i+1:] + i = _xpath.find("?") ii += 1 - self.__refsDecl = xpath + self.refsDecl = _xpath def __iter__(self): """ Iteration method @@ -879,7 +877,7 @@ def isEmpty(self): :return: True if nothing was setup :rtype: bool """ - return self.refsDecl is None and self.scope is None and self.xpath is None + return self.refsDecl is None def __export__(self, output=None, **kwargs): if output == Mimetypes.XML.CTS: @@ -917,6 +915,11 @@ def __export__(self, output=None, **kwargs): regexp="\.".join(["(\w+)"]*self.refsDecl.count("$")) ) + def export(self, output=None, **kwargs): + if self.refsDecl: + return super(Citation, self).export(output=output, **kwargs) + return "" + @staticmethod def ingest(resource, xpath=".//tei:cRefPattern"): """ Ingest a resource and store data in its instance diff --git a/MyCapytain/resolvers/cts/local.py b/MyCapytain/resolvers/cts/local.py index ba76d12f..dc589277 100644 --- a/MyCapytain/resolvers/cts/local.py +++ b/MyCapytain/resolvers/cts/local.py @@ -12,8 +12,11 @@ from MyCapytain.errors import InvalidURN, UnknownObjectError, UndispatchedTextError from MyCapytain.resolvers.prototypes import Resolver from MyCapytain.resolvers.utils import CollectionDispatcher -from MyCapytain.resources.collections.cts import XmlCtsTextInventoryMetadata, XmlCtsTextgroupMetadata, XmlCtsWorkMetadata, XmlCtsCitation, XmlCtsTextMetadata as InventoryText, \ +from MyCapytain.resources.collections.cts import XmlCtsTextInventoryMetadata, XmlCtsTextgroupMetadata, \ + XmlCtsWorkMetadata, XmlCtsCitation, XmlCtsTextMetadata as InventoryText, \ XmlCtsTranslationMetadata, XmlCtsEditionMetadata, XmlCtsCommentaryMetadata +from MyCapytain.resources.prototypes.cts.inventory import CtsEditionMetadata, CtsTextgroupMetadata, CtsWorkMetadata, \ + CtsCommentaryMetadata, CtsTextInventoryCollection, CtsTranslationMetadata, CtsTextInventoryMetadata from MyCapytain.resources.prototypes.cts.inventory import CtsTextInventoryCollection from MyCapytain.resources.texts.local.capitains.cts import CapitainsCtsText @@ -35,25 +38,44 @@ class CtsCapitainsLocalResolver(Resolver): """ - TEXT_CLASS = CapitainsCtsText + CLASSES = { + "text": CapitainsCtsText, + "edition": XmlCtsEditionMetadata, + "translation": XmlCtsTranslationMetadata, + "commentary": XmlCtsCommentaryMetadata, + "work": XmlCtsWorkMetadata, + "textgroup": XmlCtsTextgroupMetadata, + "inventory": XmlCtsTextInventoryMetadata, + "inventory_collection": CtsTextInventoryCollection, + "citation": XmlCtsCitation + } + DEFAULT_PAGE = 1 PER_PAGE = (1, 10, 100) # Min, Default, Mainvex, RAISE_ON_UNDISPATCHED = False + RAISE_ON_GENERIC_PARSING_ERROR = True @property def inventory(self): return self.__inventory__ + @inventory.setter + def inventory(self, value): + self.__inventory__ = value + @property def texts(self): return self.inventory.readableDescendants - def __init__(self, resource, name=None, logger=None, dispatcher=None): + def __init__(self, resource, name=None, logger=None, dispatcher=None, autoparse=True): """ Initiate the XMLResolver """ + self.classes = {} + self.classes.update(type(self).CLASSES) + if dispatcher is None: - inventory_collection = CtsTextInventoryCollection(identifier="defaultTic") - ti = XmlCtsTextInventoryMetadata("default") + inventory_collection = self.classes["inventory_collection"](identifier="defaultTic") + ti = self.classes["inventory"]("default") ti.parent = inventory_collection ti.set_label("Default collection", "eng") self.dispatcher = CollectionDispatcher(inventory_collection) @@ -69,10 +91,10 @@ def __init__(self, resource, name=None, logger=None, dispatcher=None): if not name: self.name = "repository" - self.TEXT_CLASS = type(self).TEXT_CLASS self.works = [] - self.parse(resource) + if autoparse: + self.parse(resource) def xmlparse(self, file): """ Parse a XML file @@ -81,85 +103,210 @@ def xmlparse(self, file): """ return xmlparser(file) + def read(self, identifier, path): + """ Retrieve and parse a text given an identifier + + :param identifier: Identifier of the text + :type identifier: str + :param path: Path of the text + :type path: str + :return: Parsed Text + :rtype: CapitainsCtsText + """ + with open(path) as f: + o = self.classes["text"](urn=identifier, resource=self.xmlparse(f)) + return o + + def _parse_textgroup_wrapper(self, cts_file): + """ Wraps with a Try/Except the textgroup parsing from a cts file + + :param cts_file: Path to the CTS File + :type cts_file: str + :return: CtsTextgroupMetadata + """ + try: + return self._parse_textgroup(cts_file) + except Exception as E: + self.logger.error("Error parsing %s ", cts_file) + if self.RAISE_ON_GENERIC_PARSING_ERROR: + raise E + + def _parse_textgroup(self, cts_file): + """ Parses a textgroup from a cts file + + :param cts_file: Path to the CTS File + :type cts_file: str + :return: CtsTextgroupMetadata and Current file + """ + with io.open(cts_file) as __xml__: + return self.classes["textgroup"].parse( + resource=__xml__, + _cls_dict=self.classes + ), cts_file + + def _parse_work_wrapper(self, cts_file, textgroup): + """ Wraps with a Try/Except the Work parsing from a cts file + + :param cts_file: Path to the CTS File + :type cts_file: str + :param textgroup: Textgroup to which the Work is a part of + :type textgroup: CtsTextgroupMetadata + :return: Parsed Work and the Texts, as well as the current file directory + """ + try: + return self._parse_work(cts_file, textgroup) + except Exception as E: + self.logger.error("Error parsing %s ", cts_file) + if self.RAISE_ON_GENERIC_PARSING_ERROR: + raise E + + def _parse_work(self, cts_file, textgroup): + """ Parses a work from a cts file + + :param cts_file: Path to the CTS File + :type cts_file: str + :param textgroup: Textgroup to which the Work is a part of + :type textgroup: CtsTextgroupMetadata + :return: Parsed Work and the Texts, as well as the current file directory + """ + with io.open(cts_file) as __xml__: + work, texts = self.classes["work"].parse( + resource=__xml__, + parent=textgroup, + _cls_dict=self.classes, + _with_children=True + ) + + return work, texts, os.path.dirname(cts_file) + + def _parse_text(self, text, directory): + """ Complete the TextMetadata object with its citation scheme by parsing the original text + + :param text: Text Metadata collection + :type text: XmlCtsTextMetadata + :param directory: Directory in which the metadata was found and where the text file should be + :type directory: str + :returns: True if all went well + :rtype: bool + """ + text_id, text_metadata = text.id, text + text_metadata.path = "{directory}/{textgroup}.{work}.{version}.xml".format( + directory=directory, + textgroup=text_metadata.urn.textgroup, + work=text_metadata.urn.work, + version=text_metadata.urn.version + ) + if os.path.isfile(text_metadata.path): + try: + text = self.read(text_id, path=text_metadata.path) + cites = list() + for cite in [c for c in text.citation][::-1]: + if len(cites) >= 1: + cites.append(self.classes["citation"]( + xpath=cite.xpath.replace("'", '"'), + scope=cite.scope.replace("'", '"'), + name=cite.name, + child=cites[-1] + )) + else: + cites.append(self.classes["citation"]( + xpath=cite.xpath.replace("'", '"'), + scope=cite.scope.replace("'", '"'), + name=cite.name + )) + del text + text_metadata.citation = cites[-1] + self.logger.info("%s has been parsed ", text_metadata.path) + if text_metadata.citation.isEmpty(): + self.logger.error("%s has no passages", text_metadata.path) + return False + return True + except Exception: + self.logger.error( + "%s does not accept parsing at some level (most probably citation) ", + text_metadata.path + ) + return False + else: + self.logger.error("%s is not present", text_metadata.path) + return False + + def _dispatch(self, textgroup, directory): + """ Run the dispatcher over a textgroup. + + :param textgroup: Textgroup object that needs to be dispatched + :param directory: Directory in which the textgroup was found + """ + if textgroup.id in self.dispatcher.collection: + self.dispatcher.collection[textgroup.id].update(textgroup) + else: + self.dispatcher.dispatch(textgroup, path=directory) + + for work_urn, work in textgroup.works.items(): + if work_urn in self.dispatcher.collection[textgroup.id].works: + self.dispatcher.collection[work_urn].update(work) + + def _dispatch_container(self, textgroup, directory): + """ Run the dispatcher over a textgroup within a try/except block + + .. note:: This extraction allows to change the dispatch routine \ + without having to care for the error dispatching + + :param textgroup: Textgroup object that needs to be dispatched + :param directory: Directory in which the textgroup was found + """ + try: + self._dispatch(textgroup, directory) + except UndispatchedTextError as E: + self.logger.error("Error dispatching %s ", directory) + if self.RAISE_ON_UNDISPATCHED is True: + raise E + + def _clean_invalids(self, invalids): + """ Optionally remove texts that were found to be invalid + + :param invalids: List of text identifiers + :type invalids: [CtsTextMetadata] + """ + pass + def parse(self, resource): - """ Parse a list of directories and reades it into a collection + """ Parse a list of directories and reads it into a collection :param resource: List of folders :return: An inventory resource and a list of CtsTextMetadata metadata-objects """ + textgroups = [] + texts = [] + invalids = [] + for folder in resource: - textgroups = glob("{base_folder}/data/*/__cts__.xml".format(base_folder=folder)) - for __cts__ in textgroups: - try: - with io.open(__cts__) as __xml__: - textgroup = XmlCtsTextgroupMetadata.parse( - resource=__xml__ - ) - tg_urn = str(textgroup.urn) - if tg_urn in self.inventory: - self.inventory[tg_urn].update(textgroup) - else: - self.dispatcher.dispatch(textgroup, path=__cts__) - - for __subcts__ in glob("{parent}/*/__cts__.xml".format(parent=os.path.dirname(__cts__))): - with io.open(__subcts__) as __xml__: - work = XmlCtsWorkMetadata.parse( - resource=__xml__, - parent=self.inventory[tg_urn] - ) - work_urn = str(work.urn) - if work_urn in self.inventory[tg_urn].works: - self.inventory[work_urn].update(work) - - for __textkey__ in work.texts: - __text__ = self.inventory[__textkey__] - __text__.path = "{directory}/{textgroup}.{work}.{version}.xml".format( - directory=os.path.dirname(__subcts__), - textgroup=__text__.urn.textgroup, - work=__text__.urn.work, - version=__text__.urn.version - ) - if os.path.isfile(__text__.path): - try: - with io.open(__text__.path) as f: - t = CapitainsCtsText(resource=self.xmlparse(f)) - cites = list() - for cite in [c for c in t.citation][::-1]: - if len(cites) >= 1: - cites.append(XmlCtsCitation( - xpath=cite.xpath.replace("'", '"'), - scope=cite.scope.replace("'", '"'), - name=cite.name, - child=cites[-1] - )) - else: - cites.append(XmlCtsCitation( - xpath=cite.xpath.replace("'", '"'), - scope=cite.scope.replace("'", '"'), - name=cite.name - )) - del t - __text__.citation = cites[-1] - self.logger.info("%s has been parsed ", __text__.path) - if __text__.citation.isEmpty() is False: - self.texts.append(__text__) - else: - self.logger.error("%s has no passages", __text__.path) - except Exception: - self.logger.error( - "%s does not accept parsing at some level (most probably citation) ", - __text__.path - ) - else: - self.logger.error("%s is not present", __text__.path) - except UndispatchedTextError as E: - self.logger.error("Error dispatching %s ", __cts__) - if self.RAISE_ON_UNDISPATCHED is True: - raise E - except Exception as E: - self.logger.error("Error parsing %s ", __cts__) - - return self.inventory, self.texts + cts_files = glob("{base_folder}/data/*/__cts__.xml".format(base_folder=folder)) + for cts_file in cts_files: + textgroup, cts_file = self._parse_textgroup(cts_file) + textgroups.append((textgroup, cts_file)) + + for textgroup, cts_textgroup_file in textgroups: + cts_work_files = glob("{parent}/*/__cts__.xml".format(parent=os.path.dirname(cts_textgroup_file))) + + for cts_work_file in cts_work_files: + _, parsed_texts, directory = self._parse_work(cts_work_file, textgroup) + texts.extend([(text, directory) for text in parsed_texts]) + + for text, directory in texts: + # If text_id is not none, the text parsing errored + if not self._parse_text(text, directory): + invalids.append(text) + + # Dispatching routine + for textgroup, textgroup_path in textgroups: + self._dispatch_container(textgroup, textgroup_path) + + # Clean invalids if there was a need + self._clean_invalids(invalids) + + self.inventory = self.dispatcher.collection + return self.inventory def __getText__(self, urn): """ Returns a CtsTextMetadata object @@ -176,7 +323,7 @@ def __getText__(self, urn): urn = [ t.id for t in self.texts - if t.id.startswith(str(urn)) and isinstance(t, XmlCtsEditionMetadata) + if t.id.startswith(str(urn)) and isinstance(t, CtsEditionMetadata) ] if len(urn) > 0: urn = URN(urn[0]) @@ -189,7 +336,7 @@ def __getText__(self, urn): if os.path.isfile(text.path): with io.open(text.path) as __xml__: - resource = self.TEXT_CLASS(urn=urn, resource=self.xmlparse(__xml__)) + resource = self.classes["text"](urn=urn, resource=self.xmlparse(__xml__)) else: resource = None self.logger.warning('The file {} is mentioned in the metadata but does not exist'.format(text.path)) @@ -291,9 +438,10 @@ def getMetadata(self, objectId=None, **filters): # We store inventory names and if there is only one we recreate the inventory inv_names = [text.parent.parent.parent.id for text in texts] if len(set(inv_names)) == 1: - inventory = XmlCtsTextInventoryMetadata(name=inv_names[0]) + inventory = self.classes["inventory"](name=inv_names[0]) else: - inventory = XmlCtsTextInventoryMetadata() + inventory = self.classes["inventory"]() + # For each text we found using the filter for text in texts: tg_urn = str(text.parent.parent.urn) @@ -301,19 +449,19 @@ def getMetadata(self, objectId=None, **filters): txt_urn = str(text.urn) # If we need to generate a textgroup object if tg_urn not in inventory.textgroups: - XmlCtsTextgroupMetadata(urn=tg_urn, parent=inventory) + self.classes["textgroup"](urn=tg_urn, parent=inventory) # If we need to generate a work object if wk_urn not in inventory.textgroups[tg_urn].works: - XmlCtsWorkMetadata(urn=wk_urn, parent=inventory.textgroups[tg_urn]) + self.classes["work"](urn=wk_urn, parent=inventory.textgroups[tg_urn]) - if isinstance(text, XmlCtsEditionMetadata): - x = XmlCtsEditionMetadata(urn=txt_urn, parent=inventory.textgroups[tg_urn].works[wk_urn]) + if isinstance(text, CtsEditionMetadata): + x = self.classes["edition"](urn=txt_urn, parent=inventory.textgroups[tg_urn].works[wk_urn]) x.citation = text.citation - elif isinstance(text, XmlCtsTranslationMetadata): - x = XmlCtsTranslationMetadata(urn=txt_urn, parent=inventory.textgroups[tg_urn].works[wk_urn], lang=text.lang) + elif isinstance(text, CtsTranslationMetadata): + x = self.classes["translation"](urn=txt_urn, parent=inventory.textgroups[tg_urn].works[wk_urn], lang=text.lang) x.citation = text.citation - elif isinstance(text, XmlCtsCommentaryMetadata): - x = XmlCtsCommentaryMetadata(urn=txt_urn, parent=inventory.textgroups[tg_urn].works[wk_urn], lang=text.lang) + elif isinstance(text, CtsCommentaryMetadata): + x = self.classes["commentary"](urn=txt_urn, parent=inventory.textgroups[tg_urn].works[wk_urn], lang=text.lang) x.citation = text.citation return inventory[objectId] diff --git a/MyCapytain/resources/collections/cts.py b/MyCapytain/resources/collections/cts.py index 9a048271..250eccc2 100644 --- a/MyCapytain/resources/collections/cts.py +++ b/MyCapytain/resources/collections/cts.py @@ -19,13 +19,16 @@ from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes, RDF_NAMESPACES +_CLASSES_DICT = {} + + class XmlCtsCitation(CitationPrototype): """ XmlCtsCitation XML implementation for CtsTextInventoryMetadata """ @staticmethod - def ingest(resource, element=None, xpath="ti:citation"): + def ingest(resource, element=None, xpath="ti:citation", _cls_dict=_CLASSES_DICT): """ Ingest xml to create a citation :param resource: XML on which to do xpath @@ -36,22 +39,23 @@ def ingest(resource, element=None, xpath="ti:citation"): """ # Reuse of of find citation results = resource.xpath(xpath, namespaces=XPATH_NAMESPACES) + CLASS = _cls_dict.get("citation", XmlCtsCitation) if len(results) > 0: - citation = XmlCtsCitation( + citation = CLASS( name=results[0].get("label"), xpath=results[0].get("xpath"), scope=results[0].get("scope") ) - if isinstance(element, XmlCtsCitation): + if isinstance(element, CLASS): element.child = citation - XmlCtsCitation.ingest( + CLASS.ingest( resource=results[0], element=element.child ) else: element = citation - XmlCtsCitation.ingest( + CLASS.ingest( resource=results[0], element=element ) @@ -75,12 +79,14 @@ def xpathDict(xml, xpath, cls, parent, **kwargs): :rtype: collections.defaultdict. :returns: Dictionary of children """ + children = [] for child in xml.xpath(xpath, namespaces=XPATH_NAMESPACES): - cls.parse( + children.append(cls.parse( resource=child, parent=parent, **kwargs - ) + )) + return children def __parse_structured_metadata__(obj, xml): @@ -135,13 +141,14 @@ def __findCitations(obj, xml, xpath="ti:citation"): """ @staticmethod - def parse_metadata(obj, xml): + def parse_metadata(obj, xml, _cls_dict=_CLASSES_DICT): """ Parse a resource to feed the object :param obj: Obj to set metadata of :type obj: XmlCtsTextMetadata :param xml: An xml representation object :type xml: lxml.etree._Element + :param _cls_dict: Dictionary of classes to generate subclasses """ for child in xml.xpath("ti:description", namespaces=XPATH_NAMESPACES): @@ -154,7 +161,7 @@ def parse_metadata(obj, xml): if lg is not None: obj.set_cts_property("label", child.text, lg) - obj.citation = XmlCtsCitation.ingest(xml, obj.citation, "ti:online/ti:citationMapping/ti:citation") + obj.citation = _cls_dict.get("citation", XmlCtsCitation).ingest(xml, obj.citation, "ti:online/ti:citationMapping/ti:citation") # Added for commentary for child in xml.xpath("ti:about", namespaces=XPATH_NAMESPACES): @@ -173,15 +180,26 @@ def parse_metadata(obj, xml): obj.metadata["namespaceMapping"][namespaceMapping.get("abbreviation")] = namespaceMapping.get("nsURI") """ + def __init__(self, *args, **kwargs): + super(XmlCtsTextMetadata, self).__init__(*args, **kwargs) + self._path = None + + @property + def path(self): + return self._path + + @path.setter + def path(self, value): + self._path = value class XmlCtsEditionMetadata(cts.CtsEditionMetadata, XmlCtsTextMetadata): """ Create an edition subtyped CtsTextMetadata object """ @staticmethod - def parse(resource, parent=None): + def parse(resource, parent=None, _cls_dict=_CLASSES_DICT): xml = xmlparser(resource) - o = XmlCtsEditionMetadata(urn=xml.get("urn"), parent=parent) - XmlCtsEditionMetadata.parse_metadata(o, xml) + o = _cls_dict.get("edition", XmlCtsEditionMetadata)(urn=xml.get("urn"), parent=parent) + type(o).parse_metadata(o, xml) return o @@ -190,14 +208,14 @@ class XmlCtsTranslationMetadata(cts.CtsTranslationMetadata, XmlCtsTextMetadata): """ Create a translation subtyped CtsTextMetadata object """ @staticmethod - def parse(resource, parent=None): + def parse(resource, parent=None, _cls_dict=_CLASSES_DICT): xml = xmlparser(resource) lang = xml.get("{http://www.w3.org/XML/1998/namespace}lang") - o = XmlCtsTranslationMetadata(urn=xml.get("urn"), parent=parent) + o = _cls_dict.get("translation", XmlCtsTranslationMetadata)(urn=xml.get("urn"), parent=parent) if lang is not None: o.lang = lang - XmlCtsTranslationMetadata.parse_metadata(o, xml) + type(o).parse_metadata(o, xml) return o @@ -205,14 +223,14 @@ class XmlCtsCommentaryMetadata(cts.CtsCommentaryMetadata, XmlCtsTextMetadata): """ Create a commentary subtyped PrototypeText object """ @staticmethod - def parse(resource, parent=None): + def parse(resource, parent=None, _cls_dict=_CLASSES_DICT): xml = xmlparser(resource) lang = xml.get("{http://www.w3.org/XML/1998/namespace}lang") - o = XmlCtsCommentaryMetadata(urn=xml.get("urn"), parent=parent) + o = _cls_dict.get("commentary", XmlCtsCommentaryMetadata)(urn=xml.get("urn"), parent=parent) if lang is not None: o.lang = lang - XmlCtsCommentaryMetadata.parse_metadata(o, xml) + type(o).parse_metadata(o, xml) return o @@ -221,16 +239,17 @@ class XmlCtsWorkMetadata(cts.CtsWorkMetadata): """ @staticmethod - def parse(resource, parent=None): + def parse(resource, parent=None, _cls_dict=_CLASSES_DICT, _with_children=False): """ Parse a resource :param resource: Element rerpresenting a work :param type: basestring, etree._Element :param parent: Parent of the object :type parent: XmlCtsTextgroupMetadata + :param _cls_dict: Dictionary of classes to generate subclasses """ xml = xmlparser(resource) - o = XmlCtsWorkMetadata(urn=xml.get("urn"), parent=parent) + o = _cls_dict.get("work", XmlCtsWorkMetadata)(urn=xml.get("urn"), parent=parent) lang = xml.get("{http://www.w3.org/XML/1998/namespace}lang") if lang is not None: @@ -242,13 +261,28 @@ def parse(resource, parent=None): o.set_cts_property("title", child.text, lg) # Parse children - xpathDict(xml=xml, xpath='ti:edition', cls=XmlCtsEditionMetadata, parent=o) - xpathDict(xml=xml, xpath='ti:translation', cls=XmlCtsTranslationMetadata, parent=o) + children = [] + children.extend(xpathDict( + xml=xml, xpath='ti:edition', + cls=_cls_dict.get("edition", XmlCtsEditionMetadata), parent=o, + _cls_dict=_cls_dict + )) + children.extend(xpathDict( + xml=xml, xpath='ti:translation', + cls=_cls_dict.get("translation", XmlCtsTranslationMetadata), parent=o, + _cls_dict=_cls_dict + )) # Added for commentary - xpathDict(xml=xml, xpath='ti:commentary', cls=XmlCtsCommentaryMetadata, parent=o) + children.extend(xpathDict( + xml=xml, xpath='ti:commentary', + cls=_cls_dict.get("commentary", XmlCtsCommentaryMetadata), parent=o, + _cls_dict=_cls_dict + )) __parse_structured_metadata__(o, xml) + if _with_children: + return o, children return o @@ -257,14 +291,15 @@ class XmlCtsTextgroupMetadata(cts.CtsTextgroupMetadata): """ @staticmethod - def parse(resource, parent=None): + def parse(resource, parent=None, _cls_dict=_CLASSES_DICT): """ Parse a textgroup resource :param resource: Element representing the textgroup :param parent: Parent of the textgroup + :param _cls_dict: Dictionary of classes to generate subclasses """ xml = xmlparser(resource) - o = XmlCtsTextgroupMetadata(urn=xml.get("urn"), parent=parent) + o = _cls_dict.get("textgroup", XmlCtsTextgroupMetadata)(urn=xml.get("urn"), parent=parent) for child in xml.xpath("ti:groupname", namespaces=XPATH_NAMESPACES): lg = child.get("{http://www.w3.org/XML/1998/namespace}lang") @@ -272,7 +307,7 @@ def parse(resource, parent=None): o.set_cts_property("groupname", child.text, lg) # Parse Works - xpathDict(xml=xml, xpath='ti:work', cls=XmlCtsWorkMetadata, parent=o) + xpathDict(xml=xml, xpath='ti:work', cls=_cls_dict.get("work", XmlCtsWorkMetadata), parent=o) __parse_structured_metadata__(o, xml) return o @@ -283,14 +318,14 @@ class XmlCtsTextInventoryMetadata(cts.CtsTextInventoryMetadata): """ @staticmethod - def parse(resource): - """ Parse a resource + def parse(resource, _cls_dict=_CLASSES_DICT): + """ Parse a resource :param resource: Element representing the text inventory - :param type: basestring, etree._Element + :param _cls_dict: Dictionary of classes to generate subclasses """ xml = xmlparser(resource) - o = XmlCtsTextInventoryMetadata(name=xml.xpath("//ti:TextInventory", namespaces=XPATH_NAMESPACES)[0].get("tiid") or "") + o = _cls_dict.get("inventory", XmlCtsTextInventoryMetadata)(name=xml.xpath("//ti:TextInventory", namespaces=XPATH_NAMESPACES)[0].get("tiid") or "") # Parse textgroups - xpathDict(xml=xml, xpath='//ti:textgroup', cls=XmlCtsTextgroupMetadata, parent=o) - return o + xpathDict(xml=xml, xpath='//ti:textgroup', cls=_cls_dict.get("textgroup", XmlCtsTextgroupMetadata), parent=o) + return o \ No newline at end of file diff --git a/MyCapytain/resources/prototypes/cts/inventory.py b/MyCapytain/resources/prototypes/cts/inventory.py index 519d4eaa..6f4ce6e6 100644 --- a/MyCapytain/resources/prototypes/cts/inventory.py +++ b/MyCapytain/resources/prototypes/cts/inventory.py @@ -28,7 +28,6 @@ class PrototypeCtsCollection(Collection): :type identifier: str,URN :cvar CTS_MODEL: String Representation of the type of collection """ - CTS_MODEL = "CtsCollection" DC_TITLE_KEY = None CTS_PROPERTIES = [] CTS_LINKS = [] @@ -39,10 +38,6 @@ class PrototypeCtsCollection(Collection): def __init__(self, identifier=""): super(PrototypeCtsCollection, self).__init__(identifier) - if hasattr(type(self), "CTS_MODEL"): - self.graph.set((self.asNode(), RDF.type, RDF_NAMESPACES.CTS.term(self.CTS_MODEL))) - self.graph.set((self.asNode(), RDF_NAMESPACES.DTS.isA, RDF_NAMESPACES.CTS.term(self.CTS_MODEL))) - self.__urn__ = "" @property @@ -135,14 +130,12 @@ def __xml_export_generic__(self, attrs, namespaces=False, lines="\n", members=No :param lines: New Line Character (Can be empty) :return: String representation of XML Nodes """ - if namespaces is True: - attrs.update(self.__namespaces_header__(cpt=(output == Mimetypes.XML.CapiTainS.CTS))) TYPE_URI = self.TYPE_URI if TYPE_URI == RDF_NAMESPACES.CTS.term("CtsTextInventoryCollection"): TYPE_URI = RDF_NAMESPACES.CTS.term("TextInventory") - strings = [make_xml_node(self.graph, TYPE_URI, close=False, attributes=attrs)] + strings = [] for pred in self.CTS_PROPERTIES: for obj in self.metadata.get(pred): strings.append( @@ -167,6 +160,11 @@ def __xml_export_generic__(self, attrs, namespaces=False, lines="\n", members=No strings.append(make_xml_node(self.graph, TYPE_URI, close=True)) + if namespaces is True: + attrs.update(self.__namespaces_header__(cpt=(output == Mimetypes.XML.CapiTainS.CTS))) + + strings = [make_xml_node(self.graph, TYPE_URI, close=False, attributes=attrs)] + strings + return lines.join(strings) def __export__(self, output=None, domain=""): @@ -445,7 +443,7 @@ def texts(self): :return: Dictionary of texts :rtype: defaultdict(:class:`PrototypeTexts`) """ - return self.__children__ + return self.children @property def lang(self): @@ -480,7 +478,7 @@ def update(self, other): elif self.urn != other.urn: raise InvalidURN("Cannot add CtsWorkMetadata %s to CtsWorkMetadata %s " % (self.urn, other.urn)) - for urn, text in other.texts.items(): + for urn, text in other.children.items(): self.texts[urn] = text self.texts[urn].parent = self self.texts[urn].resource = None @@ -566,7 +564,7 @@ def works(self): :return: Dictionary of works :rtype: defaultdict(:class:`PrototypeWorks`) """ - return self.__children__ + return self.children def update(self, other): """ Merge two Textgroup Objects. @@ -646,7 +644,7 @@ def textgroups(self): :return: Dictionary of textgroups :rtype: defaultdict(:class:`CtsTextgroupMetadata`) """ - return self.__children__ + return self.children def __len__(self): """ Get the number of text in the Inventory diff --git a/tests/common/test_reference.py b/tests/common/test_reference.py index b0b647fe..545b5df3 100644 --- a/tests/common/test_reference.py +++ b/tests/common/test_reference.py @@ -250,6 +250,7 @@ def test_set(self): a.namespace = "greekLit" self.assertEqual(str(a), "urn:cts:greekLit:phi1293.phi001.perseus-eng2:2.2") + class TestCitation(unittest.TestCase): """ Test the citation object """ def test_updateRefsdecl(self): diff --git a/tests/resolvers/cts/test_local.py b/tests/resolvers/cts/test_local.py index fb85c9ee..a86b473d 100644 --- a/tests/resolvers/cts/test_local.py +++ b/tests/resolvers/cts/test_local.py @@ -771,3 +771,47 @@ def dispatchGreekLit(collection, path=None, **kwargs): len(all.readableDescendants), 26, "There should be all 26 readable descendants in the master collection" ) + + def test_post_work_dispatching_active(self): + """ Dispatching is working after editions, we dispatch based on citation scheme""" + tic = CtsTextInventoryCollection() + poetry = CtsTextInventoryMetadata("urn:perseus:poetry", parent=tic) + prose = CtsTextInventoryMetadata("urn:perseus:prose", parent=tic) + + dispatcher = CollectionDispatcher(tic, default_inventory_name="urn:perseus:prose") + + @dispatcher.inventory("urn:perseus:poetry") + def dispatchPoetry(collection, **kwargs): + for readable in collection.readableDescendants: + for citation in readable.citation: + if citation.name == "line": + return True + return False + + resolver = CtsCapitainsLocalResolver( + ["./tests/testing_data/latinLit2"], + dispatcher=dispatcher + ) + + all = resolver.getMetadata().export(Mimetypes.XML.CTS) + poetry_stuff = resolver.getMetadata("urn:perseus:poetry").export(Mimetypes.XML.CTS) + prose_stuff = resolver.getMetadata("urn:perseus:prose").export(Mimetypes.XML.CTS) + get_graph().remove((None, None, None)) + del poetry, prose + poetry, prose = XmlCtsTextInventoryMetadata.parse(poetry_stuff), XmlCtsTextInventoryMetadata.parse(prose_stuff) + self.assertEqual( + len(poetry.textgroups), 3, + "There should be 3 textgroups in Poetry (Martial, Ovid and Juvenal)" + ) + self.assertIsInstance(poetry, CtsTextInventoryMetadata, "should be textinventory") + self.assertEqual( + len(prose.textgroups), 1, + "There should be one textgroup in Prose (Greek texts)" + ) + get_graph().remove((None, None, None)) + del poetry, prose + all = XmlCtsTextInventoryMetadata.parse(all) + self.assertEqual( + len(all.readableDescendants), 26, + "There should be all 26 readable descendants in the master collection" + ) \ No newline at end of file From 73b44e20f5a0da2ab765092adf695faac1d86f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Mon, 2 Apr 2018 21:29:15 +0200 Subject: [PATCH 02/89] Added basic retriever function for get Collection + Pagination object so that people can crawl multiple page --- MyCapytain/common/utils.py | 52 ++++++++++- MyCapytain/retrievers/dts.py | 101 ++++++++++++++++++++ tests/retrievers/test_dts.py | 174 +++++++++++++++++++++++++++++++++++ 3 files changed, 323 insertions(+), 4 deletions(-) create mode 100644 MyCapytain/retrievers/dts.py create mode 100644 tests/retrievers/test_dts.py diff --git a/MyCapytain/common/utils.py b/MyCapytain/common/utils.py index 26ef9e0c..9f1412dc 100644 --- a/MyCapytain/common/utils.py +++ b/MyCapytain/common/utils.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -.. module:: MyCapytain.common.reference - :synopsis: Common useful tools and constants +.. module:: MyCapytain.common.utils + :synopsis: Common useful tools .. moduleauthor:: Thibault Clérice @@ -10,7 +10,7 @@ from __future__ import unicode_literals import re -from collections import OrderedDict, defaultdict +from collections import OrderedDict, defaultdict, namedtuple from copy import copy from functools import reduce from io import IOBase, StringIO @@ -20,6 +20,8 @@ from six import text_type from xml.sax.saxutils import escape from rdflib import BNode, Graph, Literal, URIRef +from urllib.parse import urlparse, parse_qs, urljoin +import link_header from MyCapytain.common.constants import XPATH_NAMESPACES @@ -461,4 +463,46 @@ def expand_namespace(nsmap, string): for ns in nsmap: if isinstance(string, str) and isinstance(ns, str) and string.startswith(ns+":"): return string.replace(ns+":", nsmap[ns]) - return string \ No newline at end of file + return string + + +_Route = namedtuple("Route", ["path", "query_dict"]) +_Navigation = namedtuple("Navigation", ["prev", "next", "last", "current", "first"]) + + +def parse_pagination(headers): + """ Parses headers to create a pagination objects + + :param headers: HTTP Headers + :type headers: dict + :return: Navigation object for pagination + :rtype: _Navigation + """ + links = { + link.rel: parse_qs(link.href).get("page", None) + for link in link_header.parse(headers.get("Link", "")).links + } + return _Navigation( + links.get("previous", [None])[0], + links.get("next", [None])[0], + links.get("last", [None])[0], + links.get("current", [None])[0], + links.get("first", [None])[0] + ) + + +def parse_uri(uri, endpoint_uri): + """ Parse a URI into a Route namedtuple + + :param uri: URI or relative URI + :type uri: str + :param endpoint_uri: URI of the endpoint + :type endpoint_uri: str + :return: Parsed route + :rtype: _Route + """ + temp_parse = urlparse(uri) + return _Route( + urljoin(endpoint_uri, temp_parse.path), + parse_qs(temp_parse.query) + ) diff --git a/MyCapytain/retrievers/dts.py b/MyCapytain/retrievers/dts.py new file mode 100644 index 00000000..8c90c498 --- /dev/null +++ b/MyCapytain/retrievers/dts.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +""" +.. module:: MyCapytain.retrievers.cts5 + :synopsis: Cts5 endpoint implementation + +.. moduleauthor:: Thibault Clérice + + +""" +import MyCapytain.retrievers.prototypes +from MyCapytain import __version__ +import requests +from MyCapytain.common.utils import parse_uri, parse_pagination + + +class DTS_Retriever(MyCapytain.retrievers.prototypes.API): + def __init__(self, endpoint): + super(DTS_Retriever, self).__init__(endpoint) + self._routes = None + + def call(self, route, parameters, mimetype="application/ld+json"): + """ Call an endpoint given the parameters + + :param route: Named of the route which is called + :type route: str + :param parameters: Dictionary of parameters + :type parameters: dict + :param mimetype: Mimetype to require + :type mimetype: str + :rtype: text + """ + + parameters = { + key: str(parameters[key]) for key in parameters if parameters[key] is not None + } + parameters.update(self.routes[route].query_dict) + + request = requests.get( + self.routes[route].path, + params=parameters, + headers={ + "Accept": mimetype, + "Accept-Charset": "utf-8", + "User-Agent": "MyCapytain/{MyCapVersion} {DefaultRequestUA}".format( + MyCapVersion=__version__, + DefaultRequestUA=requests.utils.default_user_agent() + ) + } + ) + request.raise_for_status() + if request.encoding is None: + request.encoding = "utf-8" + return request.text, parse_pagination(request.headers) + + @property + def routes(self): + """ Retrieves the main routes of the DTS Collection + + Response format expected : + { + "@context": "/dts/api/contexts/EntryPoint.jsonld", + "@id": "/dts/api/", + "@type": "EntryPoint", + "collections": "/dts/api/collections/", + "documents": "/dts/api/documents/", + "navigation" : "/dts/api/navigation" + } + + :returns: Dictionary of main routes with their path + :rtype: dict + """ + if self._routes: + return self._routes + + request = requests.get(self.endpoint) + request.raise_for_status() + data = request.json() + self._routes = { + "collections": parse_uri(data["collections"], self.endpoint), + "documents": parse_uri(data["documents"], self.endpoint), + "navigation": parse_uri(data["navigation"], self.endpoint) + } + return self._routes + + def get_collection(self, collection_id=None, nav="children", page=None): + """ Makes a call on the Collection API + + :param collection_id: Id of the collection to retrieve + :param nav: Direction of the navigation + :param page: Page to retrieve + :return: Response and Navigation Tuple + :rtype: (str, MyCapytain.common.utils._Navigation) + """ + return self.call( + "collections", + { + "id": collection_id, + "nav": nav, + "page": page + } + ) diff --git a/tests/retrievers/test_dts.py b/tests/retrievers/test_dts.py new file mode 100644 index 00000000..f3bce0bb --- /dev/null +++ b/tests/retrievers/test_dts.py @@ -0,0 +1,174 @@ +import unittest + +import responses + +from MyCapytain.retrievers.dts import DTS_Retriever +from MyCapytain.common.utils import _Navigation +from urllib.parse import parse_qs, urlparse, urljoin + + +_SERVER_URI = "http://domainname.com/api/dts/" +patch_args = ("MyCapytain.retrievers.dts.requests.get", ) + + +class TestEndpointsCts5(unittest.TestCase): + """ Test Cts5 Endpoint request making """ + + def setUp(self): + self.cli = DTS_Retriever(_SERVER_URI) + + @property + def calls(self): + return list([ + ( + urljoin( + _SERVER_URI, + urlparse(call.request.url).path + ), + parse_qs(urlparse(call.request.url).query) + ) + for call in responses.calls + ]) + + def assertInCalls(self, uri, qs=None, index=None): + """ Asserts that URI and QueryString have been contacted""" + if index: + called_uri, called_qs = self.calls[index] + self.assertEqual(called_uri, uri) + self.assertEqual(called_qs, qs) + return + self.assertIn( + (uri, qs), self.calls + ) + + @staticmethod + def add_index_response(): + """ Add a simple index response for the dynamic routing """ + responses.add( + responses.GET, _SERVER_URI, + json={ + "@context": "/dts/api/contexts/EntryPoint.jsonld", + "@id": "/dts/api/", + "@type": "EntryPoint", + "collections": "./collections/", + "documents": "./documents/", + "navigation": "./navigation" + } + ) + + @staticmethod + def add_index_with_query_string_response(): + """ Add an index response which contains URI using Query String """ + responses.add( + responses.GET, _SERVER_URI, + json={ + "@context": "/dts/api/contexts/EntryPoint.jsonld", + "@id": "/dts/api/", + "@type": "EntryPoint", + "collections": "?api=collections", + "documents": "?api=documents", + "navigation": "?api=navigation" + } + ) + + @staticmethod + def add_collection_response(uri=None): + """ Adds a collection response to the given URI """ + if uri is None: + uri = _SERVER_URI+"collections/?nav=children" + responses.add( + responses.GET, uri, + json={ + "@context": { + "@base": "http://www.w3.org/ns/hydra/context.jsonld", + "dct": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#", + "dc": "http://purl.org/dc/elements/1.1/", + "tei": "http://www.tei-c.org/ns/1.0", + }, + "@id" : "lettres_de_poilus", + "@type" : "Collection", + "totalItems" : "10000", + "title": "Collection Générale de l'École Nationale des Chartes", + "dts:dublincore": { + "dc:publisher": ["École Nationale des Chartes", "https://viaf.org/viaf/167874585"], + "dc:title": [ + {"fre" : "Collection Générale de l'École Nationale des Chartes"} + ] + }, + "member": ["member 190 up to 200"], + "view": { + "@id": "/api/dts/collections/?id=lettres_de_poilus&page=19", + "@type": "PartialCollectionView", + "first": "/api/dts/collections/?id=lettres_de_poilus&page=1", + "previous": "/api/dts/collections/?id=lettres_de_poilus&page=18", + "next": "/api/dts/collections/?id=lettres_de_poilus&page=20", + "last": "/api/dts/collections/?id=lettres_de_poilus&page=500" + } + }, + headers={ + "Content-Type": "application/ld+json", + "Link": '; rel="first",' + '; rel="previous", ' + '; rel="next", ' + '; rel="last"' + } + ) + + @responses.activate + def test_routes(self): + self.add_index_response() + self.assertEqual( + self.cli.routes["collections"].path, _SERVER_URI + "collections/" + ) + self.assertEqual( + self.cli.routes["documents"].path, _SERVER_URI + "documents/" + ) + self.assertEqual( + self.cli.routes["navigation"].path, _SERVER_URI + "navigation" + ) + + @responses.activate + def test_get_collection_headers_parsing_and_hit(self): + """ Check that the right URI is connected""" + self.add_index_response() + self.add_collection_response() + + response, pagination = self.cli.get_collection() + self.assertEqual( + pagination, + _Navigation("18", "20", "500", None, "1") + ) + + self.assertInCalls(_SERVER_URI+"collections/", {"nav": ["children"]}, ) + + @responses.activate + def test_querystring_type_of_route(self): + """ Check that routes using Query String, such as ?api=documents + works with new parameters + """ + self.add_index_with_query_string_response() + self.add_collection_response( + uri=_SERVER_URI+"?api=collections&id=Hello&page=19&nav=parents" + ) + response, pagination = self.cli.get_collection( + collection_id="Hello", + nav="parents", + page=19 + ) + + self.assertEqual( + pagination, + _Navigation("18", "20", "500", None, "1") + ) + self.assertInCalls(_SERVER_URI, {}, 0) + self.assertInCalls( + uri=_SERVER_URI, + qs={ + "api": ["collections"], + "id": ["Hello"], + "page": ["19"], + "nav": ["parents"] + }, + index=1 + ) From 05d4ef410b31ce71d245d0759a774fc04f4e106d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 3 Apr 2018 16:39:11 +0200 Subject: [PATCH 03/89] =?UTF-8?q?Forgot=20a=20requirement=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 9c0ffb03..8b9e01e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ six>=1.10.0 xmlunittest>=0.3.2 rdflib-jsonld>=0.4.0 responses>=0.8.1 +LinkHeader==0.4.3 \ No newline at end of file From 4096b3835c8bfaaa27e701c07e76dd779f8cf028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 1 May 2018 18:08:10 +0200 Subject: [PATCH 04/89] Working on DTS Collection parsing --- MyCapytain/common/constants.py | 3 +- MyCapytain/common/metadata.py | 6 +- MyCapytain/errors.py | 5 ++ MyCapytain/resources/collections/dts.py | 66 ++++++++++++--------- MyCapytain/resources/prototypes/metadata.py | 6 +- MyCapytain/retrievers/dts.py | 4 +- tests/resources/collections/test_dts.py | 20 +++++++ tests/retrievers/test_dts.py | 4 +- tests/testing_data/dts/collection_1.json | 41 +++++++++++++ 9 files changed, 117 insertions(+), 38 deletions(-) create mode 100644 tests/resources/collections/test_dts.py create mode 100644 tests/testing_data/dts/collection_1.json diff --git a/MyCapytain/common/constants.py b/MyCapytain/common/constants.py index 44ecede8..ca9f5504 100644 --- a/MyCapytain/common/constants.py +++ b/MyCapytain/common/constants.py @@ -26,9 +26,10 @@ class RDF_NAMESPACES: :type CAPITAINS: Namespace """ CTS = Namespace("http://chs.harvard.edu/xmlns/cts/") - DTS = Namespace("http://w3id.org/dts-ontology/") + DTS = Namespace("https://w3id.org/dts/api#") TEI = Namespace("http://www.tei-c.org/ns/1.0/") CAPITAINS = Namespace("http://purl.org/capitains/ns/1.0#") + HYDRA = Namespace("https://www.w3.org/ns/hydra/core#") class Mimetypes: diff --git a/MyCapytain/common/metadata.py b/MyCapytain/common/metadata.py index 7145e393..cbc2080a 100644 --- a/MyCapytain/common/metadata.py +++ b/MyCapytain/common/metadata.py @@ -24,7 +24,8 @@ class Metadata(Exportable): :cvar DEFAULT_EXPORT: Default export (CTS XML Inventory) :cvar STORE: RDF Store """ - EXPORT_TO = [Mimetypes.JSON.Std, Mimetypes.XML.RDF, Mimetypes.XML.RDFa, Mimetypes.JSON.LD, Mimetypes.XML.CapiTainS.CTS] + EXPORT_TO = [Mimetypes.JSON.Std, Mimetypes.XML.RDF, Mimetypes.XML.RDFa, Mimetypes.JSON.LD, + Mimetypes.XML.CapiTainS.CTS] DEFAULT_EXPORT = Mimetypes.JSON.Std def __init__(self, node=None, *args, **kwargs): @@ -85,6 +86,9 @@ def get_single(self, key, lang=None): :param lang: Language of the triple if applicable :rtype: Literal or BNode or URIRef """ + if not isinstance(key, URIRef): + key = URIRef(key) + if lang is not None: default = None for o in self.graph.objects(self.asNode(), key): diff --git a/MyCapytain/errors.py b/MyCapytain/errors.py index bcf43997..65d74587 100644 --- a/MyCapytain/errors.py +++ b/MyCapytain/errors.py @@ -14,6 +14,11 @@ class MyCapytainException(BaseException): """ +class JsonLdCollectionMissing(MyCapytainException): + """ Error thrown when a JSON LD has now first ressource + """ + + class DuplicateReference(SyntaxWarning, MyCapytainException): """ Error generated when a duplicate is found in Reference """ diff --git a/MyCapytain/resources/collections/dts.py b/MyCapytain/resources/collections/dts.py index 9af80bf0..eb7fe392 100644 --- a/MyCapytain/resources/collections/dts.py +++ b/MyCapytain/resources/collections/dts.py @@ -1,8 +1,21 @@ from MyCapytain.resources.prototypes.metadata import Collection from rdflib import URIRef +from pyld import jsonld +from MyCapytain.errors import JsonLdCollectionMissing +from MyCapytain.common.constants import RDF_NAMESPACES + + +_hyd = RDF_NAMESPACES.HYDRA +_dts = RDF_NAMESPACES.DTS class DTSCollection(Collection): + @property + def size(self): + for value in self.metadata.get_single(RDF_NAMESPACES.HYDRA.totalItems): + return int(value) + return 0 + @staticmethod def parse(resource, mimetype="application/json+ld"): """ Given a dict representation of a json object, generate a DTS Collection @@ -11,37 +24,32 @@ def parse(resource, mimetype="application/json+ld"): :param mimetype: :return: """ + collection = jsonld.expand(resource) + if len(collection) == 0: + raise JsonLdCollectionMissing("Missing collection in JSON") + collection = collection[0] + obj = DTSCollection(identifier=resource["@id"]) - obj.type = resource["type"] - obj.version = resource["version"] - for label in resource["label"]: - obj.set_label(label["value"], label["lang"]) - for key, value in resource["metadata"].items(): + + # We retrieve first the descriptiooon and label that are dependant on Hydra + for val_dict in collection[str(_hyd.title)]: + obj.set_label(val_dict["@value"], None) + + for val_dict in collection[str(_hyd.totalItems)]: + obj.metadata.add(_hyd.totalItems, val_dict["@value"], 0) + + for val_dict in collection.get(str(_hyd.description), []): + obj.metadata.add(_hyd.description, val_dict["@value"], None) + + for key, value_set in collection[str(_dts.dublincore)][0].items(): + term = URIRef(key) + for value_dict in value_set: + obj.metadata.add(term, value_dict["@value"], value_dict.get("@language", None)) + + for key, value_set in collection[str(_dts.extensions)][0].items(): term = URIRef(key) - if isinstance(value, list): - if isinstance(value[0], dict): - for subvalue in value: - obj.metadata.add(term, subvalue["@value"], subvalue["@lang"]) - else: - for subvalue in value: - if subvalue.startswith("http") or subvalue.startswith("urn"): - obj.metadata.add(term, URIRef(subvalue)) - else: - obj.metadata.add(term, subvalue) - else: - if value.startswith("http") or value.startswith("urn"): - obj.metadata.add(term, URIRef(value)) - else: - obj.metadata.add(term, value) - - for member in resource["members"]["contents"]: - subobj = DTSCollectionShort.parse(member) - subobj.parent = member - - last = obj - for member in resource["parents"]: - subobj = DTSCollectionShort.parse(member) - last.parent = subobj + for value_dict in value_set: + obj.metadata.add(term, value_dict["@value"], value_dict.get("@language", None)) return obj diff --git a/MyCapytain/resources/prototypes/metadata.py b/MyCapytain/resources/prototypes/metadata.py index 9983aedc..06d8b976 100644 --- a/MyCapytain/resources/prototypes/metadata.py +++ b/MyCapytain/resources/prototypes/metadata.py @@ -182,8 +182,8 @@ def parent(self, parent): :return: """ self.__parent__ = parent - self.graph.set( - (self.asNode(), RDF_NAMESPACES.DTS.parent, parent.asNode()) + self.graph.add( + (self.asNode(), RDF_NAMESPACES.CAPITAINS.parent, parent.asNode()) ) parent.__add_member__(self) @@ -347,7 +347,7 @@ def __export__(self, output=None, domain=""): for member in self.members ] if self.parent: - o["@graph"][self.graph.qname(RDF_NAMESPACES.DTS.parents)] = [ + o["@graph"][self.graph.qname(RDF_NAMESPACES.CAPITAINS.parents)] = [ { "@id": member.id, RDFSLabel: LiteralToDict(member.get_label()) or member.id, diff --git a/MyCapytain/retrievers/dts.py b/MyCapytain/retrievers/dts.py index 8c90c498..6771b828 100644 --- a/MyCapytain/retrievers/dts.py +++ b/MyCapytain/retrievers/dts.py @@ -13,9 +13,9 @@ from MyCapytain.common.utils import parse_uri, parse_pagination -class DTS_Retriever(MyCapytain.retrievers.prototypes.API): +class HttpDtsRetriever(MyCapytain.retrievers.prototypes.API): def __init__(self, endpoint): - super(DTS_Retriever, self).__init__(endpoint) + super(HttpDtsRetriever, self).__init__(endpoint) self._routes = None def call(self, route, parameters, mimetype="application/ld+json"): diff --git a/tests/resources/collections/test_dts.py b/tests/resources/collections/test_dts.py new file mode 100644 index 00000000..0894cd65 --- /dev/null +++ b/tests/resources/collections/test_dts.py @@ -0,0 +1,20 @@ +from MyCapytain.resources.collections.dts import DTSCollection +from unittest import TestCase +import json + + +class TestDtsCollection(TestCase): + + def get_collection(self, number): + """ Get a collection for tests + + :param number: ID of the test collection + :return: JSON of the test collection as Python object + """ + with open("tests/testing_data/dts/collection_{}.json".format(number)) as f: + collection = json.load(f) + return collection + + def test_parse(self): + coll = self.get_collection(1) + DTSCollection.parse(coll) diff --git a/tests/retrievers/test_dts.py b/tests/retrievers/test_dts.py index f3bce0bb..8205edef 100644 --- a/tests/retrievers/test_dts.py +++ b/tests/retrievers/test_dts.py @@ -2,7 +2,7 @@ import responses -from MyCapytain.retrievers.dts import DTS_Retriever +from MyCapytain.retrievers.dts import HttpDtsRetriever from MyCapytain.common.utils import _Navigation from urllib.parse import parse_qs, urlparse, urljoin @@ -15,7 +15,7 @@ class TestEndpointsCts5(unittest.TestCase): """ Test Cts5 Endpoint request making """ def setUp(self): - self.cli = DTS_Retriever(_SERVER_URI) + self.cli = HttpDtsRetriever(_SERVER_URI) @property def calls(self): diff --git a/tests/testing_data/dts/collection_1.json b/tests/testing_data/dts/collection_1.json new file mode 100644 index 00000000..03b720c5 --- /dev/null +++ b/tests/testing_data/dts/collection_1.json @@ -0,0 +1,41 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#", + "tei": "http://www.tei-c.org/ns/1.0" + }, + "@id": "general", + "@type": "Collection", + "totalItems": "2", + "title": "Collection Générale de l'École Nationale des Chartes", + "dts:dublincore": { + "dc:publisher": ["École Nationale des Chartes", "https://viaf.org/viaf/167874585"], + "dc:title": [ + {"@language": "fre", "@value" : "Collection Générale de l'École Nationale des Chartes"} + ] + }, + "member": [ + { + "@id" : "cartulaires", + "title" : "Cartulaires", + "description": "Collection de cartulaires d'Île-de-France et de ses environs", + "@type" : "Collection", + "totalItems" : "10" + }, + { + "@id" : "lasciva_roma", + "title" : "Lasciva Roma", + "description": "Collection of primary sources of interest in the studies of Ancient World's sexuality", + "@type" : "Collection", + "totalItems" : "1" + }, + { + "@id" : "lettres_de_poilus", + "title" : "Correspondance des poilus", + "description": "Collection de lettres de poilus entre 1917 et 1918", + "@type" : "Collection", + "totalItems" : "10000" + } + ] +} \ No newline at end of file From 5d1feefdbb39399dfbd5a95cc84e2fb4e49893ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 1 May 2018 18:25:16 +0200 Subject: [PATCH 05/89] Working on exporting at the same time --- MyCapytain/resources/collections/dts.py | 6 +++++- MyCapytain/resources/prototypes/metadata.py | 16 +++++++++++++--- tests/resources/collections/test_dts.py | 4 +++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/MyCapytain/resources/collections/dts.py b/MyCapytain/resources/collections/dts.py index eb7fe392..83372836 100644 --- a/MyCapytain/resources/collections/dts.py +++ b/MyCapytain/resources/collections/dts.py @@ -7,6 +7,7 @@ _hyd = RDF_NAMESPACES.HYDRA _dts = RDF_NAMESPACES.DTS +_empty_extensions = [{}] class DTSCollection(Collection): @@ -35,6 +36,9 @@ def parse(resource, mimetype="application/json+ld"): for val_dict in collection[str(_hyd.title)]: obj.set_label(val_dict["@value"], None) + for val_dict in collection["@type"]: + obj.type = val_dict + for val_dict in collection[str(_hyd.totalItems)]: obj.metadata.add(_hyd.totalItems, val_dict["@value"], 0) @@ -46,7 +50,7 @@ def parse(resource, mimetype="application/json+ld"): for value_dict in value_set: obj.metadata.add(term, value_dict["@value"], value_dict.get("@language", None)) - for key, value_set in collection[str(_dts.extensions)][0].items(): + for key, value_set in collection.get(str(_dts.extensions), _empty_extensions)[0].items(): term = URIRef(key) for value_dict in value_set: obj.metadata.add(term, value_dict["@value"], value_dict.get("@language", None)) diff --git a/MyCapytain/resources/prototypes/metadata.py b/MyCapytain/resources/prototypes/metadata.py index 06d8b976..4942e321 100644 --- a/MyCapytain/resources/prototypes/metadata.py +++ b/MyCapytain/resources/prototypes/metadata.py @@ -312,11 +312,19 @@ def __export__(self, output=None, domain=""): RDFSLabel = self.graph.qname(RDFS.label) RDFType = self.graph.qname(RDF.type) + store = Subgraph(get_graph().namespace_manager) store.graphiter(self.graph, self.metadata, ascendants=0, descendants=1) - metadata = {} + + extensions = {} + dublincore = {} + for _, predicate, obj in store.graph: k = self.graph.qname(predicate) + if str(k).startswith(DC): + metadata = dublincore + else: + metadata = extensions if k in metadata: if isinstance(metadata[k], list): metadata[k].append(LiteralToDict(obj)) @@ -324,14 +332,16 @@ def __export__(self, output=None, domain=""): metadata[k] = [metadata[k], LiteralToDict(obj)] else: metadata[k] = LiteralToDict(obj) + o = { "@context": bindings, "@graph": { "@id": self.id, RDFType: str(self.type), RDFSLabel: LiteralToDict(self.get_label()) or self.id, - self.graph.qname(RDF_NAMESPACES.DTS.size): len(self.members), - self.graph.qname(RDF_NAMESPACES.DTS.metadata): metadata + self.graph.qname(RDF_NAMESPACES.DTS.totalItems): self.size, + self.graph.qname(RDF_NAMESPACES.DTS.dublincore): dublincore, + self.graph.qname(RDF_NAMESPACES.DTS.extensions): extensions } } version = self.version diff --git a/tests/resources/collections/test_dts.py b/tests/resources/collections/test_dts.py index 0894cd65..d1cfaa30 100644 --- a/tests/resources/collections/test_dts.py +++ b/tests/resources/collections/test_dts.py @@ -1,4 +1,5 @@ from MyCapytain.resources.collections.dts import DTSCollection +from MyCapytain.common.constants import Mimetypes from unittest import TestCase import json @@ -17,4 +18,5 @@ def get_collection(self, number): def test_parse(self): coll = self.get_collection(1) - DTSCollection.parse(coll) + from pprint import pprint + pprint(DTSCollection.parse(coll).export(Mimetypes.JSON.DTS.Std)) From 09d95f0ffdae536bf0c8dabdf0a34af29315ca78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 2 May 2018 10:00:39 +0200 Subject: [PATCH 06/89] Smallish working output --- MyCapytain/common/utils.py | 2 +- MyCapytain/resources/prototypes/metadata.py | 79 +++++++++++++-------- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/MyCapytain/common/utils.py b/MyCapytain/common/utils.py index 9f1412dc..9c5c0467 100644 --- a/MyCapytain/common/utils.py +++ b/MyCapytain/common/utils.py @@ -81,7 +81,7 @@ def LiteralToDict(value): """ if isinstance(value, Literal): if value.language is not None: - return {"@value": str(value), "@lang": value.language} + return {"@value": str(value), "@language": value.language} return value.toPython() elif isinstance(value, URIRef): return {"@id": str(value)} diff --git a/MyCapytain/resources/prototypes/metadata.py b/MyCapytain/resources/prototypes/metadata.py index 4942e321..b0080dfa 100644 --- a/MyCapytain/resources/prototypes/metadata.py +++ b/MyCapytain/resources/prototypes/metadata.py @@ -13,7 +13,8 @@ from MyCapytain.common.constants import RDF_NAMESPACES, RDFLIB_MAPPING, Mimetypes, get_graph from MyCapytain.common.base import Exportable from rdflib import URIRef, RDF, Literal, Graph, RDFS -from rdflib.namespace import SKOS, DC +from rdflib.namespace import SKOS, DC, DCTERMS +from copy import deepcopy class Collection(Exportable): @@ -304,27 +305,45 @@ def __export__(self, output=None, domain=""): """ if output == Mimetypes.JSON.DTS.Std: + + # Set-up a derived Namespace Manager nm = self.graph.namespace_manager - bindings = {} - for predicate in set(self.graph.predicates()): - prefix, namespace, name = nm.compute_qname(predicate) - bindings[prefix] = str(URIRef(namespace)) + nsm = deepcopy(nm) + nsm.bind("hydra", RDF_NAMESPACES.HYDRA) + nsm.bind("dct", DCTERMS) - RDFSLabel = self.graph.qname(RDFS.label) - RDFType = self.graph.qname(RDF.type) + # Set-up a derived graph + store = Subgraph(nsm) + store.graphiter(self.graph, self.asNode(), ascendants=0, descendants=1) + graph = store.graph - store = Subgraph(get_graph().namespace_manager) - store.graphiter(self.graph, self.metadata, ascendants=0, descendants=1) + # Build the JSON-LD @context + bindings = {} + for predicate in set(graph.predicates()): + prefix, namespace, name = nsm.compute_qname(predicate) + bindings[prefix] = str(URIRef(namespace)) + # Builds the specific Store data extensions = {} dublincore = {} + ignore_ns = [str(RDF_NAMESPACES.HYDRA), str(RDF_NAMESPACES.DTS), str(RDF), str(RDFS)] + # Builds the .dublincore and .extensions graphs for _, predicate, obj in store.graph: - k = self.graph.qname(predicate) - if str(k).startswith(DC): + k = graph.qname(predicate) + prefix, namespace, name = nsm.compute_qname(predicate) + namespace = str(namespace) + + # Ignore namespaces that are part of the root DTS object + if namespace in ignore_ns: + continue + + # Switch to the correct container depending on namespaces + if namespace == str(DCTERMS): metadata = dublincore else: metadata = extensions + if k in metadata: if isinstance(metadata[k], list): metadata[k].append(LiteralToDict(obj)) @@ -337,34 +356,32 @@ def __export__(self, output=None, domain=""): "@context": bindings, "@graph": { "@id": self.id, - RDFType: str(self.type), - RDFSLabel: LiteralToDict(self.get_label()) or self.id, - self.graph.qname(RDF_NAMESPACES.DTS.totalItems): self.size, - self.graph.qname(RDF_NAMESPACES.DTS.dublincore): dublincore, - self.graph.qname(RDF_NAMESPACES.DTS.extensions): extensions + "@type": graph.qname(self.type), + graph.qname(RDF_NAMESPACES.HYDRA.title): str(self.get_label()), + graph.qname(RDF_NAMESPACES.DTS.totalItems): self.size } } - version = self.version - if version is not None: - o["@graph"]["version"] = str(version) - if len(self.members): + + if extensions: + o[graph.qname(RDF_NAMESPACES.DTS.extensions)] = extensions + + if dublincore: + o[graph.qname(RDF_NAMESPACES.DTS.dublincore)] = dublincore + + for desc in self.graph.objects(self.asNode(), RDF_NAMESPACES.HYDRA.description): + o[self.graph.qname(RDF_NAMESPACES.HYDRA.description)] = str(desc) + + if self.size: o["@graph"][self.graph.qname(RDF_NAMESPACES.DTS.members)] = [ { "@id": member.id, - RDFSLabel: LiteralToDict(member.get_label()) or member.id, - self.graph.qname(RDF_NAMESPACES.DTS.url): domain + member.id + graph.qname(RDF_NAMESPACES.HYDRA.title): str(member.get_label()) or member.id, + graph.qname(RDF_NAMESPACES.HYDRA.totalItems): member.size, + } for member in self.members ] - if self.parent: - o["@graph"][self.graph.qname(RDF_NAMESPACES.CAPITAINS.parents)] = [ - { - "@id": member.id, - RDFSLabel: LiteralToDict(member.get_label()) or member.id, - self.graph.qname(RDF_NAMESPACES.DTS.url): domain + member.id - } - for member in self.parents - ] + del store return o elif output == Mimetypes.JSON.LD\ From 211803e17fb8793cbd538832f7a24947c6d9eddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 2 May 2018 10:48:06 +0200 Subject: [PATCH 07/89] Working import / export. Need to think about the size property as it relates to children only. --- MyCapytain/resources/collections/dts.py | 37 ++++++-------- MyCapytain/resources/prototypes/metadata.py | 45 +++++++++-------- tests/resources/collections/test_dts.py | 54 +++++++++++++++++++-- tests/testing_data/dts/collection_1.json | 2 +- 4 files changed, 94 insertions(+), 44 deletions(-) diff --git a/MyCapytain/resources/collections/dts.py b/MyCapytain/resources/collections/dts.py index 83372836..83bbe41d 100644 --- a/MyCapytain/resources/collections/dts.py +++ b/MyCapytain/resources/collections/dts.py @@ -7,10 +7,15 @@ _hyd = RDF_NAMESPACES.HYDRA _dts = RDF_NAMESPACES.DTS +_cap = RDF_NAMESPACES.CAPITAINS _empty_extensions = [{}] class DTSCollection(Collection): + def __init__(self, identifier="", *args, **kwargs): + super(DTSCollection, self).__init__(identifier, *args, **kwargs) + self._expanded = False # Not sure I'll keep this + @property def size(self): for value in self.metadata.get_single(RDF_NAMESPACES.HYDRA.totalItems): @@ -18,12 +23,14 @@ def size(self): return 0 @staticmethod - def parse(resource, mimetype="application/json+ld"): + def parse(resource, direction="children"): """ Given a dict representation of a json object, generate a DTS Collection :param resource: - :param mimetype: - :return: + :type resource: dict + :param direction: Direction of the hydra:members value + :return: DTSCollection parsed + :rtype: DTSCollection """ collection = jsonld.expand(resource) if len(collection) == 0: @@ -45,7 +52,7 @@ def parse(resource, mimetype="application/json+ld"): for val_dict in collection.get(str(_hyd.description), []): obj.metadata.add(_hyd.description, val_dict["@value"], None) - for key, value_set in collection[str(_dts.dublincore)][0].items(): + for key, value_set in collection.get(str(_dts.dublincore), _empty_extensions)[0].items(): term = URIRef(key) for value_dict in value_set: obj.metadata.add(term, value_dict["@value"], value_dict.get("@language", None)) @@ -55,21 +62,9 @@ def parse(resource, mimetype="application/json+ld"): for value_dict in value_set: obj.metadata.add(term, value_dict["@value"], value_dict.get("@language", None)) - return obj - + for member in collection.get(str(_hyd.member), []): + subcollection = DTSCollection.parse(member) + if direction == "children": + subcollection.parent = obj -class DTSCollectionShort(DTSCollection): - @staticmethod - def parse(resource): - """ Given a dict representation of a json object, generate a DTS Collection - - :param resource: - :param mimetype: - :return: - """ - obj = DTSCollectionShort(identifier=resource["@id"]) - obj.type = resource["type"] - obj.model = resource["model"] - for label in resource["label"]: - obj.set_label(label["value"], label["lang"]) - return obj \ No newline at end of file + return obj diff --git a/MyCapytain/resources/prototypes/metadata.py b/MyCapytain/resources/prototypes/metadata.py index b0080dfa..593ab0c4 100644 --- a/MyCapytain/resources/prototypes/metadata.py +++ b/MyCapytain/resources/prototypes/metadata.py @@ -292,6 +292,28 @@ def __namespaces_header__(self, cpt=None): return bindings + @staticmethod + def _export_base_dts(graph, obj, nsm): + """ Export the base DTS information in a simple reusable way + + :param graph: Current graph where the information lie + :param obj: Object for which we build info + :param nsm: Namespace manager + :return: Dict + """ + + o = { + "@id": str(obj.asNode()), + "@type": nsm.qname(obj.type), + nsm.qname(RDF_NAMESPACES.HYDRA.title): str(obj.get_label()), + nsm.qname(RDF_NAMESPACES.DTS.totalItems): obj.size + } + + for desc in graph.objects(obj.asNode(), RDF_NAMESPACES.HYDRA.description): + o[nsm.qname(RDF_NAMESPACES.HYDRA.description)] = str(desc) + + return o + def __export__(self, output=None, domain=""): """ Export the collection item in the Mimetype required. @@ -352,15 +374,8 @@ def __export__(self, output=None, domain=""): else: metadata[k] = LiteralToDict(obj) - o = { - "@context": bindings, - "@graph": { - "@id": self.id, - "@type": graph.qname(self.type), - graph.qname(RDF_NAMESPACES.HYDRA.title): str(self.get_label()), - graph.qname(RDF_NAMESPACES.DTS.totalItems): self.size - } - } + o = {"@context": bindings} + o.update(self._export_base_dts(graph, self, nsm)) if extensions: o[graph.qname(RDF_NAMESPACES.DTS.extensions)] = extensions @@ -368,17 +383,9 @@ def __export__(self, output=None, domain=""): if dublincore: o[graph.qname(RDF_NAMESPACES.DTS.dublincore)] = dublincore - for desc in self.graph.objects(self.asNode(), RDF_NAMESPACES.HYDRA.description): - o[self.graph.qname(RDF_NAMESPACES.HYDRA.description)] = str(desc) - if self.size: - o["@graph"][self.graph.qname(RDF_NAMESPACES.DTS.members)] = [ - { - "@id": member.id, - graph.qname(RDF_NAMESPACES.HYDRA.title): str(member.get_label()) or member.id, - graph.qname(RDF_NAMESPACES.HYDRA.totalItems): member.size, - - } + o[self.graph.qname(RDF_NAMESPACES.DTS.members)] = [ + self._export_base_dts(self.graph, member, nsm) for member in self.members ] diff --git a/tests/resources/collections/test_dts.py b/tests/resources/collections/test_dts.py index d1cfaa30..a7bc21c6 100644 --- a/tests/resources/collections/test_dts.py +++ b/tests/resources/collections/test_dts.py @@ -16,7 +16,55 @@ def get_collection(self, number): collection = json.load(f) return collection - def test_parse(self): + def test_parse_member(self): coll = self.get_collection(1) - from pprint import pprint - pprint(DTSCollection.parse(coll).export(Mimetypes.JSON.DTS.Std)) + parsed = DTSCollection.parse(coll) + exported = parsed.export(Mimetypes.JSON.DTS.Std) + exported["dts:members"] = sorted(exported["dts:members"], key=lambda x: x["@id"]) + exported["dts:dublincore"]["dct:publisher"] = sorted(exported["dts:dublincore"]["dct:publisher"]) + self.assertEqual( + exported, + { + '@context': { + 'dct': 'http://purl.org/dc/terms/', + 'dts': 'https://w3id.org/dts/api#', + 'hydra': 'https://www.w3.org/ns/hydra/core#', + 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#', + 'skos': 'http://www.w3.org/2004/02/skos/core#'}, + '@id': 'general', + '@type': 'hydra:Collection', + 'dts:members': [ + {'@id': '/cartulaires', + '@type': 'hydra:Collection', + 'dts:totalItems': 1, + 'hydra:description': 'Collection de cartulaires ' + "d'Île-de-France et de ses " + 'environs', + 'hydra:title': 'Cartulaires'}, + {'@id': '/lasciva_roma', + '@type': 'hydra:Collection', + 'dts:totalItems': 1, + 'hydra:description': 'Collection of primary ' + 'sources of interest in the ' + "studies of Ancient World's " + 'sexuality', + 'hydra:title': 'Lasciva Roma'}, + {'@id': '/lettres_de_poilus', + '@type': 'hydra:Collection', + 'dts:totalItems': 1, + 'hydra:description': 'Collection de lettres de ' + 'poilus entre 1917 et 1918', + 'hydra:title': 'Correspondance des poilus'}], + 'dts:totalItems': 3, + 'hydra:title': "Collection Générale de l'École Nationale des " + 'Chartes', + 'dts:dublincore': {'dct:publisher': ['https://viaf.org/viaf/167874585', + 'École Nationale des Chartes'], + 'dct:title': {'@language': 'fre', + '@value': "Collection Générale de l'École " + 'Nationale des Chartes'}}, + 'dts:extensions': {'skos:prefLabel': "Collection Générale de l'École " + 'Nationale des Chartes'} + } + ) diff --git a/tests/testing_data/dts/collection_1.json b/tests/testing_data/dts/collection_1.json index 03b720c5..45504d3b 100644 --- a/tests/testing_data/dts/collection_1.json +++ b/tests/testing_data/dts/collection_1.json @@ -7,7 +7,7 @@ }, "@id": "general", "@type": "Collection", - "totalItems": "2", + "totalItems": 3, "title": "Collection Générale de l'École Nationale des Chartes", "dts:dublincore": { "dc:publisher": ["École Nationale des Chartes", "https://viaf.org/viaf/167874585"], From e191ff16096de9b8bbf54c47eae053167568d2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 2 May 2018 11:09:07 +0200 Subject: [PATCH 08/89] Fixing old tests based on new DTS URI --- tests/common/test_metadata.py | 4 ++-- tests/resolvers/cts/test_api.py | 4 ++-- tests/resolvers/cts/test_local.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/common/test_metadata.py b/tests/common/test_metadata.py index eeb50bdc..2f503241 100644 --- a/tests/common/test_metadata.py +++ b/tests/common/test_metadata.py @@ -125,13 +125,13 @@ def test_export_exclude(self): self.assertEqual( m.export(Mimetypes.JSON.Std, exclude=[RDF_NAMESPACES.CTS.title]), { - 'http://w3id.org/dts-ontology/description': {'fre': 'Subtitle', 'eng': 'Subtitle'}, + 'https://w3id.org/dts/api#description': {'fre': 'Subtitle', 'eng': 'Subtitle'}, 'http://chs.harvard.edu/xmlns/cts/description': {'fre': 'Subtitle', 'eng': 'Subtitle'} } ) self.assertEqual( m.export(Mimetypes.JSON.Std, exclude=[RDF_NAMESPACES.CTS]), { - 'http://w3id.org/dts-ontology/description': {'fre': 'Subtitle', 'eng': 'Subtitle'} + 'https://w3id.org/dts/api#description': {'fre': 'Subtitle', 'eng': 'Subtitle'} } ) \ No newline at end of file diff --git a/tests/resolvers/cts/test_api.py b/tests/resolvers/cts/test_api.py index e2448233..9b50cf41 100644 --- a/tests/resolvers/cts/test_api.py +++ b/tests/resolvers/cts/test_api.py @@ -327,7 +327,7 @@ def test_getMetadata_full(self): "There should be one node in exported format corresponding to lat2" ) self.assertCountEqual( - [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["@graph"]["dts:members"]], + [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["dts:members"]], ["urn:cts:latinLit:phi1294", "urn:cts:latinLit:phi0959", "urn:cts:greekLit:tlg0003", "urn:cts:latinLit:phi1276"], "There should be 4 Members in DTS JSON" ) @@ -362,7 +362,7 @@ def test_getMetadata_subset(self): "There should be one node in exported format corresponding to lat2" ) self.assertCountEqual( - [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["@graph"]["dts:members"]], + [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["dts:members"]], ["urn:cts:latinLit:phi1294.phi002.perseus-lat2", "urn:cts:latinLit:phi1294.phi002.perseus-eng2"], "There should be one member in DTS JSON" ) diff --git a/tests/resolvers/cts/test_local.py b/tests/resolvers/cts/test_local.py index a86b473d..660fd178 100644 --- a/tests/resolvers/cts/test_local.py +++ b/tests/resolvers/cts/test_local.py @@ -459,7 +459,7 @@ def test_getMetadata_full(self): "There should be one node in exported format corresponding to lat2" ) self.assertCountEqual( - [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["@graph"]["dts:members"]], + [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["dts:members"]], ["urn:cts:latinLit:phi1294", "urn:cts:latinLit:phi0959", "urn:cts:greekLit:tlg0003", "urn:cts:latinLit:phi1276"], "There should be 4 Members in DTS JSON" @@ -502,7 +502,7 @@ def test_getMetadata_subset(self): "There should be one node in exported format corresponding to lat2" ) self.assertCountEqual( - [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["@graph"]["dts:members"]], + [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["dts:members"]], ["urn:cts:latinLit:phi1294.phi002.opp-eng3", "urn:cts:latinLit:phi1294.phi002.perseus-lat2"], "There should be two members in DTS JSON" ) From 27205bac65c318e0ae770908b020f762141026d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 2 May 2018 11:29:44 +0200 Subject: [PATCH 09/89] Adding PyLD --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8b9e01e0..1d4454cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ six>=1.10.0 xmlunittest>=0.3.2 rdflib-jsonld>=0.4.0 responses>=0.8.1 -LinkHeader==0.4.3 \ No newline at end of file +LinkHeader==0.4.3 +pyld==1.0.3 \ No newline at end of file From dfbd82c6ffbf160d6f3266f3a358272e926d0f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 2 May 2018 17:12:23 +0200 Subject: [PATCH 10/89] Stuck on new CitationSet object needed --- MyCapytain/resources/collections/dts.py | 11 +- MyCapytain/resources/prototypes/metadata.py | 12 +- tests/resources/collections/test_dts.py | 134 ++++++++++++++++++-- tests/testing_data/dts/collection_1.json | 6 +- tests/testing_data/dts/collection_2.json | 47 +++++++ tests/testing_data/dts/collection_3.json | 66 ++++++++++ 6 files changed, 254 insertions(+), 22 deletions(-) create mode 100644 tests/testing_data/dts/collection_2.json create mode 100644 tests/testing_data/dts/collection_3.json diff --git a/MyCapytain/resources/collections/dts.py b/MyCapytain/resources/collections/dts.py index 83bbe41d..0430067a 100644 --- a/MyCapytain/resources/collections/dts.py +++ b/MyCapytain/resources/collections/dts.py @@ -1,13 +1,15 @@ from MyCapytain.resources.prototypes.metadata import Collection -from rdflib import URIRef -from pyld import jsonld from MyCapytain.errors import JsonLdCollectionMissing from MyCapytain.common.constants import RDF_NAMESPACES +from rdflib import URIRef +from pyld import jsonld + _hyd = RDF_NAMESPACES.HYDRA _dts = RDF_NAMESPACES.DTS _cap = RDF_NAMESPACES.CAPITAINS +_tei = RDF_NAMESPACES.TEI _empty_extensions = [{}] @@ -62,6 +64,11 @@ def parse(resource, direction="children"): for value_dict in value_set: obj.metadata.add(term, value_dict["@value"], value_dict.get("@language", None)) + if str(_tei.refsDecl) in collection: + for citation in collection[str(_tei.refsDecl)]: + # Need to have citation set before going further. + continue + for member in collection.get(str(_hyd.member), []): subcollection = DTSCollection.parse(member) if direction == "children": diff --git a/MyCapytain/resources/prototypes/metadata.py b/MyCapytain/resources/prototypes/metadata.py index 593ab0c4..15e15c0d 100644 --- a/MyCapytain/resources/prototypes/metadata.py +++ b/MyCapytain/resources/prototypes/metadata.py @@ -306,7 +306,7 @@ def _export_base_dts(graph, obj, nsm): "@id": str(obj.asNode()), "@type": nsm.qname(obj.type), nsm.qname(RDF_NAMESPACES.HYDRA.title): str(obj.get_label()), - nsm.qname(RDF_NAMESPACES.DTS.totalItems): obj.size + nsm.qname(RDF_NAMESPACES.HYDRA.totalItems): obj.size } for desc in graph.objects(obj.asNode(), RDF_NAMESPACES.HYDRA.description): @@ -345,10 +345,14 @@ def __export__(self, output=None, domain=""): prefix, namespace, name = nsm.compute_qname(predicate) bindings[prefix] = str(URIRef(namespace)) + if "cap" in bindings: + del bindings["cap"] + # Builds the specific Store data extensions = {} dublincore = {} - ignore_ns = [str(RDF_NAMESPACES.HYDRA), str(RDF_NAMESPACES.DTS), str(RDF), str(RDFS)] + ignore_ns = [str(RDF_NAMESPACES.HYDRA), str(RDF_NAMESPACES.DTS), + str(RDF_NAMESPACES.CAPITAINS), str(RDF), str(RDFS)] # Builds the .dublincore and .extensions graphs for _, predicate, obj in store.graph: @@ -373,6 +377,8 @@ def __export__(self, output=None, domain=""): metadata[k] = [metadata[k], LiteralToDict(obj)] else: metadata[k] = LiteralToDict(obj) + if isinstance(metadata[k], dict): + metadata[k] = [metadata[k]] o = {"@context": bindings} o.update(self._export_base_dts(graph, self, nsm)) @@ -384,7 +390,7 @@ def __export__(self, output=None, domain=""): o[graph.qname(RDF_NAMESPACES.DTS.dublincore)] = dublincore if self.size: - o[self.graph.qname(RDF_NAMESPACES.DTS.members)] = [ + o[graph.qname(RDF_NAMESPACES.HYDRA.member)] = [ self._export_base_dts(self.graph, member, nsm) for member in self.members ] diff --git a/tests/resources/collections/test_dts.py b/tests/resources/collections/test_dts.py index a7bc21c6..47ce941e 100644 --- a/tests/resources/collections/test_dts.py +++ b/tests/resources/collections/test_dts.py @@ -1,27 +1,43 @@ from MyCapytain.resources.collections.dts import DTSCollection -from MyCapytain.common.constants import Mimetypes +from MyCapytain.common.constants import Mimetypes, set_graph, bind_graph from unittest import TestCase import json class TestDtsCollection(TestCase): + def setUp(self): + # The following line ensure that graphs care cleared between tests + set_graph(bind_graph()) def get_collection(self, number): """ Get a collection for tests - :param number: ID of the test collection - :return: JSON of the test collection as Python object + :param number: ID of the test collection + :return: JSON of the test collection as Python object """ with open("tests/testing_data/dts/collection_{}.json".format(number)) as f: collection = json.load(f) return collection - def test_parse_member(self): + def reorder_orderable(self, exported): + """ Reorded orderable keys + + :param exported: Exported Collection to DTS + :return: Sorted exported collection + """ + exported["hydra:member"] = sorted(exported["hydra:member"], key=lambda x: x["@id"]) + for key, values in exported["dts:dublincore"].items(): + if isinstance(values, list) and isinstance(values[0], str): + exported["dts:dublincore"][key] = sorted(values) + elif isinstance(values, list) and isinstance(values[0], dict): + exported["dts:dublincore"][key] = sorted(values, key=lambda x: x["@value"]) + return exported + + def test_simple_collection(self): coll = self.get_collection(1) parsed = DTSCollection.parse(coll) - exported = parsed.export(Mimetypes.JSON.DTS.Std) - exported["dts:members"] = sorted(exported["dts:members"], key=lambda x: x["@id"]) - exported["dts:dublincore"]["dct:publisher"] = sorted(exported["dts:dublincore"]["dct:publisher"]) + exported = self.reorder_orderable(parsed.export(Mimetypes.JSON.DTS.Std)) + self.assertEqual( exported, { @@ -34,17 +50,17 @@ def test_parse_member(self): 'skos': 'http://www.w3.org/2004/02/skos/core#'}, '@id': 'general', '@type': 'hydra:Collection', - 'dts:members': [ + 'hydra:member': [ {'@id': '/cartulaires', '@type': 'hydra:Collection', - 'dts:totalItems': 1, + 'hydra:totalItems': 1, 'hydra:description': 'Collection de cartulaires ' "d'Île-de-France et de ses " 'environs', 'hydra:title': 'Cartulaires'}, {'@id': '/lasciva_roma', '@type': 'hydra:Collection', - 'dts:totalItems': 1, + 'hydra:totalItems': 1, 'hydra:description': 'Collection of primary ' 'sources of interest in the ' "studies of Ancient World's " @@ -52,19 +68,109 @@ def test_parse_member(self): 'hydra:title': 'Lasciva Roma'}, {'@id': '/lettres_de_poilus', '@type': 'hydra:Collection', - 'dts:totalItems': 1, + 'hydra:totalItems': 1, 'hydra:description': 'Collection de lettres de ' 'poilus entre 1917 et 1918', 'hydra:title': 'Correspondance des poilus'}], - 'dts:totalItems': 3, + 'hydra:totalItems': 3, 'hydra:title': "Collection Générale de l'École Nationale des " 'Chartes', 'dts:dublincore': {'dct:publisher': ['https://viaf.org/viaf/167874585', 'École Nationale des Chartes'], - 'dct:title': {'@language': 'fre', + 'dct:title': [{'@language': 'fre', '@value': "Collection Générale de l'École " - 'Nationale des Chartes'}}, + 'Nationale des Chartes'}]}, 'dts:extensions': {'skos:prefLabel': "Collection Générale de l'École " 'Nationale des Chartes'} } ) + + def test_collection_single_member_with_types(self): + coll = self.get_collection(2) + parsed = DTSCollection.parse(coll) + exported = self.reorder_orderable(parsed.export(Mimetypes.JSON.DTS.Std)) + self.assertEqual( + exported, + { + "@context": { + 'dct': 'http://purl.org/dc/terms/', + 'dts': 'https://w3id.org/dts/api#', + 'hydra': 'https://www.w3.org/ns/hydra/core#', + 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#', + 'skos': 'http://www.w3.org/2004/02/skos/core#' + }, + "@id": "lasciva_roma", + "@type": "hydra:Collection", + "hydra:totalItems": 2, + "hydra:title": "Lasciva Roma", + "hydra:description": "Collection of primary sources of interest in the studies of Ancient World's sexuality", + "dts:dublincore": { + "dct:creator": [ + "Thibault Clérice", "http://orcid.org/0000-0003-1852-9204" + ], + "dct:title": [ + {"@language": "lat", "@value": "Lasciva Roma"} + ], + "dct:description": [ + { + "@language": "eng", + "@value": "Collection of primary sources of interest in " + "the studies of Ancient World's sexuality" + } + ] + }, + 'dts:extensions': {'skos:prefLabel': 'Lasciva Roma'}, + "hydra:member": [ + { + "@id": "urn:cts:latinLit:phi1103.phi001", + "hydra:title": "Priapeia", + "@type": "hydra:Collection", + "hydra:totalItems": 1 + } + ] + } + ) + + def test_collection_with_complexe_child(self): + coll = self.get_collection(3) + parsed = DTSCollection.parse(coll) + exported = self.reorder_orderable(parsed.export(Mimetypes.JSON.DTS.Std)) + self.assertEqual( + exported, + { + "@context": { + 'dct': 'http://purl.org/dc/terms/', + 'dts': 'https://w3id.org/dts/api#', + 'hydra': 'https://www.w3.org/ns/hydra/core#', + 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#', + 'skos': 'http://www.w3.org/2004/02/skos/core#', + "tei": "http://www.tei-c.org/ns/1.0" + }, + "@id": "urn:cts:latinLit:phi1103.phi001", + "@type": "hydra:Collection", + "hydra:title": "Priapeia", + "dts:dublincore": { + "dct:type": "http://chs.harvard.edu/xmlns/cts#work", + "dct:creator": [ + {"@language": "eng", "@value": "Anonymous"} + ], + "dct:language": ["eng", "lat"], + "dct:title": [{"@language": "lat", "@value": "Priapeia"}], + "dct:description": [{ + "@language": "eng", + "@value": "Anonymous lascivious Poems " + }] + }, + 'dts:extensions': {'skos:prefLabel': 'Priapeia'}, + "hydra:totalItems": 1, + "hydra:member": [{ + "@id": "urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", + "@type": "hydra:Resource", + "hydra:title": "Priapeia", + "hydra:description": "Priapeia based on the edition of Aemilius Baehrens", + "hydra:totalItems": 0 + }] + } + ) \ No newline at end of file diff --git a/tests/testing_data/dts/collection_1.json b/tests/testing_data/dts/collection_1.json index 45504d3b..7b536456 100644 --- a/tests/testing_data/dts/collection_1.json +++ b/tests/testing_data/dts/collection_1.json @@ -21,21 +21,21 @@ "title" : "Cartulaires", "description": "Collection de cartulaires d'Île-de-France et de ses environs", "@type" : "Collection", - "totalItems" : "10" + "totalItems" : 10 }, { "@id" : "lasciva_roma", "title" : "Lasciva Roma", "description": "Collection of primary sources of interest in the studies of Ancient World's sexuality", "@type" : "Collection", - "totalItems" : "1" + "totalItems" : 1 }, { "@id" : "lettres_de_poilus", "title" : "Correspondance des poilus", "description": "Collection de lettres de poilus entre 1917 et 1918", "@type" : "Collection", - "totalItems" : "10000" + "totalItems" : 10000 } ] } \ No newline at end of file diff --git a/tests/testing_data/dts/collection_2.json b/tests/testing_data/dts/collection_2.json new file mode 100644 index 00000000..755be34b --- /dev/null +++ b/tests/testing_data/dts/collection_2.json @@ -0,0 +1,47 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#", + "tei": "http://www.tei-c.org/ns/1.0" + }, + "@id": "lasciva_roma", + "@type": "Collection", + "totalItems": 2, + "title" : "Lasciva Roma", + "description": "Collection of primary sources of interest in the studies of Ancient World's sexuality", + "dts:dublincore": { + "dc:creator": [ + "Thibault Clérice", "http://orcid.org/0000-0003-1852-9204" + ], + "dc:title" : [ + {"@language": "lat", "@value": "Lasciva Roma"} + ], + "dc:description": [ + { + "@language": "eng", + "@value": "Collection of primary sources of interest in the studies of Ancient World's sexuality" + } + ] + }, + "member": [ + { + "@id" : "urn:cts:latinLit:phi1103.phi001", + "title" : "Priapeia", + "dts:dublincore": { + "dc:type": [ + "http://chs.harvard.edu/xmlns/cts#work" + ], + "dc:creator": [ + {"@language": "eng", "@value": "Anonymous"} + ], + "dc:language": ["lat", "eng"], + "dc:description": [ + { "@language": "eng", "@value": "Anonymous lascivious Poems" } + ] + }, + "@type" : "Collection", + "totalItems": 1 + } + ] +} \ No newline at end of file diff --git a/tests/testing_data/dts/collection_3.json b/tests/testing_data/dts/collection_3.json new file mode 100644 index 00000000..3472b869 --- /dev/null +++ b/tests/testing_data/dts/collection_3.json @@ -0,0 +1,66 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#", + "tei": "http://www.tei-c.org/ns/1.0" + }, + "@id": "urn:cts:latinLit:phi1103.phi001", + "@type": "Collection", + "title" : "Priapeia", + "dts:dublincore": { + "dc:type": ["http://chs.harvard.edu/xmlns/cts#work"], + "dc:creator": [ + {"@language": "eng", "@value": "Anonymous"} + ], + "dc:language": ["lat", "eng"], + "dc:title": [{"@language": "lat", "@value": "Priapeia"}], + "dc:description": [{ + "@language": "eng", + "@value": "Anonymous lascivious Poems " + }] + }, + "totalItems" : "1", + "member": [ + { + "@id" : "urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", + "@type": "Resource", + "title" : "Priapeia", + "description": "Priapeia based on the edition of Aemilius Baehrens", + "totalItems": 0, + "dts:dublincore": { + "dc:title": [{"@language": "lat", "@value": "Priapeia"}], + "dc:description": [{ + "@language": "fre", + "@value": "Poèmes anonymes lascifs" + }], + "dc:type": [ + "http://chs.harvard.edu/xmlns/cts#edition", + "dc:Text" + ], + "dc:source": ["https://archive.org/details/poetaelatinimino12baeh2"], + "dc:dateCopyrighted": 1879, + "dc:creator": [ + {"@language": "eng", "@value": "Anonymous"} + ], + "dc:contributor": ["Aemilius Baehrens"], + "dc:language": ["lat", "eng"] + }, + "dts:passage": "/api/dts/documents?id=urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", + "dts:references": "/api/dts/navigation?id=urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", + "dts:download": "https://raw.githubusercontent.com/lascivaroma/priapeia/master/data/phi1103/phi001/phi1103.phi001.lascivaroma-lat1.xml", + "tei:refsDecl": [ + { + "tei:matchPattern": "(\\w+)", + "tei:replacementPattern": "#xpath(/tei:TEI/tei:text/tei:body/tei:div/tei:div[@n='$1'])", + "@type": "poem" + }, + { + "tei:matchPattern": "(\\w+)\\.(\\w+)", + "tei:replacementPattern": "#xpath(/tei:TEI/tei:text/tei:body/tei:div/tei:div[@n='$1']//tei:l[@n='$2'])", + "@type": "line" + } + ] + } + ] +} \ No newline at end of file From 7c96e821b289b9dce10932ee3cefdcdaec1242ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 9 May 2018 14:56:14 +0200 Subject: [PATCH 11/89] Forking citation and started a Citation base object --- MyCapytain/common/reference/__init__.py | 10 + MyCapytain/common/reference/_base.py | 236 ++++++++++++++++++ .../_capitains_cts.py} | 206 +++------------ MyCapytain/resolvers/cts/local.py | 2 +- MyCapytain/resources/collections/cts.py | 2 +- .../resources/prototypes/cts/inventory.py | 2 +- MyCapytain/resources/prototypes/text.py | 12 +- .../resources/texts/local/capitains/cts.py | 4 +- MyCapytain/resources/texts/remote/cts.py | 2 +- MyCapytain/retrievers/cts5.py | 2 +- tests/common/test_reference.py | 3 +- tests/resolvers/cts/test_api.py | 4 +- tests/resolvers/cts/test_local.py | 6 +- tests/resources/collections/test_dts.py | 2 +- tests/resources/proto/test_text.py | 21 +- tests/resources/texts/base/test_tei.py | 2 +- tests/resources/texts/local/commonTests.py | 2 +- .../texts/local/test_capitains_xml_default.py | 11 +- .../test_capitains_xml_notObjectified.py | 11 +- tests/resources/texts/remote/test_cts.py | 2 +- 20 files changed, 332 insertions(+), 210 deletions(-) create mode 100644 MyCapytain/common/reference/__init__.py create mode 100644 MyCapytain/common/reference/_base.py rename MyCapytain/common/{reference.py => reference/_capitains_cts.py} (88%) diff --git a/MyCapytain/common/reference/__init__.py b/MyCapytain/common/reference/__init__.py new file mode 100644 index 00000000..de585ec6 --- /dev/null +++ b/MyCapytain/common/reference/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +""" +.. module:: MyCapytain.common.reference + :synopsis: URN related objects + +.. moduleauthor:: Thibault Clérice + +""" +from ._base import NodeId +from ._capitains_cts import Citation, Reference, URN diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py new file mode 100644 index 00000000..65c80a18 --- /dev/null +++ b/MyCapytain/common/reference/_base.py @@ -0,0 +1,236 @@ +from MyCapytain.common.base import Exportable +from copy import copy + + +class BaseCitation(Exportable): + def __repr__(self): + return self.name + + def __init__(self, name=None, children=None): + """ Initialize a BaseCitation object + + :param name: Name of the citation level + :type name: str + :param children: + """ + self._name = None + self._children = [] + + self.name = name + self.children = children + + @property + def name(self): + """ Type of the citation represented + + :rtype: str + :example: Book, Chapter, Textpart, Section, Poem... + """ + return self._name + + @name.setter + def name(self, val): + self._name = val + + @property + def children(self): + """ Children of a citation + + :rtype: [BaseCitation] + """ + return self._children or [] + + @children.setter + def children(self, val): + final_value = [] + if val is not None: + for citation in val: + if citation is None: + continue + elif not isinstance(citation, self.__class__): + raise TypeError("Citation children should be Citation") + else: + final_value.append(citation) + + self._children = final_value + + def __iter__(self): + """ Iteration method + + Loop over the citation children + + :Example: + >>> c = BaseCitation(name="line") + >>> b = BaseCitation(name="poem", children=[c]) + >>> b2 = BaseCitation(name="paragraph") + >>> a = BaseCitation(name="book", children=[b]) + >>> [e for e in a] == [a, b, c, b2], + + """ + yield from [self] + for child in self.children: + yield from child + + @property + def depth(self): + """ Depth of the citation scheme + + .. example:: If we have a Book, Poem, Line system, and the citation we are looking at is Poem, depth is 2 + + :rtype: int + :return: Depth of the citation scheme + """ + return 1 + + def __getitem__(self, item): + """ Returns the citations at the given level. + + :param item: Citation level + :type item: int + :rtype: list(BaseCitation) or BaseCitation + + .. note:: Should it be a or or always a list ? + """ + return [] + + def __len__(self): + """ Number of citation schemes covered by the object + + :rtype: int + :returns: Number of nested citations + """ + return 0 + + def match(self, passageId): + """ Given a specific passageId, matches the citation to a specific citation object + + :param passageId: Passage Identifier + :return: Citation that matches the passageId + :rtype: BaseCitation + """ + + def __getstate__(self): + """ Pickling method + + :return: dict + """ + return copy(self.__dict__) + + def __setstate__(self, dic): + self.__dict__ = dic + return self + + def isEmpty(self): + """ Check if the citation has not been set + + :return: True if nothing was setup + :rtype: bool + """ + return True + + @staticmethod + def ingest(resource, *args, **kwargs): + """ Static method to ingest a resource and produce a BaseCitation + + :return: BaseCitation root given the resource given + :rtype: BaseCitation + """ + pass + + +class NodeId(object): + """ Collection of directional references for a Tree + + :param identifier: Current object identifier + :type identifier: str + :param children: Current node Children's Identifier + :type children: [str] + :param parent: Parent of the current node + :type parent: str + :param siblings: Previous and next node of the current node + :type siblings: str + :param depth: Depth of the node in the global hierarchy of the text tree + :type depth: int + """ + def __init__(self, identifier=None, children=None, parent=None, siblings=(None, None), depth=None): + self.__children__ = children or [] + self.__parent__ = parent + self.__prev__, self.__nextId__ = siblings + self.__identifier__ = identifier + self.__depth__ = depth + + @property + def depth(self): + """ Depth of the node in the global hierarchy of the text tree + + :rtype: int + """ + return self.__depth__ + + @property + def childIds(self): + """ Children Node + + :rtype: [str] + """ + return self.__children__ + + @property + def firstId(self): + """ First child Node + + :rtype: str + """ + if len(self.__children__) == 0: + return None + return self.__children__[0] + + @property + def lastId(self): + """ Last child Node + + :rtype: str + """ + if len(self.__children__) == 0: + return None + return self.__children__[-1] + + @property + def parentId(self): + """ Parent Node + + :rtype: str + """ + return self.__parent__ + + @property + def siblingsId(self): + """ Siblings Node + + :rtype: (str, str) + """ + return self.__prev__, self.__nextId__ + + @property + def prevId(self): + """ Previous Node (Sibling) + + :rtype: str + """ + return self.__prev__ + + @property + def nextId(self): + """ Next Node (Sibling) + + :rtype: str + """ + return self.__nextId__ + + @property + def id(self): + """Current object identifier + + :rtype: str + """ + return self.__identifier__ \ No newline at end of file diff --git a/MyCapytain/common/reference.py b/MyCapytain/common/reference/_capitains_cts.py similarity index 88% rename from MyCapytain/common/reference.py rename to MyCapytain/common/reference/_capitains_cts.py index ee495b7f..34e3406b 100644 --- a/MyCapytain/common/reference.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -1,25 +1,20 @@ -# -*- coding: utf-8 -*- -""" -.. module:: MyCapytain.common.reference - :synopsis: URN related objects - -.. moduleauthor:: Thibault Clérice - -""" -from __future__ import unicode_literals -from six import text_type -from copy import copy import re +from copy import copy + from lxml.etree import _Element -from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes, get_graph, RDF_NAMESPACES + from MyCapytain.common.base import Exportable +from MyCapytain.common.constants import Mimetypes, get_graph, RDF_NAMESPACES, XPATH_NAMESPACES from MyCapytain.common.utils import make_xml_node +from ._base import BaseCitation + REFSDECL_SPLITTER = re.compile(r"/+[*()|\sa-zA-Z0-9:\[\]@=\\{$'\".\s]+") REFSDECL_REPLACER = re.compile(r"\$[0-9]+") SUBREFERENCE = re.compile(r"(\w*)\[?([0-9]*)\]?", re.UNICODE) REFERENCE_REPLACER = re.compile(r"(@[a-zA-Z0-9:]+)(=)([\\$'\"?0-9]{3,6})") + def __childOrNone__(liste): """ Used to parse resources in XmlCtsCitation @@ -413,7 +408,7 @@ def __len__(self): >>> a = URN(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1") >>> print(len(a)) """ - + items = [ key for key, value in self.__parsed.items() @@ -457,7 +452,7 @@ def __lt__(self, other): def __eq__(self, other): """ Equality checker for URN object - + :param other: An object to be checked against :type other: URN :rtype: boolean @@ -481,12 +476,12 @@ def __ne__(self, other): def __str__(self): """ Return full initial urn - + :rtype: basestring :returns: String representation of URN Object :Example: - >>> a = URN(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1") + >>> a = URN(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1") >>> str(a) == "urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1" """ if self.__urn is None: @@ -513,7 +508,7 @@ def upTo(self, key): :rtype: str :Example: - >>> a = URN(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1") + >>> a = URN(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1") >>> a.upTo(URN.TEXTGROUP) == "urn:cts:latinLit:phi1294" """ middle = [ @@ -630,9 +625,9 @@ def __parse__(self, urn): return parsed -class Citation(Exportable): +class Citation(BaseCitation): """ A citation object gives informations about the scheme - + :param name: Name of the citation (e.g. "book") :type name: basestring :param xpath: Xpath of the citation (As described by CTS norm) @@ -664,9 +659,9 @@ class Citation(Exportable): def __init__(self, name=None, xpath=None, scope=None, refsDecl=None, child=None): """ Initialize a XmlCtsCitation object """ - self.__name = None + super(Citation, self).__init__(name=name, children=[child]) + self.__refsDecl = None - self.__child = None self.name = name if scope and xpath: @@ -674,26 +669,10 @@ def __init__(self, name=None, xpath=None, scope=None, refsDecl=None, child=None) else: self.refsDecl = refsDecl - if child is not None: - self.child = child - - @property - def name(self): - """ Type of the citation represented - - :type: text_type - :example: Book, Chapter, Textpart, Section, Poem... - """ - return self.__name - - @name.setter - def name(self, val): - self.__name = val - @property def xpath(self): """ CtsTextInventoryMetadata xpath property of a citation (ie. identifier of the last element of the citation) - + :type: basestring :Example: //tei:l[@n="?"] """ @@ -708,12 +687,12 @@ def xpath(self, new_xpath): @property def scope(self): """ CtsTextInventoryMetadata scope property of a citation (ie. identifier of all element but the last of the citation) - + :type: basestring :Example: /tei:TEI/tei:text/tei:body/tei:div """ return self._parseXpathScope()[0] - + @scope.setter def scope(self, new_scope): if new_scope is not None and self.refsDecl: @@ -728,7 +707,7 @@ def refsDecl(self): :Example: /tei:TEI/tei:text/tei:body/tei:div//tei:l[@n='$1'] """ return self.__refsDecl - + @refsDecl.setter def refsDecl(self, val): if val is not None: @@ -741,12 +720,15 @@ def child(self): :type: XmlCtsCitation or None :Example: XmlCtsCitation.name==poem would have a child XmlCtsCitation.name==line """ - return self.__child - + if len(self.children): + return self.children[0] + @child.setter def child(self, val): - if isinstance(val, self.__class__): - self.__child = val + if val: + self.children = [val] + else: + self.children = [] @property def attribute(self): @@ -769,7 +751,7 @@ def _parseXpathScope(self): def _fromScopeXpathToRefsDecl(self, scope, xpath): """ Update xpath and scope property when refsDecl is updated - + """ if scope is not None and xpath is not None: _xpath = scope + xpath @@ -781,26 +763,6 @@ def _fromScopeXpathToRefsDecl(self, scope, xpath): ii += 1 self.refsDecl = _xpath - def __iter__(self): - """ Iteration method - - Loop over the citation childs - - :Example: - >>> c = XmlCtsCitation(name="line") - >>> b = XmlCtsCitation(name="poem", child=c) - >>> a = XmlCtsCitation(name="book", child=b) - >>> [e for e in a] == [a, b, c] - - """ - e = self - while e is not None: - yield e - if hasattr(e, "child") and e.child is not None: - e = e.child - else: - break - def __getitem__(self, item): if not isinstance(item, int) or item > len(self)-1: raise KeyError("XmlCtsCitation index is too big") @@ -812,7 +774,11 @@ def __len__(self): :rtype: int :returns: Number of nested citations """ - return len([item for item in self]) + return len([x for x in self]) + + def match(self, passageId): + ref = Reference(passageId) + return self[len(ref)-1] def fill(self, passage=None, xpath=None): """ Fill the xpath with given informations @@ -842,7 +808,7 @@ def fill(self, passage=None, xpath=None): xpath = self.xpath replacement = r"\1" - if isinstance(passage, text_type): + if isinstance(passage, str): replacement = r"\1\2'" + passage + "'" return REFERENCE_REPLACER.sub(replacement, xpath) @@ -856,7 +822,7 @@ def fill(self, passage=None, xpath=None): ) passage = iter(passage) return REFERENCE_REPLACER.sub( - lambda m: REF_REPLACER(m, passage), + lambda m: _ref_replacer(m, passage), self.refsDecl ) @@ -954,7 +920,7 @@ def ingest(resource, xpath=".//tei:cRefPattern"): return resources[-1] -def REF_REPLACER(match, passage): +def _ref_replacer(match, passage): """ Helper to replace xpath/scope/refsDecl on iteration with passage value :param match: A RegExp match @@ -970,102 +936,4 @@ def REF_REPLACER(match, passage): if ref is None: return groups[0] else: - return "{1}='{0}'".format(ref, groups[0]) - - -class NodeId(object): - """ Collection of directional references for a Tree - - :param identifier: Current object identifier - :type identifier: str - :param children: Current node Children's Identifier - :type children: [str] - :param parent: Parent of the current node - :type parent: str - :param siblings: Previous and next node of the current node - :type siblings: str - :param depth: Depth of the node in the global hierarchy of the text tree - :type depth: int - """ - def __init__(self, identifier=None, children=None, parent=None, siblings=(None, None), depth=None): - self.__children__ = children or [] - self.__parent__ = parent - self.__prev__, self.__nextId__ = siblings - self.__identifier__ = identifier - self.__depth__ = depth - - @property - def depth(self): - """ Depth of the node in the global hierarchy of the text tree - - :rtype: int - """ - return self.__depth__ - - @property - def childIds(self): - """ Children Node - - :rtype: [str] - """ - return self.__children__ - - @property - def firstId(self): - """ First child Node - - :rtype: str - """ - if len(self.__children__) == 0: - return None - return self.__children__[0] - - @property - def lastId(self): - """ Last child Node - - :rtype: str - """ - if len(self.__children__) == 0: - return None - return self.__children__[-1] - - @property - def parentId(self): - """ Parent Node - - :rtype: str - """ - return self.__parent__ - - @property - def siblingsId(self): - """ Siblings Node - - :rtype: (str, str) - """ - return self.__prev__, self.__nextId__ - - @property - def prevId(self): - """ Previous Node (Sibling) - - :rtype: str - """ - return self.__prev__ - - @property - def nextId(self): - """ Next Node (Sibling) - - :rtype: str - """ - return self.__nextId__ - - @property - def id(self): - """Current object identifier - - :rtype: str - """ - return self.__identifier__ + return "{1}='{0}'".format(ref, groups[0]) \ No newline at end of file diff --git a/MyCapytain/resolvers/cts/local.py b/MyCapytain/resolvers/cts/local.py index dc589277..82134bcb 100644 --- a/MyCapytain/resolvers/cts/local.py +++ b/MyCapytain/resolvers/cts/local.py @@ -7,7 +7,7 @@ from glob import glob from math import ceil -from MyCapytain.common.reference import URN, Reference +from MyCapytain.common.reference._capitains_cts import Reference, URN from MyCapytain.common.utils import xmlparser from MyCapytain.errors import InvalidURN, UnknownObjectError, UndispatchedTextError from MyCapytain.resolvers.prototypes import Resolver diff --git a/MyCapytain/resources/collections/cts.py b/MyCapytain/resources/collections/cts.py index 250eccc2..6fc91201 100644 --- a/MyCapytain/resources/collections/cts.py +++ b/MyCapytain/resources/collections/cts.py @@ -14,7 +14,7 @@ from lxml.objectify import IntElement, FloatElement from MyCapytain.resources.prototypes.cts import inventory as cts -from MyCapytain.common.reference import Citation as CitationPrototype +from MyCapytain.common.reference._capitains_cts import Citation as CitationPrototype from MyCapytain.common.utils import xmlparser, expand_namespace from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes, RDF_NAMESPACES diff --git a/MyCapytain/resources/prototypes/cts/inventory.py b/MyCapytain/resources/prototypes/cts/inventory.py index 6f4ce6e6..9626b663 100644 --- a/MyCapytain/resources/prototypes/cts/inventory.py +++ b/MyCapytain/resources/prototypes/cts/inventory.py @@ -10,7 +10,7 @@ from six import text_type from MyCapytain.resources.prototypes.metadata import Collection, ResourceCollection -from MyCapytain.common.reference import URN +from MyCapytain.common.reference._capitains_cts import URN from MyCapytain.common.utils import make_xml_node, xmlparser from MyCapytain.common.constants import RDF_NAMESPACES, Mimetypes from MyCapytain.errors import InvalidURN diff --git a/MyCapytain/resources/prototypes/text.py b/MyCapytain/resources/prototypes/text.py index d9e8125d..3be9dc9b 100644 --- a/MyCapytain/resources/prototypes/text.py +++ b/MyCapytain/resources/prototypes/text.py @@ -10,7 +10,9 @@ from six import text_type from rdflib.namespace import DC from rdflib import BNode, URIRef -from MyCapytain.common.reference import URN, Citation, NodeId +from MyCapytain.common.reference import URN +from MyCapytain.common.reference._base import NodeId +from MyCapytain.common.reference._capitains_cts import Citation from MyCapytain.common.metadata import Metadata from MyCapytain.common.constants import Mimetypes, get_graph, RDF_NAMESPACES from MyCapytain.common.base import Exportable @@ -370,7 +372,7 @@ class CtsNode(InteractiveTextualNode): """ Initiate a Resource object :param urn: A URN identifier - :type urn: URN + :type urn: MyCapytain.common.reference._capitains_cts.URN :param metadata: Collection Information about the Item :type metadata: Collection :param citation: XmlCtsCitation system of the text @@ -399,7 +401,7 @@ def __init__(self, urn=None, **kwargs): def urn(self): """ URN Identifier of the object - :rtype: URN + :rtype: MyCapytain.common.reference._capitains_cts.URN """ return self.__urn__ @@ -408,7 +410,7 @@ def urn(self, value): """ Set the urn :param value: URN to be saved - :type value: URN + :type value: MyCapytain.common.reference._capitains_cts.URN :raises: *TypeError* when the value is not URN compatible """ @@ -477,7 +479,7 @@ class Passage(CtsNode): """ CapitainsCtsPassage objects possess metadata informations :param urn: A URN identifier - :type urn: URN + :type urn: MyCapytain.common.reference._capitains_cts.URN :param metadata: Collection Information about the Item :type metadata: Collection :param citation: XmlCtsCitation system of the text diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index b1dbc47e..76b9bcda 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -14,7 +14,7 @@ from MyCapytain.errors import DuplicateReference, MissingAttribute, RefsDeclError from MyCapytain.common.utils import copyNode, passageLoop, normalizeXpath from MyCapytain.common.constants import XPATH_NAMESPACES, RDF_NAMESPACES -from MyCapytain.common.reference import URN, Citation, Reference +from MyCapytain.common.reference._capitains_cts import Reference, URN, Citation from MyCapytain.resources.prototypes import text from MyCapytain.resources.texts.base.tei import TEIResource @@ -424,7 +424,7 @@ class CapitainsCtsText(__SharedMethods__, TEIResource, text.CitableText): """ Implementation of CTS tools for local files :param urn: A URN identifier - :type urn: MyCapytain.common.reference.URN + :type urn: MyCapytain.common.reference._capitains_cts.URN :param resource: A resource :type resource: lxml.etree._Element :param citation: Highest XmlCtsCitation level diff --git a/MyCapytain/resources/texts/remote/cts.py b/MyCapytain/resources/texts/remote/cts.py index 931ee816..91bd89fe 100644 --- a/MyCapytain/resources/texts/remote/cts.py +++ b/MyCapytain/resources/texts/remote/cts.py @@ -13,7 +13,7 @@ from MyCapytain.common.metadata import Metadata from MyCapytain.common.utils import xmlparser from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes, RDF_NAMESPACES -from MyCapytain.common.reference import URN, Reference +from MyCapytain.common.reference._capitains_cts import Reference, URN from MyCapytain.resources.collections import cts as CtsCollection from MyCapytain.resources.prototypes import text as prototypes from MyCapytain.resources.texts.base.tei import TEIResource diff --git a/MyCapytain/retrievers/cts5.py b/MyCapytain/retrievers/cts5.py index 45c82832..26972179 100644 --- a/MyCapytain/retrievers/cts5.py +++ b/MyCapytain/retrievers/cts5.py @@ -8,7 +8,7 @@ """ import MyCapytain.retrievers.prototypes -from MyCapytain.common.reference import Reference +from MyCapytain.common.reference._capitains_cts import Reference import requests diff --git a/tests/common/test_reference.py b/tests/common/test_reference.py index 545b5df3..802e0eda 100644 --- a/tests/common/test_reference.py +++ b/tests/common/test_reference.py @@ -3,7 +3,8 @@ from past.builtins import basestring from six import text_type as str import unittest -from MyCapytain.common.reference import URN, Reference, Citation, NodeId +from MyCapytain.common.reference._base import NodeId +from MyCapytain.common.reference._capitains_cts import Reference, URN, Citation class TestReferenceImplementation(unittest.TestCase): diff --git a/tests/resolvers/cts/test_api.py b/tests/resolvers/cts/test_api.py index 9b50cf41..f2e2a6cb 100644 --- a/tests/resolvers/cts/test_api.py +++ b/tests/resolvers/cts/test_api.py @@ -327,7 +327,7 @@ def test_getMetadata_full(self): "There should be one node in exported format corresponding to lat2" ) self.assertCountEqual( - [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["dts:members"]], + [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["hydra:member"]], ["urn:cts:latinLit:phi1294", "urn:cts:latinLit:phi0959", "urn:cts:greekLit:tlg0003", "urn:cts:latinLit:phi1276"], "There should be 4 Members in DTS JSON" ) @@ -362,7 +362,7 @@ def test_getMetadata_subset(self): "There should be one node in exported format corresponding to lat2" ) self.assertCountEqual( - [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["dts:members"]], + [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["hydra:member"]], ["urn:cts:latinLit:phi1294.phi002.perseus-lat2", "urn:cts:latinLit:phi1294.phi002.perseus-eng2"], "There should be one member in DTS JSON" ) diff --git a/tests/resolvers/cts/test_local.py b/tests/resolvers/cts/test_local.py index 660fd178..b41fd852 100644 --- a/tests/resolvers/cts/test_local.py +++ b/tests/resolvers/cts/test_local.py @@ -4,7 +4,7 @@ from MyCapytain.resolvers.cts.local import CtsCapitainsLocalResolver from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes, RDF_NAMESPACES, get_graph -from MyCapytain.common.reference import URN, Reference +from MyCapytain.common.reference._capitains_cts import Reference, URN from MyCapytain.errors import InvalidURN, UnknownObjectError, UndispatchedTextError from MyCapytain.resources.prototypes.metadata import Collection from MyCapytain.resources.collections.cts import XmlCtsTextInventoryMetadata @@ -459,7 +459,7 @@ def test_getMetadata_full(self): "There should be one node in exported format corresponding to lat2" ) self.assertCountEqual( - [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["dts:members"]], + [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["hydra:member"]], ["urn:cts:latinLit:phi1294", "urn:cts:latinLit:phi0959", "urn:cts:greekLit:tlg0003", "urn:cts:latinLit:phi1276"], "There should be 4 Members in DTS JSON" @@ -502,7 +502,7 @@ def test_getMetadata_subset(self): "There should be one node in exported format corresponding to lat2" ) self.assertCountEqual( - [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["dts:members"]], + [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["hydra:member"]], ["urn:cts:latinLit:phi1294.phi002.opp-eng3", "urn:cts:latinLit:phi1294.phi002.perseus-lat2"], "There should be two members in DTS JSON" ) diff --git a/tests/resources/collections/test_dts.py b/tests/resources/collections/test_dts.py index 47ce941e..fb4ba01c 100644 --- a/tests/resources/collections/test_dts.py +++ b/tests/resources/collections/test_dts.py @@ -146,7 +146,7 @@ def test_collection_with_complexe_child(self): 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#', 'skos': 'http://www.w3.org/2004/02/skos/core#', - "tei": "http://www.tei-c.org/ns/1.0" + # "tei": "http://www.tei-c.org/ns/1.0" }, "@id": "urn:cts:latinLit:phi1103.phi001", "@type": "hydra:Collection", diff --git a/tests/resources/proto/test_text.py b/tests/resources/proto/test_text.py index 08ce11b1..4494d49f 100644 --- a/tests/resources/proto/test_text.py +++ b/tests/resources/proto/test_text.py @@ -4,6 +4,9 @@ import unittest +import MyCapytain.common.reference._capitains_cts +from MyCapytain.common.reference._capitains_cts import URN + from MyCapytain.resources.prototypes.text import * import MyCapytain.common.reference import MyCapytain.common.metadata @@ -26,12 +29,12 @@ def test_urn(self): a = CtsNode() # Should work with string a.urn = "urn:cts:latinLit:tg.wk.v" - self.assertEqual(isinstance(a.urn, MyCapytain.common.reference.URN), True) + self.assertEqual(isinstance(a.urn, MyCapytain.common.reference._capitains_cts.URN), True) self.assertEqual(str(a.urn), "urn:cts:latinLit:tg.wk.v") # Test for URN - a.urn = MyCapytain.common.reference.URN("urn:cts:latinLit:tg.wk.v2") - self.assertEqual(isinstance(a.urn, MyCapytain.common.reference.URN), True) + a.urn = MyCapytain.common.reference._capitains_cts.URN("urn:cts:latinLit:tg.wk.v2") + self.assertEqual(isinstance(a.urn, MyCapytain.common.reference._capitains_cts.URN), True) self.assertEqual(str(a.urn), "urn:cts:latinLit:tg.wk.v2") # Test it fails if not basestring or URN @@ -93,12 +96,12 @@ def test_urn(self): a = CitableText() # Should work with string a.urn = "urn:cts:latinLit:tg.wk.v" - self.assertEqual(isinstance(a.urn, MyCapytain.common.reference.URN), True) + self.assertEqual(isinstance(a.urn, MyCapytain.common.reference._capitains_cts.URN), True) self.assertEqual(str(a.urn), "urn:cts:latinLit:tg.wk.v") # Test for URN - a.urn = MyCapytain.common.reference.URN("urn:cts:latinLit:tg.wk.v2") - self.assertEqual(isinstance(a.urn, MyCapytain.common.reference.URN), True) + a.urn = MyCapytain.common.reference._capitains_cts.URN("urn:cts:latinLit:tg.wk.v2") + self.assertEqual(isinstance(a.urn, MyCapytain.common.reference._capitains_cts.URN), True) self.assertEqual(str(a.urn), "urn:cts:latinLit:tg.wk.v2") # Test it fails if not basestring or URN @@ -120,9 +123,9 @@ def test_reffs(self): def test_citation(self): """ Test citation property setter and getter """ a = CitableText() - a.citation = MyCapytain.common.reference.Citation(name="label") - self.assertIsInstance(a.citation, MyCapytain.common.reference.Citation) + a.citation = MyCapytain.common.reference._capitains_cts.Citation(name="label") + self.assertIsInstance(a.citation, MyCapytain.common.reference._capitains_cts.Citation) #On init ? - b = CitableText(citation=MyCapytain.common.reference.Citation(name="label")) + b = CitableText(citation=MyCapytain.common.reference._capitains_cts.Citation(name="label")) self.assertEqual(b.citation.name, "label") diff --git a/tests/resources/texts/base/test_tei.py b/tests/resources/texts/base/test_tei.py index 4ac94a11..9cd4d899 100644 --- a/tests/resources/texts/base/test_tei.py +++ b/tests/resources/texts/base/test_tei.py @@ -3,7 +3,7 @@ import unittest -from MyCapytain.common.reference import Reference, Citation +from MyCapytain.common.reference._capitains_cts import Reference, Citation from MyCapytain.resources.texts.base.tei import TEIResource from MyCapytain.common.constants import Mimetypes from MyCapytain.common.utils import xmlparser diff --git a/tests/resources/texts/local/commonTests.py b/tests/resources/texts/local/commonTests.py index a0aeaa3b..8bd5680c 100644 --- a/tests/resources/texts/local/commonTests.py +++ b/tests/resources/texts/local/commonTests.py @@ -10,7 +10,7 @@ import MyCapytain.errors from MyCapytain.common.constants import Mimetypes -from MyCapytain.common.reference import Reference, URN, Citation +from MyCapytain.common.reference._capitains_cts import Reference, URN, Citation from MyCapytain.resources.texts.local.capitains.cts import CapitainsCtsText diff --git a/tests/resources/texts/local/test_capitains_xml_default.py b/tests/resources/texts/local/test_capitains_xml_default.py index 623930cf..0a10cb0e 100644 --- a/tests/resources/texts/local/test_capitains_xml_default.py +++ b/tests/resources/texts/local/test_capitains_xml_default.py @@ -8,6 +8,7 @@ from lxml import etree import MyCapytain.common.reference +import MyCapytain.common.reference._capitains_cts import MyCapytain.common.utils import MyCapytain.errors import MyCapytain.resources.texts.base.tei @@ -53,8 +54,8 @@ class TestLocalXMLPassageImplementation(CapitainsXmlPassageTests, unittest.TestC simple = False def setUp(self): - self.URN = MyCapytain.common.reference.URN("urn:cts:latinLit:phi1294.phi002.perseus-lat2") - self.URN_2 = MyCapytain.common.reference.URN("urn:cts:latinLit:phi1294.phi002.perseus-lat3") + self.URN = MyCapytain.common.reference._capitains_cts.URN("urn:cts:latinLit:phi1294.phi002.perseus-lat2") + self.URN_2 = MyCapytain.common.reference._capitains_cts.URN("urn:cts:latinLit:phi1294.phi002.perseus-lat3") self.text = open("tests/testing_data/texts/sample.xml", "rb") self.TEI = MyCapytain.resources.texts.local.capitains.cts.CapitainsCtsText(resource=self.text) with open("tests/testing_data/latinLit/data/phi1294/phi002/phi1294.phi002.perseus-lat2.xml", "rb") as f: @@ -70,8 +71,8 @@ class TestLocalXMLSimplePassageImplementation(CapitainsXmlPassageTests, unittest simple = True def setUp(self): - self.URN = MyCapytain.common.reference.URN("urn:cts:latinLit:phi1294.phi002.perseus-lat2") - self.URN_2 = MyCapytain.common.reference.URN("urn:cts:latinLit:phi1294.phi002.perseus-lat3") + self.URN = MyCapytain.common.reference._capitains_cts.URN("urn:cts:latinLit:phi1294.phi002.perseus-lat2") + self.URN_2 = MyCapytain.common.reference._capitains_cts.URN("urn:cts:latinLit:phi1294.phi002.perseus-lat3") self.text = open("tests/testing_data/texts/sample.xml", "rb") self.TEI = MyCapytain.resources.texts.local.capitains.cts.CapitainsCtsText(resource=self.text) with open("tests/testing_data/latinLit/data/phi1294/phi002/phi1294.phi002.perseus-lat2.xml", "rb") as f: @@ -89,4 +90,4 @@ def setUp(self): self.text = MyCapytain.resources.texts.local.capitains.cts.CapitainsCtsText( resource=text, urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2" ) - self.passage = self.text.getTextualNode(MyCapytain.common.reference.Reference("1.pr.2-1.pr.7")) + self.passage = self.text.getTextualNode(MyCapytain.common.reference._capitains_cts.Reference("1.pr.2-1.pr.7")) diff --git a/tests/resources/texts/local/test_capitains_xml_notObjectified.py b/tests/resources/texts/local/test_capitains_xml_notObjectified.py index fd6d2ca8..0314a716 100644 --- a/tests/resources/texts/local/test_capitains_xml_notObjectified.py +++ b/tests/resources/texts/local/test_capitains_xml_notObjectified.py @@ -8,6 +8,7 @@ from lxml import etree import MyCapytain.common.reference +import MyCapytain.common.reference._capitains_cts import MyCapytain.errors import MyCapytain.resources.texts.base.tei import MyCapytain.resources.texts.local.capitains.cts @@ -60,8 +61,8 @@ class TestLocalXMLPassageImplementation(CapitainsXmlPassageTests, unittest.TestC simple = False def setUp(self): - self.URN = MyCapytain.common.reference.URN("urn:cts:latinLit:phi1294.phi002.perseus-lat2") - self.URN_2 = MyCapytain.common.reference.URN("urn:cts:latinLit:phi1294.phi002.perseus-lat3") + self.URN = MyCapytain.common.reference._capitains_cts.URN("urn:cts:latinLit:phi1294.phi002.perseus-lat2") + self.URN_2 = MyCapytain.common.reference._capitains_cts.URN("urn:cts:latinLit:phi1294.phi002.perseus-lat3") self.text = open("tests/testing_data/texts/sample.xml", "rb") self.TEI = MyCapytain.resources.texts.local.capitains.cts.CapitainsCtsText(resource=objectifiedParser(self.text)) with open("tests/testing_data/latinLit/data/phi1294/phi002/phi1294.phi002.perseus-lat2.xml", "rb") as f: @@ -79,8 +80,8 @@ class TestLocalXMLSimplePassageImplementation(CapitainsXmlPassageTests, unittest simple = True def setUp(self): - self.URN = MyCapytain.common.reference.URN("urn:cts:latinLit:phi1294.phi002.perseus-lat2") - self.URN_2 = MyCapytain.common.reference.URN("urn:cts:latinLit:phi1294.phi002.perseus-lat3") + self.URN = MyCapytain.common.reference._capitains_cts.URN("urn:cts:latinLit:phi1294.phi002.perseus-lat2") + self.URN_2 = MyCapytain.common.reference._capitains_cts.URN("urn:cts:latinLit:phi1294.phi002.perseus-lat3") self.text = open("tests/testing_data/texts/sample.xml", "rb") self.TEI = MyCapytain.resources.texts.local.capitains.cts.CapitainsCtsText(resource=objectifiedParser(self.text)) with open("tests/testing_data/latinLit/data/phi1294/phi002/phi1294.phi002.perseus-lat2.xml", "rb") as f: @@ -98,4 +99,4 @@ def setUp(self): self.text = MyCapytain.resources.texts.local.capitains.cts.CapitainsCtsText( resource=objectifiedParser(text), urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2" ) - self.passage = self.text.getTextualNode(MyCapytain.common.reference.Reference("1.pr.2-1.pr.7")) \ No newline at end of file + self.passage = self.text.getTextualNode(MyCapytain.common.reference._capitains_cts.Reference("1.pr.2-1.pr.7")) \ No newline at end of file diff --git a/tests/resources/texts/remote/test_cts.py b/tests/resources/texts/remote/test_cts.py index 049940b0..f2b7689a 100644 --- a/tests/resources/texts/remote/test_cts.py +++ b/tests/resources/texts/remote/test_cts.py @@ -7,7 +7,7 @@ from MyCapytain.resources.texts.remote.cts import CtsPassage, CtsText from MyCapytain.retrievers.cts5 import HttpCtsRetriever -from MyCapytain.common.reference import Reference, Citation, URN +from MyCapytain.common.reference._capitains_cts import Reference, URN, Citation from MyCapytain.common.metadata import Metadata from MyCapytain.common.utils import xmlparser from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes, RDF_NAMESPACES From 698ce22b8fe07f9109f017b2455f1642bb4f25ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Fri, 11 May 2018 17:08:47 +0200 Subject: [PATCH 12/89] Reference refactorization --- MyCapytain/common/reference/_base.py | 26 +++++++++++++++++++ MyCapytain/common/reference/_capitains_cts.py | 21 ++++++++++----- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index 65c80a18..30f26729 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -2,6 +2,32 @@ from copy import copy +class BasePassageId: + def __init__(self, start=None, end=None): + self._start = start + self._end = end + + @property + def is_range(self): + return self._end is not None + + @property + def start(self): + """ Quick access property for start part + + :rtype: str + """ + return self._start + + @property + def end(self): + """ Quick access property for reference end list + + :rtype: str + """ + return self._end + + class BaseCitation(Exportable): def __repr__(self): return self.name diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index 34e3406b..620f70ca 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -3,11 +3,10 @@ from lxml.etree import _Element -from MyCapytain.common.base import Exportable from MyCapytain.common.constants import Mimetypes, get_graph, RDF_NAMESPACES, XPATH_NAMESPACES from MyCapytain.common.utils import make_xml_node -from ._base import BaseCitation +from ._base import BaseCitation, BasePassageId REFSDECL_SPLITTER = re.compile(r"/+[*()|\sa-zA-Z0-9:\[\]@=\\{$'\".\s]+") REFSDECL_REPLACER = re.compile(r"\$[0-9]+") @@ -27,7 +26,7 @@ def __childOrNone__(liste): return None -class Reference(object): +class Reference(BasePassageId): """ A reference object giving information :param reference: CapitainsCtsPassage Reference part of a Urn @@ -109,19 +108,27 @@ def highest(self): def start(self): """ Quick access property for start list - :rtype: Reference + :rtype: end """ if self.parsed[0][0] and len(self.parsed[0][0]): - return Reference(self.parsed[0][0]) + return self.parsed[0][0] @property def end(self): """ Quick access property for reference end list - :rtype: Reference + :rtype: str """ if self.parsed[1][0] and len(self.parsed[1][0]): - return Reference(self.parsed[1][0]) + return self.parsed[1][0] + + @property + def is_range(self): + """ Whether the reference in a starrt + + :rtype: str + """ + return self.parsed[1][0] and len(self.parsed[1][0]) @property def list(self): From 9b5a274daa579842c21093c60a0c657b247c2ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 16 May 2018 10:04:38 +0200 Subject: [PATCH 13/89] Implemented .match() and .root for Citation and BaseCitation + Tests --- MyCapytain/common/reference/_base.py | 18 ++++- MyCapytain/common/reference/_capitains_cts.py | 29 +++++-- tests/common/test_reference/__init__.py | 0 tests/common/test_reference/test_base.py | 31 ++++++++ .../test_capitains_cts.py} | 77 +++++++++++-------- tests/resources/collections/test_cts.py | 36 +++++++++ 6 files changed, 149 insertions(+), 42 deletions(-) create mode 100644 tests/common/test_reference/__init__.py create mode 100644 tests/common/test_reference/test_base.py rename tests/common/{test_reference.py => test_reference/test_capitains_cts.py} (84%) diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index 30f26729..9941e1fa 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -30,9 +30,9 @@ def end(self): class BaseCitation(Exportable): def __repr__(self): - return self.name + return "<{} name({})>".format(type(self).__name__, self.name) - def __init__(self, name=None, children=None): + def __init__(self, name=None, children=None, root=None): """ Initialize a BaseCitation object :param name: Name of the citation level @@ -41,10 +41,23 @@ def __init__(self, name=None, children=None): """ self._name = None self._children = [] + self._root = root self.name = name self.children = children + @property + def is_root(self): + return self._root is None + + @property + def root(self): + return self._root + + @root.setter + def root(self, value): + self._root = value + @property def name(self): """ Type of the citation represented @@ -76,6 +89,7 @@ def children(self, val): elif not isinstance(citation, self.__class__): raise TypeError("Citation children should be Citation") else: + citation.root = self.root final_value.append(citation) self._children = final_value diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index 620f70ca..63583864 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -734,6 +734,10 @@ def child(self): def child(self, val): if val: self.children = [val] + if self.is_root: + val.root = self + else: + val.root = self.root else: self.children = [] @@ -784,8 +788,17 @@ def __len__(self): return len([x for x in self]) def match(self, passageId): - ref = Reference(passageId) - return self[len(ref)-1] + """ Given a passageId matches a citation level + + :param passageId: A passage to match + :return: + """ + if not isinstance(passageId, Reference): + passageId = Reference(passageId) + + if self.is_root: + return self[len(passageId)-1] + return self.root.match(passageId) def fill(self, passage=None, xpath=None): """ Fill the xpath with given informations @@ -913,18 +926,20 @@ def ingest(resource, xpath=".//tei:cRefPattern"): return None resource = resource.xpath(xpath, namespaces=XPATH_NAMESPACES) - resources = [] + citations = [] for x in range(0, len(resource)): - resources.append( + citations.append( Citation( name=resource[x].get("n"), refsDecl=resource[x].get("replacementPattern")[7:-1], - child=__childOrNone__(resources) + child=__childOrNone__(citations) ) ) - - return resources[-1] + if len(citations) > 1: + for citation in citations[:-1]: + citation.root = citations[-1] + return citations[-1] def _ref_replacer(match, passage): diff --git a/tests/common/test_reference/__init__.py b/tests/common/test_reference/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/common/test_reference/test_base.py b/tests/common/test_reference/test_base.py new file mode 100644 index 00000000..7a5041a1 --- /dev/null +++ b/tests/common/test_reference/test_base.py @@ -0,0 +1,31 @@ +import unittest + +from MyCapytain.common.reference import NodeId + + +class TestNodeId(unittest.TestCase): + def test_setup(self): + """ Ensure basic properties works """ + n = NodeId(children=["1", "b", "d"]) + self.assertEqual(n.childIds, ["1", "b", "d"]) + self.assertEqual(n.lastId, "d") + self.assertEqual(n.firstId, "1") + self.assertEqual(n.depth, None) + self.assertEqual(n.parentId, None) + self.assertEqual(n.id, None) + self.assertEqual(n.prevId, None) + self.assertEqual(n.nextId, None) + self.assertEqual(n.siblingsId, (None, None)) + + n = NodeId(parent="1", identifier="1.1") + self.assertEqual(n.parentId, "1") + self.assertEqual(n.id, "1.1") + + n = NodeId(siblings=("1", "1.1"), depth=5) + self.assertEqual(n.prevId, "1") + self.assertEqual(n.nextId, "1.1") + self.assertEqual(n.childIds, []) + self.assertEqual(n.firstId, None) + self.assertEqual(n.lastId, None) + self.assertEqual(n.siblingsId, ("1", "1.1")) + self.assertEqual(n.depth, 5) \ No newline at end of file diff --git a/tests/common/test_reference.py b/tests/common/test_reference/test_capitains_cts.py similarity index 84% rename from tests/common/test_reference.py rename to tests/common/test_reference/test_capitains_cts.py index 802e0eda..3f77538f 100644 --- a/tests/common/test_reference.py +++ b/tests/common/test_reference/test_capitains_cts.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals -from past.builtins import basestring -from six import text_type as str import unittest -from MyCapytain.common.reference._base import NodeId -from MyCapytain.common.reference._capitains_cts import Reference, URN, Citation + +from six import text_type as str +from MyCapytain.common.utils import xmlparser +from MyCapytain.common.reference import Reference, URN, Citation class TestReferenceImplementation(unittest.TestCase): @@ -380,30 +378,43 @@ def test_fill(self): self.assertEqual(c.fill([None, None]), "/TEI/text/body/div/div[@n]//l[@n]") self.assertEqual(c.fill(["1", None]), "/TEI/text/body/div/div[@n='1']//l[@n]") - -class TestNodeId(unittest.TestCase): - def test_setup(self): - """ Ensure basic properties works """ - n = NodeId(children=["1", "b", "d"]) - self.assertEqual(n.childIds, ["1", "b", "d"]) - self.assertEqual(n.lastId, "d") - self.assertEqual(n.firstId, "1") - self.assertEqual(n.depth, None) - self.assertEqual(n.parentId, None) - self.assertEqual(n.id, None) - self.assertEqual(n.prevId, None) - self.assertEqual(n.nextId, None) - self.assertEqual(n.siblingsId, (None, None)) - - n = NodeId(parent="1", identifier="1.1") - self.assertEqual(n.parentId, "1") - self.assertEqual(n.id, "1.1") - - n = NodeId(siblings=("1", "1.1"), depth=5) - self.assertEqual(n.prevId, "1") - self.assertEqual(n.nextId, "1.1") - self.assertEqual(n.childIds, []) - self.assertEqual(n.firstId, None) - self.assertEqual(n.lastId, None) - self.assertEqual(n.siblingsId, ("1", "1.1")) - self.assertEqual(n.depth, 5) + def test_ingest_and_match(self): + """ Ensure matching and parsing XML works correctly """ + xml = xmlparser(""" + + +

This pointer pattern extracts book and poem and line

+
+ +

This pointer pattern extracts book and poem

+
+ +

This pointer pattern extracts book

+
+
+
""") + citation = Citation.ingest(xml) + # The citation that should be returned is the root + self.assertEqual(citation.name, "book", "Name should have been parsed") + self.assertEqual(citation.child.name, "poem", "Name of child should have been parsed") + self.assertEqual(citation.child.child.name, "line", "Name of descendants should have been parsed") + + self.assertEqual(citation.is_root, True, "Root should be true on root") + self.assertEqual(citation.match("1.2"), citation.child, "Matching should make use of root matching") + self.assertEqual(citation.match("1.2.4"), citation.child.child, "Matching should make use of root matching") + self.assertEqual(citation.match("1"), citation, "Matching should make use of root matching") + + self.assertEqual(citation.child.match("1.2").name, "poem", "Matching should retrieve poem at 2nd level") + self.assertEqual(citation.child.match("1.2.4").name, "line", "Matching should retrieve line at 3rd level") + self.assertEqual(citation.child.match("1").name, "book", "Matching retrieve book at 1st level") + + citation = citation.child + self.assertEqual(citation.child.match("1.2").name, "poem", "Matching should retrieve poem at 2nd level") + self.assertEqual(citation.child.match("1.2.4").name, "line", "Matching should retrieve line at 3rd level") + self.assertEqual(citation.child.match("1").name, "book", "Matching retrieve book at 1st level") diff --git a/tests/resources/collections/test_cts.py b/tests/resources/collections/test_cts.py index 4f56f598..f8e94762 100644 --- a/tests/resources/collections/test_cts.py +++ b/tests/resources/collections/test_cts.py @@ -662,3 +662,39 @@ class TestCitation(unittest.TestCase): def test_empty(self): a = XmlCtsCitation(name="none") self.assertEqual(a.export(), "") + + def test_ingest_and_match(self): + """ Ensure matching and parsing XML works correctly """ + xml = xmlparser(""" + Epigrammata Label + Epigrammes Label + W. Heraeus + G. Heraeus + + + + + + + + + + """.replace("\n", "")) + citation = (XmlCtsEditionMetadata.parse(xml)).citation + # The citation that should be returned is the root + self.assertEqual(citation.name, "book", "Name should have been parsed") + self.assertEqual(citation.child.name, "poem", "Name of child should have been parsed") + self.assertEqual(citation.child.child.name, "line", "Name of descendants should have been parsed") + self.assertEqual(citation.is_root, True, "Root should be true on root") + self.assertEqual(citation.match("1.2"), citation.child, "Matching should make use of root matching") + self.assertEqual(citation.match("1.2.4"), citation.child.child, "Matching should make use of root matching") + self.assertEqual(citation.match("1"), citation, "Matching should make use of root matching") + + self.assertEqual(citation.child.match("1.2").name, "poem", "Matching should retrieve poem at 2nd level") + self.assertEqual(citation.child.match("1.2.4").name, "line", "Matching should retrieve line at 3rd level") + self.assertEqual(citation.child.match("1").name, "book", "Matching retrieve book at 1st level") + + citation = citation.child + self.assertEqual(citation.child.match("1.2").name, "poem", "Matching should retrieve poem at 2nd level") + self.assertEqual(citation.child.match("1.2.4").name, "line", "Matching should retrieve line at 3rd level") + self.assertEqual(citation.child.match("1").name, "book", "Matching retrieve book at 1st level") From 9c666ff3d6fa933918ec2af44ae81dade19daa4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 16 May 2018 16:45:11 +0200 Subject: [PATCH 14/89] Fixed tests tied to the breaking change of Reference().start and Reference.end() in common.reference._capitains_cts to return str instead of Reference --- MyCapytain/common/reference/_base.py | 51 +++++++++++++++---- MyCapytain/common/reference/_capitains_cts.py | 12 +++-- .../resources/texts/local/capitains/cts.py | 19 +++---- .../test_reference/test_capitains_cts.py | 42 +++++++-------- 4 files changed, 79 insertions(+), 45 deletions(-) diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index 9941e1fa..a41a32c9 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -1,5 +1,6 @@ from MyCapytain.common.base import Exportable from copy import copy +from abc import abstractmethod class BasePassageId: @@ -28,8 +29,28 @@ def end(self): return self._end -class BaseCitation(Exportable): +class CitationSet: + """ A citation set is a collection of citations that can be matched using + a .match() function + + """ + @abstractmethod + def match(self, passageId): + """ Given a specific passageId, matches the citation to a specific citation object + + :param passageId: Passage Identifier + :return: Citation that matches the passageId + :rtype: BaseCitation + """ + + +class BaseCitation(Exportable, CitationSet): def __repr__(self): + """ + + :return: String representation of the object + :rtype: str + """ return "<{} name({})>".format(type(self).__name__, self.name) def __init__(self, name=None, children=None, root=None): @@ -37,7 +58,10 @@ def __init__(self, name=None, children=None, root=None): :param name: Name of the citation level :type name: str - :param children: + :param children: list of children + :type children: [BaseCitation] + :param root: Root of the citation group + :type root: CitationSet """ self._name = None self._children = [] @@ -48,14 +72,29 @@ def __init__(self, name=None, children=None, root=None): @property def is_root(self): + """ + :return: If the current object is the root of the citation set, True + :rtype: bool + """ return self._root is None @property def root(self): + """ Returns the root of the citation set + + :return: Root of the Citation set + :rtype: CitationSet + """ return self._root @root.setter def root(self, value): + """ Set the root to which the current citation is connected to + + :param value: CitationSet root of the Citation graph + :type value: CitationSet + :return: + """ self._root = value @property @@ -141,14 +180,6 @@ def __len__(self): """ return 0 - def match(self, passageId): - """ Given a specific passageId, matches the citation to a specific citation object - - :param passageId: Passage Identifier - :return: Citation that matches the passageId - :rtype: BaseCitation - """ - def __getstate__(self): """ Pickling method diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index 63583864..1a6ec12f 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -45,8 +45,10 @@ class Reference(BasePassageId): >>> b == Reference("1.1") && b != a .. note:: - While Reference(...).subreference and .list are not available for range, Reference(..).start.subreference \ - and Reference(..).end.subreference as well as .list are available + Reference(...).subreference and .list are not available for range. You will need to convert .start or .end to + a Reference + + >>> ref = Reference('1.2.3') """ def __init__(self, reference=""): @@ -96,7 +98,7 @@ def highest(self): :rtype: Reference """ if not self.end: - return self + return str(self) elif len(self.start) < len(self.end) and len(self.start): return self.start elif len(self.start) > len(self.end) and len(self.end): @@ -167,7 +169,7 @@ def __len__(self): :rtype: int """ - return len(self.highest.list) + return len(Reference(self.highest).list) def __str__(self): """ Return full reference in string format @@ -834,7 +836,7 @@ def fill(self, passage=None, xpath=None): return REFERENCE_REPLACER.sub(replacement, xpath) else: if isinstance(passage, Reference): - passage = passage.list or passage.start.list + passage = passage.list or Reference(passage.start).list elif passage is None: return REFERENCE_REPLACER.sub( r"\1", diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index 76b9bcda..22265030 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -63,9 +63,9 @@ def getTextualNode(self, subreference=None, simple=False): start, end = subreference, subreference subreference = Reference(".".join(subreference)) elif not subreference.end: - start, end = subreference.start.list, subreference.start.list + start = end = Reference(subreference.start).list else: - start, end = subreference.start.list, subreference.end.list + start, end = Reference(subreference.start).list, Reference(subreference.end).list if len(start) > len(self.citation): raise ReferenceError("URN is deeper than citation scheme") @@ -190,16 +190,17 @@ def getValidReff(self, level=None, reference=None, _debug=False): else: xml = self.getTextualNode(subreference=reference) common = [] - for index in range(0, len(reference.start.list)): + ref = Reference(reference.start) + for index in range(0, len(ref.list)): if index == (len(common) - 1): - common.append(reference.start.list[index]) + common.append(ref.list[index]) else: break passages = [common] depth = len(common) if not level: - level = len(reference.start.list) + 1 + level = len(ref.list) + 1 else: raise TypeError() @@ -525,9 +526,9 @@ def __init__(self, reference, urn=None, citation=None, resource=None, text=None) self.__depth__ = self.__depth_2__ = 1 if self.reference.start: - self.__depth_2__ = self.__depth__ = len(self.reference.start) + self.__depth_2__ = self.__depth__ = len(Reference(self.reference.start)) if self.reference and self.reference.end: - self.__depth_2__ = len(self.reference.end) + self.__depth_2__ = len(Reference(self.reference.end)) self.__prevnext__ = None # For caching purpose @@ -597,10 +598,10 @@ def siblingsId(self): document_references = list(map(str, self.__text__.getReffs(level=self.depth))) if self.reference.end: - start, end = str(self.reference.start), str(self.reference.end) + start, end = self.reference.start, self.reference.end range_length = len(self.getReffs(level=0)) else: - start = end = str(self.reference.start) + start = end = self.reference.start range_length = 1 start = document_references.index(start) diff --git a/tests/common/test_reference/test_capitains_cts.py b/tests/common/test_reference/test_capitains_cts.py index 3f77538f..5671927d 100644 --- a/tests/common/test_reference/test_capitains_cts.py +++ b/tests/common/test_reference/test_capitains_cts.py @@ -31,36 +31,36 @@ def test_highest(self): def test_properties(self): a = Reference("1.1@Achilles-1.10@Atreus[3]") - self.assertEqual(str(a.start), "1.1@Achilles") - self.assertEqual(a.start.list, ["1", "1"]) - self.assertEqual(a.start.subreference[0], "Achilles") - self.assertEqual(str(a.end), "1.10@Atreus[3]") - self.assertEqual(a.end.list, ["1", "10"]) - self.assertEqual(a.end.subreference[1], 3) - self.assertEqual(a.end.subreference, ("Atreus", 3)) + self.assertEqual(a.start, "1.1@Achilles") + self.assertEqual(Reference(a.start).list, ["1", "1"]) + self.assertEqual(Reference(a.start).subreference[0], "Achilles") + self.assertEqual(a.end, "1.10@Atreus[3]") + self.assertEqual(Reference(a.end).list, ["1", "10"]) + self.assertEqual(Reference(a.end).subreference[1], 3) + self.assertEqual(Reference(a.end).subreference, ("Atreus", 3)) def test_Unicode_Support(self): a = Reference("1.1@καὶ[0]-1.10@Ἀλκιβιάδου[3]") - self.assertEqual(str(a.start), "1.1@καὶ[0]") - self.assertEqual(a.start.list, ["1", "1"]) - self.assertEqual(a.start.subreference[0], "καὶ") - self.assertEqual(str(a.end), "1.10@Ἀλκιβιάδου[3]") - self.assertEqual(a.end.list, ["1", "10"]) - self.assertEqual(a.end.subreference[1], 3) - self.assertEqual(a.end.subreference, ("Ἀλκιβιάδου", 3)) + self.assertEqual(a.start, "1.1@καὶ[0]") + self.assertEqual(Reference(a.start).list, ["1", "1"]) + self.assertEqual(Reference(a.start).subreference[0], "καὶ") + self.assertEqual(a.end, "1.10@Ἀλκιβιάδου[3]") + self.assertEqual(Reference(a.end).list, ["1", "10"]) + self.assertEqual(Reference(a.end).subreference[1], 3) + self.assertEqual(Reference(a.end).subreference, ("Ἀλκιβιάδου", 3)) def test_NoWord_Support(self): a = Reference("1.1@[0]-1.10@Ἀλκιβιάδου[3]") self.assertEqual(str(a.start), "1.1@[0]") - self.assertEqual(a.start.subreference[0], "") - self.assertEqual(a.start.subreference[1], 0) + self.assertEqual(Reference(a.start).subreference[0], "") + self.assertEqual(Reference(a.start).subreference[1], 0) def test_No_End_Support(self): a = Reference("1.1@[0]") self.assertEqual(a.end, None) - self.assertEqual(str(a.start), "1.1@[0]") - self.assertEqual(a.start.subreference[0], "") - self.assertEqual(a.start.subreference[1], 0) + self.assertEqual(a.start, "1.1@[0]") + self.assertEqual(Reference(a.start).subreference[0], "") + self.assertEqual(Reference(a.start).subreference[1], 0) def test_equality(self): a = Reference("1.1@[0]") @@ -209,8 +209,8 @@ def test_missing_text_in_passage_emptiness(self): self.assertEqual(a.upTo(URN.PASSAGE_START), "urn:cts:greekLit:textgroup.work:1") self.assertEqual(a.upTo(URN.PASSAGE_END), "urn:cts:greekLit:textgroup.work:2") self.assertEqual(a.reference, Reference("1-2")) - self.assertEqual(a.reference.start, Reference("1")) - self.assertEqual(a.reference.end, Reference("2")) + self.assertEqual(a.reference.start, "1") + self.assertEqual(a.reference.end, "2") self.assertIsNone(a.version) def test_warning_on_empty(self): From 12fbd9d3304a584dcbb094f87b50f2109f828212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= <1929830+PonteIneptique@users.noreply.github.com> Date: Wed, 16 May 2018 17:00:49 +0200 Subject: [PATCH 15/89] Remove old badges --- README.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 059c629f..8795ab0a 100644 --- a/README.rst +++ b/README.rst @@ -2,16 +2,12 @@ :target: https://travis-ci.org/Capitains/MyCapytain .. image:: https://coveralls.io/repos/Capitains/MyCapytain/badge.svg?branch=master :target: https://coveralls.io/r/Capitains/MyCapytain?branch=master -.. image:: https://gemnasium.com/Capitains/MyCapytain.svg - :target: https://gemnasium.com/Capitains/MyCapytain .. image:: https://badge.fury.io/py/MyCapytain.svg :target: http://badge.fury.io/py/MyCapytain .. image:: https://readthedocs.org/projects/mycapytain/badge/?version=latest :target: http://mycapytain.readthedocs.org .. image:: https://zenodo.org/badge/3923/Capitains/MyCapytain.svg :target: https://zenodo.org/badge/latestdoi/3923/Capitains/MyCapytain -.. image:: https://img.shields.io/pypi/dm/MyCapytain.svg - :target: https://pypi.python.org/pypi/MyCapytain .. image:: https://api.codacy.com/project/badge/grade/8e63e69a94274422865e4f275dbf08ea :target: https://www.codacy.com/app/leponteineptique/MyCapytain .. image:: https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg @@ -105,4 +101,4 @@ If you prefer to use setup.py, you should clone and use the following git clone https://github.com/Capitains/MyCapytain.git cd MyCapytain - python setup.py install \ No newline at end of file + python setup.py install From 7c4875418257d9c7f46ed4930e208adcf851879d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Thu, 17 May 2018 18:01:24 +0200 Subject: [PATCH 16/89] Starting implementation of DTS Citation parsing Opened an issue regarding this in https://github.com/distributed-text-services/collection-api/issues/101 --- MyCapytain/common/reference/_base.py | 16 ++-- MyCapytain/common/reference/_capitains_cts.py | 11 --- MyCapytain/common/reference/_dts_1.py | 92 +++++++++++++++++++ MyCapytain/resources/collections/dts.py | 1 + .../{test_dts.py => test_dts_collection.py} | 0 tests/retrievers/test_dts.py | 2 +- tests/testing_data/dts/collection_3.json | 6 +- 7 files changed, 103 insertions(+), 25 deletions(-) create mode 100644 MyCapytain/common/reference/_dts_1.py rename tests/resources/collections/{test_dts.py => test_dts_collection.py} (100%) diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index a41a32c9..b652f83f 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -1,6 +1,6 @@ from MyCapytain.common.base import Exportable from copy import copy -from abc import abstractmethod +from abc import abstractmethod, abstractproperty class BasePassageId: @@ -63,6 +63,8 @@ def __init__(self, name=None, children=None, root=None): :param root: Root of the citation group :type root: CitationSet """ + super(BaseCitation, self).__init__() + self._name = None self._children = [] self._root = root @@ -151,6 +153,7 @@ def __iter__(self): yield from child @property + @abstractmethod def depth(self): """ Depth of the citation scheme @@ -161,6 +164,7 @@ def depth(self): """ return 1 + @abstractmethod def __getitem__(self, item): """ Returns the citations at the given level. @@ -191,6 +195,7 @@ def __setstate__(self, dic): self.__dict__ = dic return self + @abstractmethod def isEmpty(self): """ Check if the citation has not been set @@ -199,15 +204,6 @@ def isEmpty(self): """ return True - @staticmethod - def ingest(resource, *args, **kwargs): - """ Static method to ingest a resource and produce a BaseCitation - - :return: BaseCitation root given the resource given - :rtype: BaseCitation - """ - pass - class NodeId(object): """ Collection of directional references for a Tree diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index 1a6ec12f..50f2ccda 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -848,17 +848,6 @@ def fill(self, passage=None, xpath=None): self.refsDecl ) - def __getstate__(self): - """ Pickling method - - :return: dict - """ - return copy(self.__dict__) - - def __setstate__(self, dic): - self.__dict__ = dic - return self - def isEmpty(self): """ Check if the citation has not been set diff --git a/MyCapytain/common/reference/_dts_1.py b/MyCapytain/common/reference/_dts_1.py new file mode 100644 index 00000000..5a169413 --- /dev/null +++ b/MyCapytain/common/reference/_dts_1.py @@ -0,0 +1,92 @@ +from ._base import CitationSet, BaseCitation +import re +from MyCapytain.common.constants import RDF_NAMESPACES + + +_tei = RDF_NAMESPACES.TEI + + +class DtsCitationRoot(CitationSet): + """ Set of citation that are supposed + + """ + def __init__(self): + self._citation_graph = [] + + def add_child(self, child): + self._citation_graph.append(child) + + def match(self, passageId): + """ Match a passagedId against the citation graph + + :param passageId: PassageID + :return: + :rtype: Dts_Citation + """ + for citation in self._citation_graph: + if re.match(citation.pattern, passageId): + return citation + + @staticmethod + def ingest(resource, _root_class=None, _citation_class=None): + """ Ingest a list of DTS Citation object (as parsed JSON-LD) and + creates the Citation Graph + + :param resource: List of Citation objects from the + DTS Collection Endpoint (as expanded JSON-LD) + :type resource: list + :param _root_class: (Dev only) Class to use for instantiating the Citation Set + :type _root_class: class + :param _citation_class: (Dev only) Class to use for instantiating the Citation + :type _root_class: class + :return: Citation Graph + """ + _set = DtsCitationRoot() + for data in resource: + _set.add_child( + DtsCitation.ingest(data) + ) + return _set + + +class DtsCitation(BaseCitation): + def __init__(self, name=None, children=None, root=None, match_pattern=None, replacement_pattern=None): + super(DtsCitation, self).__init__(name=name, children=children, root=root) + self._match_pattern = None + self._replacement_pattern = None + + self.match_pattern = match_pattern + self.replacement_pattern = replacement_pattern + + @property + def match_pattern(self): + return self._match_pattern + + @match_pattern.setter + def match_pattern(self, value): + self._match_pattern = value + + @property + def replacement_pattern(self): + return self._replacement_pattern + + @replacement_pattern.setter + def replacement_pattern(self, value): + self._replacement_pattern = value + + @staticmethod + def ingest(resource, _citation_set=None, **kwargs): + """ Ingest a dictionary of DTS Citation object (as parsed JSON-LD) and + creates the Citation Graph + + :param resource: List of Citation objects from the + DTS Collection Endpoint (as expanded JSON-LD) + :type resource: dict + :return: + """ + return DtsCitation( + name=resource.get(_tei.term("type"), None), + root=_citation_set, + match_pattern=resource.get(_tei.term("matchPattern"), None), + replacement_pattern=resource.get(_tei.term("replacementPattern"), None), + ) diff --git a/MyCapytain/resources/collections/dts.py b/MyCapytain/resources/collections/dts.py index 0430067a..a9406c84 100644 --- a/MyCapytain/resources/collections/dts.py +++ b/MyCapytain/resources/collections/dts.py @@ -66,6 +66,7 @@ def parse(resource, direction="children"): if str(_tei.refsDecl) in collection: for citation in collection[str(_tei.refsDecl)]: + print(citation) # Need to have citation set before going further. continue diff --git a/tests/resources/collections/test_dts.py b/tests/resources/collections/test_dts_collection.py similarity index 100% rename from tests/resources/collections/test_dts.py rename to tests/resources/collections/test_dts_collection.py diff --git a/tests/retrievers/test_dts.py b/tests/retrievers/test_dts.py index 8205edef..fc30a3fb 100644 --- a/tests/retrievers/test_dts.py +++ b/tests/retrievers/test_dts.py @@ -11,7 +11,7 @@ patch_args = ("MyCapytain.retrievers.dts.requests.get", ) -class TestEndpointsCts5(unittest.TestCase): +class TestDtsParsing(unittest.TestCase): """ Test Cts5 Endpoint request making """ def setUp(self): diff --git a/tests/testing_data/dts/collection_3.json b/tests/testing_data/dts/collection_3.json index 3472b869..29b47546 100644 --- a/tests/testing_data/dts/collection_3.json +++ b/tests/testing_data/dts/collection_3.json @@ -3,7 +3,7 @@ "@vocab": "https://www.w3.org/ns/hydra/core#", "dc": "http://purl.org/dc/terms/", "dts": "https://w3id.org/dts/api#", - "tei": "http://www.tei-c.org/ns/1.0" + "tei": "http://www.tei-c.org/ns/1.0/" }, "@id": "urn:cts:latinLit:phi1103.phi001", "@type": "Collection", @@ -53,12 +53,12 @@ { "tei:matchPattern": "(\\w+)", "tei:replacementPattern": "#xpath(/tei:TEI/tei:text/tei:body/tei:div/tei:div[@n='$1'])", - "@type": "poem" + "tei:type": "poem" }, { "tei:matchPattern": "(\\w+)\\.(\\w+)", "tei:replacementPattern": "#xpath(/tei:TEI/tei:text/tei:body/tei:div/tei:div[@n='$1']//tei:l[@n='$2'])", - "@type": "line" + "tei:type": "line" } ] } From 4a836684a0022f5465bb6cce00ed0e73aaa0c3f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 10 Jul 2018 11:16:15 +0200 Subject: [PATCH 17/89] Moved from class dict to class method as this is the pythonic way... --- MyCapytain/resolvers/cts/local.py | 4 +- MyCapytain/resources/collections/cts.py | 86 ++++++++++++------------- MyCapytain/resources/collections/dts.py | 20 +++--- 3 files changed, 55 insertions(+), 55 deletions(-) diff --git a/MyCapytain/resolvers/cts/local.py b/MyCapytain/resolvers/cts/local.py index dc589277..a4162bda 100644 --- a/MyCapytain/resolvers/cts/local.py +++ b/MyCapytain/resolvers/cts/local.py @@ -140,8 +140,7 @@ def _parse_textgroup(self, cts_file): """ with io.open(cts_file) as __xml__: return self.classes["textgroup"].parse( - resource=__xml__, - _cls_dict=self.classes + resource=__xml__ ), cts_file def _parse_work_wrapper(self, cts_file, textgroup): @@ -173,7 +172,6 @@ def _parse_work(self, cts_file, textgroup): work, texts = self.classes["work"].parse( resource=__xml__, parent=textgroup, - _cls_dict=self.classes, _with_children=True ) diff --git a/MyCapytain/resources/collections/cts.py b/MyCapytain/resources/collections/cts.py index 250eccc2..199b14a9 100644 --- a/MyCapytain/resources/collections/cts.py +++ b/MyCapytain/resources/collections/cts.py @@ -27,8 +27,8 @@ class XmlCtsCitation(CitationPrototype): """ - @staticmethod - def ingest(resource, element=None, xpath="ti:citation", _cls_dict=_CLASSES_DICT): + @classmethod + def ingest(cls, resource, element=None, xpath="ti:citation"): """ Ingest xml to create a citation :param resource: XML on which to do xpath @@ -39,23 +39,22 @@ def ingest(resource, element=None, xpath="ti:citation", _cls_dict=_CLASSES_DICT) """ # Reuse of of find citation results = resource.xpath(xpath, namespaces=XPATH_NAMESPACES) - CLASS = _cls_dict.get("citation", XmlCtsCitation) if len(results) > 0: - citation = CLASS( + citation = cls( name=results[0].get("label"), xpath=results[0].get("xpath"), scope=results[0].get("scope") ) - if isinstance(element, CLASS): + if isinstance(element, cls): element.child = citation - CLASS.ingest( + cls.ingest( resource=results[0], element=element.child ) else: element = citation - CLASS.ingest( + cls.ingest( resource=results[0], element=element ) @@ -131,6 +130,7 @@ class XmlCtsTextMetadata(cts.CtsTextMetadata): """ DEFAULT_EXPORT = Mimetypes.PYTHON.ETREE + CLASS_CITATION = XmlCtsCitation @staticmethod def __findCitations(obj, xml, xpath="ti:citation"): @@ -140,15 +140,14 @@ def __findCitations(obj, xml, xpath="ti:citation"): :param xpath: Xpath to use to retrieve the xml node """ - @staticmethod - def parse_metadata(obj, xml, _cls_dict=_CLASSES_DICT): + @classmethod + def parse_metadata(cls, obj, xml): """ Parse a resource to feed the object :param obj: Obj to set metadata of :type obj: XmlCtsTextMetadata :param xml: An xml representation object :type xml: lxml.etree._Element - :param _cls_dict: Dictionary of classes to generate subclasses """ for child in xml.xpath("ti:description", namespaces=XPATH_NAMESPACES): @@ -161,7 +160,7 @@ def parse_metadata(obj, xml, _cls_dict=_CLASSES_DICT): if lg is not None: obj.set_cts_property("label", child.text, lg) - obj.citation = _cls_dict.get("citation", XmlCtsCitation).ingest(xml, obj.citation, "ti:online/ti:citationMapping/ti:citation") + obj.citation = cls.CLASS_CITATION.ingest(xml, obj.citation, "ti:online/ti:citationMapping/ti:citation") # Added for commentary for child in xml.xpath("ti:about", namespaces=XPATH_NAMESPACES): @@ -192,54 +191,57 @@ def path(self): def path(self, value): self._path = value + class XmlCtsEditionMetadata(cts.CtsEditionMetadata, XmlCtsTextMetadata): """ Create an edition subtyped CtsTextMetadata object """ - @staticmethod - def parse(resource, parent=None, _cls_dict=_CLASSES_DICT): + @classmethod + def parse(cls, resource, parent=None): xml = xmlparser(resource) - o = _cls_dict.get("edition", XmlCtsEditionMetadata)(urn=xml.get("urn"), parent=parent) - type(o).parse_metadata(o, xml) - + o = cls(urn=xml.get("urn"), parent=parent) + cls.parse_metadata(o, xml) return o class XmlCtsTranslationMetadata(cts.CtsTranslationMetadata, XmlCtsTextMetadata): """ Create a translation subtyped CtsTextMetadata object """ - @staticmethod - def parse(resource, parent=None, _cls_dict=_CLASSES_DICT): + @classmethod + def parse(cls, resource, parent=None): xml = xmlparser(resource) lang = xml.get("{http://www.w3.org/XML/1998/namespace}lang") - o = _cls_dict.get("translation", XmlCtsTranslationMetadata)(urn=xml.get("urn"), parent=parent) + o = cls(urn=xml.get("urn"), parent=parent) if lang is not None: o.lang = lang - type(o).parse_metadata(o, xml) + cls.parse_metadata(o, xml) return o class XmlCtsCommentaryMetadata(cts.CtsCommentaryMetadata, XmlCtsTextMetadata): """ Create a commentary subtyped PrototypeText object """ - @staticmethod - def parse(resource, parent=None, _cls_dict=_CLASSES_DICT): + @classmethod + def parse(cls, resource, parent=None): xml = xmlparser(resource) lang = xml.get("{http://www.w3.org/XML/1998/namespace}lang") - o = _cls_dict.get("commentary", XmlCtsCommentaryMetadata)(urn=xml.get("urn"), parent=parent) + o = cls(urn=xml.get("urn"), parent=parent) if lang is not None: o.lang = lang - type(o).parse_metadata(o, xml) + cls.parse_metadata(o, xml) return o class XmlCtsWorkMetadata(cts.CtsWorkMetadata): """ Represents a CTS Textgroup in XML """ + CLASS_EDITION = XmlCtsEditionMetadata + CLASS_TRANSLATION = XmlCtsTranslationMetadata + CLASS_COMMENTARY = XmlCtsCommentaryMetadata - @staticmethod - def parse(resource, parent=None, _cls_dict=_CLASSES_DICT, _with_children=False): + @classmethod + def parse(cls, resource, parent=None, _with_children=False): """ Parse a resource :param resource: Element rerpresenting a work @@ -249,7 +251,7 @@ def parse(resource, parent=None, _cls_dict=_CLASSES_DICT, _with_children=False): :param _cls_dict: Dictionary of classes to generate subclasses """ xml = xmlparser(resource) - o = _cls_dict.get("work", XmlCtsWorkMetadata)(urn=xml.get("urn"), parent=parent) + o = cls(urn=xml.get("urn"), parent=parent) lang = xml.get("{http://www.w3.org/XML/1998/namespace}lang") if lang is not None: @@ -264,19 +266,15 @@ def parse(resource, parent=None, _cls_dict=_CLASSES_DICT, _with_children=False): children = [] children.extend(xpathDict( xml=xml, xpath='ti:edition', - cls=_cls_dict.get("edition", XmlCtsEditionMetadata), parent=o, - _cls_dict=_cls_dict + cls=cls.CLASS_EDITION, parent=o )) children.extend(xpathDict( xml=xml, xpath='ti:translation', - cls=_cls_dict.get("translation", XmlCtsTranslationMetadata), parent=o, - _cls_dict=_cls_dict + cls=cls.CLASS_TRANSLATION, parent=o )) - # Added for commentary children.extend(xpathDict( xml=xml, xpath='ti:commentary', - cls=_cls_dict.get("commentary", XmlCtsCommentaryMetadata), parent=o, - _cls_dict=_cls_dict + cls=cls.CLASS_COMMENTARY, parent=o )) __parse_structured_metadata__(o, xml) @@ -289,9 +287,10 @@ def parse(resource, parent=None, _cls_dict=_CLASSES_DICT, _with_children=False): class XmlCtsTextgroupMetadata(cts.CtsTextgroupMetadata): """ Represents a CTS Textgroup in XML """ + CLASS_WORK = XmlCtsWorkMetadata - @staticmethod - def parse(resource, parent=None, _cls_dict=_CLASSES_DICT): + @classmethod + def parse(cls, resource, parent=None): """ Parse a textgroup resource :param resource: Element representing the textgroup @@ -299,7 +298,7 @@ def parse(resource, parent=None, _cls_dict=_CLASSES_DICT): :param _cls_dict: Dictionary of classes to generate subclasses """ xml = xmlparser(resource) - o = _cls_dict.get("textgroup", XmlCtsTextgroupMetadata)(urn=xml.get("urn"), parent=parent) + o = cls(urn=xml.get("urn"), parent=parent) for child in xml.xpath("ti:groupname", namespaces=XPATH_NAMESPACES): lg = child.get("{http://www.w3.org/XML/1998/namespace}lang") @@ -307,7 +306,7 @@ def parse(resource, parent=None, _cls_dict=_CLASSES_DICT): o.set_cts_property("groupname", child.text, lg) # Parse Works - xpathDict(xml=xml, xpath='ti:work', cls=_cls_dict.get("work", XmlCtsWorkMetadata), parent=o) + xpathDict(xml=xml, xpath='ti:work', cls=cls.CLASS_WORK, parent=o) __parse_structured_metadata__(o, xml) return o @@ -316,16 +315,17 @@ def parse(resource, parent=None, _cls_dict=_CLASSES_DICT): class XmlCtsTextInventoryMetadata(cts.CtsTextInventoryMetadata): """ Represents a CTS Inventory file """ + CLASS_TEXTGROUP = XmlCtsTextgroupMetadata - @staticmethod - def parse(resource, _cls_dict=_CLASSES_DICT): + @classmethod + def parse(cls, resource): """ Parse a resource :param resource: Element representing the text inventory :param _cls_dict: Dictionary of classes to generate subclasses """ xml = xmlparser(resource) - o = _cls_dict.get("inventory", XmlCtsTextInventoryMetadata)(name=xml.xpath("//ti:TextInventory", namespaces=XPATH_NAMESPACES)[0].get("tiid") or "") + o = cls(name=xml.xpath("//ti:TextInventory", namespaces=XPATH_NAMESPACES)[0].get("tiid") or "") # Parse textgroups - xpathDict(xml=xml, xpath='//ti:textgroup', cls=_cls_dict.get("textgroup", XmlCtsTextgroupMetadata), parent=o) - return o \ No newline at end of file + xpathDict(xml=xml, xpath='//ti:textgroup', cls=cls.CLASS_TEXTGROUP, parent=o) + return o diff --git a/MyCapytain/resources/collections/dts.py b/MyCapytain/resources/collections/dts.py index 9af80bf0..8b407330 100644 --- a/MyCapytain/resources/collections/dts.py +++ b/MyCapytain/resources/collections/dts.py @@ -3,15 +3,17 @@ class DTSCollection(Collection): - @staticmethod - def parse(resource, mimetype="application/json+ld"): + CLASS_SHORT = DTSCollectionShort + + @classmethod + def parse(cls, resource, mimetype="application/json+ld"): """ Given a dict representation of a json object, generate a DTS Collection :param resource: :param mimetype: :return: """ - obj = DTSCollection(identifier=resource["@id"]) + obj = cls(identifier=resource["@id"]) obj.type = resource["type"] obj.version = resource["version"] for label in resource["label"]: @@ -35,29 +37,29 @@ def parse(resource, mimetype="application/json+ld"): obj.metadata.add(term, value) for member in resource["members"]["contents"]: - subobj = DTSCollectionShort.parse(member) + subobj = cls.CLASS_SHORT.parse(member) subobj.parent = member last = obj for member in resource["parents"]: - subobj = DTSCollectionShort.parse(member) + subobj = cls.CLASS_SHORT.parse(member) last.parent = subobj return obj class DTSCollectionShort(DTSCollection): - @staticmethod - def parse(resource): + @classmethod + def parse(cls, resource): """ Given a dict representation of a json object, generate a DTS Collection :param resource: :param mimetype: :return: """ - obj = DTSCollectionShort(identifier=resource["@id"]) + obj = cls(identifier=resource["@id"]) obj.type = resource["type"] obj.model = resource["model"] for label in resource["label"]: obj.set_label(label["value"], label["lang"]) - return obj \ No newline at end of file + return obj From 7f7bed4ce45e0107e9debb4a76c237fdfc228e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Thu, 23 Aug 2018 16:53:53 +0200 Subject: [PATCH 18/89] Completed DTS retriever --- .../retrievers/{dts.py => dts/__init__.py} | 67 +++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) rename MyCapytain/retrievers/{dts.py => dts/__init__.py} (59%) diff --git a/MyCapytain/retrievers/dts.py b/MyCapytain/retrievers/dts/__init__.py similarity index 59% rename from MyCapytain/retrievers/dts.py rename to MyCapytain/retrievers/dts/__init__.py index 6771b828..e3415cf6 100644 --- a/MyCapytain/retrievers/dts.py +++ b/MyCapytain/retrievers/dts/__init__.py @@ -10,7 +10,7 @@ import MyCapytain.retrievers.prototypes from MyCapytain import __version__ import requests -from MyCapytain.common.utils import parse_uri, parse_pagination +from MyCapytain.common.utils import parse_uri class HttpDtsRetriever(MyCapytain.retrievers.prototypes.API): @@ -50,7 +50,7 @@ def call(self, route, parameters, mimetype="application/ld+json"): request.raise_for_status() if request.encoding is None: request.encoding = "utf-8" - return request.text, parse_pagination(request.headers) + return request @property def routes(self): @@ -88,8 +88,8 @@ def get_collection(self, collection_id=None, nav="children", page=None): :param collection_id: Id of the collection to retrieve :param nav: Direction of the navigation :param page: Page to retrieve - :return: Response and Navigation Tuple - :rtype: (str, MyCapytain.common.utils._Navigation) + :return: Request made + :rtype: requests.Request """ return self.call( "collections", @@ -99,3 +99,62 @@ def get_collection(self, collection_id=None, nav="children", page=None): "page": page } ) + + def get_navigation( + self, collection_id, + level=None, ref=None, group_size=None, max_=None, exclude=None, + page=None): + """ Make a navigation request on the DTS API + + :param collection_id: Id of the collection + :param level: Lever at which the references should be listed + :param ref: If ref is a tuple, it is treated as a range. String or int are treated as single ref + :param group_size: Size of the ranges the server should produce + :param max_: Maximum number of results + :param exclude: Exclude specific metadata. + :param page: Page + :return: Request made + :rtype: requests.Request + """ + parameters = { + "id": collection_id, + "level": level, + "groupSize": group_size, + "max": max_, + "exclude": exclude, + "page": page + } + if isinstance(ref, tuple): + parameters["start"], parameters["end"] = ref + elif ref: + parameters["ref"] = ref + + return self.call( + "navigation", + parameters + ) + + def get_document( + self, + collection_id, ref=None, mimetype="application/tei+xml, application/xml"): + """ Make a navigation request on the DTS API + + :param collection_id: Id of the collection + :param ref: If ref is a tuple, it is treated as a range. String or int are treated as single ref + :param mimetype: Media type to request + :return: Request made + :rtype: requests.Request + """ + parameters = { + "id": collection_id + } + if isinstance(ref, tuple): + parameters["start"], parameters["end"] = ref + elif ref: + parameters["ref"] = ref + + return self.call( + "navigation", + parameters, + mimetype=mimetype + ) From 5bc5e6136434d3c2638f6de8681f4df5b3b8f21a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Thu, 23 Aug 2018 18:37:35 +0200 Subject: [PATCH 19/89] Fix tests --- tests/retrievers/test_dts.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/retrievers/test_dts.py b/tests/retrievers/test_dts.py index fc30a3fb..6a82b1e3 100644 --- a/tests/retrievers/test_dts.py +++ b/tests/retrievers/test_dts.py @@ -5,7 +5,7 @@ from MyCapytain.retrievers.dts import HttpDtsRetriever from MyCapytain.common.utils import _Navigation from urllib.parse import parse_qs, urlparse, urljoin - +from MyCapytain.common.utils import parse_pagination _SERVER_URI = "http://domainname.com/api/dts/" patch_args = ("MyCapytain.retrievers.dts.requests.get", ) @@ -134,7 +134,8 @@ def test_get_collection_headers_parsing_and_hit(self): self.add_index_response() self.add_collection_response() - response, pagination = self.cli.get_collection() + req = self.cli.get_collection() + response, pagination = req.data, parse_pagination(req.headers) self.assertEqual( pagination, _Navigation("18", "20", "500", None, "1") @@ -151,11 +152,12 @@ def test_querystring_type_of_route(self): self.add_collection_response( uri=_SERVER_URI+"?api=collections&id=Hello&page=19&nav=parents" ) - response, pagination = self.cli.get_collection( + req = self.cli.get_collection( collection_id="Hello", nav="parents", page=19 ) + response, pagination = req.data, parse_pagination(req.headers) self.assertEqual( pagination, From 07667892a481fc7e447e51555c125cb68fdb313e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Thu, 23 Aug 2018 18:45:38 +0200 Subject: [PATCH 20/89] Fixed tests --- MyCapytain/retrievers/dts/__init__.py | 12 ++++++------ tests/retrievers/test_dts.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/MyCapytain/retrievers/dts/__init__.py b/MyCapytain/retrievers/dts/__init__.py index e3415cf6..6c2f0643 100644 --- a/MyCapytain/retrievers/dts/__init__.py +++ b/MyCapytain/retrievers/dts/__init__.py @@ -88,8 +88,8 @@ def get_collection(self, collection_id=None, nav="children", page=None): :param collection_id: Id of the collection to retrieve :param nav: Direction of the navigation :param page: Page to retrieve - :return: Request made - :rtype: requests.Request + :return: Response + :rtype: requests.Response """ return self.call( "collections", @@ -113,8 +113,8 @@ def get_navigation( :param max_: Maximum number of results :param exclude: Exclude specific metadata. :param page: Page - :return: Request made - :rtype: requests.Request + :return: Response + :rtype: requests.Response """ parameters = { "id": collection_id, @@ -142,8 +142,8 @@ def get_document( :param collection_id: Id of the collection :param ref: If ref is a tuple, it is treated as a range. String or int are treated as single ref :param mimetype: Media type to request - :return: Request made - :rtype: requests.Request + :return: Response + :rtype: requests.Response """ parameters = { "id": collection_id diff --git a/tests/retrievers/test_dts.py b/tests/retrievers/test_dts.py index 6a82b1e3..1f1ba88d 100644 --- a/tests/retrievers/test_dts.py +++ b/tests/retrievers/test_dts.py @@ -135,7 +135,7 @@ def test_get_collection_headers_parsing_and_hit(self): self.add_collection_response() req = self.cli.get_collection() - response, pagination = req.data, parse_pagination(req.headers) + response, pagination = req.text, parse_pagination(req.headers) self.assertEqual( pagination, _Navigation("18", "20", "500", None, "1") @@ -157,7 +157,7 @@ def test_querystring_type_of_route(self): nav="parents", page=19 ) - response, pagination = req.data, parse_pagination(req.headers) + response, pagination = req.text, parse_pagination(req.headers) self.assertEqual( pagination, From 511e28fd038ce9097093ebeed91704b8d9718feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Sat, 25 Aug 2018 08:35:27 +0200 Subject: [PATCH 21/89] Fixed typos --- MyCapytain/common/reference/_capitains_cts.py | 2 +- MyCapytain/retrievers/dts/__init__.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index 50f2ccda..00da1659 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -110,7 +110,7 @@ def highest(self): def start(self): """ Quick access property for start list - :rtype: end + :rtype: str """ if self.parsed[0][0] and len(self.parsed[0][0]): return self.parsed[0][0] diff --git a/MyCapytain/retrievers/dts/__init__.py b/MyCapytain/retrievers/dts/__init__.py index 6c2f0643..342ae570 100644 --- a/MyCapytain/retrievers/dts/__init__.py +++ b/MyCapytain/retrievers/dts/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -.. module:: MyCapytain.retrievers.cts5 - :synopsis: Cts5 endpoint implementation +.. module:: MyCapytain.retrievers.dts + :synopsis: DTS endpoint implementation .. moduleauthor:: Thibault Clérice @@ -154,7 +154,7 @@ def get_document( parameters["ref"] = ref return self.call( - "navigation", + "document", parameters, mimetype=mimetype ) From 47577b62ea10fa888aa47ce870ef828f84214d47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Mon, 27 Aug 2018 16:04:43 +0200 Subject: [PATCH 22/89] Starting to add more implementations that records information in the graph --- MyCapytain/common/reference/_base.py | 101 +++++++++++--------- MyCapytain/common/reference/_base_sparql.py | 68 +++++++++++++ MyCapytain/common/reference/_dts_1.py | 97 +++++++------------ 3 files changed, 159 insertions(+), 107 deletions(-) create mode 100644 MyCapytain/common/reference/_base_sparql.py diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index b652f83f..3938f8a8 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -1,6 +1,7 @@ from MyCapytain.common.base import Exportable + from copy import copy -from abc import abstractmethod, abstractproperty +from abc import abstractmethod class BasePassageId: @@ -34,6 +35,9 @@ class CitationSet: a .match() function """ + def __init__(self, children=None): + self._children = children or list() + @abstractmethod def match(self, passageId): """ Given a specific passageId, matches the citation to a specific citation object @@ -43,6 +47,50 @@ def match(self, passageId): :rtype: BaseCitation """ + @property + def children(self): + """ Children of a citation + + :rtype: [BaseCitation] + """ + return self._children or [] + + @children.setter + def children(self, val): + final_value = [] + if val is not None: + for citation in val: + if citation is None: + continue + elif not isinstance(citation, self.__class__): + raise TypeError("Citation children should be Citation") + else: + citation.root = self.root + final_value.append(citation) + + self._children = final_value + + def __iter__(self): + """ Iteration method + + Loop over the citation children + + :Example: + >>> c = BaseCitation(name="line") + >>> b = BaseCitation(name="poem", children=[c]) + >>> b2 = BaseCitation(name="paragraph") + >>> a = BaseCitation(name="book", children=[b]) + >>> [e for e in a] == [a, b, c, b2], + + """ + yield from [self] + for child in self.children: + yield from child + + @property + def is_root(self): + return True + class BaseCitation(Exportable, CitationSet): def __repr__(self): @@ -63,14 +111,13 @@ def __init__(self, name=None, children=None, root=None): :param root: Root of the citation group :type root: CitationSet """ - super(BaseCitation, self).__init__() + super(BaseCitation, self).__init__(children=children) self._name = None - self._children = [] - self._root = root + self._root = None self.name = name - self.children = children + self.root = root @property def is_root(self): @@ -112,46 +159,6 @@ def name(self): def name(self, val): self._name = val - @property - def children(self): - """ Children of a citation - - :rtype: [BaseCitation] - """ - return self._children or [] - - @children.setter - def children(self, val): - final_value = [] - if val is not None: - for citation in val: - if citation is None: - continue - elif not isinstance(citation, self.__class__): - raise TypeError("Citation children should be Citation") - else: - citation.root = self.root - final_value.append(citation) - - self._children = final_value - - def __iter__(self): - """ Iteration method - - Loop over the citation children - - :Example: - >>> c = BaseCitation(name="line") - >>> b = BaseCitation(name="poem", children=[c]) - >>> b2 = BaseCitation(name="paragraph") - >>> a = BaseCitation(name="book", children=[b]) - >>> [e for e in a] == [a, b, c, b2], - - """ - yield from [self] - for child in self.children: - yield from child - @property @abstractmethod def depth(self): @@ -172,7 +179,7 @@ def __getitem__(self, item): :type item: int :rtype: list(BaseCitation) or BaseCitation - .. note:: Should it be a or or always a list ? + .. note:: Should it be a dict or or always a list ? """ return [] @@ -300,4 +307,4 @@ def id(self): :rtype: str """ - return self.__identifier__ \ No newline at end of file + return self.__identifier__ diff --git a/MyCapytain/common/reference/_base_sparql.py b/MyCapytain/common/reference/_base_sparql.py new file mode 100644 index 00000000..991614dc --- /dev/null +++ b/MyCapytain/common/reference/_base_sparql.py @@ -0,0 +1,68 @@ +from rdflib import BNode + +from MyCapytain.common.constants import get_graph, RDF_NAMESPACES +from MyCapytain.common.reference._base import CitationSet, BaseCitation + + +class BaseSparqlCitationSet(CitationSet): + def __init__(self, children=None, _bnode_id=None): + self.__graph__ = get_graph() + self.__node__ = BNode(_bnode_id) + super(BaseSparqlCitationSet, self).__init__(children=children) + + @property + def graph(self): + return self.__graph__ + + def asNode(self): + return self.__node__ + + @property + def children(self): + """ Children of a citation + + :rtype: [BaseCitation] + """ + return super(BaseSparqlCitationSet, self).children + + @children.setter + def children(self, val): + super(BaseSparqlCitationSet, self).children = val + for child in self._children: + self.graph.add( + (self.asNode(), RDF_NAMESPACES.CAPITAINS.citation, child.asNode()) + ) + + +class BaseSparqlCitation(BaseSparqlCitationSet, BaseCitation): + """ Helper class that deals with Citation in the graph + """ + CITELABEL_TERM = RDF_NAMESPACES.CAPITAINS.citeLabel + + def __init__(self, name=None, children=None, root=None, _bnode_id=None): + self.__graph__ = get_graph() + self.__node__ = BNode(_bnode_id) + super(BaseSparqlCitation, self).__init__(name=name, children=children, root=root) + + @property + def graph(self): + return self.__graph__ + + def asNode(self): + return self.__node__ + + @property + def name(self): + """ Type of the citation represented + + :rtype: str + :example: Book, Chapter, Textpart, Section, Poem... + """ + return super(BaseSparqlCitation, self).name + + @name.setter + def name(self, val): + super(BaseSparqlCitation, self).name = val + self.graph.add( + (self.asNode(), self.CITELABEL_TERM, val) + ) diff --git a/MyCapytain/common/reference/_dts_1.py b/MyCapytain/common/reference/_dts_1.py index 5a169413..82fb1e80 100644 --- a/MyCapytain/common/reference/_dts_1.py +++ b/MyCapytain/common/reference/_dts_1.py @@ -1,15 +1,43 @@ from ._base import CitationSet, BaseCitation -import re -from MyCapytain.common.constants import RDF_NAMESPACES +from ..metadata import Metadata +from MyCapytain.common.constants import RDF_NAMESPACES, Mimetypes -_tei = RDF_NAMESPACES.TEI +_dts = RDF_NAMESPACES.DTS -class DtsCitationRoot(CitationSet): +class DtsCitation(Metadata, BaseCitation): + EXPORT_TO = [Mimetypes.JSON.DTS.Std] + + def __init__(self, name=None, children=None, root=None): + super(DtsCitation, self).__init__(name=name, children=children, root=root) + + + @staticmethod + def ingest(resource, _citation_set=None, **kwargs): + """ Ingest a dictionary of DTS Citation object (as parsed JSON-LD) and + creates the Citation Graph + + :param resource: List of Citation objects from the + DTS Collection Endpoint (as expanded JSON-LD) + :type resource: dict + :return: + """ + return DtsCitation( + name=resource.get(_tei.term("type"), None), + root=_citation_set, + match_pattern=resource.get(_tei.term("matchPattern"), None), + replacement_pattern=resource.get(_tei.term("replacementPattern"), None), + ) + + +class DtsCitationRoot(CitationSet, Metadata): """ Set of citation that are supposed """ + EXPORT_TO = [Mimetypes.JSON.DTS.Std] + CITATION_CLASS = DtsCitation + def __init__(self): self._citation_graph = [] @@ -17,18 +45,10 @@ def add_child(self, child): self._citation_graph.append(child) def match(self, passageId): - """ Match a passagedId against the citation graph - - :param passageId: PassageID - :return: - :rtype: Dts_Citation - """ - for citation in self._citation_graph: - if re.match(citation.pattern, passageId): - return citation + raise NotImplementedError("This function is not available for DTS Citation") - @staticmethod - def ingest(resource, _root_class=None, _citation_class=None): + @classmethod + def ingest(cls, resource, _root_class=None, _citation_class=None): """ Ingest a list of DTS Citation object (as parsed JSON-LD) and creates the Citation Graph @@ -41,52 +61,9 @@ def ingest(resource, _root_class=None, _citation_class=None): :type _root_class: class :return: Citation Graph """ - _set = DtsCitationRoot() + _set = cls() for data in resource: _set.add_child( - DtsCitation.ingest(data) + cls.ingest(data) ) return _set - - -class DtsCitation(BaseCitation): - def __init__(self, name=None, children=None, root=None, match_pattern=None, replacement_pattern=None): - super(DtsCitation, self).__init__(name=name, children=children, root=root) - self._match_pattern = None - self._replacement_pattern = None - - self.match_pattern = match_pattern - self.replacement_pattern = replacement_pattern - - @property - def match_pattern(self): - return self._match_pattern - - @match_pattern.setter - def match_pattern(self, value): - self._match_pattern = value - - @property - def replacement_pattern(self): - return self._replacement_pattern - - @replacement_pattern.setter - def replacement_pattern(self, value): - self._replacement_pattern = value - - @staticmethod - def ingest(resource, _citation_set=None, **kwargs): - """ Ingest a dictionary of DTS Citation object (as parsed JSON-LD) and - creates the Citation Graph - - :param resource: List of Citation objects from the - DTS Collection Endpoint (as expanded JSON-LD) - :type resource: dict - :return: - """ - return DtsCitation( - name=resource.get(_tei.term("type"), None), - root=_citation_set, - match_pattern=resource.get(_tei.term("matchPattern"), None), - replacement_pattern=resource.get(_tei.term("replacementPattern"), None), - ) From 625f88d355486eda44d5d83d90813d79b3ef492b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Mon, 27 Aug 2018 16:11:50 +0200 Subject: [PATCH 23/89] Revert "Starting to add more implementations that records information in the graph" This reverts commit 47577b62ea10fa888aa47ce870ef828f84214d47. --- MyCapytain/common/reference/_base.py | 101 +++++++++----------- MyCapytain/common/reference/_base_sparql.py | 68 ------------- MyCapytain/common/reference/_dts_1.py | 97 ++++++++++++------- 3 files changed, 107 insertions(+), 159 deletions(-) delete mode 100644 MyCapytain/common/reference/_base_sparql.py diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index 3938f8a8..b652f83f 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -1,7 +1,6 @@ from MyCapytain.common.base import Exportable - from copy import copy -from abc import abstractmethod +from abc import abstractmethod, abstractproperty class BasePassageId: @@ -35,9 +34,6 @@ class CitationSet: a .match() function """ - def __init__(self, children=None): - self._children = children or list() - @abstractmethod def match(self, passageId): """ Given a specific passageId, matches the citation to a specific citation object @@ -47,50 +43,6 @@ def match(self, passageId): :rtype: BaseCitation """ - @property - def children(self): - """ Children of a citation - - :rtype: [BaseCitation] - """ - return self._children or [] - - @children.setter - def children(self, val): - final_value = [] - if val is not None: - for citation in val: - if citation is None: - continue - elif not isinstance(citation, self.__class__): - raise TypeError("Citation children should be Citation") - else: - citation.root = self.root - final_value.append(citation) - - self._children = final_value - - def __iter__(self): - """ Iteration method - - Loop over the citation children - - :Example: - >>> c = BaseCitation(name="line") - >>> b = BaseCitation(name="poem", children=[c]) - >>> b2 = BaseCitation(name="paragraph") - >>> a = BaseCitation(name="book", children=[b]) - >>> [e for e in a] == [a, b, c, b2], - - """ - yield from [self] - for child in self.children: - yield from child - - @property - def is_root(self): - return True - class BaseCitation(Exportable, CitationSet): def __repr__(self): @@ -111,13 +63,14 @@ def __init__(self, name=None, children=None, root=None): :param root: Root of the citation group :type root: CitationSet """ - super(BaseCitation, self).__init__(children=children) + super(BaseCitation, self).__init__() self._name = None - self._root = None + self._children = [] + self._root = root self.name = name - self.root = root + self.children = children @property def is_root(self): @@ -159,6 +112,46 @@ def name(self): def name(self, val): self._name = val + @property + def children(self): + """ Children of a citation + + :rtype: [BaseCitation] + """ + return self._children or [] + + @children.setter + def children(self, val): + final_value = [] + if val is not None: + for citation in val: + if citation is None: + continue + elif not isinstance(citation, self.__class__): + raise TypeError("Citation children should be Citation") + else: + citation.root = self.root + final_value.append(citation) + + self._children = final_value + + def __iter__(self): + """ Iteration method + + Loop over the citation children + + :Example: + >>> c = BaseCitation(name="line") + >>> b = BaseCitation(name="poem", children=[c]) + >>> b2 = BaseCitation(name="paragraph") + >>> a = BaseCitation(name="book", children=[b]) + >>> [e for e in a] == [a, b, c, b2], + + """ + yield from [self] + for child in self.children: + yield from child + @property @abstractmethod def depth(self): @@ -179,7 +172,7 @@ def __getitem__(self, item): :type item: int :rtype: list(BaseCitation) or BaseCitation - .. note:: Should it be a dict or or always a list ? + .. note:: Should it be a or or always a list ? """ return [] @@ -307,4 +300,4 @@ def id(self): :rtype: str """ - return self.__identifier__ + return self.__identifier__ \ No newline at end of file diff --git a/MyCapytain/common/reference/_base_sparql.py b/MyCapytain/common/reference/_base_sparql.py deleted file mode 100644 index 991614dc..00000000 --- a/MyCapytain/common/reference/_base_sparql.py +++ /dev/null @@ -1,68 +0,0 @@ -from rdflib import BNode - -from MyCapytain.common.constants import get_graph, RDF_NAMESPACES -from MyCapytain.common.reference._base import CitationSet, BaseCitation - - -class BaseSparqlCitationSet(CitationSet): - def __init__(self, children=None, _bnode_id=None): - self.__graph__ = get_graph() - self.__node__ = BNode(_bnode_id) - super(BaseSparqlCitationSet, self).__init__(children=children) - - @property - def graph(self): - return self.__graph__ - - def asNode(self): - return self.__node__ - - @property - def children(self): - """ Children of a citation - - :rtype: [BaseCitation] - """ - return super(BaseSparqlCitationSet, self).children - - @children.setter - def children(self, val): - super(BaseSparqlCitationSet, self).children = val - for child in self._children: - self.graph.add( - (self.asNode(), RDF_NAMESPACES.CAPITAINS.citation, child.asNode()) - ) - - -class BaseSparqlCitation(BaseSparqlCitationSet, BaseCitation): - """ Helper class that deals with Citation in the graph - """ - CITELABEL_TERM = RDF_NAMESPACES.CAPITAINS.citeLabel - - def __init__(self, name=None, children=None, root=None, _bnode_id=None): - self.__graph__ = get_graph() - self.__node__ = BNode(_bnode_id) - super(BaseSparqlCitation, self).__init__(name=name, children=children, root=root) - - @property - def graph(self): - return self.__graph__ - - def asNode(self): - return self.__node__ - - @property - def name(self): - """ Type of the citation represented - - :rtype: str - :example: Book, Chapter, Textpart, Section, Poem... - """ - return super(BaseSparqlCitation, self).name - - @name.setter - def name(self, val): - super(BaseSparqlCitation, self).name = val - self.graph.add( - (self.asNode(), self.CITELABEL_TERM, val) - ) diff --git a/MyCapytain/common/reference/_dts_1.py b/MyCapytain/common/reference/_dts_1.py index 82fb1e80..5a169413 100644 --- a/MyCapytain/common/reference/_dts_1.py +++ b/MyCapytain/common/reference/_dts_1.py @@ -1,43 +1,15 @@ from ._base import CitationSet, BaseCitation -from ..metadata import Metadata -from MyCapytain.common.constants import RDF_NAMESPACES, Mimetypes +import re +from MyCapytain.common.constants import RDF_NAMESPACES -_dts = RDF_NAMESPACES.DTS +_tei = RDF_NAMESPACES.TEI -class DtsCitation(Metadata, BaseCitation): - EXPORT_TO = [Mimetypes.JSON.DTS.Std] - - def __init__(self, name=None, children=None, root=None): - super(DtsCitation, self).__init__(name=name, children=children, root=root) - - - @staticmethod - def ingest(resource, _citation_set=None, **kwargs): - """ Ingest a dictionary of DTS Citation object (as parsed JSON-LD) and - creates the Citation Graph - - :param resource: List of Citation objects from the - DTS Collection Endpoint (as expanded JSON-LD) - :type resource: dict - :return: - """ - return DtsCitation( - name=resource.get(_tei.term("type"), None), - root=_citation_set, - match_pattern=resource.get(_tei.term("matchPattern"), None), - replacement_pattern=resource.get(_tei.term("replacementPattern"), None), - ) - - -class DtsCitationRoot(CitationSet, Metadata): +class DtsCitationRoot(CitationSet): """ Set of citation that are supposed """ - EXPORT_TO = [Mimetypes.JSON.DTS.Std] - CITATION_CLASS = DtsCitation - def __init__(self): self._citation_graph = [] @@ -45,10 +17,18 @@ def add_child(self, child): self._citation_graph.append(child) def match(self, passageId): - raise NotImplementedError("This function is not available for DTS Citation") + """ Match a passagedId against the citation graph + + :param passageId: PassageID + :return: + :rtype: Dts_Citation + """ + for citation in self._citation_graph: + if re.match(citation.pattern, passageId): + return citation - @classmethod - def ingest(cls, resource, _root_class=None, _citation_class=None): + @staticmethod + def ingest(resource, _root_class=None, _citation_class=None): """ Ingest a list of DTS Citation object (as parsed JSON-LD) and creates the Citation Graph @@ -61,9 +41,52 @@ def ingest(cls, resource, _root_class=None, _citation_class=None): :type _root_class: class :return: Citation Graph """ - _set = cls() + _set = DtsCitationRoot() for data in resource: _set.add_child( - cls.ingest(data) + DtsCitation.ingest(data) ) return _set + + +class DtsCitation(BaseCitation): + def __init__(self, name=None, children=None, root=None, match_pattern=None, replacement_pattern=None): + super(DtsCitation, self).__init__(name=name, children=children, root=root) + self._match_pattern = None + self._replacement_pattern = None + + self.match_pattern = match_pattern + self.replacement_pattern = replacement_pattern + + @property + def match_pattern(self): + return self._match_pattern + + @match_pattern.setter + def match_pattern(self, value): + self._match_pattern = value + + @property + def replacement_pattern(self): + return self._replacement_pattern + + @replacement_pattern.setter + def replacement_pattern(self, value): + self._replacement_pattern = value + + @staticmethod + def ingest(resource, _citation_set=None, **kwargs): + """ Ingest a dictionary of DTS Citation object (as parsed JSON-LD) and + creates the Citation Graph + + :param resource: List of Citation objects from the + DTS Collection Endpoint (as expanded JSON-LD) + :type resource: dict + :return: + """ + return DtsCitation( + name=resource.get(_tei.term("type"), None), + root=_citation_set, + match_pattern=resource.get(_tei.term("matchPattern"), None), + replacement_pattern=resource.get(_tei.term("replacementPattern"), None), + ) From 2aab5e5d764903a48dff128860cfe86747880009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 28 Aug 2018 10:55:05 +0200 Subject: [PATCH 24/89] (common.Citation) Reworked BaseCitation and CitationSet respective responsabilities --- MyCapytain/common/reference/_base.py | 211 +++++++++++++++++---------- 1 file changed, 130 insertions(+), 81 deletions(-) diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index b652f83f..82f1379f 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -1,6 +1,7 @@ from MyCapytain.common.base import Exportable +from MyCapytain.errors import CitationDepthError from copy import copy -from abc import abstractmethod, abstractproperty +from abc import abstractmethod class BasePassageId: @@ -30,10 +31,68 @@ def end(self): class CitationSet: - """ A citation set is a collection of citations that can be matched using - a .match() function + """ A citation set is a collection of citations that optionnaly can be matched using a .match() function + :param children: List of Citation + :type children: [BaseCitation] """ + + def __init__(self, children=None): + self._children = [] + + if children: + self.children = children + + @property + def is_root(self): + return True + + @property + def children(self): + """ Children of a citation + + :rtype: [BaseCitation] + """ + return self._children or [] + + @children.setter + def children(self, val: list): + final_value = [] + if val is not None: + for citation in val: + if citation is None: + continue + elif not isinstance(citation, (BaseCitation, type(self))): + raise TypeError("Citation children should be Citation") + else: + if isinstance(self, BaseCitation): + citation.root = self.root + else: + citation.root = self + final_value.append(citation) + + self._children = final_value + + def add_child(self, child): + if isinstance(child, BaseCitation): + self._children.append(child) + + def __iter__(self): + """ Iteration method + + Loop over the citation children + + :Example: + >>> c = BaseCitation(name="line") + >>> b = BaseCitation(name="poem", children=[c]) + >>> b2 = BaseCitation(name="paragraph") + >>> a = BaseCitation(name="book", children=[b]) + >>> [e for e in a] == [a, b, c, b2], + + """ + for child in self.children: + yield from child + @abstractmethod def match(self, passageId): """ Given a specific passageId, matches the citation to a specific citation object @@ -43,6 +102,69 @@ def match(self, passageId): :rtype: BaseCitation """ + @property + def depth(self): + """ Depth of the citation scheme + + .. example:: If we have a Book, Poem, Line system, and the citation we are looking at is Poem, depth is 2 + + .. toDo:: It seems that we should be more pythonic and have depth == 0 means there is still an object... + + :rtype: int + :return: Depth of the citation scheme + """ + if len(self.children): + return 1 + max([child.depth for child in self.children]) + else: + return 0 + + def __getitem__(self, item): + """ Returns the citations at the given level. + + :param item: Citation level + :type item: int + :rtype: list(BaseCitation) or BaseCitation + + .. note:: Should it be a or or always a list ? + """ + if item < 0: + _item = self.depth + item + if _item < 0: + raise CitationDepthError("The negative %s is too small " % _item) + item = _item + if item == 0: + yield from self.children + else: + for child in self.children: + yield from child[item - 1] + + def __len__(self): + """ Number of citation schemes covered by the object + + :rtype: int + :returns: Number of nested citations + """ + return len(self.children) + sum([len(child) for child in self.children]) + + def __getstate__(self): + """ Pickling method + + :return: dict + """ + return copy(self.__dict__) + + def __setstate__(self, dic): + self.__dict__ = dic + return self + + def is_empty(self): + """ Check if the citation has not been set + + :return: True if nothing was setup + :rtype: bool + """ + return len(self.children) == 0 + class BaseCitation(Exportable, CitationSet): def __repr__(self): @@ -66,11 +188,11 @@ def __init__(self, name=None, children=None, root=None): super(BaseCitation, self).__init__() self._name = None - self._children = [] - self._root = root + self._root = None self.name = name self.children = children + self.root = root @property def is_root(self): @@ -87,6 +209,8 @@ def root(self): :return: Root of the Citation set :rtype: CitationSet """ + if self._root is None: + return self return self._root @root.setter @@ -112,29 +236,6 @@ def name(self): def name(self, val): self._name = val - @property - def children(self): - """ Children of a citation - - :rtype: [BaseCitation] - """ - return self._children or [] - - @children.setter - def children(self, val): - final_value = [] - if val is not None: - for citation in val: - if citation is None: - continue - elif not isinstance(citation, self.__class__): - raise TypeError("Citation children should be Citation") - else: - citation.root = self.root - final_value.append(citation) - - self._children = final_value - def __iter__(self): """ Iteration method @@ -152,58 +253,6 @@ def __iter__(self): for child in self.children: yield from child - @property - @abstractmethod - def depth(self): - """ Depth of the citation scheme - - .. example:: If we have a Book, Poem, Line system, and the citation we are looking at is Poem, depth is 2 - - :rtype: int - :return: Depth of the citation scheme - """ - return 1 - - @abstractmethod - def __getitem__(self, item): - """ Returns the citations at the given level. - - :param item: Citation level - :type item: int - :rtype: list(BaseCitation) or BaseCitation - - .. note:: Should it be a or or always a list ? - """ - return [] - - def __len__(self): - """ Number of citation schemes covered by the object - - :rtype: int - :returns: Number of nested citations - """ - return 0 - - def __getstate__(self): - """ Pickling method - - :return: dict - """ - return copy(self.__dict__) - - def __setstate__(self, dic): - self.__dict__ = dic - return self - - @abstractmethod - def isEmpty(self): - """ Check if the citation has not been set - - :return: True if nothing was setup - :rtype: bool - """ - return True - class NodeId(object): """ Collection of directional references for a Tree @@ -300,4 +349,4 @@ def id(self): :rtype: str """ - return self.__identifier__ \ No newline at end of file + return self.__identifier__ From d993b215b77e26f4b613a94ce0bcf398722df42d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 28 Aug 2018 10:55:47 +0200 Subject: [PATCH 25/89] (common.Citation) Reworked DTS Citation implementation --- MyCapytain/common/reference/__init__.py | 1 + MyCapytain/common/reference/_dts_1.py | 103 +++++--------- .../test_reference/test_capitains_dts.py | 128 ++++++++++++++++++ 3 files changed, 166 insertions(+), 66 deletions(-) create mode 100644 tests/common/test_reference/test_capitains_dts.py diff --git a/MyCapytain/common/reference/__init__.py b/MyCapytain/common/reference/__init__.py index de585ec6..0cf0c584 100644 --- a/MyCapytain/common/reference/__init__.py +++ b/MyCapytain/common/reference/__init__.py @@ -8,3 +8,4 @@ """ from ._base import NodeId from ._capitains_cts import Citation, Reference, URN +from ._dts_1 import DtsCitation, DtsCitationRoot diff --git a/MyCapytain/common/reference/_dts_1.py b/MyCapytain/common/reference/_dts_1.py index 5a169413..175e7ac8 100644 --- a/MyCapytain/common/reference/_dts_1.py +++ b/MyCapytain/common/reference/_dts_1.py @@ -1,92 +1,63 @@ from ._base import CitationSet, BaseCitation -import re from MyCapytain.common.constants import RDF_NAMESPACES -_tei = RDF_NAMESPACES.TEI +_dts = RDF_NAMESPACES.DTS +_cite_type_term = str(_dts.term("citeType")) +_cite_structure_term = str(_dts.term("citeStructure")) + + +class DtsCitation(BaseCitation): + def __init__(self, name=None, children=None, root=None): + super(DtsCitation, self).__init__(name=name, children=children, root=root) + + @classmethod + def ingest(cls, resource, root=None, **kwargs): + """ Ingest a dictionary of DTS Citation object (as parsed JSON-LD) and + creates the Citation Graph + + :param resource: List of Citation objects from the + DTS Collection Endpoint (as expanded JSON-LD) + :type resource: dict + :param root: Root of the citation tree + :type root: CitationSet + :return: + """ + cite = cls( + name=resource.get(_cite_type_term, [{"@value": None}])[0]["@value"], # Not really clean ? + root=root + ) + for subCite in resource.get(_cite_structure_term, []): + cite.add_child(cls.ingest(subCite, root=root)) + return cite + + def match(self, passageId): + raise NotImplementedError("Passage Match is not part of the DTS Standard Citation") class DtsCitationRoot(CitationSet): """ Set of citation that are supposed """ - def __init__(self): - self._citation_graph = [] - def add_child(self, child): - self._citation_graph.append(child) + CitationClass = DtsCitation def match(self, passageId): - """ Match a passagedId against the citation graph - - :param passageId: PassageID - :return: - :rtype: Dts_Citation - """ - for citation in self._citation_graph: - if re.match(citation.pattern, passageId): - return citation + raise NotImplementedError("Passage Match is not part of the DTS Standard Citation") - @staticmethod - def ingest(resource, _root_class=None, _citation_class=None): + @classmethod + def ingest(cls, resource): """ Ingest a list of DTS Citation object (as parsed JSON-LD) and creates the Citation Graph :param resource: List of Citation objects from the DTS Collection Endpoint (as expanded JSON-LD) :type resource: list - :param _root_class: (Dev only) Class to use for instantiating the Citation Set - :type _root_class: class - :param _citation_class: (Dev only) Class to use for instantiating the Citation - :type _root_class: class :return: Citation Graph """ - _set = DtsCitationRoot() + _set = cls() for data in resource: _set.add_child( - DtsCitation.ingest(data) + cls.CitationClass.ingest(data, root=_set) ) return _set - - -class DtsCitation(BaseCitation): - def __init__(self, name=None, children=None, root=None, match_pattern=None, replacement_pattern=None): - super(DtsCitation, self).__init__(name=name, children=children, root=root) - self._match_pattern = None - self._replacement_pattern = None - - self.match_pattern = match_pattern - self.replacement_pattern = replacement_pattern - - @property - def match_pattern(self): - return self._match_pattern - - @match_pattern.setter - def match_pattern(self, value): - self._match_pattern = value - - @property - def replacement_pattern(self): - return self._replacement_pattern - - @replacement_pattern.setter - def replacement_pattern(self, value): - self._replacement_pattern = value - - @staticmethod - def ingest(resource, _citation_set=None, **kwargs): - """ Ingest a dictionary of DTS Citation object (as parsed JSON-LD) and - creates the Citation Graph - - :param resource: List of Citation objects from the - DTS Collection Endpoint (as expanded JSON-LD) - :type resource: dict - :return: - """ - return DtsCitation( - name=resource.get(_tei.term("type"), None), - root=_citation_set, - match_pattern=resource.get(_tei.term("matchPattern"), None), - replacement_pattern=resource.get(_tei.term("replacementPattern"), None), - ) diff --git a/tests/common/test_reference/test_capitains_dts.py b/tests/common/test_reference/test_capitains_dts.py new file mode 100644 index 00000000..957d536b --- /dev/null +++ b/tests/common/test_reference/test_capitains_dts.py @@ -0,0 +1,128 @@ +from MyCapytain.common.reference import DtsCitationRoot +from MyCapytain.common.constants import RDF_NAMESPACES, Mimetypes +from unittest import TestCase +from pyld import jsonld + + +_dts = RDF_NAMESPACES.DTS + +_ex_1 = [ + { + "dts:citeType": "front" + }, + { + "dts:citeType": "poem", + "dts:citeStructure": [ + { + "dts:citeType": "line" + } + ] + } +] +_ex_2 = [ + { + "dts:citeType": "poem", + "dts:citeStructure": [ + { + "dts:citeType": "line" + } + ] + } +] + +_ex_3 = [ + { + "dts:citeType": "front", + "dts:citeStructure": [ + { + "dts:citeType": "paragraph" + } + ] + }, + { + "dts:citeType": "poem", + "dts:citeStructure": [ + { + "dts:citeType": "line", + "dts:citeStructure": [ + { + "dts:citeType": "word" + } + ] + } + ] + } +] + + +def _context(ex): + return jsonld.expand({ + "@context": { + "dts": "https://w3id.org/dts/api#" + }, + "dts:citeStructure": ex + })[0][str(_dts.term("citeStructure"))] + + +class TestDtsCitation(TestCase): + def test_ingest_multiple(self): + """ Test a simple ingest """ + cite = DtsCitationRoot.ingest(_context(_ex_1)) + children = {c.name: c for c in cite} + + self.assertEqual(2, cite.depth, "There should be 2 levels of citation") + self.assertEqual(3, len(cite), "There should be 3 children") + + self.assertEqual(list(cite[-1]), [children["line"]], "Last level should contain line only") + + self.assertCountEqual(list(cite[-2]), [children["poem"], children["front"]], "-2 level == level 0") + self.assertCountEqual(list(cite[-2]), list(cite[0]), "-2 level == level 0") + + self.assertEqual(cite.is_empty(), False, "The CitationSet is not empty") + self.assertEqual(cite.is_root, True, "The CitationSet is the root") + + self.assertEqual(children["line"].is_root, False) + self.assertIs(children["line"].root, cite, "The root is tied to its children") + + def test_ingest_multiple_deeper(self): + """ Test a simple ingest """ + cite = DtsCitationRoot.ingest(_context(_ex_3)) + children = {c.name: c for c in cite} + + self.assertEqual(3, cite.depth, "There should be 3 levels of citation") + self.assertEqual(5, len(cite), "There should be 5 children") + + self.assertEqual(list(cite[-1]), [children["word"]], "Last level should contain word only") + self.assertEqual(list(cite[-1]), list(cite[2]), "-1 level == level 2") + + self.assertCountEqual(list(cite[-2]), [children["paragraph"], children["line"]], "-2 level == level 1") + self.assertCountEqual(list(cite[-2]), list(cite[1]), "-2 level == level 1") + + self.assertCountEqual(list(cite[-3]), [children["front"], children["poem"]], "-3 level == level 0") + self.assertCountEqual(list(cite[-3]), list(cite[0]), "-3 level == level 0") + + self.assertEqual(cite.is_empty(), False, "The CitationSet is not empty") + self.assertEqual(cite.is_root, True, "The CitationSet is the root") + + self.assertEqual(children["word"].is_root, False) + self.assertIs(children["word"].root, cite, "The root is tied to its children") + + def test_ingest_simple_line(self): + """ Test a simple ingest """ + cite = DtsCitationRoot.ingest(_context(_ex_2)) + children = {c.name: c for c in cite} + + self.assertEqual(2, cite.depth, "There should be 3 levels of citation") + self.assertEqual(2, len(cite), "There should be 5 children") + + self.assertEqual(list(cite[-1]), [children["line"]], "Last level should contain line only") + self.assertEqual(list(cite[-1]), list(cite[1]), "-1 level == level 1") + + self.assertCountEqual(list(cite[-2]), [children["poem"]], "-2 level == level 0") + self.assertCountEqual(list(cite[-2]), list(cite[0]), "-32 level == level 0") + + self.assertEqual(cite.is_empty(), False, "The CitationSet is not empty") + self.assertEqual(cite.is_root, True, "The CitationSet is the root") + + self.assertEqual(children["poem"].is_root, False) + self.assertIs(children["poem"].root, cite, "The root is tied to its children") From a5ce7f5d60f4f3bc8813707ee80f7b1e80653662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 28 Aug 2018 10:58:44 +0200 Subject: [PATCH 26/89] (common.Citation) BREAKING : CTSCitation.isEmpty() to CTSCitation.is_empty() This change has been made to reflect common.reference.BaseCitation --- MyCapytain/common/reference/_capitains_cts.py | 2 +- MyCapytain/resolvers/cts/local.py | 2 +- MyCapytain/resources/prototypes/text.py | 2 +- MyCapytain/resources/texts/local/capitains/cts.py | 2 +- MyCapytain/resources/texts/remote/cts.py | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index 00da1659..612a5402 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -848,7 +848,7 @@ def fill(self, passage=None, xpath=None): self.refsDecl ) - def isEmpty(self): + def is_empty(self): """ Check if the citation has not been set :return: True if nothing was setup diff --git a/MyCapytain/resolvers/cts/local.py b/MyCapytain/resolvers/cts/local.py index 851392e3..c3b38ee8 100644 --- a/MyCapytain/resolvers/cts/local.py +++ b/MyCapytain/resolvers/cts/local.py @@ -215,7 +215,7 @@ def _parse_text(self, text, directory): del text text_metadata.citation = cites[-1] self.logger.info("%s has been parsed ", text_metadata.path) - if text_metadata.citation.isEmpty(): + if text_metadata.citation.is_empty(): self.logger.error("%s has no passages", text_metadata.path) return False return True diff --git a/MyCapytain/resources/prototypes/text.py b/MyCapytain/resources/prototypes/text.py index 3be9dc9b..18b81586 100644 --- a/MyCapytain/resources/prototypes/text.py +++ b/MyCapytain/resources/prototypes/text.py @@ -471,7 +471,7 @@ def set_metadata_from_collection(self, text_metadata): self.metadata.add(RDF_NAMESPACES.CTS.description, lang=lang, value=str(node)) self.set_description(str(node), lang) - if self.citation.isEmpty() and not edition.citation.isEmpty(): + if self.citation.is_empty() and not edition.citation.is_empty(): self.citation = edition.citation diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index a6ae5dbe..79b1cc89 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -452,7 +452,7 @@ def __findCRefPattern(self, xml): :type xml: lxml.etree._Element :return: None """ - if self.citation.isEmpty(): + if self.citation.is_empty(): citation = xml.xpath("//tei:refsDecl[@n='CTS']", namespaces=XPATH_NAMESPACES) if len(citation): self.citation = Citation.ingest(resource=citation[0], xpath=".//tei:cRefPattern") diff --git a/MyCapytain/resources/texts/remote/cts.py b/MyCapytain/resources/texts/remote/cts.py index 91bd89fe..733f2c6f 100644 --- a/MyCapytain/resources/texts/remote/cts.py +++ b/MyCapytain/resources/texts/remote/cts.py @@ -172,7 +172,7 @@ def __parse_request__(self, xml): self.set_description(node.text, lang) # Need to code that p - if self.citation.isEmpty() and xml.xpath("//ti:citation", namespaces=XPATH_NAMESPACES): + if self.citation.is_empty() and xml.xpath("//ti:citation", namespaces=XPATH_NAMESPACES): self.citation = CtsCollection.XmlCtsCitation.ingest( xml, xpath=".//ti:citation[not(ancestor::ti:citation)]" @@ -332,7 +332,7 @@ def reffs(self): :rtype: MyCapytain.resources.texts.tei.XmlCtsCitation """ - if self.citation.isEmpty(): + if self.citation.is_empty(): self.getLabel() return [ reff for reffs in [self.getValidReff(level=i) for i in range(1, len(self.citation) + 1)] for reff in reffs @@ -453,7 +453,7 @@ def __parse__(self): self.__prev__, self.__nextId__ = __SharedMethod__.prevnext(self.response) - if self.citation.isEmpty() and len(self.resource.xpath("//ti:citation", namespaces=XPATH_NAMESPACES)): + if self.citation.is_empty() and len(self.resource.xpath("//ti:citation", namespaces=XPATH_NAMESPACES)): self.citation = CtsCollection.XmlCtsCitation.ingest( self.response, xpath=".//ti:citation[not(ancestor::ti:citation)]" From be8497375266173da7a0500808f7c4ec10b57117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 28 Aug 2018 11:12:17 +0200 Subject: [PATCH 27/89] Revert "(common.Citation) BREAKING : CTSCitation.isEmpty() to CTSCitation.is_empty()" This reverts commit a5ce7f5d60f4f3bc8813707ee80f7b1e80653662. --- MyCapytain/common/reference/_capitains_cts.py | 2 +- MyCapytain/resolvers/cts/local.py | 2 +- MyCapytain/resources/prototypes/text.py | 2 +- MyCapytain/resources/texts/local/capitains/cts.py | 2 +- MyCapytain/resources/texts/remote/cts.py | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index 612a5402..00da1659 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -848,7 +848,7 @@ def fill(self, passage=None, xpath=None): self.refsDecl ) - def is_empty(self): + def isEmpty(self): """ Check if the citation has not been set :return: True if nothing was setup diff --git a/MyCapytain/resolvers/cts/local.py b/MyCapytain/resolvers/cts/local.py index c3b38ee8..851392e3 100644 --- a/MyCapytain/resolvers/cts/local.py +++ b/MyCapytain/resolvers/cts/local.py @@ -215,7 +215,7 @@ def _parse_text(self, text, directory): del text text_metadata.citation = cites[-1] self.logger.info("%s has been parsed ", text_metadata.path) - if text_metadata.citation.is_empty(): + if text_metadata.citation.isEmpty(): self.logger.error("%s has no passages", text_metadata.path) return False return True diff --git a/MyCapytain/resources/prototypes/text.py b/MyCapytain/resources/prototypes/text.py index 18b81586..3be9dc9b 100644 --- a/MyCapytain/resources/prototypes/text.py +++ b/MyCapytain/resources/prototypes/text.py @@ -471,7 +471,7 @@ def set_metadata_from_collection(self, text_metadata): self.metadata.add(RDF_NAMESPACES.CTS.description, lang=lang, value=str(node)) self.set_description(str(node), lang) - if self.citation.is_empty() and not edition.citation.is_empty(): + if self.citation.isEmpty() and not edition.citation.isEmpty(): self.citation = edition.citation diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index 79b1cc89..a6ae5dbe 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -452,7 +452,7 @@ def __findCRefPattern(self, xml): :type xml: lxml.etree._Element :return: None """ - if self.citation.is_empty(): + if self.citation.isEmpty(): citation = xml.xpath("//tei:refsDecl[@n='CTS']", namespaces=XPATH_NAMESPACES) if len(citation): self.citation = Citation.ingest(resource=citation[0], xpath=".//tei:cRefPattern") diff --git a/MyCapytain/resources/texts/remote/cts.py b/MyCapytain/resources/texts/remote/cts.py index 733f2c6f..91bd89fe 100644 --- a/MyCapytain/resources/texts/remote/cts.py +++ b/MyCapytain/resources/texts/remote/cts.py @@ -172,7 +172,7 @@ def __parse_request__(self, xml): self.set_description(node.text, lang) # Need to code that p - if self.citation.is_empty() and xml.xpath("//ti:citation", namespaces=XPATH_NAMESPACES): + if self.citation.isEmpty() and xml.xpath("//ti:citation", namespaces=XPATH_NAMESPACES): self.citation = CtsCollection.XmlCtsCitation.ingest( xml, xpath=".//ti:citation[not(ancestor::ti:citation)]" @@ -332,7 +332,7 @@ def reffs(self): :rtype: MyCapytain.resources.texts.tei.XmlCtsCitation """ - if self.citation.is_empty(): + if self.citation.isEmpty(): self.getLabel() return [ reff for reffs in [self.getValidReff(level=i) for i in range(1, len(self.citation) + 1)] for reff in reffs @@ -453,7 +453,7 @@ def __parse__(self): self.__prev__, self.__nextId__ = __SharedMethod__.prevnext(self.response) - if self.citation.is_empty() and len(self.resource.xpath("//ti:citation", namespaces=XPATH_NAMESPACES)): + if self.citation.isEmpty() and len(self.resource.xpath("//ti:citation", namespaces=XPATH_NAMESPACES)): self.citation = CtsCollection.XmlCtsCitation.ingest( self.response, xpath=".//ti:citation[not(ancestor::ti:citation)]" From debe1a18fc7c07a16385acfcb111800a73822401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 28 Aug 2018 11:32:33 +0200 Subject: [PATCH 28/89] (common.Citation) Multiple breaking CitationSet.is_root -> CitationSet.is_root() # Keep in check with other practices BaseCitation.is_empty() now checks if the object has children BaseCitation.is_set() checks if objects have their properties set cts.Citation.isEmpty() old behaviour is now in .is_set() and has been reversed in meaning - Old system would check if the CTS Citation was not set, old code such as `if citation.isEmpty()` should be moved to `if citation.is_set()` --- MyCapytain/common/reference/_base.py | 15 ++++++++++----- MyCapytain/common/reference/_capitains_cts.py | 8 ++++---- MyCapytain/resolvers/cts/local.py | 2 +- MyCapytain/resources/prototypes/text.py | 2 +- MyCapytain/resources/texts/local/capitains/cts.py | 2 +- MyCapytain/resources/texts/remote/cts.py | 6 +++--- tests/common/test_reference/test_capitains_cts.py | 2 +- tests/common/test_reference/test_capitains_dts.py | 14 ++++++++------ tests/resources/collections/test_cts.py | 2 +- 9 files changed, 30 insertions(+), 23 deletions(-) diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index 82f1379f..a38c4621 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -43,10 +43,6 @@ def __init__(self, children=None): if children: self.children = children - @property - def is_root(self): - return True - @property def children(self): """ Children of a citation @@ -165,6 +161,9 @@ def is_empty(self): """ return len(self.children) == 0 + def is_root(self): + return True + class BaseCitation(Exportable, CitationSet): def __repr__(self): @@ -194,7 +193,6 @@ def __init__(self, name=None, children=None, root=None): self.children = children self.root = root - @property def is_root(self): """ :return: If the current object is the root of the citation set, True @@ -202,6 +200,13 @@ def is_root(self): """ return self._root is None + def is_set(self): + """ Checks that the current object is set + + :rtype: bool + """ + return self.name is not None + @property def root(self): """ Returns the root of the citation set diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index 00da1659..4f1fd657 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -736,7 +736,7 @@ def child(self): def child(self, val): if val: self.children = [val] - if self.is_root: + if self.is_root(): val.root = self else: val.root = self.root @@ -798,7 +798,7 @@ def match(self, passageId): if not isinstance(passageId, Reference): passageId = Reference(passageId) - if self.is_root: + if self.is_root(): return self[len(passageId)-1] return self.root.match(passageId) @@ -848,13 +848,13 @@ def fill(self, passage=None, xpath=None): self.refsDecl ) - def isEmpty(self): + def is_set(self): """ Check if the citation has not been set :return: True if nothing was setup :rtype: bool """ - return self.refsDecl is None + return self.refsDecl is not None def __export__(self, output=None, **kwargs): if output == Mimetypes.XML.CTS: diff --git a/MyCapytain/resolvers/cts/local.py b/MyCapytain/resolvers/cts/local.py index 851392e3..0399fca0 100644 --- a/MyCapytain/resolvers/cts/local.py +++ b/MyCapytain/resolvers/cts/local.py @@ -215,7 +215,7 @@ def _parse_text(self, text, directory): del text text_metadata.citation = cites[-1] self.logger.info("%s has been parsed ", text_metadata.path) - if text_metadata.citation.isEmpty(): + if not text_metadata.citation.is_set(): self.logger.error("%s has no passages", text_metadata.path) return False return True diff --git a/MyCapytain/resources/prototypes/text.py b/MyCapytain/resources/prototypes/text.py index 3be9dc9b..f90c80fb 100644 --- a/MyCapytain/resources/prototypes/text.py +++ b/MyCapytain/resources/prototypes/text.py @@ -471,7 +471,7 @@ def set_metadata_from_collection(self, text_metadata): self.metadata.add(RDF_NAMESPACES.CTS.description, lang=lang, value=str(node)) self.set_description(str(node), lang) - if self.citation.isEmpty() and not edition.citation.isEmpty(): + if not self.citation.is_set() and edition.citation.is_set(): self.citation = edition.citation diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index a6ae5dbe..2a78714d 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -452,7 +452,7 @@ def __findCRefPattern(self, xml): :type xml: lxml.etree._Element :return: None """ - if self.citation.isEmpty(): + if not self.citation.is_set(): citation = xml.xpath("//tei:refsDecl[@n='CTS']", namespaces=XPATH_NAMESPACES) if len(citation): self.citation = Citation.ingest(resource=citation[0], xpath=".//tei:cRefPattern") diff --git a/MyCapytain/resources/texts/remote/cts.py b/MyCapytain/resources/texts/remote/cts.py index 91bd89fe..153a4eca 100644 --- a/MyCapytain/resources/texts/remote/cts.py +++ b/MyCapytain/resources/texts/remote/cts.py @@ -172,7 +172,7 @@ def __parse_request__(self, xml): self.set_description(node.text, lang) # Need to code that p - if self.citation.isEmpty() and xml.xpath("//ti:citation", namespaces=XPATH_NAMESPACES): + if not self.citation.is_set() and xml.xpath("//ti:citation", namespaces=XPATH_NAMESPACES): self.citation = CtsCollection.XmlCtsCitation.ingest( xml, xpath=".//ti:citation[not(ancestor::ti:citation)]" @@ -332,7 +332,7 @@ def reffs(self): :rtype: MyCapytain.resources.texts.tei.XmlCtsCitation """ - if self.citation.isEmpty(): + if not self.citation.is_set(): self.getLabel() return [ reff for reffs in [self.getValidReff(level=i) for i in range(1, len(self.citation) + 1)] for reff in reffs @@ -453,7 +453,7 @@ def __parse__(self): self.__prev__, self.__nextId__ = __SharedMethod__.prevnext(self.response) - if self.citation.isEmpty() and len(self.resource.xpath("//ti:citation", namespaces=XPATH_NAMESPACES)): + if not self.citation.is_set() and len(self.resource.xpath("//ti:citation", namespaces=XPATH_NAMESPACES)): self.citation = CtsCollection.XmlCtsCitation.ingest( self.response, xpath=".//ti:citation[not(ancestor::ti:citation)]" diff --git a/tests/common/test_reference/test_capitains_cts.py b/tests/common/test_reference/test_capitains_cts.py index 5671927d..c81812de 100644 --- a/tests/common/test_reference/test_capitains_cts.py +++ b/tests/common/test_reference/test_capitains_cts.py @@ -405,7 +405,7 @@ def test_ingest_and_match(self): self.assertEqual(citation.child.name, "poem", "Name of child should have been parsed") self.assertEqual(citation.child.child.name, "line", "Name of descendants should have been parsed") - self.assertEqual(citation.is_root, True, "Root should be true on root") + self.assertEqual(citation.is_root(), True, "Root should be true on root") self.assertEqual(citation.match("1.2"), citation.child, "Matching should make use of root matching") self.assertEqual(citation.match("1.2.4"), citation.child.child, "Matching should make use of root matching") self.assertEqual(citation.match("1"), citation, "Matching should make use of root matching") diff --git a/tests/common/test_reference/test_capitains_dts.py b/tests/common/test_reference/test_capitains_dts.py index 957d536b..225ddee7 100644 --- a/tests/common/test_reference/test_capitains_dts.py +++ b/tests/common/test_reference/test_capitains_dts.py @@ -79,9 +79,11 @@ def test_ingest_multiple(self): self.assertCountEqual(list(cite[-2]), list(cite[0]), "-2 level == level 0") self.assertEqual(cite.is_empty(), False, "The CitationSet is not empty") - self.assertEqual(cite.is_root, True, "The CitationSet is the root") + self.assertEqual(cite.is_root(), True, "The CitationSet is the root") - self.assertEqual(children["line"].is_root, False) + self.assertEqual(children["line"].is_root(), False) + self.assertEqual(children["line"].is_set(), True, "The Citation is set") + self.assertEqual(children["line"].is_empty(), True, "The citation has no more levels") self.assertIs(children["line"].root, cite, "The root is tied to its children") def test_ingest_multiple_deeper(self): @@ -102,9 +104,9 @@ def test_ingest_multiple_deeper(self): self.assertCountEqual(list(cite[-3]), list(cite[0]), "-3 level == level 0") self.assertEqual(cite.is_empty(), False, "The CitationSet is not empty") - self.assertEqual(cite.is_root, True, "The CitationSet is the root") + self.assertEqual(cite.is_root(), True, "The CitationSet is the root") - self.assertEqual(children["word"].is_root, False) + self.assertEqual(children["word"].is_root(), False) self.assertIs(children["word"].root, cite, "The root is tied to its children") def test_ingest_simple_line(self): @@ -122,7 +124,7 @@ def test_ingest_simple_line(self): self.assertCountEqual(list(cite[-2]), list(cite[0]), "-32 level == level 0") self.assertEqual(cite.is_empty(), False, "The CitationSet is not empty") - self.assertEqual(cite.is_root, True, "The CitationSet is the root") + self.assertEqual(cite.is_root(), True, "The CitationSet is the root") - self.assertEqual(children["poem"].is_root, False) + self.assertEqual(children["poem"].is_root(), False) self.assertIs(children["poem"].root, cite, "The root is tied to its children") diff --git a/tests/resources/collections/test_cts.py b/tests/resources/collections/test_cts.py index f8e94762..cb9b2aea 100644 --- a/tests/resources/collections/test_cts.py +++ b/tests/resources/collections/test_cts.py @@ -685,7 +685,7 @@ def test_ingest_and_match(self): self.assertEqual(citation.name, "book", "Name should have been parsed") self.assertEqual(citation.child.name, "poem", "Name of child should have been parsed") self.assertEqual(citation.child.child.name, "line", "Name of descendants should have been parsed") - self.assertEqual(citation.is_root, True, "Root should be true on root") + self.assertEqual(citation.is_root(), True, "Root should be true on root") self.assertEqual(citation.match("1.2"), citation.child, "Matching should make use of root matching") self.assertEqual(citation.match("1.2.4"), citation.child.child, "Matching should make use of root matching") self.assertEqual(citation.match("1"), citation, "Matching should make use of root matching") From 0de334585f6123993e87acab0eb2ebc7aa703941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 28 Aug 2018 11:35:32 +0200 Subject: [PATCH 29/89] (Travis) Removed 2.6, added 3.6 and 3.7 --- .travis.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 85a6b55b..2c1804fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,9 @@ language: python python: - - "2.7" - "3.4.5" - "3.5" + - "3.6" + - "3.7" install: - pip install -r requirements.txt @@ -12,10 +13,6 @@ install: script: - coverage run --source=MyCapytain setup.py test -matrix: - allow_failures: - - python: "2.7" - after_success: - if [[ $TRAVIS_PYTHON_VERSION == 3* ]]; then coveralls; fi From 29ac942389dc82653483af891ad3e6f1ca365ea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 28 Aug 2018 12:19:01 +0200 Subject: [PATCH 30/89] (Travis) Removed 3.7 because it does not work --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2c1804fd..0241cc30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ python: - "3.4.5" - "3.5" - "3.6" - - "3.7" install: - pip install -r requirements.txt From 7a77e4f5f7f6676209bdfef2564aca701d16cb5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 28 Aug 2018 18:26:11 +0200 Subject: [PATCH 31/89] (common.reference)(resources.collections) DTS Citation and base collection to export correctly --- MyCapytain/common/reference/__init__.py | 4 +- MyCapytain/common/reference/_base.py | 70 ++++++++++-- MyCapytain/common/reference/_dts_1.py | 9 +- MyCapytain/common/utils.py | 17 ++- MyCapytain/resources/collections/dts.py | 37 ++++-- MyCapytain/resources/prototypes/metadata.py | 68 ++++++++--- ..._capitains_dts.py => test_citation_dts.py} | 33 +++--- tests/resolvers/cts/test_api.py | 4 +- tests/resolvers/cts/test_local.py | 4 +- .../collections/test_dts_collection.py | 108 ++++++++++-------- tests/testing_data/dts/collection_3.json | 14 +-- tests/testing_data/dts/collection_4.json | 42 +++++++ 12 files changed, 289 insertions(+), 121 deletions(-) rename tests/common/test_reference/{test_capitains_dts.py => test_citation_dts.py} (77%) create mode 100644 tests/testing_data/dts/collection_4.json diff --git a/MyCapytain/common/reference/__init__.py b/MyCapytain/common/reference/__init__.py index 0cf0c584..0d7ea6a2 100644 --- a/MyCapytain/common/reference/__init__.py +++ b/MyCapytain/common/reference/__init__.py @@ -6,6 +6,6 @@ .. moduleauthor:: Thibault Clérice """ -from ._base import NodeId +from ._base import NodeId, BaseCitationSet from ._capitains_cts import Citation, Reference, URN -from ._dts_1 import DtsCitation, DtsCitationRoot +from ._dts_1 import DtsCitation, DtsCitationSet diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index a38c4621..27bbc777 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -1,4 +1,5 @@ from MyCapytain.common.base import Exportable +from MyCapytain.common.constants import get_graph, Mimetypes, RDF_NAMESPACES from MyCapytain.errors import CitationDepthError from copy import copy from abc import abstractmethod @@ -30,13 +31,18 @@ def end(self): return self._end -class CitationSet: +class BaseCitationSet(Exportable): """ A citation set is a collection of citations that optionnaly can be matched using a .match() function :param children: List of Citation :type children: [BaseCitation] """ + def __repr__(self): + return "" % id(self) + + EXPORT_TO = [Mimetypes.JSON.DTS.Std] + def __init__(self, children=None): self._children = [] @@ -79,10 +85,10 @@ def __iter__(self): Loop over the citation children :Example: - >>> c = BaseCitation(name="line") - >>> b = BaseCitation(name="poem", children=[c]) - >>> b2 = BaseCitation(name="paragraph") - >>> a = BaseCitation(name="book", children=[b]) + >>> c = BaseCitationSet(name="line") + >>> b = BaseCitationSet(name="poem", children=[c]) + >>> b2 = BaseCitationSet(name="paragraph") + >>> a = BaseCitationSet(name="book", children=[b]) >>> [e for e in a] == [a, b, c, b2], """ @@ -102,9 +108,8 @@ def match(self, passageId): def depth(self): """ Depth of the citation scheme - .. example:: If we have a Book, Poem, Line system, and the citation we are looking at is Poem, depth is 2 + .. example:: If we have a Book, Poem, Line system, and the citation we are looking at is Poem, depth is 1 - .. toDo:: It seems that we should be more pythonic and have depth == 0 means there is still an object... :rtype: int :return: Depth of the citation scheme @@ -164,8 +169,28 @@ def is_empty(self): def is_root(self): return True + def __export__(self, output=None, context=False, namespace_manager=None, **kwargs): + if output == Mimetypes.JSON.DTS.Std: + if not namespace_manager: + namespace_manager = get_graph().namespace_manager + + _out = [ + cite.export(output, context=False, namespace_manager=namespace_manager) + for cite in self.children + ] -class BaseCitation(Exportable, CitationSet): + if context: + cite_structure_term = str(namespace_manager.qname(RDF_NAMESPACES.DTS.term("citeStructure"))) + _out = { + "@context": { + cite_structure_term.split(":")[0]: str(RDF_NAMESPACES.DTS) + }, + cite_structure_term: _out + } + return _out + + +class BaseCitation(BaseCitationSet): def __repr__(self): """ @@ -182,7 +207,7 @@ def __init__(self, name=None, children=None, root=None): :param children: list of children :type children: [BaseCitation] :param root: Root of the citation group - :type root: CitationSet + :type root: BaseCitationSet """ super(BaseCitation, self).__init__() @@ -212,7 +237,7 @@ def root(self): """ Returns the root of the citation set :return: Root of the Citation set - :rtype: CitationSet + :rtype: BaseCitationSet """ if self._root is None: return self @@ -223,7 +248,7 @@ def root(self, value): """ Set the root to which the current citation is connected to :param value: CitationSet root of the Citation graph - :type value: CitationSet + :type value: BaseCitationSet :return: """ self._root = value @@ -258,6 +283,29 @@ def __iter__(self): for child in self.children: yield from child + def __export__(self, output=None, context=False, namespace_manager=None, **kwargs): + if output == Mimetypes.JSON.DTS.Std: + if not namespace_manager: + namespace_manager = get_graph().namespace_manager + + cite_type_term = str(namespace_manager.qname(RDF_NAMESPACES.DTS.term("citeType"))) + cite_structure_term = str(namespace_manager.qname(RDF_NAMESPACES.DTS.term("citeStructure"))) + + _out = { + cite_type_term: self.name + } + + if not self.is_empty(): + _out[cite_structure_term] = [ + cite.export(output, context=False, namespace_manager=namespace_manager) + for cite in self.children + ] + + if context: + _out["@context"] = {cite_type_term.split(":")[0]: str(RDF_NAMESPACES.DTS)} + + return _out + class NodeId(object): """ Collection of directional references for a Tree diff --git a/MyCapytain/common/reference/_dts_1.py b/MyCapytain/common/reference/_dts_1.py index 175e7ac8..85eec555 100644 --- a/MyCapytain/common/reference/_dts_1.py +++ b/MyCapytain/common/reference/_dts_1.py @@ -1,4 +1,4 @@ -from ._base import CitationSet, BaseCitation +from ._base import BaseCitationSet, BaseCitation from MyCapytain.common.constants import RDF_NAMESPACES @@ -20,7 +20,7 @@ def ingest(cls, resource, root=None, **kwargs): DTS Collection Endpoint (as expanded JSON-LD) :type resource: dict :param root: Root of the citation tree - :type root: CitationSet + :type root: BaseCitationSet :return: """ cite = cls( @@ -35,11 +35,14 @@ def match(self, passageId): raise NotImplementedError("Passage Match is not part of the DTS Standard Citation") -class DtsCitationRoot(CitationSet): +class DtsCitationSet(BaseCitationSet): """ Set of citation that are supposed """ + def __repr__(self): + return "" % id(self) + CitationClass = DtsCitation def match(self, passageId): diff --git a/MyCapytain/common/utils.py b/MyCapytain/common/utils.py index 9c5c0467..60507021 100644 --- a/MyCapytain/common/utils.py +++ b/MyCapytain/common/utils.py @@ -20,6 +20,7 @@ from six import text_type from xml.sax.saxutils import escape from rdflib import BNode, Graph, Literal, URIRef +from rdflib.namespace import NamespaceManager from urllib.parse import urlparse, parse_qs, urljoin import link_header @@ -72,7 +73,7 @@ def make_xml_node(graph, name, close=False, attributes=None, text="", complete=F return "<{}>".format(name) -def LiteralToDict(value): +def literal_to_dict(value): """ Transform an object value into a dict readable value :param value: Object of a triple which is not a BNode @@ -90,14 +91,24 @@ def LiteralToDict(value): return str(value) +def dict_to_literal(dict_container: dict): + if isinstance(dict_container["@value"], int): + return dict_container["@value"], + else: + return dict_container["@value"], dict_container.get("@language", None) + + class Subgraph(object): """ Utility class to generate subgraph around one or more items :param """ - def __init__(self, namespace_manager): + def __init__(self, bindings: dict = None): self.graph = Graph() - self.graph.namespace_manager = namespace_manager + self.graph.namespace_manager = NamespaceManager(self.graph) + for prefix, namespace in bindings.items(): + self.graph.namespace_manager.bind(prefix, namespace) + self.downwards = defaultdict(lambda: True) self.updwards = defaultdict(lambda: True) diff --git a/MyCapytain/resources/collections/dts.py b/MyCapytain/resources/collections/dts.py index 65490403..7c0fdd54 100644 --- a/MyCapytain/resources/collections/dts.py +++ b/MyCapytain/resources/collections/dts.py @@ -1,6 +1,8 @@ from MyCapytain.resources.prototypes.metadata import Collection from MyCapytain.errors import JsonLdCollectionMissing +from MyCapytain.common.reference import DtsCitationSet from MyCapytain.common.constants import RDF_NAMESPACES +from MyCapytain.common.utils import dict_to_literal from rdflib import URIRef from pyld import jsonld @@ -14,9 +16,21 @@ class DTSCollection(Collection): + + CitationSet = DtsCitationSet + def __init__(self, identifier="", *args, **kwargs): super(DTSCollection, self).__init__(identifier, *args, **kwargs) self._expanded = False # Not sure I'll keep this + self._citation = DtsCitationSet() + + @property + def citation(self): + return self._citation + + @citation.setter + def citation(self, citation: CitationSet): + self._citation = citation @property def size(self): @@ -24,6 +38,12 @@ def size(self): return int(value) return 0 + @property + def readable(self): + if self.type == RDF_NAMESPACES.HYDRA.Resource: + return True + return False + @classmethod def parse(cls, resource, direction="children"): """ Given a dict representation of a json object, generate a DTS Collection @@ -49,6 +69,11 @@ def parse(cls, resource, direction="children"): for val_dict in collection["@type"]: obj.type = val_dict + # We retrieve the Citation System + _cite_structure_term = str(_dts.term("citeStructure")) + if _cite_structure_term in collection and collection[_cite_structure_term]: + obj.citation = cls.CitationSet.ingest(collection[_cite_structure_term]) + for val_dict in collection[str(_hyd.totalItems)]: obj.metadata.add(_hyd.totalItems, val_dict["@value"], 0) @@ -58,21 +83,17 @@ def parse(cls, resource, direction="children"): for key, value_set in collection.get(str(_dts.dublincore), _empty_extensions)[0].items(): term = URIRef(key) for value_dict in value_set: - obj.metadata.add(term, value_dict["@value"], value_dict.get("@language", None)) + obj.metadata.add(term, *dict_to_literal(value_dict)) for key, value_set in collection.get(str(_dts.extensions), _empty_extensions)[0].items(): term = URIRef(key) for value_dict in value_set: - obj.metadata.add(term, value_dict["@value"], value_dict.get("@language", None)) - - if str(_tei.refsDecl) in collection: - for citation in collection[str(_tei.refsDecl)]: - print(citation) - # Need to have citation set before going further. - continue + print(value_dict) + obj.metadata.add(term, *dict_to_literal(value_dict)) for member in collection.get(str(_hyd.member), []): subcollection = cls.parse(member) if direction == "children": subcollection.parent = obj + return obj diff --git a/MyCapytain/resources/prototypes/metadata.py b/MyCapytain/resources/prototypes/metadata.py index 15e15c0d..a23c2393 100644 --- a/MyCapytain/resources/prototypes/metadata.py +++ b/MyCapytain/resources/prototypes/metadata.py @@ -9,12 +9,21 @@ from MyCapytain.common.metadata import Metadata from MyCapytain.errors import UnknownCollection -from MyCapytain.common.utils import Subgraph, LiteralToDict +from MyCapytain.common.utils import Subgraph, literal_to_dict from MyCapytain.common.constants import RDF_NAMESPACES, RDFLIB_MAPPING, Mimetypes, get_graph from MyCapytain.common.base import Exportable +from MyCapytain.common.reference import BaseCitationSet from rdflib import URIRef, RDF, Literal, Graph, RDFS -from rdflib.namespace import SKOS, DC, DCTERMS -from copy import deepcopy +from rdflib.namespace import SKOS, DC, DCTERMS, NamespaceManager + + +_ns_hydra_str = str(RDF_NAMESPACES.HYDRA) +_ns_cts_str = str(RDF_NAMESPACES.CTS) +_ns_dts_str = str(RDF_NAMESPACES.DTS) +_ns_dct_str = str(DCTERMS) +_ns_cap_str = str(RDF_NAMESPACES.CAPITAINS) +_ns_rdf_str = str(RDF) +_ns_rdfs_str = str(RDFS) class Collection(Exportable): @@ -292,8 +301,8 @@ def __namespaces_header__(self, cpt=None): return bindings - @staticmethod - def _export_base_dts(graph, obj, nsm): + @classmethod + def _export_base_dts(cls, graph, obj, nsm): """ Export the base DTS information in a simple reusable way :param graph: Current graph where the information lie @@ -314,7 +323,7 @@ def _export_base_dts(graph, obj, nsm): return o - def __export__(self, output=None, domain=""): + def __export__(self, output=None, domain="", namespace_manager=None): """ Export the collection item in the Mimetype required. ..note:: If current implementation does not have special mimetypes, reuses default_export method @@ -329,30 +338,39 @@ def __export__(self, output=None, domain=""): if output == Mimetypes.JSON.DTS.Std: # Set-up a derived Namespace Manager - nm = self.graph.namespace_manager - nsm = deepcopy(nm) - nsm.bind("hydra", RDF_NAMESPACES.HYDRA) - nsm.bind("dct", DCTERMS) + if not namespace_manager: + nsm = { + prefix: ns + for prefix, ns in self.graph.namespace_manager.namespaces() + if str(ns) not in [_ns_cap_str, _ns_cts_str, _ns_dts_str, _ns_dct_str, _ns_hydra_str] + } + nsm[""] = RDF_NAMESPACES.HYDRA + nsm["cts"] = RDF_NAMESPACES.CTS + nsm["dts"] = RDF_NAMESPACES.DTS + nsm["dct"] = DCTERMS + + else: + nsm = namespace_manager.namespaces() # Set-up a derived graph store = Subgraph(nsm) store.graphiter(self.graph, self.asNode(), ascendants=0, descendants=1) graph = store.graph + nsm = store.graph.namespace_manager # Build the JSON-LD @context + + ignore_ns_for_bindings = [_ns_cap_str, _ns_hydra_str, _ns_rdf_str, _ns_rdfs_str] bindings = {} for predicate in set(graph.predicates()): prefix, namespace, name = nsm.compute_qname(predicate) - bindings[prefix] = str(URIRef(namespace)) - - if "cap" in bindings: - del bindings["cap"] + if prefix not in bindings and str(namespace) not in ignore_ns_for_bindings: + bindings[prefix] = str(URIRef(namespace)) # Builds the specific Store data extensions = {} dublincore = {} - ignore_ns = [str(RDF_NAMESPACES.HYDRA), str(RDF_NAMESPACES.DTS), - str(RDF_NAMESPACES.CAPITAINS), str(RDF), str(RDFS)] + ignore_ns = [_ns_cap_str, _ns_hydra_str, _ns_rdf_str, _ns_rdfs_str, _ns_dts_str] # Builds the .dublincore and .extensions graphs for _, predicate, obj in store.graph: @@ -372,16 +390,17 @@ def __export__(self, output=None, domain=""): if k in metadata: if isinstance(metadata[k], list): - metadata[k].append(LiteralToDict(obj)) + metadata[k].append(literal_to_dict(obj)) else: - metadata[k] = [metadata[k], LiteralToDict(obj)] + metadata[k] = [metadata[k], literal_to_dict(obj)] else: - metadata[k] = LiteralToDict(obj) + metadata[k] = literal_to_dict(obj) if isinstance(metadata[k], dict): metadata[k] = [metadata[k]] o = {"@context": bindings} o.update(self._export_base_dts(graph, self, nsm)) + o["@context"]["@vocab"] = _ns_hydra_str if extensions: o[graph.qname(RDF_NAMESPACES.DTS.extensions)] = extensions @@ -395,6 +414,17 @@ def __export__(self, output=None, domain=""): for member in self.members ] + # If the system handles citation structure + if hasattr(self, "citation") and \ + isinstance(self.citation, BaseCitationSet) and \ + not self.citation.is_empty(): + + o[graph.qname(RDF_NAMESPACES.DTS.term("citeStructure"))] = self.citation.export( + Mimetypes.JSON.DTS.Std, + context=False, + namespace_manager=nsm + ) + del store return o elif output == Mimetypes.JSON.LD\ diff --git a/tests/common/test_reference/test_capitains_dts.py b/tests/common/test_reference/test_citation_dts.py similarity index 77% rename from tests/common/test_reference/test_capitains_dts.py rename to tests/common/test_reference/test_citation_dts.py index 225ddee7..e2083bce 100644 --- a/tests/common/test_reference/test_capitains_dts.py +++ b/tests/common/test_reference/test_citation_dts.py @@ -1,5 +1,5 @@ -from MyCapytain.common.reference import DtsCitationRoot -from MyCapytain.common.constants import RDF_NAMESPACES, Mimetypes +from MyCapytain.common.reference import DtsCitationSet, DtsCitation +from MyCapytain.common.constants import RDF_NAMESPACES from unittest import TestCase from pyld import jsonld @@ -22,11 +22,9 @@ _ex_2 = [ { "dts:citeType": "poem", - "dts:citeStructure": [ - { - "dts:citeType": "line" - } - ] + "dts:citeStructure": { + "dts:citeType": "line" + } } ] @@ -67,7 +65,7 @@ def _context(ex): class TestDtsCitation(TestCase): def test_ingest_multiple(self): """ Test a simple ingest """ - cite = DtsCitationRoot.ingest(_context(_ex_1)) + cite = DtsCitationSet.ingest(_context(_ex_1)) children = {c.name: c for c in cite} self.assertEqual(2, cite.depth, "There should be 2 levels of citation") @@ -78,8 +76,8 @@ def test_ingest_multiple(self): self.assertCountEqual(list(cite[-2]), [children["poem"], children["front"]], "-2 level == level 0") self.assertCountEqual(list(cite[-2]), list(cite[0]), "-2 level == level 0") - self.assertEqual(cite.is_empty(), False, "The CitationSet is not empty") - self.assertEqual(cite.is_root(), True, "The CitationSet is the root") + self.assertEqual(cite.is_empty(), False, "The BaseCitationSet is not empty") + self.assertEqual(cite.is_root(), True, "The BaseCitationSet is the root") self.assertEqual(children["line"].is_root(), False) self.assertEqual(children["line"].is_set(), True, "The Citation is set") @@ -88,7 +86,7 @@ def test_ingest_multiple(self): def test_ingest_multiple_deeper(self): """ Test a simple ingest """ - cite = DtsCitationRoot.ingest(_context(_ex_3)) + cite = DtsCitationSet.ingest(_context(_ex_3)) children = {c.name: c for c in cite} self.assertEqual(3, cite.depth, "There should be 3 levels of citation") @@ -103,15 +101,15 @@ def test_ingest_multiple_deeper(self): self.assertCountEqual(list(cite[-3]), [children["front"], children["poem"]], "-3 level == level 0") self.assertCountEqual(list(cite[-3]), list(cite[0]), "-3 level == level 0") - self.assertEqual(cite.is_empty(), False, "The CitationSet is not empty") - self.assertEqual(cite.is_root(), True, "The CitationSet is the root") + self.assertEqual(cite.is_empty(), False, "The BaseCitationSet is not empty") + self.assertEqual(cite.is_root(), True, "The BaseCitationSet is the root") self.assertEqual(children["word"].is_root(), False) self.assertIs(children["word"].root, cite, "The root is tied to its children") def test_ingest_simple_line(self): """ Test a simple ingest """ - cite = DtsCitationRoot.ingest(_context(_ex_2)) + cite = DtsCitationSet.ingest(_context(_ex_2)) children = {c.name: c for c in cite} self.assertEqual(2, cite.depth, "There should be 3 levels of citation") @@ -123,8 +121,11 @@ def test_ingest_simple_line(self): self.assertCountEqual(list(cite[-2]), [children["poem"]], "-2 level == level 0") self.assertCountEqual(list(cite[-2]), list(cite[0]), "-32 level == level 0") - self.assertEqual(cite.is_empty(), False, "The CitationSet is not empty") - self.assertEqual(cite.is_root(), True, "The CitationSet is the root") + self.assertIsInstance(cite, DtsCitationSet, "Root should be a DtsCitationSet") + self.assertEqual([type(child) for child in cite.children], [DtsCitation], "Children should be DtsCitation") + + self.assertEqual(cite.is_empty(), False, "The BaseCitationSet is not empty") + self.assertEqual(cite.is_root(), True, "The BaseCitationSet is the root") self.assertEqual(children["poem"].is_root(), False) self.assertIs(children["poem"].root, cite, "The root is tied to its children") diff --git a/tests/resolvers/cts/test_api.py b/tests/resolvers/cts/test_api.py index f2e2a6cb..c7eed630 100644 --- a/tests/resolvers/cts/test_api.py +++ b/tests/resolvers/cts/test_api.py @@ -327,7 +327,7 @@ def test_getMetadata_full(self): "There should be one node in exported format corresponding to lat2" ) self.assertCountEqual( - [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["hydra:member"]], + [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["member"]], ["urn:cts:latinLit:phi1294", "urn:cts:latinLit:phi0959", "urn:cts:greekLit:tlg0003", "urn:cts:latinLit:phi1276"], "There should be 4 Members in DTS JSON" ) @@ -362,7 +362,7 @@ def test_getMetadata_subset(self): "There should be one node in exported format corresponding to lat2" ) self.assertCountEqual( - [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["hydra:member"]], + [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["member"]], ["urn:cts:latinLit:phi1294.phi002.perseus-lat2", "urn:cts:latinLit:phi1294.phi002.perseus-eng2"], "There should be one member in DTS JSON" ) diff --git a/tests/resolvers/cts/test_local.py b/tests/resolvers/cts/test_local.py index b41fd852..9e7a23bb 100644 --- a/tests/resolvers/cts/test_local.py +++ b/tests/resolvers/cts/test_local.py @@ -459,7 +459,7 @@ def test_getMetadata_full(self): "There should be one node in exported format corresponding to lat2" ) self.assertCountEqual( - [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["hydra:member"]], + [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["member"]], ["urn:cts:latinLit:phi1294", "urn:cts:latinLit:phi0959", "urn:cts:greekLit:tlg0003", "urn:cts:latinLit:phi1276"], "There should be 4 Members in DTS JSON" @@ -502,7 +502,7 @@ def test_getMetadata_subset(self): "There should be one node in exported format corresponding to lat2" ) self.assertCountEqual( - [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["hydra:member"]], + [x["@id"] for x in metadata.export(output=Mimetypes.JSON.DTS.Std)["member"]], ["urn:cts:latinLit:phi1294.phi002.opp-eng3", "urn:cts:latinLit:phi1294.phi002.perseus-lat2"], "There should be two members in DTS JSON" ) diff --git a/tests/resources/collections/test_dts_collection.py b/tests/resources/collections/test_dts_collection.py index fb4ba01c..05765781 100644 --- a/tests/resources/collections/test_dts_collection.py +++ b/tests/resources/collections/test_dts_collection.py @@ -25,7 +25,8 @@ def reorder_orderable(self, exported): :param exported: Exported Collection to DTS :return: Sorted exported collection """ - exported["hydra:member"] = sorted(exported["hydra:member"], key=lambda x: x["@id"]) + if "member" in exported: + exported["member"] = sorted(exported["member"], key=lambda x: x["@id"]) for key, values in exported["dts:dublincore"].items(): if isinstance(values, list) and isinstance(values[0], str): exported["dts:dublincore"][key] = sorted(values) @@ -44,36 +45,34 @@ def test_simple_collection(self): '@context': { 'dct': 'http://purl.org/dc/terms/', 'dts': 'https://w3id.org/dts/api#', - 'hydra': 'https://www.w3.org/ns/hydra/core#', - 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#', + '@vocab': 'https://www.w3.org/ns/hydra/core#', 'skos': 'http://www.w3.org/2004/02/skos/core#'}, '@id': 'general', - '@type': 'hydra:Collection', - 'hydra:member': [ + '@type': 'Collection', + 'member': [ {'@id': '/cartulaires', - '@type': 'hydra:Collection', - 'hydra:totalItems': 1, - 'hydra:description': 'Collection de cartulaires ' + '@type': 'Collection', + 'totalItems': 1, + 'description': 'Collection de cartulaires ' "d'Île-de-France et de ses " 'environs', - 'hydra:title': 'Cartulaires'}, + 'title': 'Cartulaires'}, {'@id': '/lasciva_roma', - '@type': 'hydra:Collection', - 'hydra:totalItems': 1, - 'hydra:description': 'Collection of primary ' + '@type': 'Collection', + 'totalItems': 1, + 'description': 'Collection of primary ' 'sources of interest in the ' "studies of Ancient World's " 'sexuality', - 'hydra:title': 'Lasciva Roma'}, + 'title': 'Lasciva Roma'}, {'@id': '/lettres_de_poilus', - '@type': 'hydra:Collection', - 'hydra:totalItems': 1, - 'hydra:description': 'Collection de lettres de ' + '@type': 'Collection', + 'totalItems': 1, + 'description': 'Collection de lettres de ' 'poilus entre 1917 et 1918', - 'hydra:title': 'Correspondance des poilus'}], - 'hydra:totalItems': 3, - 'hydra:title': "Collection Générale de l'École Nationale des " + 'title': 'Correspondance des poilus'}], + 'totalItems': 3, + 'title': "Collection Générale de l'École Nationale des " 'Chartes', 'dts:dublincore': {'dct:publisher': ['https://viaf.org/viaf/167874585', 'École Nationale des Chartes'], @@ -95,16 +94,14 @@ def test_collection_single_member_with_types(self): "@context": { 'dct': 'http://purl.org/dc/terms/', 'dts': 'https://w3id.org/dts/api#', - 'hydra': 'https://www.w3.org/ns/hydra/core#', - 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#', + '@vocab': 'https://www.w3.org/ns/hydra/core#', 'skos': 'http://www.w3.org/2004/02/skos/core#' }, "@id": "lasciva_roma", - "@type": "hydra:Collection", - "hydra:totalItems": 2, - "hydra:title": "Lasciva Roma", - "hydra:description": "Collection of primary sources of interest in the studies of Ancient World's sexuality", + "@type": "Collection", + "totalItems": 2, + "title": "Lasciva Roma", + "description": "Collection of primary sources of interest in the studies of Ancient World's sexuality", "dts:dublincore": { "dct:creator": [ "Thibault Clérice", "http://orcid.org/0000-0003-1852-9204" @@ -121,18 +118,18 @@ def test_collection_single_member_with_types(self): ] }, 'dts:extensions': {'skos:prefLabel': 'Lasciva Roma'}, - "hydra:member": [ + "member": [ { "@id": "urn:cts:latinLit:phi1103.phi001", - "hydra:title": "Priapeia", - "@type": "hydra:Collection", - "hydra:totalItems": 1 + "title": "Priapeia", + "@type": "Collection", + "totalItems": 1 } ] } ) - def test_collection_with_complexe_child(self): + def test_collection_with_complex_child(self): coll = self.get_collection(3) parsed = DTSCollection.parse(coll) exported = self.reorder_orderable(parsed.export(Mimetypes.JSON.DTS.Std)) @@ -142,15 +139,12 @@ def test_collection_with_complexe_child(self): "@context": { 'dct': 'http://purl.org/dc/terms/', 'dts': 'https://w3id.org/dts/api#', - 'hydra': 'https://www.w3.org/ns/hydra/core#', - 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#', - 'skos': 'http://www.w3.org/2004/02/skos/core#', - # "tei": "http://www.tei-c.org/ns/1.0" + '@vocab': 'https://www.w3.org/ns/hydra/core#', + 'skos': 'http://www.w3.org/2004/02/skos/core#' }, "@id": "urn:cts:latinLit:phi1103.phi001", - "@type": "hydra:Collection", - "hydra:title": "Priapeia", + "@type": "Collection", + "title": "Priapeia", "dts:dublincore": { "dct:type": "http://chs.harvard.edu/xmlns/cts#work", "dct:creator": [ @@ -164,13 +158,35 @@ def test_collection_with_complexe_child(self): }] }, 'dts:extensions': {'skos:prefLabel': 'Priapeia'}, - "hydra:totalItems": 1, - "hydra:member": [{ + "totalItems": 1, + "member": [{ "@id": "urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", - "@type": "hydra:Resource", - "hydra:title": "Priapeia", - "hydra:description": "Priapeia based on the edition of Aemilius Baehrens", - "hydra:totalItems": 0 + "@type": "Resource", + "title": "Priapeia", + "description": "Priapeia based on the edition of Aemilius Baehrens", + "totalItems": 0 }] } - ) \ No newline at end of file + ) + + # The child_collection should be equal to the collection 4 with the fixed @context + coll_4 = self.get_collection(4) + coll_4["@context"] = { + 'dct': 'http://purl.org/dc/terms/', + 'dts': 'https://w3id.org/dts/api#', + '@vocab': 'https://www.w3.org/ns/hydra/core#', + 'skos': 'http://www.w3.org/2004/02/skos/core#' + } + # Not supported at the moment + del coll_4["dts:passage"] + del coll_4["dts:references"] + del coll_4["dts:download"] + + coll_4['dts:extensions'] = {'skos:prefLabel': 'Priapeia'} + + child_collection = parsed.members[0] + child_collection_exported = self.reorder_orderable(child_collection.export(Mimetypes.JSON.DTS.Std)) + self.assertEqual( + self.reorder_orderable(coll_4), + child_collection_exported + ) diff --git a/tests/testing_data/dts/collection_3.json b/tests/testing_data/dts/collection_3.json index 29b47546..7ff5f703 100644 --- a/tests/testing_data/dts/collection_3.json +++ b/tests/testing_data/dts/collection_3.json @@ -49,16 +49,12 @@ "dts:passage": "/api/dts/documents?id=urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", "dts:references": "/api/dts/navigation?id=urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", "dts:download": "https://raw.githubusercontent.com/lascivaroma/priapeia/master/data/phi1103/phi001/phi1103.phi001.lascivaroma-lat1.xml", - "tei:refsDecl": [ + "dts:citeStructure": [ { - "tei:matchPattern": "(\\w+)", - "tei:replacementPattern": "#xpath(/tei:TEI/tei:text/tei:body/tei:div/tei:div[@n='$1'])", - "tei:type": "poem" - }, - { - "tei:matchPattern": "(\\w+)\\.(\\w+)", - "tei:replacementPattern": "#xpath(/tei:TEI/tei:text/tei:body/tei:div/tei:div[@n='$1']//tei:l[@n='$2'])", - "tei:type": "line" + "dts:citeType": "poem", + "dts:citeStructure": { + "dts:citeType": "line" + } } ] } diff --git a/tests/testing_data/dts/collection_4.json b/tests/testing_data/dts/collection_4.json new file mode 100644 index 00000000..954bd2c6 --- /dev/null +++ b/tests/testing_data/dts/collection_4.json @@ -0,0 +1,42 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dct": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#", + "tei": "http://www.tei-c.org/ns/1.0/" + }, + "@id" : "urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", + "@type": "Resource", + "title" : "Priapeia", + "description": "Priapeia based on the edition of Aemilius Baehrens", + "totalItems": 0, + "dts:dublincore": { + "dct:title": [{"@language": "lat", "@value": "Priapeia"}], + "dct:description": [{ + "@language": "fre", + "@value": "Poèmes anonymes lascifs" + }], + "dct:type": [ + "http://chs.harvard.edu/xmlns/cts#edition", + "dc:Text" + ], + "dct:source": "https://archive.org/details/poetaelatinimino12baeh2", + "dct:dateCopyrighted": 1879, + "dct:creator": [ + {"@language": "eng", "@value": "Anonymous"} + ], + "dct:contributor": "Aemilius Baehrens", + "dct:language": ["lat", "eng"] + }, + "dts:passage": "/api/dts/documents?id=urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", + "dts:references": "/api/dts/navigation?id=urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", + "dts:download": "https://raw.githubusercontent.com/lascivaroma/priapeia/master/data/phi1103/phi001/phi1103.phi001.lascivaroma-lat1.xml", + "dts:citeStructure": [ + { + "dts:citeType": "poem", + "dts:citeStructure": [{ + "dts:citeType": "line" + }] + } + ] +} \ No newline at end of file From 9db23477e931281ccea9008874b0f021569b9bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 28 Aug 2018 18:29:55 +0200 Subject: [PATCH 32/89] (resources.prototypes.Collection) Removed domain for the moment --- MyCapytain/resources/prototypes/metadata.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/MyCapytain/resources/prototypes/metadata.py b/MyCapytain/resources/prototypes/metadata.py index a23c2393..ed645540 100644 --- a/MyCapytain/resources/prototypes/metadata.py +++ b/MyCapytain/resources/prototypes/metadata.py @@ -323,15 +323,13 @@ def _export_base_dts(cls, graph, obj, nsm): return o - def __export__(self, output=None, domain="", namespace_manager=None): + def __export__(self, output=None, namespace_manager=None): """ Export the collection item in the Mimetype required. ..note:: If current implementation does not have special mimetypes, reuses default_export method :param output: Mimetype to export to (Uses MyCapytain.common.utils.Mimetypes) :type output: str - :param domain: Domain (Necessary sometime to express some IDs) - :type domain: str :return: Object using a different representation """ From 451f3e2c2936f4bd619cdadc3f305124663636ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 28 Aug 2018 18:33:08 +0200 Subject: [PATCH 33/89] (resources.prototypes.Collection) Removed domain for the moment --- MyCapytain/resources/prototypes/cts/inventory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MyCapytain/resources/prototypes/cts/inventory.py b/MyCapytain/resources/prototypes/cts/inventory.py index 9626b663..c763b3e6 100644 --- a/MyCapytain/resources/prototypes/cts/inventory.py +++ b/MyCapytain/resources/prototypes/cts/inventory.py @@ -731,6 +731,6 @@ def __export__(self, output=None, domain="", namespaces=True): ) elif output == Mimetypes.JSON.DTS.Std: if len(self.members) > 1: - return Collection.__export__(self, output=output, domain=domain) + return Collection.__export__(self, output=output) else: - return self.members[0].export(output=output, domain=domain) + return self.members[0].export(output=output) From b780e5d79773f751998f924ac285f3f7b214e10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 29 Aug 2018 09:38:20 +0200 Subject: [PATCH 34/89] (Setup) Added two requirements that were in requirements.txt only --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5a44e6e8..4eba6eab 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,9 @@ "six>=1.10.0", "lxml>=3.6.4", "future>=0.16.0", - "rdflib-jsonld>=0.4.0" + "rdflib-jsonld>=0.4.0", + "LinkHeader>=0.4.3", + "pyld>=1.0.3" ], tests_require=[ "mock>=2.0.0", From fe8c7563d5c1778502b4d87569ca3566d1fe888f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 29 Aug 2018 12:29:22 +0200 Subject: [PATCH 35/89] (collection.DTS) Parse and export dts:citeDepth --- MyCapytain/common/reference/_dts_1.py | 14 +++++++++++ MyCapytain/resources/collections/dts.py | 5 +++- MyCapytain/resources/prototypes/metadata.py | 25 +++++++++++-------- .../collections/test_dts_collection.py | 9 ++++++- tests/testing_data/dts/collection_4.json | 1 + tests/testing_data/dts/collection_5.json | 13 ++++++++++ 6 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 tests/testing_data/dts/collection_5.json diff --git a/MyCapytain/common/reference/_dts_1.py b/MyCapytain/common/reference/_dts_1.py index 85eec555..94a5f8ac 100644 --- a/MyCapytain/common/reference/_dts_1.py +++ b/MyCapytain/common/reference/_dts_1.py @@ -40,6 +40,20 @@ class DtsCitationSet(BaseCitationSet): """ + def __init__(self, children=None): + super(DtsCitationSet, self).__init__(children=children) + self._depth = None + + @property + def depth(self) -> int: + if self._depth: + return self._depth + return super(DtsCitationSet, self).depth + + @depth.setter + def depth(self, value: int): + self._depth = value + def __repr__(self): return "" % id(self) diff --git a/MyCapytain/resources/collections/dts.py b/MyCapytain/resources/collections/dts.py index 7c0fdd54..d295703c 100644 --- a/MyCapytain/resources/collections/dts.py +++ b/MyCapytain/resources/collections/dts.py @@ -74,6 +74,10 @@ def parse(cls, resource, direction="children"): if _cite_structure_term in collection and collection[_cite_structure_term]: obj.citation = cls.CitationSet.ingest(collection[_cite_structure_term]) + _cite_depth_term = str(_dts.term("citeDepth")) + if _cite_depth_term in collection and collection[_cite_depth_term]: + obj.citation.depth = collection[_cite_depth_term][0]["@value"] + for val_dict in collection[str(_hyd.totalItems)]: obj.metadata.add(_hyd.totalItems, val_dict["@value"], 0) @@ -88,7 +92,6 @@ def parse(cls, resource, direction="children"): for key, value_set in collection.get(str(_dts.extensions), _empty_extensions)[0].items(): term = URIRef(key) for value_dict in value_set: - print(value_dict) obj.metadata.add(term, *dict_to_literal(value_dict)) for member in collection.get(str(_hyd.member), []): diff --git a/MyCapytain/resources/prototypes/metadata.py b/MyCapytain/resources/prototypes/metadata.py index ed645540..2507bb6d 100644 --- a/MyCapytain/resources/prototypes/metadata.py +++ b/MyCapytain/resources/prototypes/metadata.py @@ -20,6 +20,7 @@ _ns_hydra_str = str(RDF_NAMESPACES.HYDRA) _ns_cts_str = str(RDF_NAMESPACES.CTS) _ns_dts_str = str(RDF_NAMESPACES.DTS) +_ns_dts_str = str(RDF_NAMESPACES.DTS) _ns_dct_str = str(DCTERMS) _ns_cap_str = str(RDF_NAMESPACES.CAPITAINS) _ns_rdf_str = str(RDF) @@ -302,7 +303,7 @@ def __namespaces_header__(self, cpt=None): return bindings @classmethod - def _export_base_dts(cls, graph, obj, nsm): + def export_base_dts(cls, graph, obj, nsm): """ Export the base DTS information in a simple reusable way :param graph: Current graph where the information lie @@ -397,7 +398,7 @@ def __export__(self, output=None, namespace_manager=None): metadata[k] = [metadata[k]] o = {"@context": bindings} - o.update(self._export_base_dts(graph, self, nsm)) + o.update(self.export_base_dts(graph, self, nsm)) o["@context"]["@vocab"] = _ns_hydra_str if extensions: @@ -408,20 +409,22 @@ def __export__(self, output=None, namespace_manager=None): if self.size: o[graph.qname(RDF_NAMESPACES.HYDRA.member)] = [ - self._export_base_dts(self.graph, member, nsm) + self.export_base_dts(self.graph, member, nsm) for member in self.members ] # If the system handles citation structure if hasattr(self, "citation") and \ - isinstance(self.citation, BaseCitationSet) and \ - not self.citation.is_empty(): - - o[graph.qname(RDF_NAMESPACES.DTS.term("citeStructure"))] = self.citation.export( - Mimetypes.JSON.DTS.Std, - context=False, - namespace_manager=nsm - ) + isinstance(self.citation, BaseCitationSet): + if self.citation.depth: + o[graph.qname(RDF_NAMESPACES.DTS.term("citeDepth"))] = self.citation.depth + + if not self.citation.is_empty(): + o[graph.qname(RDF_NAMESPACES.DTS.term("citeStructure"))] = self.citation.export( + Mimetypes.JSON.DTS.Std, + context=False, + namespace_manager=nsm + ) del store return o diff --git a/tests/resources/collections/test_dts_collection.py b/tests/resources/collections/test_dts_collection.py index 05765781..32ec5050 100644 --- a/tests/resources/collections/test_dts_collection.py +++ b/tests/resources/collections/test_dts_collection.py @@ -27,7 +27,7 @@ def reorder_orderable(self, exported): """ if "member" in exported: exported["member"] = sorted(exported["member"], key=lambda x: x["@id"]) - for key, values in exported["dts:dublincore"].items(): + for key, values in exported.get("dts:dublincore", {}).items(): if isinstance(values, list) and isinstance(values[0], str): exported["dts:dublincore"][key] = sorted(values) elif isinstance(values, list) and isinstance(values[0], dict): @@ -190,3 +190,10 @@ def test_collection_with_complex_child(self): self.reorder_orderable(coll_4), child_collection_exported ) + + def test_collection_with_cite_depth_but_no_structure(self): + coll = self.get_collection(5) + parsed = DTSCollection.parse(coll) + exported = self.reorder_orderable(parsed.export(Mimetypes.JSON.DTS.Std)) + self.assertEqual(exported["dts:citeDepth"], 7, "There should be a cite depth property") + self.assertNotIn("dts:citeStructure", exported, "CiteStructure was not defined") diff --git a/tests/testing_data/dts/collection_4.json b/tests/testing_data/dts/collection_4.json index 954bd2c6..9ea0ab82 100644 --- a/tests/testing_data/dts/collection_4.json +++ b/tests/testing_data/dts/collection_4.json @@ -31,6 +31,7 @@ "dts:passage": "/api/dts/documents?id=urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", "dts:references": "/api/dts/navigation?id=urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", "dts:download": "https://raw.githubusercontent.com/lascivaroma/priapeia/master/data/phi1103/phi001/phi1103.phi001.lascivaroma-lat1.xml", + "dts:citeDepth": 2, "dts:citeStructure": [ { "dts:citeType": "poem", diff --git a/tests/testing_data/dts/collection_5.json b/tests/testing_data/dts/collection_5.json new file mode 100644 index 00000000..18b996b6 --- /dev/null +++ b/tests/testing_data/dts/collection_5.json @@ -0,0 +1,13 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dct": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id" : "urn:cts:latinLit:phi1103.phi001.lascivaroma-lat2", + "@type": "Resource", + "title" : "Priapeia", + "description": "Priapeia based on the edition of Aemilius Baehrens", + "totalItems": 0, + "dts:citeDepth": 7 +} \ No newline at end of file From 8e4021461844fb296eb5fee37da17922f07fdc9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Thu, 30 Aug 2018 08:53:30 +0200 Subject: [PATCH 36/89] (Resolver Prototype) Added Typing and Changed getReffs to output BaseReferenceSet - Created BaseReferenceSet(list) with .citation kwarg and property - ResolverPrototype.getReffs - BaseReferenceSet is a non breaking change : it's a forked class of list comprehending a citation property - Added `include_descendants` to address the long lasting issue #132 - Added `additional_parameters` to address potential particularities of specific resolvers - Added `typing` as dependency as a result - Changed BasePassageId to BaseReference --- MyCapytain/common/reference/__init__.py | 2 +- MyCapytain/common/reference/_base.py | 17 ++++++++++- MyCapytain/common/reference/_capitains_cts.py | 4 +-- MyCapytain/resolvers/prototypes.py | 30 ++++++++++++++++--- requirements.txt | 3 +- setup.py | 3 +- 6 files changed, 49 insertions(+), 10 deletions(-) diff --git a/MyCapytain/common/reference/__init__.py b/MyCapytain/common/reference/__init__.py index 0d7ea6a2..8c47f75b 100644 --- a/MyCapytain/common/reference/__init__.py +++ b/MyCapytain/common/reference/__init__.py @@ -6,6 +6,6 @@ .. moduleauthor:: Thibault Clérice """ -from ._base import NodeId, BaseCitationSet +from ._base import NodeId, BaseCitationSet, BaseReference, BaseReferenceSet from ._capitains_cts import Citation, Reference, URN from ._dts_1 import DtsCitation, DtsCitationSet diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index 27bbc777..7dae721b 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -5,7 +5,7 @@ from abc import abstractmethod -class BasePassageId: +class BaseReference: def __init__(self, start=None, end=None): self._start = start self._end = end @@ -31,6 +31,21 @@ def end(self): return self._end +class BaseReferenceSet(list): + @property + def citation(self): + return self._citation + + def __new__(cls, *args, **kwargs): + obj = list.__new__(*args, **kwargs) + obj._citation = None + + if "citation" in kwargs: + obj._citation = kwargs["citation"] + + return obj + + class BaseCitationSet(Exportable): """ A citation set is a collection of citations that optionnaly can be matched using a .match() function diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index 4f1fd657..7bbb6d39 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -6,7 +6,7 @@ from MyCapytain.common.constants import Mimetypes, get_graph, RDF_NAMESPACES, XPATH_NAMESPACES from MyCapytain.common.utils import make_xml_node -from ._base import BaseCitation, BasePassageId +from ._base import BaseCitation, BaseReference REFSDECL_SPLITTER = re.compile(r"/+[*()|\sa-zA-Z0-9:\[\]@=\\{$'\".\s]+") REFSDECL_REPLACER = re.compile(r"\$[0-9]+") @@ -26,7 +26,7 @@ def __childOrNone__(liste): return None -class Reference(BasePassageId): +class Reference(BaseReference): """ A reference object giving information :param reference: CapitainsCtsPassage Reference part of a Urn diff --git a/MyCapytain/resolvers/prototypes.py b/MyCapytain/resolvers/prototypes.py index 9f3d0b88..0721c922 100644 --- a/MyCapytain/resolvers/prototypes.py +++ b/MyCapytain/resolvers/prototypes.py @@ -7,6 +7,11 @@ """ +from typing import Tuple, Union, Optional, Dict, Any +from MyCapytain.resources.prototypes.metadata import Collection +from MyCapytain.resources.prototypes.text import TextualNode +from MyCapytain.common.reference import BaseReference, BaseReferenceSet + class Resolver(object): """ Resolver provide a native python API which returns python objects. @@ -14,7 +19,7 @@ class Resolver(object): Initiation of resolvers are dependent on the implementation of the prototype """ - def getMetadata(self, objectId=None, **filters): + def getMetadata(self, objectId: str=None, **filters) -> Collection: """ Request metadata about a text or a collection :param objectId: Object Identifier to filter on @@ -25,7 +30,13 @@ def getMetadata(self, objectId=None, **filters): """ raise NotImplementedError() - def getTextualNode(self, textId, subreference=None, prevnext=False, metadata=False): + def getTextualNode( + self, + textId: str, + subreference: Union[str, BaseReference]=None, + prevnext: bool=False, + metadata: bool=False + ) -> TextualNode: """ Retrieve a text node from the API :param textId: CtsTextMetadata Identifier @@ -41,7 +52,7 @@ def getTextualNode(self, textId, subreference=None, prevnext=False, metadata=Fal """ raise NotImplementedError() - def getSiblings(self, textId, subreference): + def getSiblings(self, textId: str, subreference: Union[str, BaseReference]) -> Tuple[str, str]: """ Retrieve the siblings of a textual node :param textId: CtsTextMetadata Identifier @@ -53,7 +64,14 @@ def getSiblings(self, textId, subreference): """ raise NotImplementedError() - def getReffs(self, textId, level=1, subreference=None): + def getReffs( + self, + textId: str, + level: int=1, + subreference: Union[str, BaseReference]=None, + include_descendants: bool=False, + additional_parameters: Optional[Dict[str, Any]]=None + ) -> BaseReferenceSet: """ Retrieve the siblings of a textual node :param textId: CtsTextMetadata Identifier @@ -62,7 +80,11 @@ def getReffs(self, textId, level=1, subreference=None): :type level: int :param subreference: CapitainsCtsPassage Reference :type subreference: str + :param include_descendants: + :param additional_parameters: :return: List of references :rtype: [str] + + ..toDo :: This starts to be a bloated function.... """ raise NotImplementedError() diff --git a/requirements.txt b/requirements.txt index 1d4454cd..0b17d5d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ xmlunittest>=0.3.2 rdflib-jsonld>=0.4.0 responses>=0.8.1 LinkHeader==0.4.3 -pyld==1.0.3 \ No newline at end of file +pyld==1.0.3 +typing \ No newline at end of file diff --git a/setup.py b/setup.py index 4eba6eab..d4b00f73 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,8 @@ "future>=0.16.0", "rdflib-jsonld>=0.4.0", "LinkHeader>=0.4.3", - "pyld>=1.0.3" + "pyld>=1.0.3", + "typing" ], tests_require=[ "mock>=2.0.0", From b7941d1eece64582a73cc14f74a98ec2684338ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Thu, 30 Aug 2018 10:19:24 +0200 Subject: [PATCH 37/89] (Reference/CtsReference) Breaking Change Completely reworked the system behind CtsReference. Patch note to be completed upon fixing tests : - CtsReference and Reference are now tuple --- MyCapytain/common/reference/__init__.py | 2 +- MyCapytain/common/reference/_base.py | 24 +- MyCapytain/common/reference/_capitains_cts.py | 240 ++++++++---------- 3 files changed, 116 insertions(+), 150 deletions(-) diff --git a/MyCapytain/common/reference/__init__.py b/MyCapytain/common/reference/__init__.py index 8c47f75b..bb64d7be 100644 --- a/MyCapytain/common/reference/__init__.py +++ b/MyCapytain/common/reference/__init__.py @@ -7,5 +7,5 @@ """ from ._base import NodeId, BaseCitationSet, BaseReference, BaseReferenceSet -from ._capitains_cts import Citation, Reference, URN +from ._capitains_cts import Citation, CtsReference, URN from ._dts_1 import DtsCitation, DtsCitationSet diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index 7dae721b..e70c63f1 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -5,14 +5,14 @@ from abc import abstractmethod -class BaseReference: - def __init__(self, start=None, end=None): - self._start = start - self._end = end +class BaseReference(tuple): + def __new__(cls, start, end=None): + obj = tuple.__new__(cls, (start, end)) + + return obj - @property def is_range(self): - return self._end is not None + return bool(self[1]) @property def start(self): @@ -20,7 +20,7 @@ def start(self): :rtype: str """ - return self._start + return self[0] @property def end(self): @@ -28,7 +28,7 @@ def end(self): :rtype: str """ - return self._end + return self[1] class BaseReferenceSet(list): @@ -36,12 +36,12 @@ class BaseReferenceSet(list): def citation(self): return self._citation - def __new__(cls, *args, **kwargs): - obj = list.__new__(*args, **kwargs) + def __new__(cls, *refs, citation=None): + obj = list.__new__(BaseReferenceSet, refs) obj._citation = None - if "citation" in kwargs: - obj._citation = kwargs["citation"] + if citation: + obj._citation = citation return obj diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index 7bbb6d39..20508c1c 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -1,6 +1,6 @@ import re from copy import copy - +from typing import List from lxml.etree import _Element from MyCapytain.common.constants import Mimetypes, get_graph, RDF_NAMESPACES, XPATH_NAMESPACES @@ -26,49 +26,107 @@ def __childOrNone__(liste): return None -class Reference(BaseReference): +class CtsWordReference(str): + def __new__(cls, word_reference): + word, counter = tuple(SUBREFERENCE.findall(word_reference)[0]) + + if len(counter) and word: + word, counter = str(word), int(counter) + elif len(counter) == 0 and word: + word, counter = str(word), 0 + + obj = str.__new__(cls, "@"+word_reference) + obj.counter = counter + obj.word = word + return word + + def __iter__(self): + return iter([self.word, self.counter]) + + +class CtsSinglePassageId(str): + def __new__(cls, str_repr: str): + # Saving the original ID + obj = str.__new__(cls, str_repr) + + # Setting up the properties that can be None + obj._sub_reference = None + + # Parsing the reference + temp_str_repr = str_repr + subreference = temp_str_repr.split("@") + if len(subreference) == 2: + obj._sub_reference = CtsWordReference(subreference[1]) + temp_str_repr = subreference[0] + + obj._list = temp_str_repr.split(".") + return obj + + @property + def list(self): + return self._list + + @property + def subreference(self): + return self._sub_reference + + def __iter__(self): + return iter(self.list) + + def __len__(self): + return len(self.list) + + @property + def depth(self): + return len(self.list) + + +class CtsReference(BaseReference): """ A reference object giving information :param reference: CapitainsCtsPassage Reference part of a Urn :type reference: basestring :Example: - >>> a = Reference(reference="1.1@Achiles[1]-1.2@Zeus[1]") - >>> b = Reference(reference="1.1") - >>> Reference("1.1-2.2.2").highest == ["1", "1"] + >>> a = CtsReference(reference="1.1@Achiles[1]-1.2@Zeus[1]") + >>> b = CtsReference(reference="1.1") + >>> CtsReference("1.1-2.2.2").highest == ["1", "1"] Reference object supports the following magic methods : len(), str() and eq(). :Example: >>> len(a) == 2 && len(b) == 1 >>> str(a) == "1.1@Achiles[1]-1.2@Zeus[1]" - >>> b == Reference("1.1") && b != a + >>> b == CtsReference("1.1") && b != a .. note:: Reference(...).subreference and .list are not available for range. You will need to convert .start or .end to a Reference - >>> ref = Reference('1.2.3') + >>> ref = CtsReference('1.2.3') """ - def __init__(self, reference=""): - self.reference = reference - if reference == "": - self.parsed = (self.__model__(), self.__model__()) + def __new__(cls, reference: str): + if "-" not in reference: + o = BaseReference.__new__(CtsReference, CtsSinglePassageId(reference)) else: - self.parsed = self.__parse__(reference) + _start, _end = tuple(reference.split("-")) + o = BaseReference.__new__(CtsReference, CtsSinglePassageId(_start), CtsSinglePassageId(_end)) + + o._str_repr = reference + return o @property def parent(self): """ Parent of the actual URN, for example, 1.1 for 1.1.1 - :rtype: Reference + :rtype: CtsReference """ if len(self.parsed[0][1]) == 1 and len(self.parsed[1][1]) <= 1: return None else: if len(self.parsed[0][1]) > 1 and len(self.parsed[1][1]) == 0: - return Reference("{0}{1}".format( + return CtsReference("{0}{1}".format( ".".join(list(self.parsed[0][1])[0:-1]), self.parsed[0][3] or "" )) @@ -77,9 +135,9 @@ def parent(self): last = list(self.parsed[1][1])[0:-1] if first == last and self.parsed[1][3] is None \ and self.parsed[0][3] is None: - return Reference(".".join(first)) + return CtsReference(".".join(first)) else: - return Reference("{0}{1}-{2}{3}".format( + return CtsReference("{0}{1}-{2}{3}".format( ".".join(first), self.parsed[0][3] or "", ".".join(list(self.parsed[1][1])[0:-1]), @@ -87,7 +145,7 @@ def parent(self): )) @property - def highest(self): + def highest(self) -> CtsSinglePassageId: """ Return highest reference level For references such as 1.1-1.2.8, with different level, it can be useful to access to the highest node in the @@ -95,10 +153,10 @@ def highest(self): .. note:: By default, this property returns the start level - :rtype: Reference + :rtype: CtsReference """ if not self.end: - return str(self) + return self.start elif len(self.start) < len(self.end) and len(self.start): return self.start elif len(self.start) > len(self.end) and len(self.end): @@ -107,41 +165,20 @@ def highest(self): return self.start @property - def start(self): + def start(self) -> CtsSinglePassageId: """ Quick access property for start list :rtype: str """ - if self.parsed[0][0] and len(self.parsed[0][0]): - return self.parsed[0][0] + return super(CtsReference, self).start @property - def end(self): + def end(self) -> CtsSinglePassageId: """ Quick access property for reference end list :rtype: str """ - if self.parsed[1][0] and len(self.parsed[1][0]): - return self.parsed[1][0] - - @property - def is_range(self): - """ Whether the reference in a starrt - - :rtype: str - """ - return self.parsed[1][0] and len(self.parsed[1][0]) - - @property - def list(self): - """ Return a list version of the object if it is a single passage - - .. note:: Access to start list and end list should be done through obj.start.list and obj.end.list - - :rtype: [str] - """ - if not self.end: - return self.parsed[0][1] + return super(CtsReference, self).end @property def subreference(self): @@ -153,9 +190,9 @@ def subreference(self): :rtype: (str, int) """ if not self.end: - return Reference.convert_subreference(*self.parsed[0][2]) + return self.start.subreference - def __len__(self): + def depth(self): """ Return depth of highest reference level For references such as 1.1-1.2.8, or simple references such as 1.a, with different level, it can be useful to @@ -169,7 +206,7 @@ def __len__(self): :rtype: int """ - return len(Reference(self.highest).list) + return len(self.highest.list) def __str__(self): """ Return full reference in string format @@ -178,12 +215,12 @@ def __str__(self): :returns: String representation of Reference Object :Example: - >>> a = Reference(reference="1.1@Achiles[1]-1.2@Zeus[1]") - >>> b = Reference(reference="1.1") + >>> a = CtsReference(reference="1.1@Achiles[1]-1.2@Zeus[1]") + >>> b = CtsReference(reference="1.1") >>> str(a) == "1.1@Achiles[1]-1.2@Zeus[1]" >>> str(b) == "1.1" """ - return self.reference + return self._str_repr def __eq__(self, other): """ Equality checker for Reference object @@ -193,85 +230,14 @@ def __eq__(self, other): :returns: Equality between other and self :Example: - >>> a = Reference(reference="1.1@Achiles[1]-1.2@Zeus[1]") - >>> b = Reference(reference="1.1") - >>> c = Reference(reference="1.1") + >>> a = CtsReference(reference="1.1@Achiles[1]-1.2@Zeus[1]") + >>> b = CtsReference(reference="1.1") + >>> c = CtsReference(reference="1.1") >>> (a == b) == False >>> (c == b) == True """ return (isinstance(other, type(self)) - and self.reference == str(other)) - - def __ne__(self, other): - """ Inequality checker for Reference object - - :param other: An object to be checked against - :rtype: boolean - :returns: Equality between other and self - """ - return not self.__eq__(other) - - @staticmethod - def __model__(): - """ 3-Tuple model for references - - First element is full text reference, - Second is list of passage identifiers - Third is subreference - - :returns: An empty list to model data - :rtype: list - """ - return [None, [], None, None] - - def __regexp__(self, subreference): - """ Split components of subreference - - :param subreference: A subreference - :type subreference: str - :rtype: List. - :returns: List where first element is a tuple representing different components - """ - return SUBREFERENCE.findall(subreference)[0] - - def __parse__(self, reference): - """ Parse references information - - :param reference: String representation of a reference - :type reference: str - :returns: Tuple representing each part of the reference - :rtype: tuple(str) - """ - - ref = reference.split("-") - element = [self.__model__(), self.__model__()] - for i in range(0, len(ref)): - r = ref[i] - element[i][0] = r - subreference = r.split("@") - if len(subreference) == 2: - element[i][2] = self.__regexp__(subreference[1]) - element[i][3] = "@" + subreference[1] - r = subreference[0] - element[i][1] = r.split(".") - element[i] = tuple(element[i]) - return tuple(element) - - @staticmethod - def convert_subreference(word, counter): - """ Convert a word and a counter into a standard tuple representation - - :param word: Word Element of the subreference - :param counter: Index of the Word - :return: Tuple representing the element - :rtype: (str, int) - """ - if len(counter) and word: - return str(word), int(counter) - elif len(counter) == 0 and word: - return str(word), 0 - else: - return "", 0 + and str(self) == str(other)) class URN(object): @@ -299,7 +265,7 @@ class URN(object): >>> a != b >>> a > b # It has more member. Only member count is compared >>> b < a - >>> len(a) == 5 # Reference is not counted to not induce count equivalencies with the optional version + >>> len(a) == 5 # CtsReference is not counted to not induce count equivalencies with the optional version >>> len(b) == 4 """ @@ -392,7 +358,7 @@ def version(self, value): def reference(self): """ Reference element of the URN - :rtype: Reference + :rtype: CtsReference :return: Reference part of the URN """ return self.__parsed["reference"] @@ -400,10 +366,10 @@ def reference(self): @reference.setter def reference(self, value): self.__urn = None - if isinstance(value, Reference): + if isinstance(value, CtsReference): self.__parsed["reference"] = value else: - self.__parsed["reference"] = Reference(value) + self.__parsed["reference"] = CtsReference(value) def __len__(self): """ Returns the len of the URN @@ -475,7 +441,7 @@ def __eq__(self, other): return isinstance(other, type(self)) and str(self) == str(other) def __ne__(self, other): - """ Inequality checker for Reference object + """ Inequality checker for CtsReference object :param other: An object to be checked against :rtype: boolean @@ -619,7 +585,7 @@ def __parse__(self, urn): parsed["cts_namespace"] = urn[2] if len(urn) == 5: - parsed["reference"] = Reference(urn[4]) + parsed["reference"] = CtsReference(urn[4]) if len(urn) >= 4: urn = urn[3].split(".") @@ -795,8 +761,8 @@ def match(self, passageId): :param passageId: A passage to match :return: """ - if not isinstance(passageId, Reference): - passageId = Reference(passageId) + if not isinstance(passageId, CtsReference): + passageId = CtsReference(passageId) if self.is_root(): return self[len(passageId)-1] @@ -806,7 +772,7 @@ def fill(self, passage=None, xpath=None): """ Fill the xpath with given informations :param passage: CapitainsCtsPassage reference - :type passage: Reference or list or None. Can be list of None and not None + :type passage: CtsReference or list or None. Can be list of None and not None :param xpath: If set to True, will return the replaced self.xpath value and not the whole self.refsDecl :type xpath: Boolean :rtype: basestring @@ -819,7 +785,7 @@ def fill(self, passage=None, xpath=None): # /TEI/text/body/div/div[@n='1']//l[@n] print(citation.fill(None)) # /TEI/text/body/div/div[@n]//l[@n] - print(citation.fill(Reference("1.1")) + print(citation.fill(CtsReference("1.1")) # /TEI/text/body/div/div[@n='1']//l[@n='1'] print(citation.fill("1", xpath=True) # //l[@n='1'] @@ -835,8 +801,8 @@ def fill(self, passage=None, xpath=None): return REFERENCE_REPLACER.sub(replacement, xpath) else: - if isinstance(passage, Reference): - passage = passage.list or Reference(passage.start).list + if isinstance(passage, CtsReference): + passage = passage.list or CtsReference(passage.start).list elif passage is None: return REFERENCE_REPLACER.sub( r"\1", From 162ee5a244bf892bdd6a16d01c6542158a82fb52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Thu, 30 Aug 2018 10:28:56 +0200 Subject: [PATCH 38/89] (Reference Breaking) Propagation of some changes, not finale --- MyCapytain/common/reference/_capitains_cts.py | 5 +- MyCapytain/errors.py | 4 +- MyCapytain/resolvers/cts/local.py | 12 +-- MyCapytain/resources/prototypes/text.py | 12 +-- .../resources/texts/local/capitains/cts.py | 101 +++++++++--------- MyCapytain/resources/texts/remote/cts.py | 32 +++--- MyCapytain/retrievers/cts5.py | 6 +- .../test_reference/test_capitains_cts.py | 82 +++++++------- tests/resolvers/cts/test_local.py | 4 +- tests/resources/texts/base/test_tei.py | 4 +- tests/resources/texts/local/commonTests.py | 92 ++++++++-------- .../texts/local/test_capitains_xml_default.py | 2 +- .../test_capitains_xml_notObjectified.py | 2 +- tests/resources/texts/remote/test_cts.py | 6 +- 14 files changed, 184 insertions(+), 180 deletions(-) diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index 20508c1c..3ad617fb 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -40,6 +40,9 @@ def __new__(cls, word_reference): obj.word = word return word + def tuple(self): + return self.word, self.counter + def __iter__(self): return iter([self.word, self.counter]) @@ -802,7 +805,7 @@ def fill(self, passage=None, xpath=None): return REFERENCE_REPLACER.sub(replacement, xpath) else: if isinstance(passage, CtsReference): - passage = passage.list or CtsReference(passage.start).list + passage = passage.start.list elif passage is None: return REFERENCE_REPLACER.sub( r"\1", diff --git a/MyCapytain/errors.py b/MyCapytain/errors.py index 5a52224c..0b5e77d2 100644 --- a/MyCapytain/errors.py +++ b/MyCapytain/errors.py @@ -20,7 +20,7 @@ class JsonLdCollectionMissing(MyCapytainException): class DuplicateReference(SyntaxWarning, MyCapytainException): - """ Error generated when a duplicate is found in Reference + """ Error generated when a duplicate is found in CtsReference """ @@ -67,7 +67,7 @@ class UnknownCollection(KeyError, MyCapytainException): """ class EmptyReference(SyntaxWarning, MyCapytainException): - """ Error generated when a duplicate is found in Reference + """ Error generated when a duplicate is found in CtsReference """ diff --git a/MyCapytain/resolvers/cts/local.py b/MyCapytain/resolvers/cts/local.py index 0399fca0..c020ca7d 100644 --- a/MyCapytain/resolvers/cts/local.py +++ b/MyCapytain/resolvers/cts/local.py @@ -7,7 +7,7 @@ from glob import glob from math import ceil -from MyCapytain.common.reference._capitains_cts import Reference, URN +from MyCapytain.common.reference._capitains_cts import CtsReference, URN from MyCapytain.common.utils import xmlparser from MyCapytain.errors import InvalidURN, UnknownObjectError, UndispatchedTextError from MyCapytain.resolvers.prototypes import Resolver @@ -469,7 +469,7 @@ def getTextualNode(self, textId, subreference=None, prevnext=False, metadata=Fal :param textId: CtsTextMetadata Identifier :type textId: str - :param subreference: CapitainsCtsPassage Reference + :param subreference: CapitainsCtsPassage CtsReference :type subreference: str :param prevnext: Retrieve graph representing previous and next passage :type prevnext: boolean @@ -480,7 +480,7 @@ def getTextualNode(self, textId, subreference=None, prevnext=False, metadata=Fal """ text, text_metadata = self.__getText__(textId) if subreference is not None: - subreference = Reference(subreference) + subreference = CtsReference(subreference) passage = text.getTextualNode(subreference) if metadata: passage.set_metadata_from_collection(text_metadata) @@ -491,13 +491,13 @@ def getSiblings(self, textId, subreference): :param textId: CtsTextMetadata Identifier :type textId: str - :param subreference: CapitainsCtsPassage Reference + :param subreference: CapitainsCtsPassage CtsReference :type subreference: str :return: Tuple of references :rtype: (str, str) """ text, inventory = self.__getText__(textId) - passage = text.getTextualNode(Reference(subreference)) + passage = text.getTextualNode(CtsReference(subreference)) return passage.siblingsId def getReffs(self, textId, level=1, subreference=None): @@ -507,7 +507,7 @@ def getReffs(self, textId, level=1, subreference=None): :type textId: str :param level: Depth for retrieval :type level: int - :param subreference: CapitainsCtsPassage Reference + :param subreference: CapitainsCtsPassage CtsReference :type subreference: str :return: List of references :rtype: [str] diff --git a/MyCapytain/resources/prototypes/text.py b/MyCapytain/resources/prototypes/text.py index f90c80fb..b10ef3d3 100644 --- a/MyCapytain/resources/prototypes/text.py +++ b/MyCapytain/resources/prototypes/text.py @@ -227,23 +227,23 @@ def __init__(self, identifier=None, **kwargs): def getTextualNode(self, subreference): """ Retrieve a passage and store it in the object - :param subreference: Reference of the passage to retrieve - :type subreference: str or Node or Reference + :param subreference: CtsReference of the passage to retrieve + :type subreference: str or Node or CtsReference :rtype: TextualNode :returns: Object representing the passage - :raises: *TypeError* when reference is not a list or a Reference + :raises: *TypeError* when reference is not a list or a CtsReference """ raise NotImplementedError() def getReffs(self, level=1, subreference=None): - """ Reference available at a given level + """ CtsReference available at a given level :param level: Depth required. If not set, should retrieve first encountered level (1 based) :type level: Int :param passage: Subreference (optional) - :type passage: Reference + :type passage: CtsReference :rtype: [text_type] :returns: List of levels """ @@ -429,7 +429,7 @@ def getValidReff(self, level=1, reference=None): :param level: Depth required. If not set, should retrieve first encountered level (1 based) :type level: Int :param passage: Subreference (optional) - :type passage: Reference + :type passage: CtsReference :rtype: List.text_type :returns: List of levels """ diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index 2a78714d..afe712ab 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -14,7 +14,7 @@ from MyCapytain.errors import DuplicateReference, MissingAttribute, RefsDeclError, EmptyReference, CitationDepthError, MissingRefsDecl from MyCapytain.common.utils import copyNode, passageLoop, normalizeXpath from MyCapytain.common.constants import XPATH_NAMESPACES, RDF_NAMESPACES -from MyCapytain.common.reference._capitains_cts import Reference, URN, Citation +from MyCapytain.common.reference._capitains_cts import CtsReference, URN, Citation from MyCapytain.resources.prototypes import text from MyCapytain.resources.texts.base.tei import TEIResource @@ -39,7 +39,7 @@ def __makePassageKwargs__(urn, reference): return kwargs -class __SharedMethods__: +class _SharedMethods: """ Set of shared methods between objects in local TEI. Avoid recoding functions """ @@ -47,7 +47,7 @@ def getTextualNode(self, subreference=None, simple=False): """ Finds a passage in the current text :param subreference: Identifier of the subreference / passages - :type subreference: Union[list, Reference] + :type subreference: Union[list, CtsReference] :param simple: If set to true, retrieves nodes up to the given one, cleaning non required siblings. :type simple: boolean :rtype: CapitainsCtsPassage, ContextPassage @@ -57,15 +57,16 @@ def getTextualNode(self, subreference=None, simple=False): if subreference is None: return self._getSimplePassage() - if isinstance(subreference, str): - subreference = Reference(subreference) - if isinstance(subreference, list): - start, end = subreference, subreference - subreference = Reference(".".join(subreference)) - elif not subreference.end: - start = end = Reference(subreference.start).list + if not isinstance(subreference, CtsReference): + if isinstance(subreference, str): + subreference = CtsReference(subreference) + elif isinstance(subreference, list): + subreference = CtsReference(".".join(subreference)) + + if not subreference.end: + start = end = subreference.start.list else: - start, end = Reference(subreference.start).list, Reference(subreference.end).list + start, end = subreference.start.list, subreference.end.list if len(start) > len(self.citation): raise CitationDepthError("URN is deeper than citation scheme") @@ -119,7 +120,7 @@ def _getSimplePassage(self, reference=None): ) resource = self.resource.xpath( - self.citation[len(reference)-1].fill(reference), + self.citation[reference.depth()-1].fill(reference), namespaces=XPATH_NAMESPACES ) @@ -147,7 +148,7 @@ def textObject(self): return text def getReffs(self, level=1, subreference=None): - """ Reference available at a given level + """ CtsReference available at a given level :param level: Depth required. If not set, should retrieve first encountered level (1 based) :type level: Int @@ -162,7 +163,7 @@ def getReffs(self, level=1, subreference=None): if hasattr(self, "reference"): subreference = self.reference else: - subreference = Reference(subreference) + subreference = CtsReference(subreference) return self.getValidReff(level, subreference) def getValidReff(self, level=None, reference=None, _debug=False): @@ -171,7 +172,7 @@ def getValidReff(self, level=None, reference=None, _debug=False): :param level: Depth required. If not set, should retrieve first encountered level (1 based) :type level: int :param reference: CapitainsCtsPassage Reference - :type reference: Reference + :type reference: CtsReference :param _debug: Check on passages duplicates :type _debug: bool :returns: List of levels @@ -183,24 +184,24 @@ def getValidReff(self, level=None, reference=None, _debug=False): depth = 0 xml = self.textObject.xml if reference: - if isinstance(reference, Reference): + if isinstance(reference, CtsReference): if reference.end is None: - passages = [reference.list] + passages = [reference.start.list] depth = len(passages[0]) else: xml = self.getTextualNode(subreference=reference) common = [] - ref = Reference(reference.start) - for index in range(0, len(ref.list)): + + for index in range(0, len(reference.start)): if index == (len(common) - 1): - common.append(ref.list[index]) + common.append(reference.start.list[index]) else: break passages = [common] depth = len(common) if not level: - level = len(ref.list) + 1 + level = len(reference.start.list) + 1 else: raise TypeError() @@ -279,13 +280,13 @@ def tostring(self, *args, **kwargs): return etree.tostring(self.resource, *args, **kwargs) -class __SimplePassage__(__SharedMethods__, TEIResource, text.Passage): +class __SimplePassage__(_SharedMethods, TEIResource, text.Passage): """ CapitainsCtsPassage for simple and quick parsing of texts :param resource: Element representing the passage :type resource: etree._Element :param reference: CapitainsCtsPassage reference - :type reference: Reference + :type reference: CtsReference :param urn: URN of the source text or of the passage :type urn: URN :param citation: XmlCtsCitation scheme of the text @@ -309,10 +310,10 @@ def __init__(self, resource, reference, citation, text, urn=None): @property def reference(self): - """ URN CapitainsCtsPassage Reference + """ URN CapitainsCtsPassage CtsReference - :return: Reference - :rtype: Reference + :return: CtsReference + :rtype: CtsReference """ return self.__reference__ @@ -324,7 +325,7 @@ def reference(self, value): def childIds(self): """ Children of the passage - :rtype: None, Reference + :rtype: None, CtsReference :returns: Dictionary of chidren, where key are subreferences """ if self.depth >= len(self.citation): @@ -341,7 +342,7 @@ def getReffs(self, level=1, subreference=None): :param level: Depth required. If not set, should retrieve first encountered level (1 based) :type level: Int :param subreference: Subreference (optional) - :type subreference: Reference + :type subreference: CtsReference :rtype: List.basestring :returns: List of levels """ @@ -356,8 +357,8 @@ def getTextualNode(self, subreference=None): :param subreference: :return: """ - if not isinstance(subreference, Reference): - subreference = Reference(subreference) + if not isinstance(subreference, CtsReference): + subreference = CtsReference(subreference) return self.textObject.getTextualNode(subreference) @property @@ -365,7 +366,7 @@ def nextId(self): """ Next passage :returns: Next passage at same level - :rtype: None, Reference + :rtype: None, CtsReference """ return self.siblingsId[1] @@ -374,7 +375,7 @@ def prevId(self): """ Get the Previous passage reference :returns: Previous passage reference at the same level - :rtype: None, Reference + :rtype: None, CtsReference """ return self.siblingsId[0] @@ -402,17 +403,17 @@ def siblingsId(self): # If the passage is already at the beginning _prev = None elif start - range_length < 0: - _prev = Reference(document_references[0]) + _prev = CtsReference(document_references[0]) else: - _prev = Reference(document_references[start-1]) + _prev = CtsReference(document_references[start - 1]) if start + 1 == len(document_references): # If the passage is already at the end _next = None elif start + range_length > len(document_references): - _next = Reference(document_references[-1]) + _next = CtsReference(document_references[-1]) else: - _next = Reference(document_references[start + 1]) + _next = CtsReference(document_references[start + 1]) self.__prevnext__ = (_prev, _next) return self.__prevnext__ @@ -426,7 +427,7 @@ def textObject(self): return self.__text__ -class CapitainsCtsText(__SharedMethods__, TEIResource, text.CitableText): +class CapitainsCtsText(_SharedMethods, TEIResource, text.CitableText): """ Implementation of CTS tools for local files :param urn: A URN identifier @@ -471,7 +472,7 @@ def test(self): raise E -class CapitainsCtsPassage(__SharedMethods__, TEIResource, text.Passage): +class CapitainsCtsPassage(_SharedMethods, TEIResource, text.Passage): """ CapitainsCtsPassage class for local texts which rebuilds the tree up to the passage. For design purposes, some people would prefer the output of GetPassage to be consistent. ContextPassage rebuilds @@ -503,7 +504,7 @@ class will build an XML tree looking like the following :param reference: CapitainsCtsPassage reference - :type reference: Reference + :type reference: CtsReference :param urn: URN of the source text or of the passage :type urn: URN :param citation: XmlCtsCitation scheme of the text @@ -533,15 +534,15 @@ def __init__(self, reference, urn=None, citation=None, resource=None, text=None) self.__depth__ = self.__depth_2__ = 1 if self.reference.start: - self.__depth_2__ = self.__depth__ = len(Reference(self.reference.start)) + self.__depth_2__ = self.__depth__ = len(CtsReference(self.reference.start)) if self.reference and self.reference.end: - self.__depth_2__ = len(Reference(self.reference.end)) + self.__depth_2__ = len(CtsReference(self.reference.end)) self.__prevnext__ = None # For caching purpose @property def reference(self): - """ Reference of the object + """ CtsReference of the object """ return self.__reference__ @@ -549,7 +550,7 @@ def reference(self): def childIds(self): """ Children of the passage - :rtype: None, Reference + :rtype: None, CtsReference :returns: Dictionary of chidren, where key are subreferences """ self.__raiseDepth__() @@ -566,7 +567,7 @@ def nextId(self): """ Next passage :returns: Next passage at same level - :rtype: None, Reference + :rtype: None, CtsReference """ return self.siblingsId[1] @@ -575,7 +576,7 @@ def prevId(self): """ Get the Previous passage reference :returns: Previous passage reference at the same level - :rtype: None, Reference + :rtype: None, CtsReference """ return self.siblingsId[0] @@ -650,19 +651,19 @@ def next(self): """ Next CapitainsCtsPassage (Interactive CapitainsCtsPassage) """ if self.nextId is not None: - return __SharedMethods__.getTextualNode(self.__text__, self.nextId) + return _SharedMethods.getTextualNode(self.__text__, self.nextId) @property def prev(self): """ Previous CapitainsCtsPassage (Interactive CapitainsCtsPassage) """ if self.prevId is not None: - return __SharedMethods__.getTextualNode(self.__text__, self.prevId) + return _SharedMethods.getTextualNode(self.__text__, self.prevId) def getTextualNode(self, subreference=None, *args, **kwargs): - if not isinstance(subreference, Reference): - subreference = Reference(subreference) - X = __SharedMethods__.getTextualNode(self, subreference) + if not isinstance(subreference, CtsReference): + subreference = CtsReference(subreference) + X = _SharedMethods.getTextualNode(self, subreference) X.__text__ = self.__text__ return X diff --git a/MyCapytain/resources/texts/remote/cts.py b/MyCapytain/resources/texts/remote/cts.py index 153a4eca..06054bb1 100644 --- a/MyCapytain/resources/texts/remote/cts.py +++ b/MyCapytain/resources/texts/remote/cts.py @@ -13,7 +13,7 @@ from MyCapytain.common.metadata import Metadata from MyCapytain.common.utils import xmlparser from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes, RDF_NAMESPACES -from MyCapytain.common.reference._capitains_cts import Reference, URN +from MyCapytain.common.reference._capitains_cts import CtsReference, URN from MyCapytain.resources.collections import cts as CtsCollection from MyCapytain.resources.prototypes import text as prototypes from MyCapytain.resources.texts.base.tei import TEIResource @@ -59,7 +59,7 @@ def getValidReff(self, level=1, reference=None): :param level: Depth required. If not set, should retrieve first encountered level (1 based) :type level: Int :param reference: CapitainsCtsPassage reference - :type reference: Reference + :type reference: CtsReference :rtype: list(str) :returns: List of levels """ @@ -83,16 +83,16 @@ def getValidReff(self, level=1, reference=None): def getTextualNode(self, subreference=None): """ Retrieve a passage and store it in the object - :param subreference: Reference of the passage (Note : if given a list, this should be a list of string that \ + :param subreference: CtsReference of the passage (Note : if given a list, this should be a list of string that \ compose the reference) - :type subreference: Union[Reference, URN, str, list] + :type subreference: Union[CtsReference, URN, str, list] :rtype: CtsPassage :returns: Object representing the passage - :raises: *TypeError* when reference is not a list or a Reference + :raises: *TypeError* when reference is not a list or a CtsReference """ if isinstance(subreference, URN): urn = str(subreference) - elif isinstance(subreference, Reference): + elif isinstance(subreference, CtsReference): urn = "{0}:{1}".format(self.urn, str(subreference)) elif isinstance(subreference, str): if ":" in subreference: @@ -115,7 +115,7 @@ def getReffs(self, level=1, subreference=None): :param level: Depth required. If not set, should retrieve first encountered level (1 based) :type level: Int :param subreference: Subreference (optional) - :type subreference: Reference + :type subreference: CtsReference :rtype: [text_type] :returns: List of levels """ @@ -128,7 +128,7 @@ def getPassagePlus(self, reference=None): """ Retrieve a passage and informations around it and store it in the object :param reference: Reference of the passage - :type reference: Reference or List of text_type + :type reference: CtsReference or List of text_type :rtype: CtsPassage :returns: Object representing the passage :raises: *TypeError* when reference is not a list or a Reference @@ -197,9 +197,9 @@ def getLabel(self): def getPrevNextUrn(self, reference): """ Get the previous URN of a reference of the text - :param reference: Reference from which to find siblings - :type reference: Union[Reference, str] - :return: (Previous CapitainsCtsPassage Reference,Next CapitainsCtsPassage Reference) + :param reference: CtsReference from which to find siblings + :type reference: Union[CtsReference, str] + :return: (Previous CapitainsCtsPassage CtsReference,Next CapitainsCtsPassage CtsReference) """ _prev, _next = __SharedMethod__.prevnext( self.retriever.getPrevNextUrn( @@ -217,8 +217,8 @@ def getPrevNextUrn(self, reference): def getFirstUrn(self, reference=None): """ Get the first children URN for a given resource - :param reference: Reference from which to find child (If None, find first reference) - :type reference: Reference, str + :param reference: CtsReference from which to find child (If None, find first reference) + :type reference: CtsReference, str :return: Children URN :rtype: URN """ @@ -415,7 +415,7 @@ def prevId(self): def parentId(self): """ Shortcut for getting the parent passage identifier - :rtype: Reference + :rtype: CtsReference :returns: Following passage reference """ return str(self.urn.reference.parent) @@ -424,7 +424,7 @@ def parentId(self): def nextId(self): """ Shortcut for getting the following passage identifier - :rtype: Reference + :rtype: CtsReference :returns: Following passage reference """ if self.__nextId__ is False: @@ -436,7 +436,7 @@ def nextId(self): def siblingsId(self): """ Shortcut for getting the previous and next passage identifier - :rtype: Reference + :rtype: CtsReference :returns: Following passage reference """ if self.__nextId__ is False or self.__prev__ is False: diff --git a/MyCapytain/retrievers/cts5.py b/MyCapytain/retrievers/cts5.py index 26972179..2634ab07 100644 --- a/MyCapytain/retrievers/cts5.py +++ b/MyCapytain/retrievers/cts5.py @@ -8,7 +8,7 @@ """ import MyCapytain.retrievers.prototypes -from MyCapytain.common.reference._capitains_cts import Reference +from MyCapytain.common.reference._capitains_cts import CtsReference import requests @@ -224,10 +224,10 @@ def getReffs(self, textId, level=1, subreference=None): if subreference: textId = "{}:{}".format(textId, subreference) if subreference: - if isinstance(subreference, Reference): + if isinstance(subreference, CtsReference): depth += len(subreference) else: - depth += len(Reference(subreference)) + depth += len(CtsReference(subreference)) if level: level = max(depth, level) return self.getValidReff(urn=textId, level=level) diff --git a/tests/common/test_reference/test_capitains_cts.py b/tests/common/test_reference/test_capitains_cts.py index c81812de..df85d727 100644 --- a/tests/common/test_reference/test_capitains_cts.py +++ b/tests/common/test_reference/test_capitains_cts.py @@ -2,7 +2,7 @@ from six import text_type as str from MyCapytain.common.utils import xmlparser -from MyCapytain.common.reference import Reference, URN, Citation +from MyCapytain.common.reference import CtsReference, URN, Citation class TestReferenceImplementation(unittest.TestCase): @@ -10,74 +10,74 @@ class TestReferenceImplementation(unittest.TestCase): """ Test how reference reacts """ def test_str_function(self): - a = Reference("1-1") + a = CtsReference("1-1") self.assertEqual(str(a), "1-1") def test_len_ref(self): - a = Reference("1.1@Achilles[0]-1.10@Atreus[3]") + a = CtsReference("1.1@Achilles[0]-1.10@Atreus[3]") self.assertEqual(len(a), 2) - a = Reference("1.1.1") + a = CtsReference("1.1.1") self.assertEqual(len(a), 3) def test_highest(self): self.assertEqual( - str((Reference("1.1-1.2.8")).highest), "1.1", + str((CtsReference("1.1-1.2.8")).highest), "1.1", "1.1 is higher" ) self.assertEqual( - str((Reference("1.1-2")).highest), "2", + str((CtsReference("1.1-2")).highest), "2", "2 is higher" ) def test_properties(self): - a = Reference("1.1@Achilles-1.10@Atreus[3]") + a = CtsReference("1.1@Achilles-1.10@Atreus[3]") self.assertEqual(a.start, "1.1@Achilles") - self.assertEqual(Reference(a.start).list, ["1", "1"]) - self.assertEqual(Reference(a.start).subreference[0], "Achilles") + self.assertEqual(CtsReference(a.start).list, ["1", "1"]) + self.assertEqual(CtsReference(a.start).subreference[0], "Achilles") self.assertEqual(a.end, "1.10@Atreus[3]") - self.assertEqual(Reference(a.end).list, ["1", "10"]) - self.assertEqual(Reference(a.end).subreference[1], 3) - self.assertEqual(Reference(a.end).subreference, ("Atreus", 3)) + self.assertEqual(CtsReference(a.end).list, ["1", "10"]) + self.assertEqual(CtsReference(a.end).subreference[1], 3) + self.assertEqual(CtsReference(a.end).subreference, ("Atreus", 3)) def test_Unicode_Support(self): - a = Reference("1.1@καὶ[0]-1.10@Ἀλκιβιάδου[3]") + a = CtsReference("1.1@καὶ[0]-1.10@Ἀλκιβιάδου[3]") self.assertEqual(a.start, "1.1@καὶ[0]") - self.assertEqual(Reference(a.start).list, ["1", "1"]) - self.assertEqual(Reference(a.start).subreference[0], "καὶ") + self.assertEqual(a.start.list, ["1", "1"]) + self.assertEqual(a.start.subreference.word, "καὶ") self.assertEqual(a.end, "1.10@Ἀλκιβιάδου[3]") - self.assertEqual(Reference(a.end).list, ["1", "10"]) - self.assertEqual(Reference(a.end).subreference[1], 3) - self.assertEqual(Reference(a.end).subreference, ("Ἀλκιβιάδου", 3)) + self.assertEqual(a.end.list, ["1", "10"]) + self.assertEqual(a.end.subreference.word, 3) + self.assertEqual(a.end.subreference.tuple, ("Ἀλκιβιάδου", 3)) def test_NoWord_Support(self): - a = Reference("1.1@[0]-1.10@Ἀλκιβιάδου[3]") - self.assertEqual(str(a.start), "1.1@[0]") - self.assertEqual(Reference(a.start).subreference[0], "") - self.assertEqual(Reference(a.start).subreference[1], 0) + a = CtsReference("1.1@[0]-1.10@Ἀλκιβιάδου[3]") + self.assertEqual(a.start, "1.1@[0]") + self.assertEqual(a.start.subreference.word, "") + self.assertEqual(a.start.subreference.counter, 0) def test_No_End_Support(self): - a = Reference("1.1@[0]") + a = CtsReference("1.1@[0]") self.assertEqual(a.end, None) self.assertEqual(a.start, "1.1@[0]") - self.assertEqual(Reference(a.start).subreference[0], "") - self.assertEqual(Reference(a.start).subreference[1], 0) + self.assertEqual(a.start.subreference.word, "") + self.assertEqual(a.start.subreference.counter, 0) def test_equality(self): - a = Reference("1.1@[0]") - b = Reference("1.1@[0]") - c = Reference("1.1@[1]") + a = CtsReference("1.1@[0]") + b = CtsReference("1.1@[0]") + c = CtsReference("1.1@[1]") d = "1.1@[0]" self.assertEqual(a, b) self.assertNotEqual(a, c) self.assertNotEqual(a, d) def test_get_parent(self): - a = Reference("1.1") - b = Reference("1") - c = Reference("1.1-2.3") - d = Reference("1.1-1.2") - e = Reference("1.1@Something[0]-1.2@SomethingElse[2]") - f = Reference("1-2") + a = CtsReference("1.1") + b = CtsReference("1") + c = CtsReference("1.1-2.3") + d = CtsReference("1.1-1.2") + e = CtsReference("1.1@Something[0]-1.2@SomethingElse[2]") + f = CtsReference("1-2") self.assertEqual(str(a.parent), "1") self.assertEqual(b.parent, None) @@ -104,7 +104,7 @@ def test_properties(self): self.assertEqual(a.textgroup, "tlg0012") self.assertEqual(a.work, "tlg001") self.assertEqual(a.version, "mth-01") - self.assertEqual(a.reference, Reference("1.1@Achilles-1.10@the[2]")) + self.assertEqual(a.reference, CtsReference("1.1@Achilles-1.10@the[2]")) def test_upTo(self): a = URN("urn:cts:greekLit:tlg0012.tlg001.mth-01:1.1@Achilles-1.10@the[2]") @@ -158,7 +158,7 @@ def test_set(self): self.assertEqual(a.work, "wk") self.assertEqual(str(a), "urn:cts:ns:tg.wk") a.reference = "1-2" - self.assertEqual(a.reference, Reference("1-2")) + self.assertEqual(a.reference, CtsReference("1-2")) self.assertEqual(str(a), "urn:cts:ns:tg.wk:1-2") a.version = "vs" self.assertEqual(a.version, "vs") @@ -194,7 +194,7 @@ def test_no_end_text_emptiness(self): self.assertEqual(a.upTo(URN.VERSION), "urn:cts:greekLit:textgroup.work.text") self.assertEqual(a.upTo(URN.PASSAGE), "urn:cts:greekLit:textgroup.work.text:1") self.assertEqual(a.upTo(URN.NO_PASSAGE), "urn:cts:greekLit:textgroup.work.text") - self.assertEqual(a.reference, Reference("1")) + self.assertEqual(a.reference, CtsReference("1")) self.assertIsNone(a.reference.end) def test_missing_text_in_passage_emptiness(self): @@ -208,7 +208,7 @@ def test_missing_text_in_passage_emptiness(self): self.assertEqual(a.upTo(URN.PASSAGE), "urn:cts:greekLit:textgroup.work:1-2") self.assertEqual(a.upTo(URN.PASSAGE_START), "urn:cts:greekLit:textgroup.work:1") self.assertEqual(a.upTo(URN.PASSAGE_END), "urn:cts:greekLit:textgroup.work:2") - self.assertEqual(a.reference, Reference("1-2")) + self.assertEqual(a.reference, CtsReference("1-2")) self.assertEqual(a.reference.start, "1") self.assertEqual(a.reference.end, "2") self.assertIsNone(a.version) @@ -236,7 +236,7 @@ def test_lower(self): def test_set(self): a = URN("urn:cts:latinLit:phi1294.phi002.perseus-lat2") - a.reference = Reference("1.1") + a.reference = CtsReference("1.1") self.assertEqual(str(a), "urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1") a.reference = "2.2" self.assertEqual(str(a), "urn:cts:latinLit:phi1294.phi002.perseus-lat2:2.2") @@ -369,8 +369,8 @@ def test_fill(self): scope="/TEI/text/body/div/div[@n=\"?\"]", xpath="//l[@n=\"?\"]" ) - self.assertEqual(c.fill(Reference("1.2")), "/TEI/text/body/div/div[@n='1']//l[@n='2']") - self.assertEqual(c.fill(Reference("1.1")), "/TEI/text/body/div/div[@n='1']//l[@n='1']") + self.assertEqual(c.fill(CtsReference("1.2")), "/TEI/text/body/div/div[@n='1']//l[@n='2']") + self.assertEqual(c.fill(CtsReference("1.1")), "/TEI/text/body/div/div[@n='1']//l[@n='1']") self.assertEqual(c.fill(None), "/TEI/text/body/div/div[@n]//l[@n]") self.assertEqual(c.fill("1", xpath=True), "//l[@n='1']") self.assertEqual(c.fill("2", xpath=True), "//l[@n='2']") diff --git a/tests/resolvers/cts/test_local.py b/tests/resolvers/cts/test_local.py index 9e7a23bb..eacef312 100644 --- a/tests/resolvers/cts/test_local.py +++ b/tests/resolvers/cts/test_local.py @@ -4,7 +4,7 @@ from MyCapytain.resolvers.cts.local import CtsCapitainsLocalResolver from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes, RDF_NAMESPACES, get_graph -from MyCapytain.common.reference._capitains_cts import Reference, URN +from MyCapytain.common.reference._capitains_cts import CtsReference, URN from MyCapytain.errors import InvalidURN, UnknownObjectError, UndispatchedTextError from MyCapytain.resources.prototypes.metadata import Collection from MyCapytain.resources.collections.cts import XmlCtsTextInventoryMetadata @@ -46,7 +46,7 @@ def test_text_resource(self): "Object has a citation property of length 4" ) self.assertEqual( - text.getTextualNode(Reference("1.1.1.1")).export(output=Mimetypes.PLAINTEXT), + text.getTextualNode(CtsReference("1.1.1.1")).export(output=Mimetypes.PLAINTEXT), "Ho ! Saki, pass around and offer the bowl (of love for God) : ### ", "It should be possible to retrieve text" ) diff --git a/tests/resources/texts/base/test_tei.py b/tests/resources/texts/base/test_tei.py index 9cd4d899..e7d1d765 100644 --- a/tests/resources/texts/base/test_tei.py +++ b/tests/resources/texts/base/test_tei.py @@ -3,7 +3,7 @@ import unittest -from MyCapytain.common.reference._capitains_cts import Reference, Citation +from MyCapytain.common.reference._capitains_cts import CtsReference, Citation from MyCapytain.resources.texts.base.tei import TEIResource from MyCapytain.common.constants import Mimetypes from MyCapytain.common.utils import xmlparser @@ -65,7 +65,7 @@ def test_ingest_multiple(self): """This pointer pattern extracts line""" ) self.assertEqual( - a.child.child.fill(Reference("1.2.3")), + a.child.child.fill(CtsReference("1.2.3")), "/tei:TEI/tei:text/tei:body/tei:div/tei:div[@n=\'1\' and @type=\'section\']/tei:div[@n=\'2\']/tei:l[@n=\'3\']" ) diff --git a/tests/resources/texts/local/commonTests.py b/tests/resources/texts/local/commonTests.py index 6d415840..6654fe21 100644 --- a/tests/resources/texts/local/commonTests.py +++ b/tests/resources/texts/local/commonTests.py @@ -10,7 +10,7 @@ import MyCapytain.errors from MyCapytain.common.constants import Mimetypes -from MyCapytain.common.reference._capitains_cts import Reference, URN, Citation +from MyCapytain.common.reference._capitains_cts import CtsReference, URN, Citation from MyCapytain.resources.texts.local.capitains.cts import CapitainsCtsText @@ -122,32 +122,32 @@ def testValidReffs(self): # Test with reference and level self.assertEqual( - str(self.TEI.getValidReff(reference=Reference("2.1"), level=3)[1]), + str(self.TEI.getValidReff(reference=CtsReference("2.1"), level=3)[1]), "2.1.2" ) self.assertEqual( - str(self.TEI.getValidReff(reference=Reference("2.1"), level=3)[-1]), + str(self.TEI.getValidReff(reference=CtsReference("2.1"), level=3)[-1]), "2.1.12" ) self.assertEqual( - self.TEI.getValidReff(reference=Reference("2.38-2.39"), level=3), + self.TEI.getValidReff(reference=CtsReference("2.38-2.39"), level=3), ["2.38.1", "2.38.2", "2.39.1", "2.39.2"] ) # Test with reference and level autocorrected because too small self.assertEqual( - str(self.TEI.getValidReff(reference=Reference("2.1"), level=0)[-1]), + str(self.TEI.getValidReff(reference=CtsReference("2.1"), level=0)[-1]), "2.1.12", "Level should be autocorrected to len(citation) + 1" ) self.assertEqual( - str(self.TEI.getValidReff(reference=Reference("2.1"), level=2)[-1]), + str(self.TEI.getValidReff(reference=CtsReference("2.1"), level=2)[-1]), "2.1.12", "Level should be autocorrected to len(citation) + 1 even if level == len(citation)" ) self.assertEqual( - self.TEI.getValidReff(reference=Reference("2.1-2.2")), + self.TEI.getValidReff(reference=CtsReference("2.1-2.2")), [ '2.1.1', '2.1.2', '2.1.3', '2.1.4', '2.1.5', '2.1.6', '2.1.7', '2.1.8', '2.1.9', '2.1.10', '2.1.11', '2.1.12', '2.2.1', '2.2.2', '2.2.3', '2.2.4', '2.2.5', '2.2.6' @@ -155,31 +155,31 @@ def testValidReffs(self): "It could be possible to ask for range reffs children") self.assertEqual( - self.TEI.getValidReff(reference=Reference("2.1-2.2"), level=2), + self.TEI.getValidReff(reference=CtsReference("2.1-2.2"), level=2), ['2.1', '2.2'], "It could be possible to ask for range References reference at the same level in between milestone") self.assertEqual( - self.TEI.getValidReff(reference=Reference("1.38-2.2"), level=2), + self.TEI.getValidReff(reference=CtsReference("1.38-2.2"), level=2), ['1.38', '1.39', '2.pr', '2.1', '2.2'], "It could be possible to ask for range References reference at the same level in between milestone " "across higher levels") self.assertEqual( - self.TEI.getValidReff(reference=Reference("1.1.1-1.1.4"), level=3), + self.TEI.getValidReff(reference=CtsReference("1.1.1-1.1.4"), level=3), ['1.1.1', '1.1.2', '1.1.3', '1.1.4'], "It could be possible to ask for range reffs in between at the same level cross higher level") # Test when already too deep self.assertEqual( - self.TEI.getValidReff(reference=Reference("2.1.1"), level=3), + self.TEI.getValidReff(reference=CtsReference("2.1.1"), level=3), [], "Asking for a level too deep should return nothing" ) # Test wrong citation with self.assertRaises(KeyError): - self.TEI.getValidReff(reference=Reference("2.hellno"), level=3) + self.TEI.getValidReff(reference=CtsReference("2.hellno"), level=3) def test_nested_dict(self): """ Check the nested dict export of a local.CtsTextMetadata object """ @@ -294,12 +294,12 @@ def test_get_passage(self, simple): a = self.TEI.getTextualNode(["1", "pr", "2"], simple=simple) self.assertEqual(a.export(output=Mimetypes.PLAINTEXT), "tum, ut de illis queri non possit quisquis de se bene ") # With reference - a = self.TEI.getTextualNode(Reference("2.5.5"), simple=simple) + a = self.TEI.getTextualNode(CtsReference("2.5.5"), simple=simple) self.assertEqual(a.export(output=Mimetypes.PLAINTEXT), "Saepe domi non es, cum sis quoque, saepe negaris: ") @call_with_simple def test_get_passage_autoparse(self, simple): - a = self.TEI.getTextualNode(Reference("2.5.5"), simple=simple) + a = self.TEI.getTextualNode(CtsReference("2.5.5"), simple=simple) self.assertEqual( a.export(output=Mimetypes.PLAINTEXT), "Saepe domi non es, cum sis quoque, saepe negaris: ", "CtsTextMetadata are automatically parsed in GetPassage hypercontext = False" @@ -307,27 +307,27 @@ def test_get_passage_autoparse(self, simple): def test_get_Passage_context_no_double_slash(self): """ Check that get CapitainsCtsPassage contexts return right information """ - simple = self.TEI.getTextualNode(Reference("1.pr.2")) + simple = self.TEI.getTextualNode(CtsReference("1.pr.2")) str_simple = simple.tostring(encoding=str) text = CapitainsCtsText( resource=str_simple, citation=self.TEI.citation ) self.assertEqual( - text.getTextualNode(Reference("1.pr.2"), simple=True).export( + text.getTextualNode(CtsReference("1.pr.2"), simple=True).export( output=Mimetypes.PLAINTEXT).strip(), "tum, ut de illis queri non possit quisquis de se bene", "Ensure passage finding with context is fully TEI / Capitains compliant (One reference CapitainsCtsPassage)" ) - simple = self.TEI.getTextualNode(Reference("1.pr.2-1.pr.7")) + simple = self.TEI.getTextualNode(CtsReference("1.pr.2-1.pr.7")) str_simple = simple.tostring(encoding=str) text = CapitainsCtsText( resource=str_simple, citation=self.TEI.citation ) self.assertEqual( - text.getTextualNode(Reference("1.pr.2"), simple=True).export( + text.getTextualNode(CtsReference("1.pr.2"), simple=True).export( output=Mimetypes.PLAINTEXT ).strip(), "tum, ut de illis queri non possit quisquis de se bene", @@ -335,7 +335,7 @@ def test_get_Passage_context_no_double_slash(self): "parent range CapitainsCtsPassage)" ) self.assertEqual( - text.getTextualNode(Reference("1.pr.3"), simple=True).export( + text.getTextualNode(CtsReference("1.pr.3"), simple=True).export( output=Mimetypes.PLAINTEXT).strip(), "senserit, cum salva infimarum quoque personarum re-", "Ensure passage finding with context is fully TEI / Capitains compliant (Same level same " @@ -348,20 +348,20 @@ def test_get_Passage_context_no_double_slash(self): "parent range CapitainsCtsPassage)" ) - simple = self.TEI.getTextualNode(Reference("1.pr.2-1.1.6")) + simple = self.TEI.getTextualNode(CtsReference("1.pr.2-1.1.6")) str_simple = simple.tostring(encoding=str) text = CapitainsCtsText( resource=str_simple, citation=self.TEI.citation ) self.assertEqual( - text.getTextualNode(Reference("1.pr.2"), simple=True).export( + text.getTextualNode(CtsReference("1.pr.2"), simple=True).export( output=Mimetypes.PLAINTEXT).strip(), "tum, ut de illis queri non possit quisquis de se bene", "Ensure passage finding with context is fully TEI / Capitains compliant (Same level range CapitainsCtsPassage)" ) self.assertEqual( - text.getTextualNode(Reference("1.1.6"), simple=True).export( + text.getTextualNode(CtsReference("1.1.6"), simple=True).export( output=Mimetypes.PLAINTEXT).strip(), "Rari post cineres habent poetae.", "Ensure passage finding with context is fully TEI / Capitains compliant (Same level range CapitainsCtsPassage)" @@ -378,20 +378,20 @@ def test_get_Passage_context_no_double_slash(self): "Ensure passage finding with context is fully TEI / Capitains compliant (Same level range CapitainsCtsPassage)" ) - simple = self.TEI.getTextualNode(Reference("1.pr.2-1.2")) + simple = self.TEI.getTextualNode(CtsReference("1.pr.2-1.2")) str_simple = simple.tostring(encoding=str) text = CapitainsCtsText( resource=str_simple, citation=self.TEI.citation ) self.assertEqual( - text.getTextualNode(Reference("1.pr.2"), simple=True).export( + text.getTextualNode(CtsReference("1.pr.2"), simple=True).export( output=Mimetypes.PLAINTEXT).strip(), "tum, ut de illis queri non possit quisquis de se bene", "Ensure passage finding with context is fully TEI / Capitains compliant (Different level range CapitainsCtsPassage)" ) self.assertEqual( - text.getTextualNode(Reference("1.1.6"), simple=True).export( + text.getTextualNode(CtsReference("1.1.6"), simple=True).export( output=Mimetypes.PLAINTEXT).strip(), "Rari post cineres habent poetae.", "Ensure passage finding with context is fully TEI / Capitains compliant (Different level range CapitainsCtsPassage)" @@ -436,14 +436,14 @@ def test_citation_length_error(self, simple): def test_ensure_passage_is_not_removed(self): """ In range, passage in between could be removed from the original text by error """ - self.TEI.getTextualNode(Reference("1.pr.1-1.2.5")) + self.TEI.getTextualNode(CtsReference("1.pr.1-1.2.5")) orig_refs = self.TEI.getValidReff(level=3) self.assertIn("1.pr.1", orig_refs) self.assertIn("1.1.1", orig_refs) self.assertIn("1.2.4", orig_refs) self.assertIn("1.2.5", orig_refs) - self.TEI.getTextualNode(Reference("1.pr-1.2")) + self.TEI.getTextualNode(CtsReference("1.pr-1.2")) orig_refs = self.TEI.getValidReff(level=3) self.assertIn("1.pr.1", orig_refs) self.assertIn("1.1.1", orig_refs) @@ -451,7 +451,7 @@ def test_ensure_passage_is_not_removed(self): self.assertIn("1.2.5", orig_refs) def test_get_passage_hypercontext_complex_xpath(self): - simple = self.text_complex.getTextualNode(Reference("pr.1-1.2")) + simple = self.text_complex.getTextualNode(CtsReference("pr.1-1.2")) str_simple = simple.tostring(encoding=str) text = CapitainsCtsText( resource=str_simple, @@ -459,13 +459,13 @@ def test_get_passage_hypercontext_complex_xpath(self): ) self.assertIn( "Pervincis tandem", - text.getTextualNode(Reference("pr.1"), simple=True).export( + text.getTextualNode(CtsReference("pr.1"), simple=True).export( output=Mimetypes.PLAINTEXT, exclude=["tei:note"]).strip(), "Ensure passage finding with context is fully TEI / Capitains compliant (Different level range CapitainsCtsPassage)" ) self.assertEqual( - text.getTextualNode(Reference("1.2"), simple=True).export( + text.getTextualNode(CtsReference("1.2"), simple=True).export( output=Mimetypes.PLAINTEXT).strip(), "lusimus quos in Suebae gratiam virgunculae,", "Ensure passage finding with context is fully TEI / Capitains compliant (Different level range CapitainsCtsPassage)" @@ -480,7 +480,7 @@ def test_get_passage_hypercontext_complex_xpath(self): @call_with_simple def test_Text_text_function(self, simple): - simple = self.seneca.getTextualNode(Reference("1"), simple=simple) + simple = self.seneca.getTextualNode(CtsReference("1"), simple=simple) str_simple = simple.tostring(encoding=str) text = CapitainsCtsText( resource=str_simple, @@ -493,7 +493,7 @@ def test_Text_text_function(self, simple): ) def test_get_passage_hyper_context_double_slash_xpath(self): - simple = self.seneca.getTextualNode(Reference("1-10")) + simple = self.seneca.getTextualNode(CtsReference("1-10")) str_simple = simple.export( output=Mimetypes.XML.Std ) @@ -502,7 +502,7 @@ def test_get_passage_hyper_context_double_slash_xpath(self): citation=self.seneca.citation ) self.assertEqual( - text.getTextualNode(Reference("1"), simple=True).export( + text.getTextualNode(CtsReference("1"), simple=True).export( output=Mimetypes.PLAINTEXT, exclude=["tei:note"] ).strip(), @@ -510,7 +510,7 @@ def test_get_passage_hyper_context_double_slash_xpath(self): "Ensure passage finding with context is fully TEI / Capitains compliant (Different level range CapitainsCtsPassage)" ) self.assertEqual( - text.getTextualNode(Reference("10"), simple=True).export( + text.getTextualNode(CtsReference("10"), simple=True).export( output=Mimetypes.PLAINTEXT ).strip(), "aversa superis regna manesque impios", @@ -522,14 +522,14 @@ def test_get_passage_hyper_context_double_slash_xpath(self): "Ensure passage finding with context is fully TEI / Capitains compliant (Different level range CapitainsCtsPassage)" ) - simple = self.seneca.getTextualNode(Reference("1")) + simple = self.seneca.getTextualNode(CtsReference("1")) str_simple = simple.tostring(encoding=str) text = CapitainsCtsText( resource=str_simple, citation=self.seneca.citation ) self.assertEqual( - text.getTextualNode(Reference("1"), simple=True).export( + text.getTextualNode(CtsReference("1"), simple=True).export( output=Mimetypes.PLAINTEXT, exclude=["tei:note"] ).strip(), @@ -697,28 +697,28 @@ def test_siblingsId(self, simple): # Ranges if simple is False: # Start - p, n = self.FULL_EPIGRAMMATA.getTextualNode(Reference("1-2"), simple=simple).siblingsId + p, n = self.FULL_EPIGRAMMATA.getTextualNode(CtsReference("1-2"), simple=simple).siblingsId self.assertEqual((None, "3-4"), (p, str(n)), "First node should have right siblings") - p, n = self.FULL_EPIGRAMMATA.getTextualNode(Reference("1-5"), simple=simple).siblingsId + p, n = self.FULL_EPIGRAMMATA.getTextualNode(CtsReference("1-5"), simple=simple).siblingsId self.assertEqual((None, "6-10"), (p, str(n)), "First node should have right siblings") - p, n = self.FULL_EPIGRAMMATA.getTextualNode(Reference("1-9"), simple=simple).siblingsId + p, n = self.FULL_EPIGRAMMATA.getTextualNode(CtsReference("1-9"), simple=simple).siblingsId self.assertEqual((None, "10-14"), (p, str(n)), "First node should have right siblings") # End - p, n = self.FULL_EPIGRAMMATA.getTextualNode(Reference("12-14"), simple=simple).siblingsId + p, n = self.FULL_EPIGRAMMATA.getTextualNode(CtsReference("12-14"), simple=simple).siblingsId self.assertEqual(("9-11", None), (str(p), n), "Last node should have right siblings") - p, n = self.FULL_EPIGRAMMATA.getTextualNode(Reference("11-14"), simple=simple).siblingsId + p, n = self.FULL_EPIGRAMMATA.getTextualNode(CtsReference("11-14"), simple=simple).siblingsId self.assertEqual(("7-10", None), (str(p), n), "Last node should have right siblings") - p, n = self.FULL_EPIGRAMMATA.getTextualNode(Reference("5-14"), simple=simple).siblingsId + p, n = self.FULL_EPIGRAMMATA.getTextualNode(CtsReference("5-14"), simple=simple).siblingsId self.assertEqual(("1-4", None), (str(p), n), "Should take the rest") # Middle - p, n = self.FULL_EPIGRAMMATA.getTextualNode(Reference("5-6"), simple=simple).siblingsId + p, n = self.FULL_EPIGRAMMATA.getTextualNode(CtsReference("5-6"), simple=simple).siblingsId self.assertEqual(("3-4", "7-8"), (str(p), str(n)), "Middle node should have right siblings") - p, n = self.FULL_EPIGRAMMATA.getTextualNode(Reference("5-8"), simple=simple).siblingsId + p, n = self.FULL_EPIGRAMMATA.getTextualNode(CtsReference("5-8"), simple=simple).siblingsId self.assertEqual(("1-4", "9-12"), (str(p), str(n)), "Middle node should have right siblings") - p, n = self.FULL_EPIGRAMMATA.getTextualNode(Reference("5-10"), simple=simple).siblingsId + p, n = self.FULL_EPIGRAMMATA.getTextualNode(CtsReference("5-10"), simple=simple).siblingsId self.assertEqual(("1-4", "11-14"), (str(p), str(n)), "Middle node should have right siblings") # NONE ! - p, n = self.FULL_EPIGRAMMATA.getTextualNode(Reference("1-14"), simple=simple).siblingsId + p, n = self.FULL_EPIGRAMMATA.getTextualNode(CtsReference("1-14"), simple=simple).siblingsId self.assertEqual((None, None), (p, n), "If whole range, nothing !") diff --git a/tests/resources/texts/local/test_capitains_xml_default.py b/tests/resources/texts/local/test_capitains_xml_default.py index 0a10cb0e..63dabfb7 100644 --- a/tests/resources/texts/local/test_capitains_xml_default.py +++ b/tests/resources/texts/local/test_capitains_xml_default.py @@ -90,4 +90,4 @@ def setUp(self): self.text = MyCapytain.resources.texts.local.capitains.cts.CapitainsCtsText( resource=text, urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2" ) - self.passage = self.text.getTextualNode(MyCapytain.common.reference._capitains_cts.Reference("1.pr.2-1.pr.7")) + self.passage = self.text.getTextualNode(MyCapytain.common.reference._capitains_cts.CtsReference("1.pr.2-1.pr.7")) diff --git a/tests/resources/texts/local/test_capitains_xml_notObjectified.py b/tests/resources/texts/local/test_capitains_xml_notObjectified.py index 0314a716..c79b5423 100644 --- a/tests/resources/texts/local/test_capitains_xml_notObjectified.py +++ b/tests/resources/texts/local/test_capitains_xml_notObjectified.py @@ -99,4 +99,4 @@ def setUp(self): self.text = MyCapytain.resources.texts.local.capitains.cts.CapitainsCtsText( resource=objectifiedParser(text), urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2" ) - self.passage = self.text.getTextualNode(MyCapytain.common.reference._capitains_cts.Reference("1.pr.2-1.pr.7")) \ No newline at end of file + self.passage = self.text.getTextualNode(MyCapytain.common.reference._capitains_cts.CtsReference("1.pr.2-1.pr.7")) \ No newline at end of file diff --git a/tests/resources/texts/remote/test_cts.py b/tests/resources/texts/remote/test_cts.py index f2b7689a..e21c8b1d 100644 --- a/tests/resources/texts/remote/test_cts.py +++ b/tests/resources/texts/remote/test_cts.py @@ -7,7 +7,7 @@ from MyCapytain.resources.texts.remote.cts import CtsPassage, CtsText from MyCapytain.retrievers.cts5 import HttpCtsRetriever -from MyCapytain.common.reference._capitains_cts import Reference, URN, Citation +from MyCapytain.common.reference._capitains_cts import CtsReference, URN, Citation from MyCapytain.common.metadata import Metadata from MyCapytain.common.utils import xmlparser from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes, RDF_NAMESPACES @@ -115,7 +115,7 @@ def test_getvalidreff(self, requests): ) # Test with a ref as subreference - reffs = text.getValidReff(reference=Reference("1.pr")) + reffs = text.getValidReff(reference=CtsReference("1.pr")) requests.assert_called_with( "http://services.perseids.org/remote/cts", params={ @@ -154,7 +154,7 @@ def test_getpassage_variabletypes(self, requests): requests.return_value.text = GET_PASSAGE # Test with -1 - _ = text.getTextualNode(subreference=Reference("1.1")) + _ = text.getTextualNode(subreference=CtsReference("1.1")) requests.assert_called_with( "http://services.perseids.org/remote/cts", params={ From 6f0c245561d7dd3c5fdd005f158f780265c9f1b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Thu, 30 Aug 2018 15:35:45 +0200 Subject: [PATCH 39/89] (CtsReferenceSet/CtsReference) Breaking change and massive rework of getReffs and getSiblings functions These changes are related to the needs behind MyCapytain adoption of upcoming DTS. Reuse of native object were prefered. - Introduction of `Reference` and `ReferenceSet` - `Reference` is a derivation tuple that have two properties (`start`, `end`) and one method `is_range()` - `start` and `end` are string or string-derivation - `ReferenceSet` is a list derivative and contains References - It has a `.citation` property about the structure of the current set - (Breaking) Resolver Prototype and derivation returns ReferenceSet objects now in `getReffs()` - (cts.Reference) Renamed CtsReference. - Parsing of sub-information is now on the fly on avoid performance hit with the move to ReferenceSet in `.getReffs()` - **Warning** : `len(CtsReference("1.1"))` should be replaced with `CtsReference("1.1").depth` - Supporting this new change, CtsReference is now a tuple. It still has a special `str(ref)` behavior as CTS URN do work with range-identifiers - `CtsReference.start` and `CtsReference.end` are now `CtsSinglePassageId` - (cts.Reference) Introduction of CtsSinglePassageId - `CtsSinglePassageId` is derivation of str - It has a `list` property to support former syntax : `CtsReference("1.1").start.list` and returns the same type - It has a `depth` and `len` which are equivalent in this specific case - `subreference` is now parsed on the fly as `CtsWordReference` - (cts.Reference) Introduction of `CtsWordReference` - Has `.word` and `.counter` properties for `@Achilles[1]` - Update examples - Update documentation --- MyCapytain/common/reference/__init__.py | 2 +- MyCapytain/common/reference/_base.py | 19 +++- MyCapytain/common/reference/_capitains_cts.py | 100 +++++++++--------- MyCapytain/resolvers/prototypes.py | 2 +- MyCapytain/resources/texts/base/tei.py | 2 +- .../resources/texts/local/capitains/cts.py | 39 ++++--- MyCapytain/resources/texts/remote/cts.py | 16 +-- MyCapytain/retrievers/cts5.py | 4 +- .../test_reference/test_capitains_cts.py | 23 ++-- tests/resolvers/cts/test_local.py | 34 +++--- tests/resources/texts/local/commonTests.py | 40 +++---- 11 files changed, 150 insertions(+), 131 deletions(-) diff --git a/MyCapytain/common/reference/__init__.py b/MyCapytain/common/reference/__init__.py index bb64d7be..34134795 100644 --- a/MyCapytain/common/reference/__init__.py +++ b/MyCapytain/common/reference/__init__.py @@ -7,5 +7,5 @@ """ from ._base import NodeId, BaseCitationSet, BaseReference, BaseReferenceSet -from ._capitains_cts import Citation, CtsReference, URN +from ._capitains_cts import Citation, CtsReference, CtsReferenceSet, URN from ._dts_1 import DtsCitation, DtsCitationSet diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index e70c63f1..062c2d86 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -37,7 +37,7 @@ def citation(self): return self._citation def __new__(cls, *refs, citation=None): - obj = list.__new__(BaseReferenceSet, refs) + obj = list.__new__(cls, refs) obj._citation = None if citation: @@ -130,7 +130,7 @@ def depth(self): :return: Depth of the citation scheme """ if len(self.children): - return 1 + max([child.depth for child in self.children]) + return max([child.depth for child in self.children]) else: return 0 @@ -321,6 +321,21 @@ def __export__(self, output=None, context=False, namespace_manager=None, **kwarg return _out + @property + def depth(self): + """ Depth of the citation scheme + + .. example:: If we have a Book, Poem, Line system, and the citation we are looking at is Poem, depth is 1 + + + :rtype: int + :return: Depth of the citation scheme + """ + if len(self.children): + return 1 + max([child.depth for child in self.children]) + else: + return 1 + class NodeId(object): """ Collection of directional references for a Tree diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index 3ad617fb..df62b978 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -1,12 +1,12 @@ import re from copy import copy -from typing import List +from typing import Optional, List, Union from lxml.etree import _Element from MyCapytain.common.constants import Mimetypes, get_graph, RDF_NAMESPACES, XPATH_NAMESPACES from MyCapytain.common.utils import make_xml_node -from ._base import BaseCitation, BaseReference +from ._base import BaseCitation, BaseReference, BaseReferenceSet REFSDECL_SPLITTER = re.compile(r"/+[*()|\sa-zA-Z0-9:\[\]@=\\{$'\".\s]+") REFSDECL_REPLACER = re.compile(r"\$[0-9]+") @@ -27,18 +27,19 @@ def __childOrNone__(liste): class CtsWordReference(str): - def __new__(cls, word_reference): + def __new__(cls, word_reference: str): word, counter = tuple(SUBREFERENCE.findall(word_reference)[0]) - if len(counter) and word: - word, counter = str(word), int(counter) - elif len(counter) == 0 and word: - word, counter = str(word), 0 + if counter: + counter = int(counter) + else: + counter = 0 obj = str.__new__(cls, "@"+word_reference) obj.counter = counter obj.word = word - return word + + return obj def tuple(self): return self.word, self.counter @@ -58,30 +59,34 @@ def __new__(cls, str_repr: str): # Parsing the reference temp_str_repr = str_repr subreference = temp_str_repr.split("@") + if len(subreference) == 2: obj._sub_reference = CtsWordReference(subreference[1]) temp_str_repr = subreference[0] - obj._list = temp_str_repr.split(".") + obj._list = temp_str_repr return obj @property - def list(self): - return self._list + def list(self) -> List[str]: + return list(iter(self)) @property - def subreference(self): - return self._sub_reference + def subreference(self) -> Optional[CtsWordReference]: + subref = self.split("@") + if len(subref) == 2: + return CtsWordReference(subref[1]) - def __iter__(self): - return iter(self.list) + def __iter__(self) -> List[str]: + subref = self.split("@")[0] + yield from subref.split(".") - def __len__(self): - return len(self.list) + def __len__(self) -> int: + return self.count(".") + 1 @property - def depth(self): - return len(self.list) + def depth(self) -> int: + return self.count(".") + 1 class CtsReference(BaseReference): @@ -125,26 +130,29 @@ def parent(self): :rtype: CtsReference """ - if len(self.parsed[0][1]) == 1 and len(self.parsed[1][1]) <= 1: + if self.start.depth == 1 and (self.end is None or self.end.depth <= 1): return None else: - if len(self.parsed[0][1]) > 1 and len(self.parsed[1][1]) == 0: + if self.start.depth > 1 and (self.end is None or self.end.depth == 0): return CtsReference("{0}{1}".format( - ".".join(list(self.parsed[0][1])[0:-1]), - self.parsed[0][3] or "" + ".".join(self.start.list[:-1]), + self.start.subreference or "" )) - elif len(self.parsed[0][1]) > 1 and len(self.parsed[1][1]) > 1: - first = list(self.parsed[0][1])[0:-1] - last = list(self.parsed[1][1])[0:-1] - if first == last and self.parsed[1][3] is None \ - and self.parsed[0][3] is None: - return CtsReference(".".join(first)) + elif self.start.depth > 1 and self.end is not None and self.end.depth > 1: + _start = self.start.list[0:-1] + _end = self.end.list[0:-1] + if _start == _end and \ + self.start.subreference is None and \ + self.end.subreference is None: + return CtsReference( + ".".join(_start) + ) else: return CtsReference("{0}{1}-{2}{3}".format( - ".".join(first), - self.parsed[0][3] or "", - ".".join(list(self.parsed[1][1])[0:-1]), - self.parsed[1][3] or "" + ".".join(_start), + self.start.subreference or "", + ".".join(_end), + self.end.subreference or "" )) @property @@ -195,6 +203,7 @@ def subreference(self): if not self.end: return self.start.subreference + @property def depth(self): """ Return depth of highest reference level @@ -225,22 +234,17 @@ def __str__(self): """ return self._str_repr - def __eq__(self, other): - """ Equality checker for Reference object - :param other: An object to be checked against - :rtype: boolean - :returns: Equality between other and self +class CtsReferenceSet(BaseReferenceSet): + def __contains__(self, item): + return BaseReferenceSet.__contains__(self, item) or \ + CtsReference(item) - :Example: - >>> a = CtsReference(reference="1.1@Achiles[1]-1.2@Zeus[1]") - >>> b = CtsReference(reference="1.1") - >>> c = CtsReference(reference="1.1") - >>> (a == b) == False - >>> (c == b) == True - """ - return (isinstance(other, type(self)) - and str(self) == str(other)) + def index(self, obj: Union[str, CtsReference], *args, **kwargs) -> int: + _o = obj + if not isinstance(obj, CtsReference): + _o = CtsReference(obj) + return super(CtsReferenceSet, self).index(_o) class URN(object): @@ -768,7 +772,7 @@ def match(self, passageId): passageId = CtsReference(passageId) if self.is_root(): - return self[len(passageId)-1] + return self[passageId.depth-1] return self.root.match(passageId) def fill(self, passage=None, xpath=None): diff --git a/MyCapytain/resolvers/prototypes.py b/MyCapytain/resolvers/prototypes.py index 0721c922..5de8da0e 100644 --- a/MyCapytain/resolvers/prototypes.py +++ b/MyCapytain/resolvers/prototypes.py @@ -52,7 +52,7 @@ def getTextualNode( """ raise NotImplementedError() - def getSiblings(self, textId: str, subreference: Union[str, BaseReference]) -> Tuple[str, str]: + def getSiblings(self, textId: str, subreference: Union[str, BaseReference]) -> Tuple[BaseReference, BaseReference]: """ Retrieve the siblings of a textual node :param textId: CtsTextMetadata Identifier diff --git a/MyCapytain/resources/texts/base/tei.py b/MyCapytain/resources/texts/base/tei.py index 0cfcdf2e..ae476111 100644 --- a/MyCapytain/resources/texts/base/tei.py +++ b/MyCapytain/resources/texts/base/tei.py @@ -102,7 +102,7 @@ def __export__(self, output=Mimetypes.PLAINTEXT, exclude=None, _preformatted=Fal reffs = self.getReffs(level=len(self.citation)) text = nested_ordered_dictionary() for reff in reffs: - _r = reff.split(".") + _r = str(reff).split(".") # Only works for non range of course nested_set(text, _r, self.getTextualNode(_r).export( Mimetypes.PLAINTEXT, exclude=exclude, diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index afe712ab..9513da0e 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -14,7 +14,7 @@ from MyCapytain.errors import DuplicateReference, MissingAttribute, RefsDeclError, EmptyReference, CitationDepthError, MissingRefsDecl from MyCapytain.common.utils import copyNode, passageLoop, normalizeXpath from MyCapytain.common.constants import XPATH_NAMESPACES, RDF_NAMESPACES -from MyCapytain.common.reference._capitains_cts import CtsReference, URN, Citation +from MyCapytain.common.reference import CtsReference, URN, Citation, CtsReferenceSet from MyCapytain.resources.prototypes import text from MyCapytain.resources.texts.base.tei import TEIResource @@ -68,7 +68,7 @@ def getTextualNode(self, subreference=None, simple=False): else: start, end = subreference.start.list, subreference.end.list - if len(start) > len(self.citation): + if len(start) > self.citation.root.depth: raise CitationDepthError("URN is deeper than citation scheme") if simple is True: @@ -120,7 +120,7 @@ def _getSimplePassage(self, reference=None): ) resource = self.resource.xpath( - self.citation[reference.depth()-1].fill(reference), + self.citation[reference.depth-1].fill(reference), namespaces=XPATH_NAMESPACES ) @@ -147,7 +147,7 @@ def textObject(self): text = self return text - def getReffs(self, level=1, subreference=None): + def getReffs(self, level=1, subreference=None) -> CtsReferenceSet: """ CtsReference available at a given level :param level: Depth required. If not set, should retrieve first encountered level (1 based) @@ -166,7 +166,7 @@ def getReffs(self, level=1, subreference=None): subreference = CtsReference(subreference) return self.getValidReff(level, subreference) - def getValidReff(self, level=None, reference=None, _debug=False): + def getValidReff(self, level: int=None, reference: CtsReference=None, _debug: bool=False) -> CtsReferenceSet: """ Retrieve valid passages directly :param level: Depth required. If not set, should retrieve first encountered level (1 based) @@ -176,7 +176,6 @@ def getValidReff(self, level=None, reference=None, _debug=False): :param _debug: Check on passages duplicates :type _debug: bool :returns: List of levels - :rtype: list(basestring, str) .. note:: GetValidReff works for now as a loop using CapitainsCtsPassage, subinstances of CtsTextMetadata, to retrieve the valid \ informations. Maybe something is more powerfull ? @@ -213,7 +212,7 @@ def getValidReff(self, level=None, reference=None, _debug=False): if level <= len(passages[0]) and reference is not None: level = len(passages[0]) + 1 if level > len(self.citation): - return [] + return CtsReferenceSet() nodes = [None] * (level - depth) @@ -256,7 +255,7 @@ def getValidReff(self, level=None, reference=None, _debug=False): print(empties) warnings.warn(message, EmptyReference) - return passages + return CtsReferenceSet([CtsReference(reff) for reff in passages]) def xpath(self, *args, **kwargs): """ Perform XPath on the passage XML @@ -305,7 +304,7 @@ def __init__(self, resource, reference, citation, text, urn=None): self.__children__ = None self.__depth__ = 0 if reference is not None: - self.__depth__ = len(reference) + self.__depth__ = reference.depth self.__prevnext__ = None @property @@ -391,29 +390,29 @@ def siblingsId(self): if self.__prevnext__ is not None: return self.__prevnext__ - document_references = list(map(str, self.__text__.getReffs(level=self.depth))) + document_references = self.__text__.getReffs(level=self.depth) range_length = 1 if self.reference.end is not None: range_length = len(self.getReffs()) - start = document_references.index(str(self.reference.start)) + start = document_references.index(self.reference.start) if start == 0: # If the passage is already at the beginning _prev = None elif start - range_length < 0: - _prev = CtsReference(document_references[0]) + _prev = document_references[0] else: - _prev = CtsReference(document_references[start - 1]) + _prev = document_references[start - 1] if start + 1 == len(document_references): # If the passage is already at the end _next = None elif start + range_length > len(document_references): - _next = CtsReference(document_references[-1]) + _next = document_references[-1] else: - _next = CtsReference(document_references[start + 1]) + _next = document_references[start + 1] self.__prevnext__ = (_prev, _next) return self.__prevnext__ @@ -533,10 +532,10 @@ def __init__(self, reference, urn=None, citation=None, resource=None, text=None) self.__children__ = None self.__depth__ = self.__depth_2__ = 1 - if self.reference.start: - self.__depth_2__ = self.__depth__ = len(CtsReference(self.reference.start)) - if self.reference and self.reference.end: - self.__depth_2__ = len(CtsReference(self.reference.end)) + if self.reference and self.reference.start: + self.__depth_2__ = self.__depth__ = self.reference.start.depth + if self.reference.is_range() and self.reference.end: + self.__depth_2__ = self.reference.end.depth self.__prevnext__ = None # For caching purpose @@ -603,7 +602,7 @@ def siblingsId(self): if self.__prevnext__: return self.__prevnext__ - document_references = list(map(str, self.__text__.getReffs(level=self.depth))) + document_references = self.__text__.getReffs(level=self.depth) if self.reference.end: start, end = self.reference.start, self.reference.end diff --git a/MyCapytain/resources/texts/remote/cts.py b/MyCapytain/resources/texts/remote/cts.py index 06054bb1..4dbfaa96 100644 --- a/MyCapytain/resources/texts/remote/cts.py +++ b/MyCapytain/resources/texts/remote/cts.py @@ -20,7 +20,7 @@ from MyCapytain.errors import MissingAttribute -class __SharedMethod__(prototypes.InteractiveTextualNode): +class _SharedMethod(prototypes.InteractiveTextualNode): """ Set of methods shared by CtsTextMetadata and CapitainsCtsPassage :param retriever: Retriever used to retrieve other data @@ -35,10 +35,10 @@ def depth(self): :rtype: int """ if self.urn.reference: - return len(self.urn.reference) + return self.urn.reference.depth def __init__(self, retriever=None, *args, **kwargs): - super(__SharedMethod__, self).__init__(*args, **kwargs) + super(_SharedMethod, self).__init__(*args, **kwargs) self.__retriever__ = retriever self.__first__ = False self.__last__ = False @@ -201,7 +201,7 @@ def getPrevNextUrn(self, reference): :type reference: Union[CtsReference, str] :return: (Previous CapitainsCtsPassage CtsReference,Next CapitainsCtsPassage CtsReference) """ - _prev, _next = __SharedMethod__.prevnext( + _prev, _next = _SharedMethod.prevnext( self.retriever.getPrevNextUrn( urn="{}:{}".format( str( @@ -232,7 +232,7 @@ def getFirstUrn(self, reference=None): ) else: urn = str(self.urn) - _first = __SharedMethod__.firstUrn( + _first = _SharedMethod.firstUrn( self.retriever.getFirstUrn( urn ) @@ -307,7 +307,7 @@ def prevnext(resource): return _prev, _next -class CtsText(__SharedMethod__, prototypes.CitableText): +class CtsText(_SharedMethod, prototypes.CitableText): """ API CtsTextMetadata object :param urn: A URN identifier @@ -372,7 +372,7 @@ def export(self, output=Mimetypes.PLAINTEXT, exclude=None, **kwargs): return self.getTextualNode().export(output, exclude) -class CtsPassage(__SharedMethod__, prototypes.Passage, TEIResource): +class CtsPassage(_SharedMethod, prototypes.Passage, TEIResource): """ CapitainsCtsPassage representing :param urn: @@ -451,7 +451,7 @@ def __parse__(self): self.response = self.resource self.resource = self.resource.xpath("//ti:passage/tei:TEI", namespaces=XPATH_NAMESPACES)[0] - self.__prev__, self.__nextId__ = __SharedMethod__.prevnext(self.response) + self.__prev__, self.__nextId__ = _SharedMethod.prevnext(self.response) if not self.citation.is_set() and len(self.resource.xpath("//ti:citation", namespaces=XPATH_NAMESPACES)): self.citation = CtsCollection.XmlCtsCitation.ingest( diff --git a/MyCapytain/retrievers/cts5.py b/MyCapytain/retrievers/cts5.py index 2634ab07..fcf76be7 100644 --- a/MyCapytain/retrievers/cts5.py +++ b/MyCapytain/retrievers/cts5.py @@ -225,9 +225,9 @@ def getReffs(self, textId, level=1, subreference=None): textId = "{}:{}".format(textId, subreference) if subreference: if isinstance(subreference, CtsReference): - depth += len(subreference) + depth += subreference.depth else: - depth += len(CtsReference(subreference)) + depth += (CtsReference(subreference)).depth if level: level = max(depth, level) return self.getValidReff(urn=textId, level=level) diff --git a/tests/common/test_reference/test_capitains_cts.py b/tests/common/test_reference/test_capitains_cts.py index df85d727..6f997e8e 100644 --- a/tests/common/test_reference/test_capitains_cts.py +++ b/tests/common/test_reference/test_capitains_cts.py @@ -13,11 +13,11 @@ def test_str_function(self): a = CtsReference("1-1") self.assertEqual(str(a), "1-1") - def test_len_ref(self): + def test_depth_ref(self): a = CtsReference("1.1@Achilles[0]-1.10@Atreus[3]") - self.assertEqual(len(a), 2) + self.assertEqual(a.depth, 2) a = CtsReference("1.1.1") - self.assertEqual(len(a), 3) + self.assertEqual(a.depth, 3) def test_highest(self): self.assertEqual( @@ -32,12 +32,12 @@ def test_highest(self): def test_properties(self): a = CtsReference("1.1@Achilles-1.10@Atreus[3]") self.assertEqual(a.start, "1.1@Achilles") - self.assertEqual(CtsReference(a.start).list, ["1", "1"]) - self.assertEqual(CtsReference(a.start).subreference[0], "Achilles") + self.assertEqual(a.start.list, ["1", "1"]) + self.assertEqual(a.start.subreference.word, "Achilles") self.assertEqual(a.end, "1.10@Atreus[3]") - self.assertEqual(CtsReference(a.end).list, ["1", "10"]) - self.assertEqual(CtsReference(a.end).subreference[1], 3) - self.assertEqual(CtsReference(a.end).subreference, ("Atreus", 3)) + self.assertEqual(a.end.list, ["1", "10"]) + self.assertEqual(a.end.subreference.counter, 3) + self.assertEqual(a.end.subreference.tuple(), ("Atreus", 3)) def test_Unicode_Support(self): a = CtsReference("1.1@καὶ[0]-1.10@Ἀλκιβιάδου[3]") @@ -46,8 +46,8 @@ def test_Unicode_Support(self): self.assertEqual(a.start.subreference.word, "καὶ") self.assertEqual(a.end, "1.10@Ἀλκιβιάδου[3]") self.assertEqual(a.end.list, ["1", "10"]) - self.assertEqual(a.end.subreference.word, 3) - self.assertEqual(a.end.subreference.tuple, ("Ἀλκιβιάδου", 3)) + self.assertEqual(a.end.subreference.counter, 3) + self.assertEqual(a.end.subreference.tuple(), ("Ἀλκιβιάδου", 3)) def test_NoWord_Support(self): a = CtsReference("1.1@[0]-1.10@Ἀλκιβιάδου[3]") @@ -79,9 +79,10 @@ def test_get_parent(self): e = CtsReference("1.1@Something[0]-1.2@SomethingElse[2]") f = CtsReference("1-2") - self.assertEqual(str(a.parent), "1") + self.assertEqual(a.parent, CtsReference("1")) self.assertEqual(b.parent, None) self.assertEqual(str(c.parent), "1-2") + self.assertEqual(c.parent, CtsReference("1-2"), "Output should also be CtsReference") self.assertEqual(str(d.parent), "1") self.assertEqual(str(e.parent), "1@Something[0]-1@SomethingElse[2]") self.assertEqual(f.parent, None) diff --git a/tests/resolvers/cts/test_local.py b/tests/resolvers/cts/test_local.py index eacef312..3e734f04 100644 --- a/tests/resolvers/cts/test_local.py +++ b/tests/resolvers/cts/test_local.py @@ -183,11 +183,11 @@ def test_getPassage_full(self): "GetPassage should always return passages objects" ) - children = list(passage.getReffs()) + children = passage.getReffs() # We check the passage is able to perform further requests and is well instantiated self.assertEqual( - children[0], '1', + children[0], CtsReference('1'), "Resource should be string identifiers" ) @@ -231,7 +231,7 @@ def test_getPassage_subreference(self): children = list(passage.getReffs()) self.assertEqual( - children[0], '1.1.1', + str(children[0]), '1.1.1', "Resource should be string identifiers" ) @@ -289,7 +289,7 @@ def test_getPassage_full_metadata(self): children = list(passage.getReffs(level=3)) # We check the passage is able to perform further requests and is well instantiated self.assertEqual( - children[0], '1.pr.1', + children[0], CtsReference('1.pr.1'), "Resource should be string identifiers" ) @@ -319,11 +319,11 @@ def test_getPassage_prevnext(self): "GetPassage should always return passages objects" ) self.assertEqual( - passage.prevId, "1.pr", + passage.prevId, CtsReference("1.pr"), "Previous CapitainsCtsPassage ID should be parsed" ) self.assertEqual( - passage.nextId, "1.2", + passage.nextId, CtsReference("1.2"), "Next CapitainsCtsPassage ID should be parsed" ) @@ -342,7 +342,7 @@ def test_getPassage_prevnext(self): # We check the passage is able to perform further requests and is well instantiated self.assertEqual( - children[0], '1.1.1', + str(children[0]), '1.1.1', "Resource should be string identifiers" ) @@ -392,11 +392,11 @@ def test_getPassage_metadata_prevnext(self): "Local Inventory Files should be parsed and aggregated correctly" ) self.assertEqual( - passage.prevId, "1.pr", + passage.prevId, CtsReference("1.pr"), "Previous CapitainsCtsPassage ID should be parsed" ) self.assertEqual( - passage.nextId, "1.2", + passage.nextId, CtsReference("1.2"), "Next CapitainsCtsPassage ID should be parsed" ) children = list(passage.getReffs()) @@ -414,7 +414,7 @@ def test_getPassage_metadata_prevnext(self): # We check the passage is able to perform further requests and is well instantiated self.assertEqual( - children[0], '1.1.1', + str(children[0]), '1.1.1', "Resource should be string identifiers" ) @@ -539,11 +539,11 @@ def test_getSiblings(self): textId="urn:cts:latinLit:phi1294.phi002.perseus-lat2", subreference="1.1" ) self.assertEqual( - previous, "1.pr", + previous, CtsReference("1.pr"), "Previous should be well computed" ) self.assertEqual( - nextious, "1.2", + nextious, CtsReference("1.2"), "Previous should be well computed" ) @@ -557,7 +557,7 @@ def test_getSiblings_nextOnly(self): "Previous Should not exist" ) self.assertEqual( - nextious, "1.1", + nextious, CtsReference("1.1"), "Next should be well computed" ) @@ -567,7 +567,7 @@ def test_getSiblings_prevOnly(self): textId="urn:cts:latinLit:phi1294.phi002.perseus-lat2", subreference="14.223" ) self.assertEqual( - previous, "14.222", + previous, CtsReference("14.222"), "Previous should be well computed" ) self.assertEqual( @@ -583,7 +583,7 @@ def test_getReffs_full(self): "There should be 14 books" ) self.assertEqual( - reffs[0], "1" + reffs[0], CtsReference("1") ) reffs = self.resolver.getReffs(textId="urn:cts:latinLit:phi1294.phi002.perseus-lat2", level=2) @@ -592,7 +592,7 @@ def test_getReffs_full(self): "There should be 1527 poems" ) self.assertEqual( - reffs[0], "1.pr" + reffs[0], CtsReference("1.pr") ) reffs = self.resolver.getReffs( @@ -605,7 +605,7 @@ def test_getReffs_full(self): "There should be 6 references" ) self.assertEqual( - reffs[0], "1.1.1" + reffs[0], CtsReference("1.1.1") ) diff --git a/tests/resources/texts/local/commonTests.py b/tests/resources/texts/local/commonTests.py index 6654fe21..6fb53b65 100644 --- a/tests/resources/texts/local/commonTests.py +++ b/tests/resources/texts/local/commonTests.py @@ -131,7 +131,7 @@ def testValidReffs(self): ) self.assertEqual( self.TEI.getValidReff(reference=CtsReference("2.38-2.39"), level=3), - ["2.38.1", "2.38.2", "2.39.1", "2.39.2"] + [CtsReference("2.38.1"), CtsReference("2.38.2"), CtsReference("2.39.1"), CtsReference("2.39.2")] ) # Test with reference and level autocorrected because too small @@ -148,26 +148,26 @@ def testValidReffs(self): self.assertEqual( self.TEI.getValidReff(reference=CtsReference("2.1-2.2")), - [ + [CtsReference(ref) for ref in[ '2.1.1', '2.1.2', '2.1.3', '2.1.4', '2.1.5', '2.1.6', '2.1.7', '2.1.8', '2.1.9', '2.1.10', '2.1.11', '2.1.12', '2.2.1', '2.2.2', '2.2.3', '2.2.4', '2.2.5', '2.2.6' - ], + ]], "It could be possible to ask for range reffs children") self.assertEqual( self.TEI.getValidReff(reference=CtsReference("2.1-2.2"), level=2), - ['2.1', '2.2'], + [CtsReference('2.1'), CtsReference('2.2')], "It could be possible to ask for range References reference at the same level in between milestone") self.assertEqual( self.TEI.getValidReff(reference=CtsReference("1.38-2.2"), level=2), - ['1.38', '1.39', '2.pr', '2.1', '2.2'], + [CtsReference(ref) for ref in ['1.38', '1.39', '2.pr', '2.1', '2.2']], "It could be possible to ask for range References reference at the same level in between milestone " "across higher levels") self.assertEqual( self.TEI.getValidReff(reference=CtsReference("1.1.1-1.1.4"), level=3), - ['1.1.1', '1.1.2', '1.1.3', '1.1.4'], + [CtsReference(ref) for ref in ['1.1.1', '1.1.2', '1.1.3', '1.1.4']], "It could be possible to ask for range reffs in between at the same level cross higher level") # Test when already too deep @@ -264,11 +264,11 @@ def test_xml_with_xml_id(self): "Word should be there !" ) self.assertEqual( - text.getReffs(level=2), [ + text.getReffs(level=2), [CtsReference(ref) for ref in [ '1.C_w_000001', '1.C_w_000002', '1.C_w_000003', '1.C_w_000004', '1.C_w_000005', '1.C_w_000006', '1.C_w_000007', '2.C_w_000008', '2.C_w_000009', '2.C_w_000010', '2.C_w_000011', '2.C_w_000012', '2.C_w_000013', '2.C_w_000014' - ], + ]], "XML:IDs and N should be retrieved." ) @@ -579,7 +579,7 @@ def test_next(self, simple): """ Test next property """ # Normal passage checking # self.TEI.parse() - p = self.TEI.getTextualNode(["1", "pr", "1"], simple=simple) + p = self.TEI.getTextualNode("1.pr.1", simple=simple) self.assertEqual(str(p.next.reference), "1.pr.2") # End of lowest level passage checking but not end of parent level @@ -742,7 +742,7 @@ def __init__(self, *args, **kwargs): def test_errors(self): """ Ensure that some results throws errors according to some standards """ - passage = self.text.getTextualNode(MyCapytain.common.reference.Reference("1.pr.2-1.2")) + passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("1.pr.2-1.2")) with self.assertRaises(MyCapytain.errors.InvalidSiblingRequest, msg="Different range passage have no siblings"): a = passage.next @@ -750,7 +750,7 @@ def test_errors(self): a = passage.prev def test_prevnext_on_first_passage(self): - passage = self.text.getTextualNode(MyCapytain.common.reference.Reference("1.pr.1-1.2.1")) + passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("1.pr.1-1.2.1")) self.assertEqual( str(passage.nextId), "1.2.2-1.5.2", "Next reff should be the same length as sibling" @@ -761,7 +761,7 @@ def test_prevnext_on_first_passage(self): ) def test_prevnext_on_close_to_first_passage(self): - passage = self.text.getTextualNode(MyCapytain.common.reference.Reference("1.pr.10-1.2.1")) + passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("1.pr.10-1.2.1")) self.assertEqual( str(passage.nextId), "1.2.2-1.4.1", "Next reff should be the same length as sibling" @@ -772,7 +772,7 @@ def test_prevnext_on_close_to_first_passage(self): ) def test_prevnext_on_last_passage(self): - passage = self.text.getTextualNode(MyCapytain.common.reference.Reference("2.39.2-2.40.8")) + passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("2.39.2-2.40.8")) self.assertEqual( passage.nextId, None, "Next reff should be none if we are on the last passage of the text" @@ -783,7 +783,7 @@ def test_prevnext_on_last_passage(self): ) def test_prevnext_on_close_to_last_passage(self): - passage = self.text.getTextualNode(MyCapytain.common.reference.Reference("2.39.2-2.40.5")) + passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("2.39.2-2.40.5")) self.assertEqual( str(passage.nextId), "2.40.6-2.40.8", "Next reff should finish at the end of the text, no matter the length of the reference" @@ -794,7 +794,7 @@ def test_prevnext_on_close_to_last_passage(self): ) def test_prevnext(self): - passage = self.text.getTextualNode(MyCapytain.common.reference.Reference("1.pr.5-1.pr.6")) + passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("1.pr.5-1.pr.6")) self.assertEqual( str(passage.nextId), "1.pr.7-1.pr.8", "Next reff should be the same length as sibling" @@ -803,7 +803,7 @@ def test_prevnext(self): str(passage.prevId), "1.pr.3-1.pr.4", "Prev reff should be the same length as sibling" ) - passage = self.text.getTextualNode(MyCapytain.common.reference.Reference("1.pr.5")) + passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("1.pr.5")) self.assertEqual( str(passage.nextId), "1.pr.6", "Next reff should be the same length as sibling" @@ -813,7 +813,7 @@ def test_prevnext(self): "Prev reff should be the same length as sibling" ) - passage = self.text.getTextualNode(MyCapytain.common.reference.Reference("1.pr")) + passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("1.pr")) self.assertEqual( str(passage.nextId), "1.1", "Next reff should be the same length as sibling" @@ -823,7 +823,7 @@ def test_prevnext(self): "Prev reff should be None when at the start" ) - passage = self.text.getTextualNode(MyCapytain.common.reference.Reference("2.40")) + passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("2.40")) self.assertEqual( str(passage.prevId), "2.39", "Prev reff should be the same length as sibling" @@ -834,7 +834,7 @@ def test_prevnext(self): ) def test_first_list(self): - passage = self.text.getTextualNode(MyCapytain.common.reference.Reference("2.39")) + passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("2.39")) self.assertEqual( str(passage.firstId), "2.39.1", "First reff should be the first" @@ -844,7 +844,7 @@ def test_first_list(self): "Last reff should be the last" ) - passage = self.text.getTextualNode(MyCapytain.common.reference.Reference("2.39-2.40")) + passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("2.39-2.40")) self.assertEqual( str(passage.firstId), "2.39.1", "First reff should be the first" From 65115589944b544597311db2c395328d36ebb47b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Thu, 30 Aug 2018 17:09:36 +0200 Subject: [PATCH 40/89] (CtsReference) Added a condition on __new__() to deal with tuple in case of picle.load --- MyCapytain/common/reference/_capitains_cts.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index df62b978..fedd123e 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -1,6 +1,6 @@ import re from copy import copy -from typing import Optional, List, Union +from typing import Optional, List, Union, Tuple from lxml.etree import _Element from MyCapytain.common.constants import Mimetypes, get_graph, RDF_NAMESPACES, XPATH_NAMESPACES @@ -114,8 +114,21 @@ class CtsReference(BaseReference): >>> ref = CtsReference('1.2.3') """ - def __new__(cls, reference: str): - if "-" not in reference: + def __new__(cls, reference: Union[str, Tuple[str, Optional[str]]]): + # pickle.load will try to feed the tuple back ! + if isinstance(reference, tuple): + if reference[1]: + o = BaseReference.__new__( + CtsReference, + CtsSinglePassageId(reference[0]), + CtsSinglePassageId(reference[1]) + ) + else: + o = BaseReference.__new__( + CtsReference, + CtsSinglePassageId(reference[0]) + ) + elif "-" not in reference: o = BaseReference.__new__(CtsReference, CtsSinglePassageId(reference)) else: _start, _end = tuple(reference.split("-")) From a4475781c54410f73ea436e837a7a9e5ad279a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Sat, 1 Sep 2018 10:00:54 +0200 Subject: [PATCH 41/89] Fixed some properties to work better, mostly reference --- MyCapytain/common/reference/_base.py | 90 ++++++++++--------- MyCapytain/common/reference/_capitains_cts.py | 38 +++++--- MyCapytain/common/utils.py | 1 + MyCapytain/errors.py | 9 ++ .../resources/texts/local/capitains/cts.py | 11 ++- tests/resources/texts/local/commonTests.py | 26 +++--- 6 files changed, 105 insertions(+), 70 deletions(-) diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index 062c2d86..ba43722c 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -5,47 +5,6 @@ from abc import abstractmethod -class BaseReference(tuple): - def __new__(cls, start, end=None): - obj = tuple.__new__(cls, (start, end)) - - return obj - - def is_range(self): - return bool(self[1]) - - @property - def start(self): - """ Quick access property for start part - - :rtype: str - """ - return self[0] - - @property - def end(self): - """ Quick access property for reference end list - - :rtype: str - """ - return self[1] - - -class BaseReferenceSet(list): - @property - def citation(self): - return self._citation - - def __new__(cls, *refs, citation=None): - obj = list.__new__(cls, refs) - obj._citation = None - - if citation: - obj._citation = citation - - return obj - - class BaseCitationSet(Exportable): """ A citation set is a collection of citations that optionnaly can be matched using a .match() function @@ -337,6 +296,55 @@ def depth(self): return 1 +class BaseReference(tuple): + def __new__(cls, *refs): + if len(refs) == 1 and not isinstance(refs[0], tuple): + refs = refs[0], None + obj = tuple.__new__(cls, refs) + + return obj + + def is_range(self): + return bool(self[1]) + + @property + def start(self): + """ Quick access property for start part + + :rtype: str + """ + return self[0] + + @property + def end(self): + """ Quick access property for reference end list + + :rtype: str + """ + return self[1] + + +class BaseReferenceSet(tuple): + def __new__(cls, *refs, citation: BaseCitationSet=None, level: int=1): + if len(refs) == 1 and not isinstance(refs, BaseReference): + refs = refs[0] + obj = tuple.__new__(cls, refs) + obj._citation = None + obj._level = level + + if citation: + obj._citation = citation + return obj + + @property + def citation(self) -> BaseCitationSet: + return self._citation + + @property + def level(self) -> int: + return self._level + + class NodeId(object): """ Collection of directional references for a Tree diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index fedd123e..5c774210 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -114,27 +114,41 @@ class CtsReference(BaseReference): >>> ref = CtsReference('1.2.3') """ - def __new__(cls, reference: Union[str, Tuple[str, Optional[str]]]): + def __new__(cls, *references): # pickle.load will try to feed the tuple back ! - if isinstance(reference, tuple): - if reference[1]: + if len(references) == 2: + start, end = references + o = BaseReference.__new__( + CtsReference, + CtsSinglePassageId(start), + CtsSinglePassageId(end) + ) + o._str_repr = start + "-" + end + return o + + references, *_ = references + if isinstance(references, tuple): + if references[1]: o = BaseReference.__new__( CtsReference, - CtsSinglePassageId(reference[0]), - CtsSinglePassageId(reference[1]) + CtsSinglePassageId(references[0]), + CtsSinglePassageId(references[1]) ) else: o = BaseReference.__new__( CtsReference, - CtsSinglePassageId(reference[0]) + CtsSinglePassageId(references[0]) ) - elif "-" not in reference: - o = BaseReference.__new__(CtsReference, CtsSinglePassageId(reference)) - else: - _start, _end = tuple(reference.split("-")) - o = BaseReference.__new__(CtsReference, CtsSinglePassageId(_start), CtsSinglePassageId(_end)) + o._str_repr = references + + elif isinstance(references, str): + if "-" not in references: + o = BaseReference.__new__(CtsReference, CtsSinglePassageId(references)) + else: + _start, _end = tuple(references.split("-")) + o = BaseReference.__new__(CtsReference, CtsSinglePassageId(_start), CtsSinglePassageId(_end)) + o._str_repr = references - o._str_repr = reference return o @property diff --git a/MyCapytain/common/utils.py b/MyCapytain/common/utils.py index 60507021..5322e71d 100644 --- a/MyCapytain/common/utils.py +++ b/MyCapytain/common/utils.py @@ -25,6 +25,7 @@ import link_header from MyCapytain.common.constants import XPATH_NAMESPACES +from MyCapytain.errors import CapitainsXPathError __strip = re.compile("([ ]{2,})+") __parser__ = etree.XMLParser(collect_ids=False, resolve_entities=False) diff --git a/MyCapytain/errors.py b/MyCapytain/errors.py index 0b5e77d2..f3da71ea 100644 --- a/MyCapytain/errors.py +++ b/MyCapytain/errors.py @@ -79,3 +79,12 @@ class CitationDepthError(UnknownObjectError, MyCapytainException): class MissingRefsDecl(Exception, MyCapytainException): """ A text has no properly encoded refsDecl """ + + +class CapitainsXPathError(Exception): + def __init__(self, message): + super(CapitainsXPathError, self).__init__() + self.message = message + + def __repr__(self): + return "CapitainsXPathError("+self.message+")" \ No newline at end of file diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index 9513da0e..509e1a1e 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -162,7 +162,7 @@ def getReffs(self, level=1, subreference=None) -> CtsReferenceSet: if not subreference: if hasattr(self, "reference"): subreference = self.reference - else: + elif not isinstance(subreference, CtsReference): subreference = CtsReference(subreference) return self.getValidReff(level, subreference) @@ -212,7 +212,7 @@ def getValidReff(self, level: int=None, reference: CtsReference=None, _debug: bo if level <= len(passages[0]) and reference is not None: level = len(passages[0]) + 1 if level > len(self.citation): - return CtsReferenceSet() + raise CitationDepthError("The required level is too deep") nodes = [None] * (level - depth) @@ -255,7 +255,12 @@ def getValidReff(self, level: int=None, reference: CtsReference=None, _debug: bo print(empties) warnings.warn(message, EmptyReference) - return CtsReferenceSet([CtsReference(reff) for reff in passages]) + references = CtsReferenceSet( + [CtsReference(reff) for reff in passages], + citation=self.citation, + level=level + ) + return references def xpath(self, *args, **kwargs): """ Perform XPath on the passage XML diff --git a/tests/resources/texts/local/commonTests.py b/tests/resources/texts/local/commonTests.py index 6fb53b65..2663b426 100644 --- a/tests/resources/texts/local/commonTests.py +++ b/tests/resources/texts/local/commonTests.py @@ -10,7 +10,7 @@ import MyCapytain.errors from MyCapytain.common.constants import Mimetypes -from MyCapytain.common.reference._capitains_cts import CtsReference, URN, Citation +from MyCapytain.common.reference._capitains_cts import CtsReference, URN, Citation, CtsReferenceSet from MyCapytain.resources.texts.local.capitains.cts import CapitainsCtsText @@ -131,7 +131,7 @@ def testValidReffs(self): ) self.assertEqual( self.TEI.getValidReff(reference=CtsReference("2.38-2.39"), level=3), - [CtsReference("2.38.1"), CtsReference("2.38.2"), CtsReference("2.39.1"), CtsReference("2.39.2")] + (CtsReference("2.38.1"), CtsReference("2.38.2"), CtsReference("2.39.1"), CtsReference("2.39.2")) ) # Test with reference and level autocorrected because too small @@ -148,34 +148,32 @@ def testValidReffs(self): self.assertEqual( self.TEI.getValidReff(reference=CtsReference("2.1-2.2")), - [CtsReference(ref) for ref in[ + CtsReferenceSet(CtsReference(ref) for ref in[ '2.1.1', '2.1.2', '2.1.3', '2.1.4', '2.1.5', '2.1.6', '2.1.7', '2.1.8', '2.1.9', '2.1.10', '2.1.11', '2.1.12', '2.2.1', '2.2.2', '2.2.3', '2.2.4', '2.2.5', '2.2.6' - ]], + ]), "It could be possible to ask for range reffs children") self.assertEqual( self.TEI.getValidReff(reference=CtsReference("2.1-2.2"), level=2), - [CtsReference('2.1'), CtsReference('2.2')], + CtsReferenceSet(CtsReference('2.1'), CtsReference('2.2')), "It could be possible to ask for range References reference at the same level in between milestone") self.assertEqual( self.TEI.getValidReff(reference=CtsReference("1.38-2.2"), level=2), - [CtsReference(ref) for ref in ['1.38', '1.39', '2.pr', '2.1', '2.2']], + CtsReferenceSet(CtsReference(ref) for ref in ['1.38', '1.39', '2.pr', '2.1', '2.2']), "It could be possible to ask for range References reference at the same level in between milestone " "across higher levels") self.assertEqual( self.TEI.getValidReff(reference=CtsReference("1.1.1-1.1.4"), level=3), - [CtsReference(ref) for ref in ['1.1.1', '1.1.2', '1.1.3', '1.1.4']], + CtsReferenceSet(CtsReference(ref) for ref in ['1.1.1', '1.1.2', '1.1.3', '1.1.4']), "It could be possible to ask for range reffs in between at the same level cross higher level") - # Test when already too deep - self.assertEqual( - self.TEI.getValidReff(reference=CtsReference("2.1.1"), level=3), - [], + # Test level too deep + with self.assertRaises(MyCapytain.errors.CitationDepthError): "Asking for a level too deep should return nothing" - ) + self.TEI.getValidReff(reference=CtsReference("2.1.1"), level=3) # Test wrong citation with self.assertRaises(KeyError): @@ -264,11 +262,11 @@ def test_xml_with_xml_id(self): "Word should be there !" ) self.assertEqual( - text.getReffs(level=2), [CtsReference(ref) for ref in [ + text.getReffs(level=2), CtsReferenceSet(CtsReference(ref) for ref in [ '1.C_w_000001', '1.C_w_000002', '1.C_w_000003', '1.C_w_000004', '1.C_w_000005', '1.C_w_000006', '1.C_w_000007', '2.C_w_000008', '2.C_w_000009', '2.C_w_000010', '2.C_w_000011', '2.C_w_000012', '2.C_w_000013', '2.C_w_000014' - ]], + ]), "XML:IDs and N should be retrieved." ) From e4b79c6f81cfc6d508231a1ad3790a3f2f63165a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Sun, 2 Sep 2018 11:36:57 +0200 Subject: [PATCH 42/89] (Breaking) A Passage.citation is now the citation at the passage level. Change to to resolve issues with your code. --- .../resources/texts/local/capitains/cts.py | 23 +++++++++++-------- tests/resolvers/cts/test_local.py | 8 +++++-- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index 509e1a1e..c50d9b23 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -74,8 +74,9 @@ def getTextualNode(self, subreference=None, simple=False): if simple is True: return self._getSimplePassage(subreference) - citation_start = [citation for citation in self.citation][len(start)-1] - citation_end = [citation for citation in self.citation][len(end)-1] + citation_start = self.citation.root[len(start)-1] + citation_end = self.citation.root[len(end)-1] + start, end = citation_start.fill(passage=start), citation_end.fill(passage=end) start, end = normalizeXpath(start.split("/")[2:]), normalizeXpath(end.split("/")[2:]) @@ -92,11 +93,12 @@ def getTextualNode(self, subreference=None, simple=False): urn, subreference = URN("{}:{}".format(self.urn, subreference)), subreference else: urn, subreference = None, subreference + return CapitainsCtsPassage( urn=urn, resource=root, text=self, - citation=self.citation, + citation=citation_start, reference=subreference ) @@ -115,12 +117,13 @@ def _getSimplePassage(self, reference=None): resource=self.resource, reference=None, urn=self.urn, - citation=self.citation, + citation=self.citation.root, text=self ) + subcitation = self.citation.root[reference.depth-1] resource = self.resource.xpath( - self.citation[reference.depth-1].fill(reference), + subcitation.fill(reference), namespaces=XPATH_NAMESPACES ) @@ -131,7 +134,7 @@ def _getSimplePassage(self, reference=None): resource[0], reference=reference, urn=self.urn, - citation=self.citation, + citation=subcitation, text=self.textObject ) @@ -211,12 +214,12 @@ def getValidReff(self, level: int=None, reference: CtsReference=None, _debug: bo level = 1 if level <= len(passages[0]) and reference is not None: level = len(passages[0]) + 1 - if level > len(self.citation): + if level > len(self.citation.root): raise CitationDepthError("The required level is too deep") nodes = [None] * (level - depth) - citations = [citation for citation in self.citation] + citations = [citation for citation in self.citation.root] while len(nodes) >= 1: passages = [ @@ -332,7 +335,7 @@ def childIds(self): :rtype: None, CtsReference :returns: Dictionary of chidren, where key are subreferences """ - if self.depth >= len(self.citation): + if self.depth >= len(self.citation.root): return [] elif self.__children__ is not None: return self.__children__ @@ -558,7 +561,7 @@ def childIds(self): :returns: Dictionary of chidren, where key are subreferences """ self.__raiseDepth__() - if self.depth >= len(self.citation): + if self.depth >= len(self.citation.root): return [] elif self.__children__ is not None: return self.__children__ diff --git a/tests/resolvers/cts/test_local.py b/tests/resolvers/cts/test_local.py index 3e734f04..98f044f6 100644 --- a/tests/resolvers/cts/test_local.py +++ b/tests/resolvers/cts/test_local.py @@ -384,11 +384,15 @@ def test_getPassage_metadata_prevnext(self): "Local Inventory Files should be parsed and aggregated correctly" ) self.assertEqual( - passage.citation.name, "book", + passage.citation.name, "poem", "Local Inventory Files should be parsed and aggregated correctly" ) self.assertEqual( - len(passage.citation), 3, + passage.citation.root.name, "book", + "Local Inventory Files should be parsed and aggregated correctly" + ) + self.assertEqual( + len(passage.citation.root), 3, "Local Inventory Files should be parsed and aggregated correctly" ) self.assertEqual( From 0869531f454e1b2818eb0008d3fd7f198e52eaa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Sun, 2 Sep 2018 11:43:45 +0200 Subject: [PATCH 43/89] (Propagation of previous) Propagated citation of current level to CtsReferenceSet in .getReffs() --- MyCapytain/resources/texts/local/capitains/cts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index c50d9b23..954b41a4 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -260,7 +260,7 @@ def getValidReff(self, level: int=None, reference: CtsReference=None, _debug: bo references = CtsReferenceSet( [CtsReference(reff) for reff in passages], - citation=self.citation, + citation=self.citation.root[level-1], level=level ) return references From fe9541240460a389dfdecc0eec49d98aca1ab5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Mon, 3 Sep 2018 16:22:36 +0200 Subject: [PATCH 44/89] Fixed an issue regarding str(CtsReference) were it would hold a tuple value --- MyCapytain/common/reference/_capitains_cts.py | 9 ++++----- MyCapytain/resolvers/cts/local.py | 8 +++++--- MyCapytain/resources/texts/local/capitains/cts.py | 6 +++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index 5c774210..b2017cd0 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -92,9 +92,6 @@ def depth(self) -> int: class CtsReference(BaseReference): """ A reference object giving information - :param reference: CapitainsCtsPassage Reference part of a Urn - :type reference: basestring - :Example: >>> a = CtsReference(reference="1.1@Achiles[1]-1.2@Zeus[1]") >>> b = CtsReference(reference="1.1") @@ -127,7 +124,9 @@ def __new__(cls, *references): return o references, *_ = references - if isinstance(references, tuple): + if not references: + return None + elif isinstance(references, tuple): if references[1]: o = BaseReference.__new__( CtsReference, @@ -139,7 +138,7 @@ def __new__(cls, *references): CtsReference, CtsSinglePassageId(references[0]) ) - o._str_repr = references + o._str_repr = "-".join([r for r in references if r]) elif isinstance(references, str): if "-" not in references: diff --git a/MyCapytain/resolvers/cts/local.py b/MyCapytain/resolvers/cts/local.py index c020ca7d..508548ca 100644 --- a/MyCapytain/resolvers/cts/local.py +++ b/MyCapytain/resolvers/cts/local.py @@ -479,14 +479,14 @@ def getTextualNode(self, textId, subreference=None, prevnext=False, metadata=Fal :rtype: CapitainsCtsPassage """ text, text_metadata = self.__getText__(textId) - if subreference is not None: + if subreference is not None and not isinstance(subreference, CtsReference): subreference = CtsReference(subreference) passage = text.getTextualNode(subreference) if metadata: passage.set_metadata_from_collection(text_metadata) return passage - def getSiblings(self, textId, subreference): + def getSiblings(self, textId, subreference: CtsReference): """ Retrieve the siblings of a textual node :param textId: CtsTextMetadata Identifier @@ -497,7 +497,9 @@ def getSiblings(self, textId, subreference): :rtype: (str, str) """ text, inventory = self.__getText__(textId) - passage = text.getTextualNode(CtsReference(subreference)) + if not isinstance(subreference, CtsReference): + subreference = CtsReference(subreference) + passage = text.getTextualNode(subreference) return passage.siblingsId def getReffs(self, textId, level=1, subreference=None): diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index 954b41a4..fc4b02a0 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -90,9 +90,9 @@ def getTextualNode(self, subreference=None, simple=False): root = passageLoop(xml, root, start, end) if self.urn: - urn, subreference = URN("{}:{}".format(self.urn, subreference)), subreference + urn = URN("{}:{}".format(self.urn, subreference)) else: - urn, subreference = None, subreference + urn = None return CapitainsCtsPassage( urn=urn, @@ -650,7 +650,7 @@ def siblingsId(self): else: _next = "{}-{}".format(document_references[end+1], document_references[end + range_length]) - self.__prevnext__ = (_prev, _next) + self.__prevnext__ = (CtsReference(_prev), CtsReference(_next)) return self.__prevnext__ @property From edfdc90fab894620c63589e18deb2dca27de7ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 12 Sep 2018 10:16:45 +0900 Subject: [PATCH 45/89] Trying to enhance the doc --- MyCapytain/common/reference/_base.py | 48 ++++++++++++++----- MyCapytain/common/reference/_capitains_cts.py | 48 ++++++++++++++----- doc/MyCapytain.api.rst | 12 +---- 3 files changed, 74 insertions(+), 34 deletions(-) diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index ba43722c..693ed9c8 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -33,6 +33,10 @@ def children(self): @children.setter def children(self, val: list): + """ Sets children + + :param val: List of citation children + """ final_value = [] if val is not None: for citation in val: @@ -50,6 +54,11 @@ def children(self, val: list): self._children = final_value def add_child(self, child): + """ Adds a child to the CitationSet + + :param child: Child citation to add + :return: + """ if isinstance(child, BaseCitation): self._children.append(child) @@ -82,7 +91,8 @@ def match(self, passageId): def depth(self): """ Depth of the citation scheme - .. example:: If we have a Book, Poem, Line system, and the citation we are looking at is Poem, depth is 1 + .. example:: If we have a Book, Poem, Line system, + and the citation we are looking at is Poem, depth is 2 :rtype: int @@ -100,7 +110,6 @@ def __getitem__(self, item): :type item: int :rtype: list(BaseCitation) or BaseCitation - .. note:: Should it be a or or always a list ? """ if item < 0: _item = self.depth + item @@ -132,7 +141,7 @@ def __setstate__(self, dic): self.__dict__ = dic return self - def is_empty(self): + def is_empty(self) -> bool: """ Check if the citation has not been set :return: True if nothing was setup @@ -140,7 +149,11 @@ def is_empty(self): """ return len(self.children) == 0 - def is_root(self): + def is_root(self) -> bool: + """ Check if the Citation is the root + + :return: + """ return True def __export__(self, output=None, context=False, namespace_manager=None, **kwargs): @@ -173,7 +186,7 @@ def __repr__(self): """ return "<{} name({})>".format(type(self).__name__, self.name) - def __init__(self, name=None, children=None, root=None): + def __init__(self, name: str=None, children: list=None, root: BaseCitationSet=None): """ Initialize a BaseCitation object :param name: Name of the citation level @@ -192,14 +205,14 @@ def __init__(self, name=None, children=None, root=None): self.children = children self.root = root - def is_root(self): + def is_root(self) -> str: """ :return: If the current object is the root of the citation set, True :rtype: bool """ return self._root is None - def is_set(self): + def is_set(self) -> bool: """ Checks that the current object is set :rtype: bool @@ -207,7 +220,7 @@ def is_set(self): return self.name is not None @property - def root(self): + def root(self) -> BaseCitationSet: """ Returns the root of the citation set :return: Root of the Citation set @@ -228,7 +241,7 @@ def root(self, value): self._root = value @property - def name(self): + def name(self) -> str: """ Type of the citation represented :rtype: str @@ -281,7 +294,7 @@ def __export__(self, output=None, context=False, namespace_manager=None, **kwarg return _out @property - def depth(self): + def depth(self) -> int: """ Depth of the citation scheme .. example:: If we have a Book, Poem, Line system, and the citation we are looking at is Poem, depth is 1 @@ -297,6 +310,12 @@ def depth(self): class BaseReference(tuple): + """ BaseReference represents a passage identifier, range or not + + It is made of two major properties : .start and .end + + To check if the object is a range, you can use the method .is_range() + """ def __new__(cls, *refs): if len(refs) == 1 and not isinstance(refs[0], tuple): refs = refs[0], None @@ -304,7 +323,7 @@ def __new__(cls, *refs): return obj - def is_range(self): + def is_range(self) -> int: return bool(self[1]) @property @@ -325,6 +344,13 @@ def end(self): class BaseReferenceSet(tuple): + """ A BaseReferenceSet is a set of Reference (= a bag of identifier) + that can carry citation and level information (what kind of reference is this reference ? + Where am I in the levels of the current document ?) + + It can be iterate like a tuple and has a .citation and .level property + + """ def __new__(cls, *refs, citation: BaseCitationSet=None, level: int=1): if len(refs) == 1 and not isinstance(refs, BaseReference): refs = refs[0] diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index b2017cd0..c9adbe54 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -1,5 +1,4 @@ import re -from copy import copy from typing import Optional, List, Union, Tuple from lxml.etree import _Element @@ -14,7 +13,7 @@ REFERENCE_REPLACER = re.compile(r"(@[a-zA-Z0-9:]+)(=)([\\$'\"?0-9]{3,6})") -def __childOrNone__(liste): +def _child_or_none(liste): """ Used to parse resources in XmlCtsCitation :param liste: List of item @@ -27,6 +26,13 @@ def __childOrNone__(liste): class CtsWordReference(str): + """ A CTSWordReference is the specific part of a CTS identifier that + identifies a word in a given passage. It contains the text in its + .word property and the index of this word in its .counter identifier + + It can be returned as a tuple using .tuple() + + """ def __new__(cls, word_reference: str): word, counter = tuple(SUBREFERENCE.findall(word_reference)[0]) @@ -41,7 +47,7 @@ def __new__(cls, word_reference: str): return obj - def tuple(self): + def tuple(self) -> Tuple[str, int]: return self.word, self.counter def __iter__(self): @@ -49,6 +55,21 @@ def __iter__(self): class CtsSinglePassageId(str): + """ A CtsSinglePassageId identifies part of a range, or a non-range passage + such as 1.1.1 or 1.2.2 in 1.2.2-1.2.3. + + It provides a list version of itself through the .list property and + links to its subreference using the .subreference property (Word and Index identifier) + + If you iter over it, it returns each passage of the hierarchy, so + + >>> iter((CtsSinglePassageId("1.2.3"))) == iter(["1", "2", "3"]) + + len() and .depth returns the depth of the passage + + >>> (CtsSinglePassageId("1.2.4")).depth == 3 + >>> len(CtsSinglePassageId("1.2.4")) == 3 + """ def __new__(cls, str_repr: str): # Saving the original ID obj = str.__new__(cls, str_repr) @@ -93,9 +114,9 @@ class CtsReference(BaseReference): """ A reference object giving information :Example: - >>> a = CtsReference(reference="1.1@Achiles[1]-1.2@Zeus[1]") - >>> b = CtsReference(reference="1.1") - >>> CtsReference("1.1-2.2.2").highest == ["1", "1"] + >>> a = CtsReference("1.1@Achiles[1]-1.2@Zeus[1]") + >>> b = CtsReference("1.1") + >>> CtsReference("1.1-2.2.2").highest == CtsSinglePassageId("1.1") Reference object supports the following magic methods : len(), str() and eq(). @@ -151,7 +172,7 @@ def __new__(cls, *references): return o @property - def parent(self): + def parent(self) -> Optional['CtsReference']: """ Parent of the actual URN, for example, 1.1 for 1.1.1 :rtype: CtsReference @@ -253,8 +274,8 @@ def __str__(self): :returns: String representation of Reference Object :Example: - >>> a = CtsReference(reference="1.1@Achiles[1]-1.2@Zeus[1]") - >>> b = CtsReference(reference="1.1") + >>> a = CtsReference("1.1@Achiles[1]-1.2@Zeus[1]") + >>> b = CtsReference("1.1") >>> str(a) == "1.1@Achiles[1]-1.2@Zeus[1]" >>> str(b) == "1.1" """ @@ -262,9 +283,12 @@ def __str__(self): class CtsReferenceSet(BaseReferenceSet): - def __contains__(self, item): + """ A CTS version of the BaseReferenceSet + + """ + def __contains__(self, item: str) -> bool: return BaseReferenceSet.__contains__(self, item) or \ - CtsReference(item) + BaseReferenceSet.__contains__(self, CtsReference(item)) def index(self, obj: Union[str, CtsReference], *args, **kwargs) -> int: _o = obj @@ -923,7 +947,7 @@ def ingest(resource, xpath=".//tei:cRefPattern"): Citation( name=resource[x].get("n"), refsDecl=resource[x].get("replacementPattern")[7:-1], - child=__childOrNone__(citations) + child=_child_or_none(citations) ) ) if len(citations) > 1: diff --git a/doc/MyCapytain.api.rst b/doc/MyCapytain.api.rst index 26cb8820..0350a892 100644 --- a/doc/MyCapytain.api.rst +++ b/doc/MyCapytain.api.rst @@ -23,17 +23,7 @@ Constants URN, References and Citations ***************************** -.. autoclass:: MyCapytain.common.reference.NodeId - :members: - -.. autoclass:: MyCapytain.common.reference.URN - :members: - -.. autoclass:: MyCapytain.common.reference.Reference - :members: - -.. autoclass:: MyCapytain.common.reference.Citation - :members: fill, __iter__, __len__ +.. automodule:: MyCapytain.common.reference Metadata containers ******************* From 7de4866192b9d49db34641aea61abb1a944dfd28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 12 Sep 2018 10:22:21 +0900 Subject: [PATCH 46/89] Doc to use each class better --- doc/MyCapytain.api.rst | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/doc/MyCapytain.api.rst b/doc/MyCapytain.api.rst index 0350a892..ae63f437 100644 --- a/doc/MyCapytain.api.rst +++ b/doc/MyCapytain.api.rst @@ -23,7 +23,28 @@ Constants URN, References and Citations ***************************** -.. automodule:: MyCapytain.common.reference +MyCapytain Base Objects ++++++++++++++++++++++++ + +.. autoclass:: MyCapytain.common.reference.NodeId +.. autoclass:: MyCapytain.common.reference.BaseCitationSet +.. autoclass:: MyCapytain.common.reference.BaseReference +.. autoclass:: MyCapytain.common.reference.BaseReferenceSet + +Canonical Text Services Objects ++++++++++++++++++++++++++++++++ + +.. autoclass:: MyCapytain.common.reference.Citation +.. autoclass:: MyCapytain.common.reference.CtsReference +.. autoclass:: MyCapytain.common.reference.CtsReferenceSet + +Distributed Text Services Objects ++++++++++++++++++++++++++++++++++ + +.. autoclass:: MyCapytain.common.reference.URN +.. autoclass:: MyCapytain.common.reference.DtsCitation +.. autoclass:: MyCapytain.common.reference.DtsCitationSet + Metadata containers ******************* From 2c9cc22b50d714402c6e85e6f022e494b0de8f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Thu, 13 Sep 2018 12:24:04 +0900 Subject: [PATCH 47/89] Moved common.utils to package for ease of maintenance --- MyCapytain/common/metadata.py | 2 +- MyCapytain/common/reference/__init__.py | 2 +- MyCapytain/common/reference/_capitains_cts.py | 2 +- MyCapytain/common/reference/_dts_1.py | 18 +- MyCapytain/common/utils/__init__.py | 16 ++ MyCapytain/common/utils/_dts.py | 0 MyCapytain/common/utils/_generic.py | 64 +++++ MyCapytain/common/utils/_graph.py | 82 ++++++ MyCapytain/common/utils/_http.py | 45 ++++ MyCapytain/common/utils/_json_ld.py | 26 ++ MyCapytain/common/{utils.py => utils/xml.py} | 241 +----------------- MyCapytain/resolvers/cts/local.py | 2 +- MyCapytain/resolvers/dts/__init__.py | 0 MyCapytain/resolvers/dts/api_v1.py | 95 +++++++ MyCapytain/resources/collections/cts.py | 3 +- .../resources/prototypes/cts/inventory.py | 2 +- MyCapytain/resources/prototypes/metadata.py | 3 +- MyCapytain/resources/texts/base/tei.py | 3 +- .../resources/texts/local/capitains/cts.py | 2 +- MyCapytain/resources/texts/remote/cts.py | 2 +- requirements.txt | 3 +- .../test_reference/test_capitains_cts.py | 2 +- tests/common/test_utils.py | 5 +- tests/resolvers/cts/test_api.py | 2 +- tests/resolvers/dts/__init__.py | 0 tests/resolvers/dts/api_v1/__init__.py | 60 +++++ .../dts/api_v1/data/navigation/example1.json | 15 ++ tests/resolvers/dts/api_v1/data/root.json | 8 + tests/resources/collections/test_cts.py | 4 +- tests/resources/texts/base/test_tei.py | 2 +- .../test_capitains_xml_notObjectified.py | 2 +- tests/resources/texts/remote/test_cts.py | 2 +- tests/retrievers/test_dts.py | 3 +- 33 files changed, 459 insertions(+), 259 deletions(-) create mode 100644 MyCapytain/common/utils/__init__.py create mode 100644 MyCapytain/common/utils/_dts.py create mode 100644 MyCapytain/common/utils/_generic.py create mode 100644 MyCapytain/common/utils/_graph.py create mode 100644 MyCapytain/common/utils/_http.py create mode 100644 MyCapytain/common/utils/_json_ld.py rename MyCapytain/common/{utils.py => utils/xml.py} (57%) create mode 100644 MyCapytain/resolvers/dts/__init__.py create mode 100644 MyCapytain/resolvers/dts/api_v1.py create mode 100644 tests/resolvers/dts/__init__.py create mode 100644 tests/resolvers/dts/api_v1/__init__.py create mode 100644 tests/resolvers/dts/api_v1/data/navigation/example1.json create mode 100644 tests/resolvers/dts/api_v1/data/root.json diff --git a/MyCapytain/common/metadata.py b/MyCapytain/common/metadata.py index cbc2080a..7fcdca5e 100644 --- a/MyCapytain/common/metadata.py +++ b/MyCapytain/common/metadata.py @@ -8,7 +8,7 @@ """ from __future__ import unicode_literals -from MyCapytain.common.utils import make_xml_node +from MyCapytain.common.utils.xml import make_xml_node from MyCapytain.common.constants import Mimetypes, get_graph from MyCapytain.common.base import Exportable from rdflib import BNode, Literal, Graph, URIRef, term diff --git a/MyCapytain/common/reference/__init__.py b/MyCapytain/common/reference/__init__.py index 34134795..1fcc0d40 100644 --- a/MyCapytain/common/reference/__init__.py +++ b/MyCapytain/common/reference/__init__.py @@ -8,4 +8,4 @@ """ from ._base import NodeId, BaseCitationSet, BaseReference, BaseReferenceSet from ._capitains_cts import Citation, CtsReference, CtsReferenceSet, URN -from ._dts_1 import DtsCitation, DtsCitationSet +from ._dts_1 import DtsCitation, DtsCitationSet, DtsReference, DtsReferenceSet diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index c9adbe54..04cf7145 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -3,7 +3,7 @@ from lxml.etree import _Element from MyCapytain.common.constants import Mimetypes, get_graph, RDF_NAMESPACES, XPATH_NAMESPACES -from MyCapytain.common.utils import make_xml_node +from MyCapytain.common.utils.xml import make_xml_node from ._base import BaseCitation, BaseReference, BaseReferenceSet diff --git a/MyCapytain/common/reference/_dts_1.py b/MyCapytain/common/reference/_dts_1.py index 94a5f8ac..3c03f88d 100644 --- a/MyCapytain/common/reference/_dts_1.py +++ b/MyCapytain/common/reference/_dts_1.py @@ -1,4 +1,5 @@ -from ._base import BaseCitationSet, BaseCitation +from ._base import BaseCitationSet, BaseCitation, BaseReference, BaseReferenceSet +from ..metadata import Metadata from MyCapytain.common.constants import RDF_NAMESPACES @@ -7,6 +8,21 @@ _cite_structure_term = str(_dts.term("citeStructure")) +class DtsReference(BaseReference): + def __new__(cls, *refs, metadata: Metadata=None): + o = super(DtsReference).__new__(*refs) + if metadata: + o._metadata = metadata + else: + o._metadata = Metadata() # toDo : Figure how to deal with Refs ID in the Sparql Graph + + +class DtsReferenceSet(BaseReferenceSet): + def __contains__(self, item: str) -> bool: + return BaseReferenceSet.__contains__(self, item) or \ + BaseReferenceSet.__contains__(self, DtsReference(item)) + + class DtsCitation(BaseCitation): def __init__(self, name=None, children=None, root=None): super(DtsCitation, self).__init__(name=name, children=children, root=root) diff --git a/MyCapytain/common/utils/__init__.py b/MyCapytain/common/utils/__init__.py new file mode 100644 index 00000000..30be1382 --- /dev/null +++ b/MyCapytain/common/utils/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +""" +.. module:: MyCapytain.common.utils + :synopsis: Common useful tools + +.. moduleauthor:: Thibault Clérice + + +""" + +from ._generic import ( + OrderedDefaultDict, nested_ordered_dictionary, nested_get, nested_set, normalize +) +from ._graph import Subgraph, expand_namespace +from ._http import parse_pagination, parse_uri +from ._json_ld import dict_to_literal, literal_to_dict diff --git a/MyCapytain/common/utils/_dts.py b/MyCapytain/common/utils/_dts.py new file mode 100644 index 00000000..e69de29b diff --git a/MyCapytain/common/utils/_generic.py b/MyCapytain/common/utils/_generic.py new file mode 100644 index 00000000..8af59b7d --- /dev/null +++ b/MyCapytain/common/utils/_generic.py @@ -0,0 +1,64 @@ +import re +from collections import OrderedDict +from functools import reduce + + +class OrderedDefaultDict(OrderedDict): + """ Extension of Default Dict that makes an OrderedDefaultDict + + :param default_factory__: Default class to initiate + """ + + def __init__(self, default_factory=None, *args, **kwargs): + super(OrderedDefaultDict, self).__init__(*args, **kwargs) + self.default_factory = default_factory + + def __missing__(self, key): + if self.default_factory is None: + raise KeyError(key) + val = self[key] = self.default_factory() + return val + + +def nested_ordered_dictionary(): + """ Helper to create a nested ordered default dictionary + + :rtype OrderedDefaultDict: + :return: Nested Ordered Default Dictionary instance + """ + return OrderedDefaultDict(nested_ordered_dictionary) + + +def nested_get(dictionary, keys): + """ Get value in dictionary for dictionary[keys[0]][keys[1]][keys[..n]] + + :param dictionary: An input dictionary + :param keys: Keys where to store data + :return: + """ + return reduce(lambda d, k: d[k], keys, dictionary) + + +def nested_set(dictionary, keys, value): + """ Set value in dictionary for dictionary[keys[0]][keys[1]][keys[..n]] + + :param dictionary: An input dictionary + :param keys: Keys where to store data + :param value: Value to set at keys** target + :return: None + """ + nested_get(dictionary, keys[:-1])[keys[-1]] = value + + +_strip = re.compile("([ ]{2,})+") + + +def normalize(string): + """ Remove double-or-more spaces in a string + + :param string: A string to change + :type string: text_type + :rtype: text_type + :returns: Clean string + """ + return _strip.sub(" ", string) \ No newline at end of file diff --git a/MyCapytain/common/utils/_graph.py b/MyCapytain/common/utils/_graph.py new file mode 100644 index 00000000..fe043b08 --- /dev/null +++ b/MyCapytain/common/utils/_graph.py @@ -0,0 +1,82 @@ +from collections import defaultdict + +from rdflib import Graph, BNode +from rdflib.namespace import NamespaceManager + + +class Subgraph(object): + """ Utility class to generate subgraph around one or more items + + :param + """ + def __init__(self, bindings: dict = None): + self.graph = Graph() + self.graph.namespace_manager = NamespaceManager(self.graph) + for prefix, namespace in bindings.items(): + self.graph.namespace_manager.bind(prefix, namespace) + + self.downwards = defaultdict(lambda: True) + self.updwards = defaultdict(lambda: True) + + def graphiter(self, graph, target, ascendants=0, descendants=1): + """ Iter on a graph to finds object connected + + :param graph: Graph to serialize + :type graph: Graph + :param target: Node to iterate over + :type target: Node + :param ascendants: Number of level to iter over upwards (-1 = No Limit) + :param descendants: Number of level to iter over downwards (-1 = No limit) + :return: + """ + + asc = 0 + ascendants + if asc != 0: + asc -= 1 + + desc = 0 + descendants + if desc != 0: + desc -= 1 + + t = str(target) + + if descendants != 0 and self.downwards[t] is True: + self.downwards[t] = False + for pred, obj in graph.predicate_objects(target): + if desc == 0 and isinstance(obj, BNode): + continue + self.add((target, pred, obj)) + + # Retrieve triples about the object + if desc != 0 and self.downwards[str(obj)] is True: + self.graphiter(graph, target=obj, ascendants=0, descendants=desc) + + if ascendants != 0 and self.updwards[t] is True: + self.updwards[t] = False + for s, p in graph.subject_predicates(object=target): + if desc == 0 and isinstance(s, BNode): + continue + self.add((s, p, target)) + + # Retrieve triples about the parent as object + if asc != 0 and self.updwards[str(s)] is True: + self.graphiter(graph, target=s, ascendants=asc, descendants=0) + + def serialize(self, *args, **kwargs): + return self.graph.serialize(*args, **kwargs) + + def add(self, *args, **kwargs): + self.graph.add(*args, **kwargs) + + +def expand_namespace(nsmap, string): + """ If the string starts with a known prefix in nsmap, replace it by full URI + + :param nsmap: Dictionary of prefix -> uri of namespace + :param string: String in which to replace the namespace + :return: Expanded string with no namespace + """ + for ns in nsmap: + if isinstance(string, str) and isinstance(ns, str) and string.startswith(ns+":"): + return string.replace(ns+":", nsmap[ns]) + return string \ No newline at end of file diff --git a/MyCapytain/common/utils/_http.py b/MyCapytain/common/utils/_http.py new file mode 100644 index 00000000..c4e731ca --- /dev/null +++ b/MyCapytain/common/utils/_http.py @@ -0,0 +1,45 @@ +from collections import namedtuple +from urllib.parse import parse_qs, urlparse, urljoin + +import link_header + +_Route = namedtuple("Route", ["path", "query_dict"]) +_Navigation = namedtuple("Navigation", ["prev", "next", "last", "current", "first"]) + + +def parse_pagination(headers): + """ Parses headers to create a pagination objects + + :param headers: HTTP Headers + :type headers: dict + :return: Navigation object for pagination + :rtype: _Navigation + """ + links = { + link.rel: parse_qs(link.href).get("page", None) + for link in link_header.parse(headers.get("Link", "")).links + } + return _Navigation( + links.get("previous", [None])[0], + links.get("next", [None])[0], + links.get("last", [None])[0], + links.get("current", [None])[0], + links.get("first", [None])[0] + ) + + +def parse_uri(uri, endpoint_uri): + """ Parse a URI into a Route namedtuple + + :param uri: URI or relative URI + :type uri: str + :param endpoint_uri: URI of the endpoint + :type endpoint_uri: str + :return: Parsed route + :rtype: _Route + """ + temp_parse = urlparse(uri) + return _Route( + urljoin(endpoint_uri, temp_parse.path), + parse_qs(temp_parse.query) + ) \ No newline at end of file diff --git a/MyCapytain/common/utils/_json_ld.py b/MyCapytain/common/utils/_json_ld.py new file mode 100644 index 00000000..169b3c29 --- /dev/null +++ b/MyCapytain/common/utils/_json_ld.py @@ -0,0 +1,26 @@ +from rdflib import Literal, URIRef + + +def literal_to_dict(value): + """ Transform an object value into a dict readable value + + :param value: Object of a triple which is not a BNode + :type value: Literal or URIRef + :return: dict or str or list + """ + if isinstance(value, Literal): + if value.language is not None: + return {"@value": str(value), "@language": value.language} + return value.toPython() + elif isinstance(value, URIRef): + return {"@id": str(value)} + elif value is None: + return None + return str(value) + + +def dict_to_literal(dict_container: dict): + if isinstance(dict_container["@value"], int): + return dict_container["@value"], + else: + return dict_container["@value"], dict_container.get("@language", None) \ No newline at end of file diff --git a/MyCapytain/common/utils.py b/MyCapytain/common/utils/xml.py similarity index 57% rename from MyCapytain/common/utils.py rename to MyCapytain/common/utils/xml.py index 5322e71d..6c31d309 100644 --- a/MyCapytain/common/utils.py +++ b/MyCapytain/common/utils/xml.py @@ -1,34 +1,16 @@ -# -*- coding: utf-8 -*- -""" -.. module:: MyCapytain.common.utils - :synopsis: Common useful tools - -.. moduleauthor:: Thibault Clérice - - -""" -from __future__ import unicode_literals - -import re -from collections import OrderedDict, defaultdict, namedtuple from copy import copy -from functools import reduce -from io import IOBase, StringIO +from io import StringIO, IOBase +from xml.sax.saxutils import escape -from lxml import etree -from lxml.objectify import ObjectifiedElement, parse, SubElement, Element +from lxml.etree import SubElement +import lxml.etree as etree +from lxml.objectify import ObjectifiedElement, parse, Element from six import text_type -from xml.sax.saxutils import escape -from rdflib import BNode, Graph, Literal, URIRef -from rdflib.namespace import NamespaceManager -from urllib.parse import urlparse, parse_qs, urljoin -import link_header from MyCapytain.common.constants import XPATH_NAMESPACES -from MyCapytain.errors import CapitainsXPathError -__strip = re.compile("([ ]{2,})+") -__parser__ = etree.XMLParser(collect_ids=False, resolve_entities=False) + +_parser = etree.XMLParser(collect_ids=False, resolve_entities=False) def make_xml_node(graph, name, close=False, attributes=None, text="", complete=False, innerXML=""): @@ -74,96 +56,6 @@ def make_xml_node(graph, name, close=False, attributes=None, text="", complete=F return "<{}>".format(name) -def literal_to_dict(value): - """ Transform an object value into a dict readable value - - :param value: Object of a triple which is not a BNode - :type value: Literal or URIRef - :return: dict or str or list - """ - if isinstance(value, Literal): - if value.language is not None: - return {"@value": str(value), "@language": value.language} - return value.toPython() - elif isinstance(value, URIRef): - return {"@id": str(value)} - elif value is None: - return None - return str(value) - - -def dict_to_literal(dict_container: dict): - if isinstance(dict_container["@value"], int): - return dict_container["@value"], - else: - return dict_container["@value"], dict_container.get("@language", None) - - -class Subgraph(object): - """ Utility class to generate subgraph around one or more items - - :param - """ - def __init__(self, bindings: dict = None): - self.graph = Graph() - self.graph.namespace_manager = NamespaceManager(self.graph) - for prefix, namespace in bindings.items(): - self.graph.namespace_manager.bind(prefix, namespace) - - self.downwards = defaultdict(lambda: True) - self.updwards = defaultdict(lambda: True) - - def graphiter(self, graph, target, ascendants=0, descendants=1): - """ Iter on a graph to finds object connected - - :param graph: Graph to serialize - :type graph: Graph - :param target: Node to iterate over - :type target: Node - :param ascendants: Number of level to iter over upwards (-1 = No Limit) - :param descendants: Number of level to iter over downwards (-1 = No limit) - :return: - """ - - asc = 0 + ascendants - if asc != 0: - asc -= 1 - - desc = 0 + descendants - if desc != 0: - desc -= 1 - - t = str(target) - - if descendants != 0 and self.downwards[t] is True: - self.downwards[t] = False - for pred, obj in graph.predicate_objects(target): - if desc == 0 and isinstance(obj, BNode): - continue - self.add((target, pred, obj)) - - # Retrieve triples about the object - if desc != 0 and self.downwards[str(obj)] is True: - self.graphiter(graph, target=obj, ascendants=0, descendants=desc) - - if ascendants != 0 and self.updwards[t] is True: - self.updwards[t] = False - for s, p in graph.subject_predicates(object=target): - if desc == 0 and isinstance(s, BNode): - continue - self.add((s, p, target)) - - # Retrieve triples about the parent as object - if asc != 0 and self.updwards[str(s)] is True: - self.graphiter(graph, target=s, ascendants=asc, descendants=0) - - def serialize(self, *args, **kwargs): - return self.graph.serialize(*args, **kwargs) - - def add(self, *args, **kwargs): - self.graph.add(*args, **kwargs) - - def xmliter(node): """ Provides a simple XML Iter method which complies with either _Element or _ObjectifiedElement @@ -176,23 +68,6 @@ def xmliter(node): return node -def normalize(string): - """ Remove double-or-more spaces in a string - - :param string: A string to change - :type string: text_type - :rtype: text_type - :returns: Clean string - """ - return __strip.sub(" ", string) - -#: Dictionary of namespace that can be useful - -#: Dictionary of RDF Prefixes - -#: Mapping of known domains to RDF Classical Prefixes - - def xmlparser(xml, objectify=True): """ Parse xml @@ -416,105 +291,3 @@ def passageLoop(parent, new_tree, xpath1, xpath2=None, preceding_siblings=False, passageLoop(result_2, node, queue_2, None, preceding_siblings=True) return new_tree - - -class OrderedDefaultDict(OrderedDict): - """ Extension of Default Dict that makes an OrderedDefaultDict - - :param default_factory__: Default class to initiate - """ - - def __init__(self, default_factory=None, *args, **kwargs): - super(OrderedDefaultDict, self).__init__(*args, **kwargs) - self.default_factory = default_factory - - def __missing__(self, key): - if self.default_factory is None: - raise KeyError(key) - val = self[key] = self.default_factory() - return val - - -def nested_ordered_dictionary(): - """ Helper to create a nested ordered default dictionary - - :rtype OrderedDefaultDict: - :return: Nested Ordered Default Dictionary instance - """ - return OrderedDefaultDict(nested_ordered_dictionary) - - -def nested_get(dictionary, keys): - """ Get value in dictionary for dictionary[keys[0]][keys[1]][keys[..n]] - - :param dictionary: An input dictionary - :param keys: Keys where to store data - :return: - """ - return reduce(lambda d, k: d[k], keys, dictionary) - - -def nested_set(dictionary, keys, value): - """ Set value in dictionary for dictionary[keys[0]][keys[1]][keys[..n]] - - :param dictionary: An input dictionary - :param keys: Keys where to store data - :param value: Value to set at keys** target - :return: None - """ - nested_get(dictionary, keys[:-1])[keys[-1]] = value - - -def expand_namespace(nsmap, string): - """ If the string starts with a known prefix in nsmap, replace it by full URI - - :param nsmap: Dictionary of prefix -> uri of namespace - :param string: String in which to replace the namespace - :return: Expanded string with no namespace - """ - for ns in nsmap: - if isinstance(string, str) and isinstance(ns, str) and string.startswith(ns+":"): - return string.replace(ns+":", nsmap[ns]) - return string - - -_Route = namedtuple("Route", ["path", "query_dict"]) -_Navigation = namedtuple("Navigation", ["prev", "next", "last", "current", "first"]) - - -def parse_pagination(headers): - """ Parses headers to create a pagination objects - - :param headers: HTTP Headers - :type headers: dict - :return: Navigation object for pagination - :rtype: _Navigation - """ - links = { - link.rel: parse_qs(link.href).get("page", None) - for link in link_header.parse(headers.get("Link", "")).links - } - return _Navigation( - links.get("previous", [None])[0], - links.get("next", [None])[0], - links.get("last", [None])[0], - links.get("current", [None])[0], - links.get("first", [None])[0] - ) - - -def parse_uri(uri, endpoint_uri): - """ Parse a URI into a Route namedtuple - - :param uri: URI or relative URI - :type uri: str - :param endpoint_uri: URI of the endpoint - :type endpoint_uri: str - :return: Parsed route - :rtype: _Route - """ - temp_parse = urlparse(uri) - return _Route( - urljoin(endpoint_uri, temp_parse.path), - parse_qs(temp_parse.query) - ) diff --git a/MyCapytain/resolvers/cts/local.py b/MyCapytain/resolvers/cts/local.py index 508548ca..e34f4025 100644 --- a/MyCapytain/resolvers/cts/local.py +++ b/MyCapytain/resolvers/cts/local.py @@ -8,7 +8,7 @@ from math import ceil from MyCapytain.common.reference._capitains_cts import CtsReference, URN -from MyCapytain.common.utils import xmlparser +from MyCapytain.common.utils.xml import xmlparser from MyCapytain.errors import InvalidURN, UnknownObjectError, UndispatchedTextError from MyCapytain.resolvers.prototypes import Resolver from MyCapytain.resolvers.utils import CollectionDispatcher diff --git a/MyCapytain/resolvers/dts/__init__.py b/MyCapytain/resolvers/dts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/MyCapytain/resolvers/dts/api_v1.py b/MyCapytain/resolvers/dts/api_v1.py new file mode 100644 index 00000000..0f27f39d --- /dev/null +++ b/MyCapytain/resolvers/dts/api_v1.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +""" +.. module:: MyCapytain.resolvers.cts.remote + :synopsis: Resolver built for CTS APIs + +.. moduleauthor:: Thibault Clérice + + +""" + +from typing import Union, Optional, Any, Dict + +from MyCapytain.resolvers.prototypes import Resolver +from MyCapytain.common.reference import BaseReference, BaseReferenceSet, \ + DtsReference, DtsReferenceSet +from MyCapytain.retrievers.dts import HttpDtsRetriever +from MyCapytain.common.utils import dict_to_literal + +from rdflib import URIRef +from pyld.jsonld import expand + + +def _parse_ref(ref_dict): + if "https://w3id.org/dts/api#ref" in ref_dict: + refs = ref_dict["https://w3id.org/dts/api#"], + elif "https://w3id.org/dts/api#start" in ref_dict and \ + "https://w3id.org/dts/api#end" in ref_dict: + refs = ref_dict["https://w3id.org/dts/api#start"], ref_dict["https://w3id.org/dts/api#end"] + else: + return None # Maybe Raise ? + + obj = DtsReference(*refs) + + for key, value_set in ref_dict.get("https://w3id.org/dts/api#dublincore", [{}])[0].items(): + term = URIRef(key) + for value_dict in value_set: + obj.metadata.add(term, *dict_to_literal(value_dict)) + + for key, value_set in ref_dict.get("https://w3id.org/dts/api#extensions", [{}])[0].items(): + term = URIRef(key) + for value_dict in value_set: + obj.metadata.add(term, *dict_to_literal(value_dict)) + + return obj + + +class HttpDtsResolver(Resolver): + """ HttpDtsResolver provide a resolver for DTS API http endpoint. + + :param endpoint: DTS API Retriever + """ + def __init__(self, endpoint: Union[str, HttpDtsRetriever]): + if not isinstance(endpoint, HttpDtsRetriever): + endpoint = HttpDtsRetriever(endpoint) + self._endpoint = endpoint + + @property + def endpoint(self) -> HttpDtsRetriever: + """ DTS Endpoint of the resolver + + :return: DTS Endpoint + :rtype: HttpDtsRetriever + """ + return self._endpoint + + def getReffs( + self, + textId: str, + level: int=1, + subreference: Union[str, BaseReference]=None, + include_descendants: bool=False, + additional_parameters: Optional[Dict[str, Any]]=None + ) -> DtsReferenceSet: + if not additional_parameters: + additional_parameters = {} + + reffs = [] + response = self.endpoint.get_navigation( + textId, level=level, ref=subreference, + group_size=additional_parameters.get("group_by", 1), + exclude=additional_parameters.get("exclude", None) + ) + response.raise_for_status() + + data = response.json() + data = expand(data) + members = data[0].get("https://w3id.org/dts/api#member", None) + + reffs.extend([ + _parse_ref(ref) + for ref in members + ]) + + reffs = DtsReferenceSet(*reffs) + diff --git a/MyCapytain/resources/collections/cts.py b/MyCapytain/resources/collections/cts.py index 870f49e9..feb25337 100644 --- a/MyCapytain/resources/collections/cts.py +++ b/MyCapytain/resources/collections/cts.py @@ -15,7 +15,8 @@ from MyCapytain.resources.prototypes.cts import inventory as cts from MyCapytain.common.reference._capitains_cts import Citation as CitationPrototype -from MyCapytain.common.utils import xmlparser, expand_namespace +from MyCapytain.common.utils import expand_namespace +from MyCapytain.common.utils.xml import xmlparser from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes, RDF_NAMESPACES diff --git a/MyCapytain/resources/prototypes/cts/inventory.py b/MyCapytain/resources/prototypes/cts/inventory.py index c763b3e6..9b167b84 100644 --- a/MyCapytain/resources/prototypes/cts/inventory.py +++ b/MyCapytain/resources/prototypes/cts/inventory.py @@ -11,7 +11,7 @@ from MyCapytain.resources.prototypes.metadata import Collection, ResourceCollection from MyCapytain.common.reference._capitains_cts import URN -from MyCapytain.common.utils import make_xml_node, xmlparser +from MyCapytain.common.utils.xml import make_xml_node, xmlparser from MyCapytain.common.constants import RDF_NAMESPACES, Mimetypes from MyCapytain.errors import InvalidURN from collections import defaultdict diff --git a/MyCapytain/resources/prototypes/metadata.py b/MyCapytain/resources/prototypes/metadata.py index 2507bb6d..e40ad656 100644 --- a/MyCapytain/resources/prototypes/metadata.py +++ b/MyCapytain/resources/prototypes/metadata.py @@ -9,7 +9,8 @@ from MyCapytain.common.metadata import Metadata from MyCapytain.errors import UnknownCollection -from MyCapytain.common.utils import Subgraph, literal_to_dict +from MyCapytain.common.utils import literal_to_dict +from MyCapytain.common.utils import Subgraph from MyCapytain.common.constants import RDF_NAMESPACES, RDFLIB_MAPPING, Mimetypes, get_graph from MyCapytain.common.base import Exportable from MyCapytain.common.reference import BaseCitationSet diff --git a/MyCapytain/resources/texts/base/tei.py b/MyCapytain/resources/texts/base/tei.py index ae476111..d411c512 100644 --- a/MyCapytain/resources/texts/base/tei.py +++ b/MyCapytain/resources/texts/base/tei.py @@ -9,7 +9,8 @@ from lxml.etree import tostring from MyCapytain.common.constants import Mimetypes, XPATH_NAMESPACES -from MyCapytain.common.utils import xmlparser, nested_ordered_dictionary, nested_set, normalize +from MyCapytain.common.utils import nested_ordered_dictionary, nested_set, normalize +from MyCapytain.common.utils.xml import xmlparser from MyCapytain.resources.prototypes.text import InteractiveTextualNode diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index fc4b02a0..547df0a4 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -12,7 +12,7 @@ import warnings from MyCapytain.errors import DuplicateReference, MissingAttribute, RefsDeclError, EmptyReference, CitationDepthError, MissingRefsDecl -from MyCapytain.common.utils import copyNode, passageLoop, normalizeXpath +from MyCapytain.common.utils.xml import copyNode, normalizeXpath, passageLoop from MyCapytain.common.constants import XPATH_NAMESPACES, RDF_NAMESPACES from MyCapytain.common.reference import CtsReference, URN, Citation, CtsReferenceSet diff --git a/MyCapytain/resources/texts/remote/cts.py b/MyCapytain/resources/texts/remote/cts.py index 4dbfaa96..d8f4d0e3 100644 --- a/MyCapytain/resources/texts/remote/cts.py +++ b/MyCapytain/resources/texts/remote/cts.py @@ -11,7 +11,7 @@ from __future__ import unicode_literals from MyCapytain.common.metadata import Metadata -from MyCapytain.common.utils import xmlparser +from MyCapytain.common.utils.xml import xmlparser from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes, RDF_NAMESPACES from MyCapytain.common.reference._capitains_cts import CtsReference, URN from MyCapytain.resources.collections import cts as CtsCollection diff --git a/requirements.txt b/requirements.txt index 0b17d5d9..7bfcd45c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ rdflib-jsonld>=0.4.0 responses>=0.8.1 LinkHeader==0.4.3 pyld==1.0.3 -typing \ No newline at end of file +typing +requests_mock \ No newline at end of file diff --git a/tests/common/test_reference/test_capitains_cts.py b/tests/common/test_reference/test_capitains_cts.py index 6f997e8e..e2c4adbd 100644 --- a/tests/common/test_reference/test_capitains_cts.py +++ b/tests/common/test_reference/test_capitains_cts.py @@ -1,7 +1,7 @@ import unittest from six import text_type as str -from MyCapytain.common.utils import xmlparser +from MyCapytain.common.utils.xml import xmlparser from MyCapytain.common.reference import CtsReference, URN, Citation diff --git a/tests/common/test_utils.py b/tests/common/test_utils.py index 7d743c96..284aaa2b 100644 --- a/tests/common/test_utils.py +++ b/tests/common/test_utils.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from six import text_type as str import unittest -from MyCapytain.common.utils import * -from copy import copy as deepcopy + +from MyCapytain.common.utils.xml import normalizeXpath class TestUtils(unittest.TestCase): diff --git a/tests/resolvers/cts/test_api.py b/tests/resolvers/cts/test_api.py index c7eed630..cd9485b9 100644 --- a/tests/resolvers/cts/test_api.py +++ b/tests/resolvers/cts/test_api.py @@ -1,6 +1,6 @@ from MyCapytain.resolvers.cts.api import HttpCtsResolver from MyCapytain.retrievers.cts5 import HttpCtsRetriever -from MyCapytain.common.utils import xmlparser +from MyCapytain.common.utils.xml import xmlparser from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes from MyCapytain.resources.prototypes.text import Passage from MyCapytain.resources.collections.cts import XmlCtsTextInventoryMetadata, XmlCtsTextgroupMetadata, XmlCtsWorkMetadata, XmlCtsTextMetadata diff --git a/tests/resolvers/dts/__init__.py b/tests/resolvers/dts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/resolvers/dts/api_v1/__init__.py b/tests/resolvers/dts/api_v1/__init__.py new file mode 100644 index 00000000..7891ae2c --- /dev/null +++ b/tests/resolvers/dts/api_v1/__init__.py @@ -0,0 +1,60 @@ +import os.path +import json +import typing +import unittest +import requests_mock +from MyCapytain.resolvers.dts.api_v1 import HttpDtsResolver + + +# Set-up for the test classes +_cur_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__) + ) +) + + +def quote(string): + return string.replace(":", "%3A") + + +def _load_mock(*files: str) -> str: + """ Easily load a mock file + + :param endpoint: Endpoint that is being tested + :param example: Example to load + :return: Example data + """ + fname = os.path.abspath( + os.path.join( + _cur_path, + "data", + *files + ) + ) + with open(fname) as fopen: + data = fopen.read() + return data + + +def _load_json_mock(endpoint: str, example: str) -> typing.Union[dict, list]: + return json.loads(_load_mock(endpoint, example)) + + +class TestHttpDtsResolver(unittest.TestCase): + def setUp(self): + self.root_uri = "http://foobar.com/api/dts" + self.resolver = HttpDtsResolver(self.root_uri) + + @requests_mock.mock() + def test_navigation_simple(self, mock_set): + _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?level=1&groupSize=1&id="+_id, + text=_load_mock("navigation", "example1.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id) + print(reffs) + diff --git a/tests/resolvers/dts/api_v1/data/navigation/example1.json b/tests/resolvers/dts/api_v1/data/navigation/example1.json new file mode 100644 index 00000000..e74d9023 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/navigation/example1.json @@ -0,0 +1,15 @@ +{ + "@context": { + "hydra": "https://www.w3.org/ns/hydra/core#", + "@vocab": "https://w3id.org/dts/api#" + }, + "@id":"/api/dts/navigation/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc", + "citeDepth" : 2, + "level": 1, + "hydra:member": [ + {"ref": "1"}, + {"ref": "2"}, + {"ref": "3"} + ], + "passage": "/dts/api/document/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc{&ref}{&start}{&end}" +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/root.json b/tests/resolvers/dts/api_v1/data/root.json new file mode 100644 index 00000000..233880cc --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/root.json @@ -0,0 +1,8 @@ +{ + "@context": "/api/dts/contexts/EntryPoint.jsonld", + "@id": "/api/dts/", + "@type": "EntryPoint", + "collections": "/api/dts/collections/", + "documents": "/api/dts/documents/", + "navigation" : "/api/dts/navigation" +} \ No newline at end of file diff --git a/tests/resources/collections/test_cts.py b/tests/resources/collections/test_cts.py index cb9b2aea..ed710c15 100644 --- a/tests/resources/collections/test_cts.py +++ b/tests/resources/collections/test_cts.py @@ -5,14 +5,12 @@ from io import open, StringIO from operator import attrgetter -from rdflib import Literal, URIRef - import lxml.etree as etree import xmlunittest +from MyCapytain.common import constants from MyCapytain.resources.collections.cts import * from MyCapytain.resources.prototypes.text import CtsNode -from MyCapytain.common import constants class XML_Compare(object): diff --git a/tests/resources/texts/base/test_tei.py b/tests/resources/texts/base/test_tei.py index e7d1d765..a391f425 100644 --- a/tests/resources/texts/base/test_tei.py +++ b/tests/resources/texts/base/test_tei.py @@ -6,7 +6,7 @@ from MyCapytain.common.reference._capitains_cts import CtsReference, Citation from MyCapytain.resources.texts.base.tei import TEIResource from MyCapytain.common.constants import Mimetypes -from MyCapytain.common.utils import xmlparser +from MyCapytain.common.utils.xml import xmlparser class TestTEICitation(unittest.TestCase): diff --git a/tests/resources/texts/local/test_capitains_xml_notObjectified.py b/tests/resources/texts/local/test_capitains_xml_notObjectified.py index c79b5423..a325dfb4 100644 --- a/tests/resources/texts/local/test_capitains_xml_notObjectified.py +++ b/tests/resources/texts/local/test_capitains_xml_notObjectified.py @@ -12,7 +12,7 @@ import MyCapytain.errors import MyCapytain.resources.texts.base.tei import MyCapytain.resources.texts.local.capitains.cts -from MyCapytain.common.utils import xmlparser +from MyCapytain.common.utils.xml import xmlparser from tests.resources.texts.local.commonTests import CapitainsXmlTextTest, CapitainsXmlPassageTests, CapitainsXMLRangePassageTests diff --git a/tests/resources/texts/remote/test_cts.py b/tests/resources/texts/remote/test_cts.py index e21c8b1d..3249998d 100644 --- a/tests/resources/texts/remote/test_cts.py +++ b/tests/resources/texts/remote/test_cts.py @@ -9,7 +9,7 @@ from MyCapytain.retrievers.cts5 import HttpCtsRetriever from MyCapytain.common.reference._capitains_cts import CtsReference, URN, Citation from MyCapytain.common.metadata import Metadata -from MyCapytain.common.utils import xmlparser +from MyCapytain.common.utils.xml import xmlparser from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes, RDF_NAMESPACES from MyCapytain.errors import MissingAttribute import mock diff --git a/tests/retrievers/test_dts.py b/tests/retrievers/test_dts.py index 1f1ba88d..c95ae4a5 100644 --- a/tests/retrievers/test_dts.py +++ b/tests/retrievers/test_dts.py @@ -3,9 +3,8 @@ import responses from MyCapytain.retrievers.dts import HttpDtsRetriever -from MyCapytain.common.utils import _Navigation +from MyCapytain.common.utils._http import _Navigation, parse_pagination from urllib.parse import parse_qs, urlparse, urljoin -from MyCapytain.common.utils import parse_pagination _SERVER_URI = "http://domainname.com/api/dts/" patch_args = ("MyCapytain.retrievers.dts.requests.get", ) From a7a760a9b04e461968056d56104e12f8d52d073c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Fri, 14 Sep 2018 16:54:03 +0900 Subject: [PATCH 48/89] (DTS Resolver) Added getReffs with full tests. --- MyCapytain/common/metadata.py | 12 + MyCapytain/common/reference/_base.py | 11 +- MyCapytain/common/reference/_dts_1.py | 52 +++- MyCapytain/common/utils/_dts.py | 0 MyCapytain/common/utils/dts.py | 21 ++ MyCapytain/resolvers/dts/api_v1.py | 54 ++-- MyCapytain/resources/collections/dts.py | 14 +- MyCapytain/retrievers/dts/__init__.py | 17 +- tests/resolvers/dts/api_v1/__init__.py | 264 +++++++++++++++++- .../dts/api_v1/data/navigation/example1.json | 1 + .../dts/api_v1/data/navigation/example2.json | 18 ++ .../dts/api_v1/data/navigation/example3.json | 14 + .../dts/api_v1/data/navigation/example4.json | 17 ++ .../dts/api_v1/data/navigation/example5.json | 15 + .../dts/api_v1/data/navigation/example6.json | 19 ++ .../dts/api_v1/data/navigation/example7.json | 16 ++ .../dts/api_v1/data/navigation/example8.json | 20 ++ .../dts/api_v1/data/navigation/example9.json | 56 ++++ 18 files changed, 574 insertions(+), 47 deletions(-) delete mode 100644 MyCapytain/common/utils/_dts.py create mode 100644 MyCapytain/common/utils/dts.py create mode 100644 tests/resolvers/dts/api_v1/data/navigation/example2.json create mode 100644 tests/resolvers/dts/api_v1/data/navigation/example3.json create mode 100644 tests/resolvers/dts/api_v1/data/navigation/example4.json create mode 100644 tests/resolvers/dts/api_v1/data/navigation/example5.json create mode 100644 tests/resolvers/dts/api_v1/data/navigation/example6.json create mode 100644 tests/resolvers/dts/api_v1/data/navigation/example7.json create mode 100644 tests/resolvers/dts/api_v1/data/navigation/example8.json create mode 100644 tests/resolvers/dts/api_v1/data/navigation/example9.json diff --git a/MyCapytain/common/metadata.py b/MyCapytain/common/metadata.py index 7fcdca5e..98102083 100644 --- a/MyCapytain/common/metadata.py +++ b/MyCapytain/common/metadata.py @@ -12,6 +12,7 @@ from MyCapytain.common.constants import Mimetypes, get_graph from MyCapytain.common.base import Exportable from rdflib import BNode, Literal, Graph, URIRef, term +from typing import Union class Metadata(Exportable): @@ -47,6 +48,17 @@ def graph(self): """ return self.__graph__ + def set(self, key: URIRef, value: Union[Literal, BNode, URIRef, str, int], lang: str=None): + if not isinstance(value, Literal) and lang is not None: + value = Literal(value, lang=lang) + elif not isinstance(value, (BNode, URIRef)): + value, _type = term._castPythonToLiteral(value) + if _type is None: + value = Literal(value) + else: + value = Literal(value, datatype=_type) + self.graph.set((self.asNode(), key, value)) + def add(self, key, value, lang=None): """ Add a triple to the graph related to this node diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index 693ed9c8..90ab3000 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -319,6 +319,7 @@ class BaseReference(tuple): def __new__(cls, *refs): if len(refs) == 1 and not isinstance(refs[0], tuple): refs = refs[0], None + obj = tuple.__new__(cls, refs) return obj @@ -358,7 +359,7 @@ def __new__(cls, *refs, citation: BaseCitationSet=None, level: int=1): obj._citation = None obj._level = level - if citation: + if citation is not None: obj._citation = citation return obj @@ -370,6 +371,14 @@ def citation(self) -> BaseCitationSet: def level(self) -> int: return self._level + def __repr__(self): + return "<{typ} ({repr}) level:{level}, citation:{citation}>".format( + typ=type(self).__name__, + repr=", ".join([str(s) for s in self]), + level=self.level, + citation=str(self.citation) + ) + class NodeId(object): """ Collection of directional references for a Tree diff --git a/MyCapytain/common/reference/_dts_1.py b/MyCapytain/common/reference/_dts_1.py index 3c03f88d..0fa9b9fe 100644 --- a/MyCapytain/common/reference/_dts_1.py +++ b/MyCapytain/common/reference/_dts_1.py @@ -9,24 +9,72 @@ class DtsReference(BaseReference): - def __new__(cls, *refs, metadata: Metadata=None): - o = super(DtsReference).__new__(*refs) + def __new__(cls, *refs, metadata: Metadata=None, type_: str=None): + o = BaseReference.__new__(cls, *refs) if metadata: o._metadata = metadata else: o._metadata = Metadata() # toDo : Figure how to deal with Refs ID in the Sparql Graph + if type_: + o.type = type_ + return o + + @property + def metadata(self) -> Metadata: + return self._metadata + + @property + def type(self): + return self.metadata.get_single(_dts.term("citeType")) + + @type.setter + def type(self, value): + self.metadata.set(_dts.term("citeType"), value) + + def __eq__(self, other): + return super(DtsReference, self).__eq__(other) and \ + isinstance(other, DtsReference) and \ + self.type == other.type + + def __repr__(self): + return " [{}]>".format( + "><".join([str(x) for x in self if x]), + self.type + ) + class DtsReferenceSet(BaseReferenceSet): def __contains__(self, item: str) -> bool: return BaseReferenceSet.__contains__(self, item) or \ BaseReferenceSet.__contains__(self, DtsReference(item)) + def __eq__(self, other): + return super(DtsReferenceSet, self).__eq__(other) and \ + self.level == other.level and \ + isinstance(other, DtsReferenceSet) and \ + self.citation == other.citation + class DtsCitation(BaseCitation): def __init__(self, name=None, children=None, root=None): super(DtsCitation, self).__init__(name=name, children=children, root=root) + def __eq__(self, other): + return isinstance(other, BaseCitation) and \ + self.name == other.name and \ + self.children == other.children and \ + ( + ( + not self.is_root() and + not other.is_root() and + self.root == other.root + ) + or ( + self.is_root() and other.is_root() + ) + ) + @classmethod def ingest(cls, resource, root=None, **kwargs): """ Ingest a dictionary of DTS Citation object (as parsed JSON-LD) and diff --git a/MyCapytain/common/utils/_dts.py b/MyCapytain/common/utils/_dts.py deleted file mode 100644 index e69de29b..00000000 diff --git a/MyCapytain/common/utils/dts.py b/MyCapytain/common/utils/dts.py new file mode 100644 index 00000000..80a2b9bb --- /dev/null +++ b/MyCapytain/common/utils/dts.py @@ -0,0 +1,21 @@ +from ._json_ld import dict_to_literal +from rdflib import URIRef +from ..metadata import Metadata + + +def parse_metadata(metadata_obj: Metadata, metadata_dictionary: dict) -> None: + """ Adds to a Metadata object any DublinCore or dts:Extensions object + found in the given dictionary + + :param metadata_obj: + :param metadata_dictionary: + """ + for key, value_set in metadata_dictionary.get("https://w3id.org/dts/api#dublincore", [{}])[0].items(): + term = URIRef(key) + for value_dict in value_set: + metadata_obj.add(term, *dict_to_literal(value_dict)) + + for key, value_set in metadata_dictionary.get("https://w3id.org/dts/api#extensions", [{}])[0].items(): + term = URIRef(key) + for value_dict in value_set: + metadata_obj.add(term, *dict_to_literal(value_dict)) diff --git a/MyCapytain/resolvers/dts/api_v1.py b/MyCapytain/resolvers/dts/api_v1.py index 0f27f39d..a15d8d49 100644 --- a/MyCapytain/resolvers/dts/api_v1.py +++ b/MyCapytain/resolvers/dts/api_v1.py @@ -12,34 +12,33 @@ from MyCapytain.resolvers.prototypes import Resolver from MyCapytain.common.reference import BaseReference, BaseReferenceSet, \ - DtsReference, DtsReferenceSet + DtsReference, DtsReferenceSet, DtsCitation from MyCapytain.retrievers.dts import HttpDtsRetriever -from MyCapytain.common.utils import dict_to_literal +from MyCapytain.common.utils.dts import parse_metadata -from rdflib import URIRef from pyld.jsonld import expand -def _parse_ref(ref_dict): +_empty = [{"@value": None}] + + +def _parse_ref(ref_dict, default_type :str =None): if "https://w3id.org/dts/api#ref" in ref_dict: - refs = ref_dict["https://w3id.org/dts/api#"], + refs = ref_dict["https://w3id.org/dts/api#ref"][0]["@value"], elif "https://w3id.org/dts/api#start" in ref_dict and \ "https://w3id.org/dts/api#end" in ref_dict: - refs = ref_dict["https://w3id.org/dts/api#start"], ref_dict["https://w3id.org/dts/api#end"] + refs = ( + ref_dict["https://w3id.org/dts/api#start"][0]["@value"], + ref_dict["https://w3id.org/dts/api#end"][0]["@value"] + ) else: return None # Maybe Raise ? + type_ = default_type + if "https://w3id.org/dts/api#citeType" in ref_dict: + type_ = ref_dict["https://w3id.org/dts/api#citeType"][0]["@value"] - obj = DtsReference(*refs) - - for key, value_set in ref_dict.get("https://w3id.org/dts/api#dublincore", [{}])[0].items(): - term = URIRef(key) - for value_dict in value_set: - obj.metadata.add(term, *dict_to_literal(value_dict)) - - for key, value_set in ref_dict.get("https://w3id.org/dts/api#extensions", [{}])[0].items(): - term = URIRef(key) - for value_dict in value_set: - obj.metadata.add(term, *dict_to_literal(value_dict)) + obj = DtsReference(*refs, type_=type_) + parse_metadata(obj.metadata, ref_dict) return obj @@ -77,19 +76,30 @@ def getReffs( reffs = [] response = self.endpoint.get_navigation( textId, level=level, ref=subreference, - group_size=additional_parameters.get("group_by", 1), - exclude=additional_parameters.get("exclude", None) + exclude=additional_parameters.get("exclude", None), + group_by=additional_parameters.get("groupBy", 1) ) response.raise_for_status() data = response.json() data = expand(data) - members = data[0].get("https://w3id.org/dts/api#member", None) + + default_type = data[0].get("https://w3id.org/dts/api#citeType", _empty)[0]["@value"] + + members = data[0].get("https://www.w3.org/ns/hydra/core#member", []) reffs.extend([ - _parse_ref(ref) + _parse_ref(ref, default_type=default_type) for ref in members ]) - reffs = DtsReferenceSet(*reffs) + citation = None + if default_type: + citation = DtsCitation(name=default_type) + reffs = DtsReferenceSet( + *reffs, + level=data[0].get("https://w3id.org/dts/api#level", _empty)[0]["@value"], + citation=citation + ) + return reffs diff --git a/MyCapytain/resources/collections/dts.py b/MyCapytain/resources/collections/dts.py index d295703c..72b9eaa5 100644 --- a/MyCapytain/resources/collections/dts.py +++ b/MyCapytain/resources/collections/dts.py @@ -2,9 +2,9 @@ from MyCapytain.errors import JsonLdCollectionMissing from MyCapytain.common.reference import DtsCitationSet from MyCapytain.common.constants import RDF_NAMESPACES -from MyCapytain.common.utils import dict_to_literal +from MyCapytain.common.utils.dts import parse_metadata + -from rdflib import URIRef from pyld import jsonld @@ -84,15 +84,7 @@ def parse(cls, resource, direction="children"): for val_dict in collection.get(str(_hyd.description), []): obj.metadata.add(_hyd.description, val_dict["@value"], None) - for key, value_set in collection.get(str(_dts.dublincore), _empty_extensions)[0].items(): - term = URIRef(key) - for value_dict in value_set: - obj.metadata.add(term, *dict_to_literal(value_dict)) - - for key, value_set in collection.get(str(_dts.extensions), _empty_extensions)[0].items(): - term = URIRef(key) - for value_dict in value_set: - obj.metadata.add(term, *dict_to_literal(value_dict)) + parse_metadata(obj.metadata, collection) for member in collection.get(str(_hyd.member), []): subcollection = cls.parse(member) diff --git a/MyCapytain/retrievers/dts/__init__.py b/MyCapytain/retrievers/dts/__init__.py index 342ae570..c5b2f133 100644 --- a/MyCapytain/retrievers/dts/__init__.py +++ b/MyCapytain/retrievers/dts/__init__.py @@ -9,6 +9,7 @@ """ import MyCapytain.retrievers.prototypes from MyCapytain import __version__ +from MyCapytain.common.reference import BaseReference import requests from MyCapytain.common.utils import parse_uri @@ -101,15 +102,14 @@ def get_collection(self, collection_id=None, nav="children", page=None): ) def get_navigation( - self, collection_id, - level=None, ref=None, group_size=None, max_=None, exclude=None, - page=None): + self, collection_id, level=None, ref=None, + group_by=None, max_=None, exclude=None, page=None): """ Make a navigation request on the DTS API :param collection_id: Id of the collection :param level: Lever at which the references should be listed :param ref: If ref is a tuple, it is treated as a range. String or int are treated as single ref - :param group_size: Size of the ranges the server should produce + :param group_by: Size of the ranges the server should produce :param max_: Maximum number of results :param exclude: Exclude specific metadata. :param page: Page @@ -119,13 +119,16 @@ def get_navigation( parameters = { "id": collection_id, "level": level, - "groupSize": group_size, + "groupBy": group_by, "max": max_, "exclude": exclude, "page": page } - if isinstance(ref, tuple): - parameters["start"], parameters["end"] = ref + if isinstance(ref, BaseReference): + if ref.is_range(): + parameters["start"], parameters["end"] = ref + else: + parameters["ref"] = ref.start elif ref: parameters["ref"] = ref diff --git a/tests/resolvers/dts/api_v1/__init__.py b/tests/resolvers/dts/api_v1/__init__.py index 7891ae2c..53b93488 100644 --- a/tests/resolvers/dts/api_v1/__init__.py +++ b/tests/resolvers/dts/api_v1/__init__.py @@ -4,8 +4,10 @@ import unittest import requests_mock from MyCapytain.resolvers.dts.api_v1 import HttpDtsResolver - - +from MyCapytain.common.reference import DtsReferenceSet, DtsReference, DtsCitation +from MyCapytain.common.metadata import Metadata +from MyCapytain.common.constants import Mimetypes +from rdflib.term import URIRef # Set-up for the test classes _cur_path = os.path.abspath( os.path.join( @@ -48,13 +50,267 @@ def setUp(self): @requests_mock.mock() def test_navigation_simple(self, mock_set): + """ Example 1 of Public Draft. Includes on top of it a citeType=poem""" _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" mock_set.get(self.root_uri, text=_load_mock("root.json")) mock_set.get( - self.root_uri+"/navigation?level=1&groupSize=1&id="+_id, + self.root_uri+"/navigation?level=1&groupBy=1&id="+_id, text=_load_mock("navigation", "example1.json"), complete_qs=True ) reffs = self.resolver.getReffs(_id) - print(reffs) + self.assertEqual( + DtsReferenceSet( + DtsReference("1", type_="poem"), + DtsReference("2", type_="poem"), + DtsReference("3", type_="poem"), + level=1, + citation=DtsCitation("poem") + ), + reffs, + "References are parsed with types and level" + ) + + @requests_mock.mock() + def test_navigation_simple_level(self, mock_set): + """ Test navigation second level """ + _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?level=2&groupBy=1&id="+_id, + text=_load_mock("navigation", "example2.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id, level=2) + + self.assertEqual( + DtsReferenceSet( + DtsReference("1.1"), + DtsReference("1.2"), + DtsReference("2.1"), + DtsReference("2.2"), + DtsReference("3.1"), + DtsReference("3.2"), + level=2 + ), + reffs, + "Resolver forwards level=2 parameter" + ) + + @requests_mock.mock() + def test_navigation_simple_ref_deeper_level(self, mock_set): + """ Test navigation second level from single reff """ + _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?ref=1&level=1&groupBy=1&id="+_id, + text=_load_mock("navigation", "example3.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id, subreference="1") + + self.assertEqual( + DtsReferenceSet( + DtsReference("1.1"), + DtsReference("1.2"), + level=2 + ), + reffs, + "Resolver forwards subreference parameter" + ) + + @requests_mock.mock() + def test_navigation_simple_level_ref(self, mock_set): + """ Test navigation 2 level deeper than requested reff""" + _id = "urn:cts:latinLit:phi1294.phi001.perseus-lat2" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?ref=1&level=2&groupBy=1&id="+_id, + text=_load_mock("navigation", "example4.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id, subreference="1", level=2) + + self.assertEqual( + DtsReferenceSet( + DtsReference("1.1.1"), + DtsReference("1.1.2"), + DtsReference("1.2.1"), + DtsReference("1.2.2"), + level=3 + ), + reffs, + "Resolver forwards level + subreference parameter correctly" + ) + + @requests_mock.mock() + def test_navigation_ask_in_range(self, mock_set): + """ Test that ranges are correctly requested""" + _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?start=1&end=3&level=1&groupBy=1&id="+_id, + text=_load_mock("navigation", "example5.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id, subreference=DtsReference(1, 3)) + + self.assertEqual( + DtsReferenceSet( + DtsReference("1"), + DtsReference("2"), + DtsReference("3"), + level=1 + ), + reffs, + "Resolver handles range reference as subreference parameter" + ) + + @requests_mock.mock() + def test_navigation_ask_in_range_with_level(self, mock_set): + """ Test that ranges are correctly requested""" + _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?start=1&end=3&level=2&groupBy=1&id="+_id, + text=_load_mock("navigation", "example6.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id, subreference=DtsReference(1, 3), level=2) + + self.assertEqual( + DtsReferenceSet( + DtsReference("1.1"), + DtsReference("1.2"), + DtsReference("2.1"), + DtsReference("2.2"), + DtsReference("3.1"), + DtsReference("3.2"), + level=2 + ), + reffs, + "Resolver forwards correctly range subreference and level" + ) + + @requests_mock.mock() + def test_navigation_with_level_and_group(self, mock_set): + """ Test that ranges are correctly parsed""" + _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?ref=1&level=2&groupBy=2&id="+_id, + text=_load_mock("navigation", "example7.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs( + _id, additional_parameters={"groupBy": 2}, + subreference=DtsReference(1), level=2 + ) + + self.assertEqual( + DtsReferenceSet( + DtsReference("1.1.1", "1.1.2"), + DtsReference("1.2.1", "1.2.2"), + level=3 + ), + reffs, + "groupBy additional parameter is correctly forward + ranges are parsed" + ) + + @requests_mock.mock() + def test_navigation_with_different_types(self, mock_set): + """ Test a navigation where a root type of passage is defined but some are redefined """ + _id = "http://data.bnf.fr/ark:/12148/cb11936111v" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?level=1&groupBy=1&id="+_id, + text=_load_mock("navigation", "example8.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id) + + self.assertEqual( + DtsReferenceSet( + DtsReference("Av", type_="preface"), + DtsReference("Pr", type_="preface"), + DtsReference("1", type_="letter"), + DtsReference("2", type_="letter"), + DtsReference("3", type_="letter"), + level=1, + citation=DtsCitation(name="letter") + ), + reffs, + "Test that default Reference type is overridden when due" + ) + + @requests_mock.mock() + def test_navigation_with_advanced_metadata(self, mock_set): + """ Test references parsing with advanced metadata """ + _id = "http://data.bnf.fr/ark:/12148/cb11936111v" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?level=1&groupBy=1&id="+_id, + text=_load_mock("navigation", "example9.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id) + + self.assertEqual( + DtsReferenceSet( + DtsReference("Av"), + DtsReference("Pr"), + DtsReference("1"), + DtsReference("2"), + DtsReference("3"), + level=1 + ), + reffs, + "Test that default Reference type is overridden when due" + ) + + ref_Av_metadata = Metadata() + ref_Av_metadata.add(URIRef("http://purl.org/dc/terms/title"), "Avertissement de l'Éditeur") + self.assertEqual( + ref_Av_metadata.export(Mimetypes.JSON.Std), + reffs[0].metadata.export(Mimetypes.JSON.Std), + "References metadata should be parsed correctly" + ) + + ref_Pr_metadata = Metadata() + ref_Pr_metadata.add(URIRef("http://purl.org/dc/terms/title"), "Préface") + self.assertEqual( + ref_Pr_metadata.export(Mimetypes.JSON.Std), + reffs[1].metadata.export(Mimetypes.JSON.Std), + "References metadata should be parsed correctly" + ) + + ref_1_metadata = Metadata() + ref_1_metadata.add(URIRef("http://purl.org/dc/terms/title"), "Lettre 1") + ref_1_metadata.add(URIRef("http://foo.bar/ontology#fictionalSender"), "Cécile Volanges") + ref_1_metadata.add(URIRef("http://foo.bar/ontology#fictionalRecipient"), "Sophie Carnay") + self.assertEqual( + ref_1_metadata.export(Mimetypes.JSON.Std), + reffs[2].metadata.export(Mimetypes.JSON.Std), + "References metadata should be parsed correctly" + ) + + ref_2_metadata = Metadata() + ref_2_metadata.add(URIRef("http://purl.org/dc/terms/title"), "Lettre 2") + ref_2_metadata.add(URIRef("http://foo.bar/ontology#fictionalSender"), "La Marquise de Merteuil") + ref_2_metadata.add(URIRef("http://foo.bar/ontology#fictionalRecipient"), "Vicomte de Valmont") + self.assertEqual( + ref_2_metadata.export(Mimetypes.JSON.Std), + reffs[3].metadata.export(Mimetypes.JSON.Std), + "References metadata should be parsed correctly" + ) + + ref_3_metadata = Metadata() + ref_3_metadata.add(URIRef("http://purl.org/dc/terms/title"), "Lettre 3") + ref_3_metadata.add(URIRef("http://foo.bar/ontology#fictionalSender"), "Cécile Volanges") + ref_3_metadata.add(URIRef("http://foo.bar/ontology#fictionalRecipient"), "Sophie Carnay") + self.assertEqual( + ref_3_metadata.export(Mimetypes.JSON.Std), + reffs[4].metadata.export(Mimetypes.JSON.Std), + "References metadata should be parsed correctly" + ) diff --git a/tests/resolvers/dts/api_v1/data/navigation/example1.json b/tests/resolvers/dts/api_v1/data/navigation/example1.json index e74d9023..98bf17a3 100644 --- a/tests/resolvers/dts/api_v1/data/navigation/example1.json +++ b/tests/resolvers/dts/api_v1/data/navigation/example1.json @@ -5,6 +5,7 @@ }, "@id":"/api/dts/navigation/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc", "citeDepth" : 2, + "citeType": "poem", "level": 1, "hydra:member": [ {"ref": "1"}, diff --git a/tests/resolvers/dts/api_v1/data/navigation/example2.json b/tests/resolvers/dts/api_v1/data/navigation/example2.json new file mode 100644 index 00000000..c696c1c9 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/navigation/example2.json @@ -0,0 +1,18 @@ +{ + "@context": { + "hyd": "https://www.w3.org/ns/hydra/core#", + "@vocab": "https://w3id.org/dts/api#" + }, + "@id":"/api/dts/navigation/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc&level=2", + "citeDepth" : 2, + "level": 2, + "hyd:member": [ + {"ref": "1.1"}, + {"ref": "1.2"}, + {"ref": "2.1"}, + {"ref": "2.2"}, + {"ref": "3.1"}, + {"ref": "3.2"} + ], + "passage": "/dts/api/document/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc{&ref}{&start}{&end}" +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/navigation/example3.json b/tests/resolvers/dts/api_v1/data/navigation/example3.json new file mode 100644 index 00000000..3c300f39 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/navigation/example3.json @@ -0,0 +1,14 @@ +{ + "@context": { + "hyd": "https://www.w3.org/ns/hydra/core#", + "@vocab": "https://w3id.org/dts/api#" + }, + "@id":"/api/dts/navigation/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc&ref=1", + "citeDepth" : 2, + "level": 2, + "hyd:member": [ + {"ref": "1.1"}, + {"ref": "1.2"} + ], + "passage": "/dts/api/document/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc{&ref}{&start}{&end}" +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/navigation/example4.json b/tests/resolvers/dts/api_v1/data/navigation/example4.json new file mode 100644 index 00000000..2c35fc46 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/navigation/example4.json @@ -0,0 +1,17 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id":"/api/dts/navigation/?id=urn:cts:latinLit:phi1294.phi001.perseus-lat2&ref=1", + "dts:citeDepth" : 3, + "dts:level": 3, + "member": [ + {"dts:ref": "1.1.1"}, + {"dts:ref": "1.1.2"}, + {"dts:ref": "1.2.1"}, + {"dts:ref": "1.2.2"} + ], + "dts:passage": "/dts/api/document/?id=urn:cts:latinLit:phi1294.phi001.perseus-lat2{&ref}{&start}{&end}" +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/navigation/example5.json b/tests/resolvers/dts/api_v1/data/navigation/example5.json new file mode 100644 index 00000000..2d9e423d --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/navigation/example5.json @@ -0,0 +1,15 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dts": "https://w3id.org/dts/api#" + }, + "@id":"/api/dts/navigation/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc&level=0&start=1&end=3", + "dts:citeDepth" : 1, + "dts:level": 1, + "member": [ + {"dts:ref": "1"}, + {"dts:ref": "2"}, + {"dts:ref": "3"} + ], + "dts:passage": "/dts/api/document/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc{&ref}{&start}{&end}" +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/navigation/example6.json b/tests/resolvers/dts/api_v1/data/navigation/example6.json new file mode 100644 index 00000000..630a68d1 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/navigation/example6.json @@ -0,0 +1,19 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id":"/api/dts/navigation/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc&level=1&start=1&end=3", + "dts:citeDepth" : 2, + "dts:level": 2, + "member": [ + {"dts:ref": "1.1"}, + {"dts:ref": "1.2"}, + {"dts:ref": "2.1"}, + {"dts:ref": "2.2"}, + {"dts:ref": "3.1"}, + {"dts:ref": "3.2"} + ], + "dts:passage": "/dts/api/document/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc{&ref}{&start}{&end}" +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/navigation/example7.json b/tests/resolvers/dts/api_v1/data/navigation/example7.json new file mode 100644 index 00000000..13c26ef3 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/navigation/example7.json @@ -0,0 +1,16 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#", + "tei": "http://www.tei-c.org/ns/1.0" + }, + "@id":"/api/dts/navigation/?id=urn:cts:latinLit:phi1294.phi001.perseus-lat2&ref=1&level=2&groupSize=2", + "dts:citeDepth" : 3, + "dts:level": 3, + "member": [ + {"dts:start": "1.1.1", "dts:end": "1.1.2"}, + {"dts:start": "1.2.1", "dts:end": "1.2.2"} + ], + "dts:passage": "/dts/api/document/?id=urn:cts:latinLit:phi1294.phi001.perseus-lat2{&ref}{&start}{&end}" +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/navigation/example8.json b/tests/resolvers/dts/api_v1/data/navigation/example8.json new file mode 100644 index 00000000..ae3db6f1 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/navigation/example8.json @@ -0,0 +1,20 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#", + "foo": "http://foo.bar/ontology" + }, + "@id":"/api/dts/navigation/?id=http://data.bnf.fr/ark:/12148/cb11936111v", + "dts:citeDepth" : 1, + "dts:level": 1, + "dts:citeType": "letter", + "member": [ + { "dts:ref": "Av", "dts:citeType": "preface"}, + { "dts:ref": "Pr", "dts:citeType": "preface"}, + { "dts:ref": "1" }, + { "dts:ref": "2" }, + { "dts:ref": "3" } + ], + "dts:passage": "/dts/api/document/?id=http://data.bnf.fr/ark:/12148/cb11936111v{&ref}{&start}{&end}" +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/navigation/example9.json b/tests/resolvers/dts/api_v1/data/navigation/example9.json new file mode 100644 index 00000000..a6247ab3 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/navigation/example9.json @@ -0,0 +1,56 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#", + "foo": "http://foo.bar/ontology#" + }, + "@id":"/api/dts/navigation/?id=http://data.bnf.fr/ark:/12148/cb11936111v", + "dts:citeDepth" : 1, + "dts:level": 1, + "member": [ + { + "dts:ref": "Av", + "dts:dublincore": { + "dc:title": "Avertissement de l'Éditeur" + } + }, + { + "dts:ref": "Pr", + "dts:dublincore": { + "dc:title": "Préface" + } + }, + { + "dts:ref": "1", + "dts:dublincore": { + "dc:title": "Lettre 1" + }, + "dts:extensions": { + "foo:fictionalSender": "Cécile Volanges", + "foo:fictionalRecipient": "Sophie Carnay" + } + }, + { + "dts:ref": "2", + "dts:dublincore": { + "dc:title": "Lettre 2" + }, + "dts:extensions": { + "foo:fictionalSender": "La Marquise de Merteuil", + "foo:fictionalRecipient": "Vicomte de Valmont" + } + }, + { + "dts:ref": "3", + "dts:dublincore": { + "dc:title": "Lettre 3" + }, + "dts:extensions": { + "foo:fictionalSender": "Cécile Volanges", + "foo:fictionalRecipient": "Sophie Carnay" + } + } + ], + "dts:passage": "/dts/api/document/?id=http://data.bnf.fr/ark:/12148/cb11936111v{&ref}{&start}{&end}" +} \ No newline at end of file From 4a606ee9b112385ecbe4bd6e2d483bcf0a74bbfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Fri, 14 Sep 2018 17:17:07 +0900 Subject: [PATCH 49/89] (Clean-up) Adding __all__ to all modules which should to limit import of unwanted resources --- MyCapytain/common/base.py | 5 +++ MyCapytain/common/constants.py | 11 +++++++ MyCapytain/common/metadata.py | 5 +++ MyCapytain/common/utils/dts.py | 5 +++ MyCapytain/common/utils/xml.py | 11 +++++++ MyCapytain/resolvers/cts/api.py | 4 +++ MyCapytain/resolvers/cts/local.py | 5 +++ MyCapytain/resolvers/dts/api_v1.py | 4 +++ MyCapytain/resolvers/prototypes.py | 5 +++ MyCapytain/resolvers/utils.py | 5 +++ MyCapytain/resources/collections/cts.py | 31 +++++++++++++------ MyCapytain/resources/collections/dts.py | 5 +++ .../resources/prototypes/cts/inventory.py | 12 +++++++ MyCapytain/resources/prototypes/metadata.py | 9 ++++-- MyCapytain/resources/prototypes/text.py | 14 +++++++-- MyCapytain/resources/texts/base/tei.py | 19 +++++++----- .../resources/texts/local/capitains/cts.py | 30 +++++++++++------- MyCapytain/resources/texts/remote/cts.py | 10 ++++-- MyCapytain/retrievers/cts5.py | 5 +++ MyCapytain/retrievers/dts/__init__.py | 5 +++ MyCapytain/retrievers/prototypes.py | 7 +++++ tests/resources/collections/test_cts.py | 3 ++ tests/resources/proto/test_text.py | 4 +-- tests/resources/texts/base/test_tei.py | 12 +++---- 24 files changed, 182 insertions(+), 44 deletions(-) diff --git a/MyCapytain/common/base.py b/MyCapytain/common/base.py index 2cf2fb03..7170c3a6 100644 --- a/MyCapytain/common/base.py +++ b/MyCapytain/common/base.py @@ -1,6 +1,11 @@ from inspect import getmro +__all__ = [ + "Exportable" +] + + class Exportable(object): """ Objects that supports Export diff --git a/MyCapytain/common/constants.py b/MyCapytain/common/constants.py index ca9f5504..9079c751 100644 --- a/MyCapytain/common/constants.py +++ b/MyCapytain/common/constants.py @@ -2,6 +2,17 @@ from rdflib.namespace import SKOS +__all__ = [ + "XPATH_NAMESPACES", + "RDF_NAMESPACES", + "Mimetypes", + "GRAPH_BINDINGS", + "bind_graph", + "get_graph", + "set_graph", + "RDFLIB_MAPPING" +] + #: List of XPath Namespaces used in guidelines XPATH_NAMESPACES = { "tei": "http://www.tei-c.org/ns/1.0", diff --git a/MyCapytain/common/metadata.py b/MyCapytain/common/metadata.py index 98102083..6b9f39fc 100644 --- a/MyCapytain/common/metadata.py +++ b/MyCapytain/common/metadata.py @@ -15,6 +15,11 @@ from typing import Union +__all__ = [ + "Metadata" +] + + class Metadata(Exportable): """ A metadatum aggregation object provided to centralize metadata diff --git a/MyCapytain/common/utils/dts.py b/MyCapytain/common/utils/dts.py index 80a2b9bb..8173c60c 100644 --- a/MyCapytain/common/utils/dts.py +++ b/MyCapytain/common/utils/dts.py @@ -3,6 +3,11 @@ from ..metadata import Metadata +__all__ = [ + "parse_metadata" +] + + def parse_metadata(metadata_obj: Metadata, metadata_dictionary: dict) -> None: """ Adds to a Metadata object any DublinCore or dts:Extensions object found in the given dictionary diff --git a/MyCapytain/common/utils/xml.py b/MyCapytain/common/utils/xml.py index 6c31d309..8b9d15d8 100644 --- a/MyCapytain/common/utils/xml.py +++ b/MyCapytain/common/utils/xml.py @@ -10,6 +10,17 @@ from MyCapytain.common.constants import XPATH_NAMESPACES +__all__ = [ + "make_xml_node", + "xmlparser", + "normalizeXpath", + "xmliter", + "performXpath", + "copyNode", + "passageLoop" +] + + _parser = etree.XMLParser(collect_ids=False, resolve_entities=False) diff --git a/MyCapytain/resolvers/cts/api.py b/MyCapytain/resolvers/cts/api.py index b419e868..0fa8b1de 100644 --- a/MyCapytain/resolvers/cts/api.py +++ b/MyCapytain/resolvers/cts/api.py @@ -14,6 +14,10 @@ from MyCapytain.retrievers.cts5 import HttpCtsRetriever +__all__ = [ + "HttpCtsResolver" +] + class HttpCtsResolver(Resolver): """ HttpCtsResolver provide a resolver for CTS API http endpoint. diff --git a/MyCapytain/resolvers/cts/local.py b/MyCapytain/resolvers/cts/local.py index e34f4025..9a5296d8 100644 --- a/MyCapytain/resolvers/cts/local.py +++ b/MyCapytain/resolvers/cts/local.py @@ -21,6 +21,11 @@ from MyCapytain.resources.texts.local.capitains.cts import CapitainsCtsText +__all__ = [ + "CtsCapitainsLocalResolver" +] + + class CtsCapitainsLocalResolver(Resolver): """ XML Folder Based resolver. CtsTextMetadata and metadata resolver based on local directories diff --git a/MyCapytain/resolvers/dts/api_v1.py b/MyCapytain/resolvers/dts/api_v1.py index a15d8d49..b6ba2dbe 100644 --- a/MyCapytain/resolvers/dts/api_v1.py +++ b/MyCapytain/resolvers/dts/api_v1.py @@ -19,6 +19,10 @@ from pyld.jsonld import expand +__all__ = [ + "HttpDtsResolver" +] + _empty = [{"@value": None}] diff --git a/MyCapytain/resolvers/prototypes.py b/MyCapytain/resolvers/prototypes.py index 5de8da0e..eaeec6d3 100644 --- a/MyCapytain/resolvers/prototypes.py +++ b/MyCapytain/resolvers/prototypes.py @@ -13,6 +13,11 @@ from MyCapytain.common.reference import BaseReference, BaseReferenceSet +__all__ = [ + "Resolver" +] + + class Resolver(object): """ Resolver provide a native python API which returns python objects. diff --git a/MyCapytain/resolvers/utils.py b/MyCapytain/resolvers/utils.py index b6155041..96b5c768 100644 --- a/MyCapytain/resolvers/utils.py +++ b/MyCapytain/resolvers/utils.py @@ -1,6 +1,11 @@ from MyCapytain.errors import UndispatchedTextError +__all__ = [ + "CollectionDispatcher" +] + + class CollectionDispatcher: """ Collection Dispatcher provides a utility to divide automatically texts and collections \ into different collections diff --git a/MyCapytain/resources/collections/cts.py b/MyCapytain/resources/collections/cts.py index feb25337..67bcc95b 100644 --- a/MyCapytain/resources/collections/cts.py +++ b/MyCapytain/resources/collections/cts.py @@ -20,6 +20,17 @@ from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes, RDF_NAMESPACES +__all__ = [ + "XmlCtsCitation", + "XmlCtsWorkMetadata", + "XmlCtsCommentaryMetadata", + "XmlCtsTranslationMetadata", + "XmlCtsEditionMetadata", + "XmlCtsTextgroupMetadata", + "XmlCtsTextInventoryMetadata", + "XmlCtsTextMetadata" +] + _CLASSES_DICT = {} @@ -65,7 +76,7 @@ def ingest(cls, resource, element=None, xpath="ti:citation"): return None -def xpathDict(xml, xpath, cls, parent, **kwargs): +def _xpathDict(xml, xpath, cls, parent, **kwargs): """ Returns a default Dict given certain information :param xml: An xml tree @@ -89,7 +100,7 @@ def xpathDict(xml, xpath, cls, parent, **kwargs): return children -def __parse_structured_metadata__(obj, xml): +def _parse_structured_metadata(obj, xml): """ Parse an XML object for structured metadata :param obj: Object whose metadata are parsed @@ -167,7 +178,7 @@ def parse_metadata(cls, obj, xml): for child in xml.xpath("ti:about", namespaces=XPATH_NAMESPACES): obj.set_link(RDF_NAMESPACES.CTS.term("about"), child.get('urn')) - __parse_structured_metadata__(obj, xml) + _parse_structured_metadata(obj, xml) """ online = xml.xpath("ti:online", namespaces=NS) @@ -265,20 +276,20 @@ def parse(cls, resource, parent=None, _with_children=False): # Parse children children = [] - children.extend(xpathDict( + children.extend(_xpathDict( xml=xml, xpath='ti:edition', cls=cls.CLASS_EDITION, parent=o )) - children.extend(xpathDict( + children.extend(_xpathDict( xml=xml, xpath='ti:translation', cls=cls.CLASS_TRANSLATION, parent=o )) - children.extend(xpathDict( + children.extend(_xpathDict( xml=xml, xpath='ti:commentary', cls=cls.CLASS_COMMENTARY, parent=o )) - __parse_structured_metadata__(o, xml) + _parse_structured_metadata(o, xml) if _with_children: return o, children @@ -307,9 +318,9 @@ def parse(cls, resource, parent=None): o.set_cts_property("groupname", child.text, lg) # Parse Works - xpathDict(xml=xml, xpath='ti:work', cls=cls.CLASS_WORK, parent=o) + _xpathDict(xml=xml, xpath='ti:work', cls=cls.CLASS_WORK, parent=o) - __parse_structured_metadata__(o, xml) + _parse_structured_metadata(o, xml) return o @@ -328,5 +339,5 @@ def parse(cls, resource): xml = xmlparser(resource) o = cls(name=xml.xpath("//ti:TextInventory", namespaces=XPATH_NAMESPACES)[0].get("tiid") or "") # Parse textgroups - xpathDict(xml=xml, xpath='//ti:textgroup', cls=cls.CLASS_TEXTGROUP, parent=o) + _xpathDict(xml=xml, xpath='//ti:textgroup', cls=cls.CLASS_TEXTGROUP, parent=o) return o diff --git a/MyCapytain/resources/collections/dts.py b/MyCapytain/resources/collections/dts.py index 72b9eaa5..8be7da15 100644 --- a/MyCapytain/resources/collections/dts.py +++ b/MyCapytain/resources/collections/dts.py @@ -8,6 +8,11 @@ from pyld import jsonld +__all__ = [ + "DTSCollection" +] + + _hyd = RDF_NAMESPACES.HYDRA _dts = RDF_NAMESPACES.DTS _cap = RDF_NAMESPACES.CAPITAINS diff --git a/MyCapytain/resources/prototypes/cts/inventory.py b/MyCapytain/resources/prototypes/cts/inventory.py index 9b167b84..316d6748 100644 --- a/MyCapytain/resources/prototypes/cts/inventory.py +++ b/MyCapytain/resources/prototypes/cts/inventory.py @@ -20,6 +20,18 @@ from rdflib import RDF, Literal, URIRef from rdflib.namespace import DC +__all__ = [ + "PrototypeCtsCollection", + "CtsTextInventoryCollection", + "CtsEditionMetadata", + "CtsWorkMetadata", + "CtsCommentaryMetadata", + "CtsTextgroupMetadata", + "CtsTextInventoryMetadata", + "CtsTextMetadata", + "CtsTranslationMetadata" +] + class PrototypeCtsCollection(Collection): """ Resource represents any resource from the inventory diff --git a/MyCapytain/resources/prototypes/metadata.py b/MyCapytain/resources/prototypes/metadata.py index e40ad656..007d939e 100644 --- a/MyCapytain/resources/prototypes/metadata.py +++ b/MyCapytain/resources/prototypes/metadata.py @@ -15,13 +15,18 @@ from MyCapytain.common.base import Exportable from MyCapytain.common.reference import BaseCitationSet from rdflib import URIRef, RDF, Literal, Graph, RDFS -from rdflib.namespace import SKOS, DC, DCTERMS, NamespaceManager +from rdflib.namespace import SKOS, DC, DCTERMS + + +__all__ = [ + "Collection", + "ResourceCollection" +] _ns_hydra_str = str(RDF_NAMESPACES.HYDRA) _ns_cts_str = str(RDF_NAMESPACES.CTS) _ns_dts_str = str(RDF_NAMESPACES.DTS) -_ns_dts_str = str(RDF_NAMESPACES.DTS) _ns_dct_str = str(DCTERMS) _ns_cap_str = str(RDF_NAMESPACES.CAPITAINS) _ns_rdf_str = str(RDF) diff --git a/MyCapytain/resources/prototypes/text.py b/MyCapytain/resources/prototypes/text.py index b10ef3d3..dc3ca666 100644 --- a/MyCapytain/resources/prototypes/text.py +++ b/MyCapytain/resources/prototypes/text.py @@ -10,15 +10,23 @@ from six import text_type from rdflib.namespace import DC from rdflib import BNode, URIRef -from MyCapytain.common.reference import URN -from MyCapytain.common.reference._base import NodeId -from MyCapytain.common.reference._capitains_cts import Citation +from MyCapytain.common.reference import URN, Citation, NodeId from MyCapytain.common.metadata import Metadata from MyCapytain.common.constants import Mimetypes, get_graph, RDF_NAMESPACES from MyCapytain.common.base import Exportable from MyCapytain.resources.prototypes.metadata import Collection +__all__ = [ + "TextualElement", + "TextualGraph", + "TextualNode", + "CitableText", + "InteractiveTextualNode", + "CtsNode" +] + + class TextualElement(Exportable): """ Node representing a text passage. diff --git a/MyCapytain/resources/texts/base/tei.py b/MyCapytain/resources/texts/base/tei.py index d411c512..96bdd680 100644 --- a/MyCapytain/resources/texts/base/tei.py +++ b/MyCapytain/resources/texts/base/tei.py @@ -14,7 +14,12 @@ from MyCapytain.resources.prototypes.text import InteractiveTextualNode -class TEIResource(InteractiveTextualNode): +__all__ = [ + "TeiResource" +] + + +class TeiResource(InteractiveTextualNode): """ TEI Encoded Resource :param resource: XML Resource that needs to be parsed into a CapitainsCtsPassage/CtsTextMetadata @@ -29,16 +34,16 @@ class TEIResource(InteractiveTextualNode): DEFAULT_EXPORT = Mimetypes.PLAINTEXT PLAINTEXT_STRING_JOIN = " " - def __init__(self, resource, **kwargs): - super(TEIResource, self).__init__(**kwargs) + def __init__(self, resource: str, **kwargs): + super(TeiResource, self).__init__(**kwargs) self.resource = xmlparser(resource) - self.__plaintext_string_join__ = ""+self.PLAINTEXT_STRING_JOIN + self._plaintext_string_join = "" + self.PLAINTEXT_STRING_JOIN @property - def plaintext_string_join(self): + def plaintext_string_join(self) -> str: """ String used to join xml node's texts in export """ - return self.__plaintext_string_join__ + return self._plaintext_string_join @plaintext_string_join.setter def plaintext_string_join(self, value): @@ -48,7 +53,7 @@ def plaintext_string_join(self, value): :type value: str :return: """ - self.__plaintext_string_join__ = value + self._plaintext_string_join = value def __str__(self): """ CtsTextMetadata based representation of the passage diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index 547df0a4..f617c9d8 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -17,13 +17,19 @@ from MyCapytain.common.reference import CtsReference, URN, Citation, CtsReferenceSet from MyCapytain.resources.prototypes import text -from MyCapytain.resources.texts.base.tei import TEIResource +from MyCapytain.resources.texts.base.tei import TeiResource from MyCapytain.errors import InvalidSiblingRequest, InvalidURN -from lxml import etree +import lxml.etree as etree -def __makePassageKwargs__(urn, reference): +__all__ = [ + "CapitainsCtsText", + "CapitainsCtsPassage" +] + + +def _makePassageKwargs(urn, reference): """ Little helper used by CapitainsCtsPassage here to comply with parents args :param urn: URN String @@ -39,7 +45,7 @@ def __makePassageKwargs__(urn, reference): return kwargs -class _SharedMethods: +class _SharedMethods(TeiResource): """ Set of shared methods between objects in local TEI. Avoid recoding functions """ @@ -113,7 +119,7 @@ def _getSimplePassage(self, reference=None): :rtype: CapitainsCtsPassage """ if reference is None: - return __SimplePassage__( + return _SimplePassage( resource=self.resource, reference=None, urn=self.urn, @@ -130,7 +136,7 @@ def _getSimplePassage(self, reference=None): if len(resource) != 1: raise InvalidURN - return __SimplePassage__( + return _SimplePassage( resource[0], reference=reference, urn=self.urn, @@ -287,7 +293,7 @@ def tostring(self, *args, **kwargs): return etree.tostring(self.resource, *args, **kwargs) -class __SimplePassage__(_SharedMethods, TEIResource, text.Passage): +class _SimplePassage(_SharedMethods, text.Passage): """ CapitainsCtsPassage for simple and quick parsing of texts :param resource: Element representing the passage @@ -302,10 +308,10 @@ class __SimplePassage__(_SharedMethods, TEIResource, text.Passage): :type text: CapitainsCtsText """ def __init__(self, resource, reference, citation, text, urn=None): - super(__SimplePassage__, self).__init__( + super(_SimplePassage, self).__init__( resource=resource, citation=citation, - **__makePassageKwargs__(urn, reference) + **_makePassageKwargs(urn, reference) ) self.__text__ = text self.__reference__ = reference @@ -434,7 +440,7 @@ def textObject(self): return self.__text__ -class CapitainsCtsText(_SharedMethods, TEIResource, text.CitableText): +class CapitainsCtsText(_SharedMethods, text.CitableText): """ Implementation of CTS tools for local files :param urn: A URN identifier @@ -479,7 +485,7 @@ def test(self): raise E -class CapitainsCtsPassage(_SharedMethods, TEIResource, text.Passage): +class CapitainsCtsPassage(_SharedMethods, text.Passage): """ CapitainsCtsPassage class for local texts which rebuilds the tree up to the passage. For design purposes, some people would prefer the output of GetPassage to be consistent. ContextPassage rebuilds @@ -531,7 +537,7 @@ def __init__(self, reference, urn=None, citation=None, resource=None, text=None) super(CapitainsCtsPassage, self).__init__( citation=citation, resource=resource, - **__makePassageKwargs__(urn, reference) + **_makePassageKwargs(urn, reference) ) if urn is not None and urn.reference is not None: reference = urn.reference diff --git a/MyCapytain/resources/texts/remote/cts.py b/MyCapytain/resources/texts/remote/cts.py index d8f4d0e3..40b39d72 100644 --- a/MyCapytain/resources/texts/remote/cts.py +++ b/MyCapytain/resources/texts/remote/cts.py @@ -16,10 +16,16 @@ from MyCapytain.common.reference._capitains_cts import CtsReference, URN from MyCapytain.resources.collections import cts as CtsCollection from MyCapytain.resources.prototypes import text as prototypes -from MyCapytain.resources.texts.base.tei import TEIResource +from MyCapytain.resources.texts.base.tei import TeiResource from MyCapytain.errors import MissingAttribute +__all__ = [ + "CtsPassage", + "CtsText" +] + + class _SharedMethod(prototypes.InteractiveTextualNode): """ Set of methods shared by CtsTextMetadata and CapitainsCtsPassage @@ -372,7 +378,7 @@ def export(self, output=Mimetypes.PLAINTEXT, exclude=None, **kwargs): return self.getTextualNode().export(output, exclude) -class CtsPassage(_SharedMethod, prototypes.Passage, TEIResource): +class CtsPassage(_SharedMethod, prototypes.Passage, TeiResource): """ CapitainsCtsPassage representing :param urn: diff --git a/MyCapytain/retrievers/cts5.py b/MyCapytain/retrievers/cts5.py index fcf76be7..ac35030b 100644 --- a/MyCapytain/retrievers/cts5.py +++ b/MyCapytain/retrievers/cts5.py @@ -12,6 +12,11 @@ import requests +__all__ = [ + "HttpCtsRetriever" +] + + class HttpCtsRetriever(MyCapytain.retrievers.prototypes.CtsRetriever): """ Basic integration of the MyCapytain.retrievers.proto.CTS abstraction """ diff --git a/MyCapytain/retrievers/dts/__init__.py b/MyCapytain/retrievers/dts/__init__.py index c5b2f133..94a71282 100644 --- a/MyCapytain/retrievers/dts/__init__.py +++ b/MyCapytain/retrievers/dts/__init__.py @@ -14,6 +14,11 @@ from MyCapytain.common.utils import parse_uri +__all__ = [ + "HttpDtsRetriever" +] + + class HttpDtsRetriever(MyCapytain.retrievers.prototypes.API): def __init__(self, endpoint): super(HttpDtsRetriever, self).__init__(endpoint) diff --git a/MyCapytain/retrievers/prototypes.py b/MyCapytain/retrievers/prototypes.py index 8cca11e8..e0bacf06 100644 --- a/MyCapytain/retrievers/prototypes.py +++ b/MyCapytain/retrievers/prototypes.py @@ -9,6 +9,13 @@ """ +__all__ = [ + "API", + "CitableTextServiceRetriever", + "CtsRetriever" +] + + class API(object): """ API Prototype object diff --git a/tests/resources/collections/test_cts.py b/tests/resources/collections/test_cts.py index ed710c15..2024cf45 100644 --- a/tests/resources/collections/test_cts.py +++ b/tests/resources/collections/test_cts.py @@ -11,6 +11,9 @@ from MyCapytain.common import constants from MyCapytain.resources.collections.cts import * from MyCapytain.resources.prototypes.text import CtsNode +from MyCapytain.common.utils.xml import xmlparser +from MyCapytain.common.constants import Mimetypes, RDF_NAMESPACES, XPATH_NAMESPACES +from rdflib import URIRef, Literal, XSD class XML_Compare(object): diff --git a/tests/resources/proto/test_text.py b/tests/resources/proto/test_text.py index 4494d49f..e2400e49 100644 --- a/tests/resources/proto/test_text.py +++ b/tests/resources/proto/test_text.py @@ -4,10 +4,10 @@ import unittest -import MyCapytain.common.reference._capitains_cts -from MyCapytain.common.reference._capitains_cts import URN +from MyCapytain.common.reference import URN, Citation from MyCapytain.resources.prototypes.text import * +from MyCapytain.common.constants import RDF_NAMESPACES import MyCapytain.common.reference import MyCapytain.common.metadata diff --git a/tests/resources/texts/base/test_tei.py b/tests/resources/texts/base/test_tei.py index a391f425..e9772002 100644 --- a/tests/resources/texts/base/test_tei.py +++ b/tests/resources/texts/base/test_tei.py @@ -4,7 +4,7 @@ import unittest from MyCapytain.common.reference._capitains_cts import CtsReference, Citation -from MyCapytain.resources.texts.base.tei import TEIResource +from MyCapytain.resources.texts.base.tei import TeiResource from MyCapytain.common.constants import Mimetypes from MyCapytain.common.utils.xml import xmlparser @@ -106,7 +106,7 @@ def test_ingest_single_and(self): class TestTEIPassage(unittest.TestCase): def test_text(self): """ Test text attribute """ - P = TEIResource( + P = TeiResource( identifier="dummy", resource=xmlparser('Ibis hellob ab excusso missus in astra sago. ') ) @@ -117,7 +117,7 @@ def test_text(self): def test_str(self): """ Test STR conversion of xml """ - P = TEIResource( + P = TeiResource( identifier="dummy", resource=xmlparser('Ibis hellob ab excusso missus in astra sago. ') ) @@ -125,7 +125,7 @@ def test_str(self): def test_xml(self): X = xmlparser('Ibis hellob ab excusso missus in astra sago. ') - P = TEIResource( + P = TeiResource( identifier="dummy", resource=X ) @@ -133,7 +133,7 @@ def test_xml(self): def test_exportable_capacities(self): X = xmlparser('Ibis hellob ab excusso missus in astra sago. ') - P = TEIResource( + P = TeiResource( identifier="dummy", resource=X ) @@ -147,7 +147,7 @@ def test_changing_space(self): """ Test when user change default value of export joining char """ X = xmlparser("""in- geniumingenium ll.v(G). -nio B. in ganea Jnatus""") - P = TEIResource( + P = TeiResource( identifier="dummy", resource=X ) From 5af344e3c0ffa69bb9d43a611d64cd7d2088baa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Fri, 14 Sep 2018 18:02:59 +0200 Subject: [PATCH 50/89] (DTS Resolver) Started implementing getMetadata() --- MyCapytain/common/utils/_json_ld.py | 2 + MyCapytain/resolvers/dts/api_v1.py | 11 +++++ MyCapytain/resources/collections/dts.py | 44 ++++++++++++++++--- MyCapytain/resources/prototypes/metadata.py | 5 ++- tests/resolvers/dts/api_v1/__init__.py | 41 ++++++++++++++++- .../dts/api_v1/data/collection/example1.json | 40 +++++++++++++++++ tests/resolvers/dts/api_v1/data/root.json | 4 +- .../collections/test_dts_collection.py | 10 ++--- 8 files changed, 140 insertions(+), 17 deletions(-) create mode 100644 tests/resolvers/dts/api_v1/data/collection/example1.json diff --git a/MyCapytain/common/utils/_json_ld.py b/MyCapytain/common/utils/_json_ld.py index 169b3c29..ffcdeb67 100644 --- a/MyCapytain/common/utils/_json_ld.py +++ b/MyCapytain/common/utils/_json_ld.py @@ -20,6 +20,8 @@ def literal_to_dict(value): def dict_to_literal(dict_container: dict): + """ Transforms a JSON+LD PyLD dictionary into + an RDFLib object""" if isinstance(dict_container["@value"], int): return dict_container["@value"], else: diff --git a/MyCapytain/resolvers/dts/api_v1.py b/MyCapytain/resolvers/dts/api_v1.py index b6ba2dbe..cbf026ea 100644 --- a/MyCapytain/resolvers/dts/api_v1.py +++ b/MyCapytain/resolvers/dts/api_v1.py @@ -15,6 +15,7 @@ DtsReference, DtsReferenceSet, DtsCitation from MyCapytain.retrievers.dts import HttpDtsRetriever from MyCapytain.common.utils.dts import parse_metadata +from MyCapytain.resources.collections.dts import DtsCollection from pyld.jsonld import expand @@ -66,6 +67,16 @@ def endpoint(self) -> HttpDtsRetriever: """ return self._endpoint + def getMetadata(self, objectId: str=None, **filters) -> DtsCollection: + req = self.endpoint.get_collection(objectId) + req.raise_for_status() + + collection = DtsCollection.parse(req.json()) + # Pagination is not completed upon first query. + # Pagination will be treated direction in the DtsCollection + + return collection + def getReffs( self, textId: str, diff --git a/MyCapytain/resources/collections/dts.py b/MyCapytain/resources/collections/dts.py index 8be7da15..2579b116 100644 --- a/MyCapytain/resources/collections/dts.py +++ b/MyCapytain/resources/collections/dts.py @@ -5,11 +5,12 @@ from MyCapytain.common.utils.dts import parse_metadata +from typing import List from pyld import jsonld __all__ = [ - "DTSCollection" + "DtsCollection" ] @@ -20,12 +21,12 @@ _empty_extensions = [{}] -class DTSCollection(Collection): +class DtsCollection(Collection): CitationSet = DtsCitationSet def __init__(self, identifier="", *args, **kwargs): - super(DTSCollection, self).__init__(identifier, *args, **kwargs) + super(DtsCollection, self).__init__(identifier, *args, **kwargs) self._expanded = False # Not sure I'll keep this self._citation = DtsCitationSet() @@ -57,7 +58,7 @@ def parse(cls, resource, direction="children"): :type resource: dict :param direction: Direction of the hydra:members value :return: DTSCollection parsed - :rtype: DTSCollection + :rtype: DtsCollection """ collection = jsonld.expand(resource) @@ -90,10 +91,39 @@ def parse(cls, resource, direction="children"): obj.metadata.add(_hyd.description, val_dict["@value"], None) parse_metadata(obj.metadata, collection) + members = cls.parse_member( + collection, obj, direction + ) + if direction == "children": + obj.children.update({ + coll.id: coll + for coll in members + }) + else: + obj.parents.extend(members) - for member in collection.get(str(_hyd.member), []): + return obj + + @classmethod + def parse_member( + cls, + obj: dict, + collection: "DtsCollection", + direction) -> List["DtsCollection"]: + """ Parse the member value of a Collection response + and returns the list of object while setting the graph + relationship based on `direction` + + :param obj: PyLD parsed JSON+LD + :param collection: Collection attached to the member property + :param direction: Direction of the member (children, parent) + """ + members = [] + + for member in obj.get(str(_hyd.member), []): subcollection = cls.parse(member) if direction == "children": - subcollection.parent = obj + subcollection.parent = collection + members.append(subcollection) - return obj + return members diff --git a/MyCapytain/resources/prototypes/metadata.py b/MyCapytain/resources/prototypes/metadata.py index 007d939e..a7b09094 100644 --- a/MyCapytain/resources/prototypes/metadata.py +++ b/MyCapytain/resources/prototypes/metadata.py @@ -16,6 +16,7 @@ from MyCapytain.common.reference import BaseCitationSet from rdflib import URIRef, RDF, Literal, Graph, RDFS from rdflib.namespace import SKOS, DC, DCTERMS +from typing import List __all__ = [ @@ -162,7 +163,7 @@ def set_label(self, label, lang): ]) @property - def children(self): + def children(self) -> dict: """ Dictionary of childrens {Identifier: Collection} :rtype: dict @@ -170,7 +171,7 @@ def children(self): return self.__children__ @property - def parents(self): + def parents(self) -> List["Collection"]: """ Iterator to find parents of current collection, from closest to furthest :rtype: Generator[:class:`Collection`] diff --git a/tests/resolvers/dts/api_v1/__init__.py b/tests/resolvers/dts/api_v1/__init__.py index 53b93488..a7020c81 100644 --- a/tests/resolvers/dts/api_v1/__init__.py +++ b/tests/resolvers/dts/api_v1/__init__.py @@ -43,7 +43,46 @@ def _load_json_mock(endpoint: str, example: str) -> typing.Union[dict, list]: return json.loads(_load_mock(endpoint, example)) -class TestHttpDtsResolver(unittest.TestCase): +class TestHttpDtsResolverCollection(unittest.TestCase): + def setUp(self): + self.root_uri = "http://foobar.com/api/dts" + self.resolver = HttpDtsResolver(self.root_uri) + + @requests_mock.mock() + def test_simple_root_access(self, mock_set): + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/collections?nav=children", + text=_load_mock("collection", "example1.json"), + complete_qs=True + ) + collection = self.resolver.getMetadata() + self.assertEqual( + 3, collection.size, + "There should be 3 collections" + ) + self.assertEqual( + "Cartulaires", str(collection["/cartulaires"].get_label()), + "Titles of subcollection and subcollection should be " + "stored under their IDs" + ) + self.assertEqual( + "Collection Générale de l'École Nationale des Chartes", + str(collection.get_label()), + "Label of the main collection should be correctly parsed" + ) + self.assertEqual( + ["https://viaf.org/viaf/167874585", "École Nationale des Chartes"], + sorted([ + str(obj) + for obj in collection.metadata.get( + URIRef("http://purl.org/dc/terms/publisher") + ) + ]) + ) + + +class TestHttpDtsResolverNavigation(unittest.TestCase): def setUp(self): self.root_uri = "http://foobar.com/api/dts" self.resolver = HttpDtsResolver(self.root_uri) diff --git a/tests/resolvers/dts/api_v1/data/collection/example1.json b/tests/resolvers/dts/api_v1/data/collection/example1.json new file mode 100644 index 00000000..27d251fc --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/collection/example1.json @@ -0,0 +1,40 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id": "general", + "@type": "Collection", + "totalItems": 3, + "title": "Collection Générale de l'École Nationale des Chartes", + "dts:dublincore": { + "dc:publisher": ["École Nationale des Chartes", "https://viaf.org/viaf/167874585"], + "dc:title": [ + {"@language": "fr", "@value": "Collection Générale de l'École Nationale des Chartes"} + ] + }, + "member": [ + { + "@id" : "cartulaires", + "title" : "Cartulaires", + "description": "Collection de cartulaires d'Île-de-France et de ses environs", + "@type" : "Collection", + "totalItems" : 10 + }, + { + "@id" : "lasciva_roma", + "title" : "Lasciva Roma", + "description": "Collection of primary sources of interest in the studies of Ancient World's sexuality", + "@type" : "Collection", + "totalItems" : 1 + }, + { + "@id" : "lettres_de_poilus", + "title" : "Correspondance des poilus", + "description": "Collection de lettres de poilus entre 1917 et 1918", + "@type" : "Collection", + "totalItems" : 10000 + } + ] +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/root.json b/tests/resolvers/dts/api_v1/data/root.json index 233880cc..9497072a 100644 --- a/tests/resolvers/dts/api_v1/data/root.json +++ b/tests/resolvers/dts/api_v1/data/root.json @@ -2,7 +2,7 @@ "@context": "/api/dts/contexts/EntryPoint.jsonld", "@id": "/api/dts/", "@type": "EntryPoint", - "collections": "/api/dts/collections/", - "documents": "/api/dts/documents/", + "collections": "/api/dts/collections", + "documents": "/api/dts/documents", "navigation" : "/api/dts/navigation" } \ No newline at end of file diff --git a/tests/resources/collections/test_dts_collection.py b/tests/resources/collections/test_dts_collection.py index 32ec5050..4bcddef3 100644 --- a/tests/resources/collections/test_dts_collection.py +++ b/tests/resources/collections/test_dts_collection.py @@ -1,4 +1,4 @@ -from MyCapytain.resources.collections.dts import DTSCollection +from MyCapytain.resources.collections.dts import DtsCollection from MyCapytain.common.constants import Mimetypes, set_graph, bind_graph from unittest import TestCase import json @@ -36,7 +36,7 @@ def reorder_orderable(self, exported): def test_simple_collection(self): coll = self.get_collection(1) - parsed = DTSCollection.parse(coll) + parsed = DtsCollection.parse(coll) exported = self.reorder_orderable(parsed.export(Mimetypes.JSON.DTS.Std)) self.assertEqual( @@ -86,7 +86,7 @@ def test_simple_collection(self): def test_collection_single_member_with_types(self): coll = self.get_collection(2) - parsed = DTSCollection.parse(coll) + parsed = DtsCollection.parse(coll) exported = self.reorder_orderable(parsed.export(Mimetypes.JSON.DTS.Std)) self.assertEqual( exported, @@ -131,7 +131,7 @@ def test_collection_single_member_with_types(self): def test_collection_with_complex_child(self): coll = self.get_collection(3) - parsed = DTSCollection.parse(coll) + parsed = DtsCollection.parse(coll) exported = self.reorder_orderable(parsed.export(Mimetypes.JSON.DTS.Std)) self.assertEqual( exported, @@ -193,7 +193,7 @@ def test_collection_with_complex_child(self): def test_collection_with_cite_depth_but_no_structure(self): coll = self.get_collection(5) - parsed = DTSCollection.parse(coll) + parsed = DtsCollection.parse(coll) exported = self.reorder_orderable(parsed.export(Mimetypes.JSON.DTS.Std)) self.assertEqual(exported["dts:citeDepth"], 7, "There should be a cite depth property") self.assertNotIn("dts:citeStructure", exported, "CiteStructure was not defined") From 400d903828e3c2a9053e226c43b30b8dc77d4d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 2 Oct 2018 08:48:50 +0200 Subject: [PATCH 51/89] (DTS resolver) Now handles pagination at Navigation endpoint --- MyCapytain/resolvers/dts/api_v1.py | 65 +++++++++++++------ tests/resolvers/dts/api_v1/__init__.py | 40 ++++++++++++ .../dts/api_v1/data/collection/example2.json | 46 +++++++++++++ .../dts/api_v1/data/collection/example3.json | 64 ++++++++++++++++++ .../data/navigation/paginated/page1.json | 23 +++++++ .../data/navigation/paginated/page2.json | 24 +++++++ .../data/navigation/paginated/page3.json | 23 +++++++ 7 files changed, 264 insertions(+), 21 deletions(-) create mode 100644 tests/resolvers/dts/api_v1/data/collection/example2.json create mode 100644 tests/resolvers/dts/api_v1/data/collection/example3.json create mode 100644 tests/resolvers/dts/api_v1/data/navigation/paginated/page1.json create mode 100644 tests/resolvers/dts/api_v1/data/navigation/paginated/page2.json create mode 100644 tests/resolvers/dts/api_v1/data/navigation/paginated/page3.json diff --git a/MyCapytain/resolvers/dts/api_v1.py b/MyCapytain/resolvers/dts/api_v1.py index cbf026ea..1b38dff6 100644 --- a/MyCapytain/resolvers/dts/api_v1.py +++ b/MyCapytain/resolvers/dts/api_v1.py @@ -9,6 +9,7 @@ """ from typing import Union, Optional, Any, Dict +import re from MyCapytain.resolvers.prototypes import Resolver from MyCapytain.common.reference import BaseReference, BaseReferenceSet, \ @@ -25,6 +26,7 @@ ] _empty = [{"@value": None}] +_re_page = re.compile("page=(\d+)") def _parse_ref(ref_dict, default_type :str =None): @@ -88,33 +90,54 @@ def getReffs( if not additional_parameters: additional_parameters = {} - reffs = [] - response = self.endpoint.get_navigation( - textId, level=level, ref=subreference, - exclude=additional_parameters.get("exclude", None), - group_by=additional_parameters.get("groupBy", 1) - ) - response.raise_for_status() - - data = response.json() - data = expand(data) - - default_type = data[0].get("https://w3id.org/dts/api#citeType", _empty)[0]["@value"] - - members = data[0].get("https://www.w3.org/ns/hydra/core#member", []) - - reffs.extend([ - _parse_ref(ref, default_type=default_type) - for ref in members - ]) + references = [] + default_type = None + level_ = level + + page = 1 + while page: + kwargs = dict( + level = level, ref = subreference, + exclude=additional_parameters.get("exclude", None), + group_by=additional_parameters.get("groupBy", 1) + ) + if page != 1: + kwargs["page"] = page + + response = self.endpoint.get_navigation(textId, **kwargs) + response.raise_for_status() + + data = response.json() + data = expand(data) + if not len(data): + raise Exception("We'll see this one later") # toDo: What error should it be ? + data = data[0] + + level_ = data.get("https://w3id.org/dts/api#level", _empty)[0]["@value"] + default_type = data.get("https://w3id.org/dts/api#citeType", _empty)[0]["@value"] + members = data.get("https://www.w3.org/ns/hydra/core#member", []) + + references.extend([ + _parse_ref(ref, default_type=default_type) + for ref in members + ]) + + page = None + if "https://www.w3.org/ns/hydra/core#view" in data: + if "https://www.w3.org/ns/hydra/core#next" in data["https://www.w3.org/ns/hydra/core#view"][0]: + page = _re_page.findall( + data["https://www.w3.org/ns/hydra/core#view"] + [0]["https://www.w3.org/ns/hydra/core#next"] + [0]["@value"] + )[0] citation = None if default_type: citation = DtsCitation(name=default_type) reffs = DtsReferenceSet( - *reffs, - level=data[0].get("https://w3id.org/dts/api#level", _empty)[0]["@value"], + *references, + level=level_, citation=citation ) return reffs diff --git a/tests/resolvers/dts/api_v1/__init__.py b/tests/resolvers/dts/api_v1/__init__.py index a7020c81..e78656c0 100644 --- a/tests/resolvers/dts/api_v1/__init__.py +++ b/tests/resolvers/dts/api_v1/__init__.py @@ -353,3 +353,43 @@ def test_navigation_with_advanced_metadata(self, mock_set): reffs[4].metadata.export(Mimetypes.JSON.Std), "References metadata should be parsed correctly" ) + + @requests_mock.mock() + def test_navigation_paginated(self, mock_set): + """ Check that all pagination is browsed """ + _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?level=1&groupBy=1&id="+_id, + text=_load_mock("navigation", "paginated/page1.json"), + complete_qs=True + ) + mock_set.get( + self.root_uri+"/navigation?page=2&level=1&groupBy=1&id="+_id, + text=_load_mock("navigation", "paginated/page2.json"), + complete_qs=True + ) + mock_set.get( + self.root_uri+"/navigation?page=3&level=1&groupBy=1&id="+_id, + text=_load_mock("navigation", "paginated/page3.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id) + + self.assertEqual( + DtsReferenceSet( + DtsReference("1", type_="poem"), + DtsReference("2", type_="poem"), + DtsReference("3", type_="poem"), + DtsReference("4", type_="poem"), + DtsReference("5", type_="poem"), + DtsReference("6", type_="poem"), + DtsReference("7", type_="poem"), + DtsReference("8", type_="poem"), + DtsReference("9", type_="poem"), + level=1, + citation=DtsCitation("poem") + ), + reffs, + "Resolvers follows view property" + ) \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/collection/example2.json b/tests/resolvers/dts/api_v1/data/collection/example2.json new file mode 100644 index 00000000..bd881b6d --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/collection/example2.json @@ -0,0 +1,46 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id": "lasciva_roma", + "@type": "Collection", + "totalItems": 1, + "title" : "Lasciva Roma", + "description": "Collection of primary sources of interest in the studies of Ancient World's sexuality", + "dts:dublincore": { + "dc:creator": [ + "Thibault Clérice", "http://orcid.org/0000-0003-1852-9204" + ], + "dc:title" : [ + {"@lang": "la", "@value": "Lasciva Roma"}, + ], + "dc:description": [ + { + "@lang": "en", + "@value": "Collection of primary sources of interest in the studies of Ancient World's sexuality" + } + ], + }, + "member": [ + { + "@id" : "urn:cts:latinLit:phi1103.phi001", + "title" : "Priapeia", + "dts:dublincore": { + "dc:type": [ + "http://chs.harvard.edu/xmlns/cts#work" + ], + "dc:creator": [ + {"@lang": "en", "@value": "Anonymous"} + ], + "dc:language": ["la", "en"], + "dc:description": [ + { "@lang": "en", "@value": "Anonymous lascivious Poems" } + ], + }, + "@type" : "Collection", + "totalItems": 1 + } + ] +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/collection/example3.json b/tests/resolvers/dts/api_v1/data/collection/example3.json new file mode 100644 index 00000000..adb0474f --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/collection/example3.json @@ -0,0 +1,64 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#", + }, + "@id": "urn:cts:latinLit:phi1103.phi001", + "@type": "Collection", + "title" : "Priapeia", + "dts:dublincore": { + "dc:type": ["http://chs.harvard.edu/xmlns/cts#work"], + "dc:creator": [ + {"@lang": "en", "@value": "Anonymous"} + ], + "dc:language": ["la", "en"], + "dc:title": [{"@lang": "la", "@value": "Priapeia"}], + "dc:description": [{ + "@lang": "en", + "@value": "Anonymous lascivious Poems " + }] + }, + "totalItems" : 1, + "member": [ + { + "@id" : "urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", + "@type": "Resource", + "title" : "Priapeia", + "description": "Priapeia based on the edition of Aemilius Baehrens", + "totalItems": 0, + "dts:dublincore": { + "dc:title": [{"@lang": "la", "@value": "Priapeia"}], + "dc:description": [{ + "@lang": "en", + "@value": "Anonymous lascivious Poems " + }], + "dc:type": [ + "http://chs.harvard.edu/xmlns/cts#edition", + "dc:Text" + ], + "dc:source": ["https://archive.org/details/poetaelatinimino12baeh2"], + "dc:dateCopyrighted": 1879, + "dc:creator": [ + {"@lang": "en", "@value": "Anonymous"} + ], + "dc:contributor": ["Aemilius Baehrens"], + "dc:language": ["la", "en"] + }, + "dts:passage": "/api/dts/documents?id=urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", + "dts:references": "/api/dts/navigation?id=urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", + "dts:download": "https://raw.githubusercontent.com/lascivaroma/priapeia/master/data/phi1103/phi001/phi1103.phi001.lascivaroma-lat1.xml", + "dts:citeDepth": 2, + "dts:citeStructure": [ + { + "dts:citeType": "poem", + "dts:citeStructure": [ + { + "dts:citeType": "line" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/navigation/paginated/page1.json b/tests/resolvers/dts/api_v1/data/navigation/paginated/page1.json new file mode 100644 index 00000000..2e4fd7eb --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/navigation/paginated/page1.json @@ -0,0 +1,23 @@ +{ + "@context": { + "hydra": "https://www.w3.org/ns/hydra/core#", + "@vocab": "https://w3id.org/dts/api#" + }, + "@id":"/api/dts/navigation/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc", + "citeDepth" : 2, + "citeType": "poem", + "level": 1, + "hydra:member": [ + {"ref": "1"}, + {"ref": "2"}, + {"ref": "3"} + ], + "passage": "/dts/api/document/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc{&ref}{&start}{&end}", + "hydra:view": { + "@id": "/api/dts/collections/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grcs&page=1", + "@type": "PartialCollectionView", + "hydra:first": "/api/dts/collections/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc&page=1", + "hydra:next": "/api/dts/collections/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc&page=2", + "hydra:last": "/api/dts/collections/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc&page=3" + } +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/navigation/paginated/page2.json b/tests/resolvers/dts/api_v1/data/navigation/paginated/page2.json new file mode 100644 index 00000000..c59f05d5 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/navigation/paginated/page2.json @@ -0,0 +1,24 @@ +{ + "@context": { + "hydra": "https://www.w3.org/ns/hydra/core#", + "@vocab": "https://w3id.org/dts/api#" + }, + "@id":"/api/dts/navigation/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc", + "citeDepth" : 2, + "citeType": "poem", + "level": 1, + "hydra:member": [ + {"ref": "4"}, + {"ref": "5"}, + {"ref": "6"} + ], + "passage": "/dts/api/document/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc{&ref}{&start}{&end}", + "hydra:view": { + "@id": "/api/dts/collections/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grcs&page=2", + "@type": "PartialCollectionView", + "hydra:first": "/api/dts/collections/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc&page=1", + "hydra:previous": "/api/dts/collections/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc&page=1", + "hydra:next": "/api/dts/collections/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc&page=3", + "hydra:last": "/api/dts/collections/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc&page=3" + } +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/navigation/paginated/page3.json b/tests/resolvers/dts/api_v1/data/navigation/paginated/page3.json new file mode 100644 index 00000000..1df54064 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/navigation/paginated/page3.json @@ -0,0 +1,23 @@ +{ + "@context": { + "hydra": "https://www.w3.org/ns/hydra/core#", + "@vocab": "https://w3id.org/dts/api#" + }, + "@id":"/api/dts/navigation/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc", + "citeDepth" : 2, + "citeType": "poem", + "level": 1, + "hydra:member": [ + {"ref": "7"}, + {"ref": "8"}, + {"ref": "9"} + ], + "passage": "/dts/api/document/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc{&ref}{&start}{&end}", + "hydra:view": { + "@id": "/api/dts/collections/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grcs&page=3", + "@type": "PartialCollectionView", + "hydra:first": "/api/dts/collections/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc&page=1", + "hydra:previous": "/api/dts/collections/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc&page=2", + "hydra:last": "/api/dts/collections/?id=urn:cts:greekLit:tlg0012.tlg001.opp-grc&page=3" + } +} \ No newline at end of file From 39ac127098c4e9f662b784338db8a3fa857377ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 2 Oct 2018 08:50:58 +0200 Subject: [PATCH 52/89] (DTSResolver) Formatting --- MyCapytain/resolvers/dts/api_v1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MyCapytain/resolvers/dts/api_v1.py b/MyCapytain/resolvers/dts/api_v1.py index 1b38dff6..ee79941e 100644 --- a/MyCapytain/resolvers/dts/api_v1.py +++ b/MyCapytain/resolvers/dts/api_v1.py @@ -97,7 +97,7 @@ def getReffs( page = 1 while page: kwargs = dict( - level = level, ref = subreference, + level=level, ref=subreference, exclude=additional_parameters.get("exclude", None), group_by=additional_parameters.get("groupBy", 1) ) From ccd5944aa76469cc6e31affca454ff6781d78c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Mon, 15 Oct 2018 11:10:51 +0200 Subject: [PATCH 53/89] (DTS Resolver) Proxy system to handle pagination for references, children and parents --- MyCapytain/resolvers/dts/api_v1.py | 8 +- .../resources/collections/dts/__init__.py | 2 + .../collections/{dts.py => dts/_base.py} | 38 +- .../resources/collections/dts/_resolver.py | 146 +++++++ MyCapytain/resources/prototypes/metadata.py | 14 +- MyCapytain/retrievers/dts/__init__.py | 15 +- tests/resolvers/dts/api_v1/__init__.py | 395 ------------------ tests/resolvers/dts/api_v1/base.py | 43 ++ .../resolvers/dts/api_v1/test_collections.py | 41 ++ tests/resolvers/dts/api_v1/test_navigation.py | 315 ++++++++++++++ tests/retrievers/test_dts.py | 2 +- 11 files changed, 600 insertions(+), 419 deletions(-) create mode 100644 MyCapytain/resources/collections/dts/__init__.py rename MyCapytain/resources/collections/{dts.py => dts/_base.py} (78%) create mode 100644 MyCapytain/resources/collections/dts/_resolver.py create mode 100644 tests/resolvers/dts/api_v1/base.py create mode 100644 tests/resolvers/dts/api_v1/test_collections.py create mode 100644 tests/resolvers/dts/api_v1/test_navigation.py diff --git a/MyCapytain/resolvers/dts/api_v1.py b/MyCapytain/resolvers/dts/api_v1.py index ee79941e..685be0a2 100644 --- a/MyCapytain/resolvers/dts/api_v1.py +++ b/MyCapytain/resolvers/dts/api_v1.py @@ -16,7 +16,7 @@ DtsReference, DtsReferenceSet, DtsCitation from MyCapytain.retrievers.dts import HttpDtsRetriever from MyCapytain.common.utils.dts import parse_metadata -from MyCapytain.resources.collections.dts import DtsCollection +from MyCapytain.resources.collections.dts import HttpResolverDtsCollection from pyld.jsonld import expand @@ -69,13 +69,13 @@ def endpoint(self) -> HttpDtsRetriever: """ return self._endpoint - def getMetadata(self, objectId: str=None, **filters) -> DtsCollection: + def getMetadata(self, objectId: str=None, **filters) -> HttpResolverDtsCollection: req = self.endpoint.get_collection(objectId) req.raise_for_status() - collection = DtsCollection.parse(req.json()) + collection = HttpResolverDtsCollection.parse(req.json(), resolver=self) # Pagination is not completed upon first query. - # Pagination will be treated direction in the DtsCollection + # Pagination will be treated direction in the HttpResolverDtsCollection return collection diff --git a/MyCapytain/resources/collections/dts/__init__.py b/MyCapytain/resources/collections/dts/__init__.py new file mode 100644 index 00000000..f8819503 --- /dev/null +++ b/MyCapytain/resources/collections/dts/__init__.py @@ -0,0 +1,2 @@ +from ._base import DtsCollection +from ._resolver import HttpResolverDtsCollection diff --git a/MyCapytain/resources/collections/dts.py b/MyCapytain/resources/collections/dts/_base.py similarity index 78% rename from MyCapytain/resources/collections/dts.py rename to MyCapytain/resources/collections/dts/_base.py index 2579b116..61f1efca 100644 --- a/MyCapytain/resources/collections/dts.py +++ b/MyCapytain/resources/collections/dts/_base.py @@ -29,6 +29,7 @@ def __init__(self, identifier="", *args, **kwargs): super(DtsCollection, self).__init__(identifier, *args, **kwargs) self._expanded = False # Not sure I'll keep this self._citation = DtsCitationSet() + self._parents = set() @property def citation(self): @@ -50,8 +51,24 @@ def readable(self): return True return False + @property + def parents(self): + return self._parents + + @parents.setter + def parents(self, value): + self._parents = value + + @property + def parent(self): + raise NotImplementedError("In DTS, parent only exists in plural, via .parents") + + @parent.setter + def parent(self, value): + raise NotImplementedError("In DTS, parent only exists in plural, via .parents") + @classmethod - def parse(cls, resource, direction="children"): + def parse(cls, resource, direction="children", **additional_parameters) -> "DtsCollection": """ Given a dict representation of a json object, generate a DTS Collection :param resource: @@ -66,8 +83,10 @@ def parse(cls, resource, direction="children"): raise JsonLdCollectionMissing("Missing collection in JSON") collection = collection[0] - obj = cls(identifier=resource["@id"]) - + obj = cls( + identifier=resource["@id"], + **additional_parameters + ) # We retrieve first the descriptiooon and label that are dependant on Hydra for val_dict in collection[str(_hyd.title)]: obj.set_label(val_dict["@value"], None) @@ -92,15 +111,15 @@ def parse(cls, resource, direction="children"): parse_metadata(obj.metadata, collection) members = cls.parse_member( - collection, obj, direction + collection, obj, direction, **additional_parameters ) - if direction == "children": + if direction == "children": # ToDo: Should be in a third function ? obj.children.update({ coll.id: coll for coll in members }) else: - obj.parents.extend(members) + obj.parents.add(members) return obj @@ -109,7 +128,8 @@ def parse_member( cls, obj: dict, collection: "DtsCollection", - direction) -> List["DtsCollection"]: + direction: str, + **additional_parameters) -> List["DtsCollection"]: """ Parse the member value of a Collection response and returns the list of object while setting the graph relationship based on `direction` @@ -121,9 +141,9 @@ def parse_member( members = [] for member in obj.get(str(_hyd.member), []): - subcollection = cls.parse(member) + subcollection = cls.parse(member, **additional_parameters) if direction == "children": - subcollection.parent = collection + subcollection.parents.update({collection}) members.append(subcollection) return members diff --git a/MyCapytain/resources/collections/dts/_resolver.py b/MyCapytain/resources/collections/dts/_resolver.py new file mode 100644 index 00000000..d3480ddd --- /dev/null +++ b/MyCapytain/resources/collections/dts/_resolver.py @@ -0,0 +1,146 @@ +from MyCapytain.common.constants import RDF_NAMESPACES +from pyld.jsonld import expand +import re + +from ._base import DtsCollection + + +_hyd = RDF_NAMESPACES.HYDRA +_empty = [{"@value": None}] +_re_page = re.compile("page=(\d+)") + + +class PaginatedProxy: + def __init__(self, proxied, update_lambda, condition_lambda): + self._proxied = proxied + self._condition_lambda = condition_lambda + self._update_lambda = update_lambda + + def __getattr__(self, item): + if item == "update": + return self._proxied.update + if item == "add": + return self._proxied.add + if item == "set": + return self.set + else: + if not self._condition_lambda(): + self._update_lambda() + return getattr(self._proxied, item) + + def set(self, value): + self._proxied = value + + +class HttpResolverDtsCollection(DtsCollection): + def __init__( + self, + identifier: str, + resolver: "HttpDtsResolver", + metadata_parsed=True, *args, **kwargs): + super(HttpResolverDtsCollection, self).__init__(identifier, *args, **kwargs) + + self._children = PaginatedProxy( + self._children, + lambda: self._parse_paginated_members(direction="children"), + lambda: self._parsed["children"] + ) + self._parents = PaginatedProxy( + self._parents, + lambda: self._parse_paginated_members(direction="parents"), + lambda: self._parsed["parents"] + ) + + self._resolver = resolver + self._metadata_parsed = metadata_parsed + + self._parsed = { + "children": False, + "parents": False, + "metadata": False + } + self._last_page_parsed = { + "children": None, + "parents": None, + } + + def _parse_paginated_members(self, direction="children"): + """ Launch parsing of children + """ + + page = self._last_page_parsed[direction] + if not page: + page = 1 + while page: + if page > 1: + response = self._resolver.endpoint.get_collection( + collection_id=self.id, + page=page, + nav=direction + ) + else: + response = self._resolver.endpoint.get_collection( + collection_id=self.id, + nav=direction + ) + response.raise_for_status() + + data = response.json() + data = expand(data) + + self.parse_member(obj=data, collection=self, direction=direction) + self._last_page_parsed[direction] = page + + page = None + if "https://www.w3.org/ns/hydra/core#view" in data: + if "https://www.w3.org/ns/hydra/core#next" in data["https://www.w3.org/ns/hydra/core#view"][0]: + page = _re_page.findall( + data["https://www.w3.org/ns/hydra/core#view"] + [0]["https://www.w3.org/ns/hydra/core#next"] + [0]["@value"] + )[0] + + self._parsed[direction] = True + + @property + def children(self): + if not self._parsed["children"]: + self._parse_paginated_members(direction="children") + return super(HttpResolverDtsCollection, self).children + + @property + def parents(self): + if not self._parsed["parents"]: + self._parse_paginated_members(direction="parents") + return super(HttpResolverDtsCollection, self).parents + + @classmethod + def parse_member( + cls, + obj: dict, + collection: "DtsCollection", + direction: str, + **additional_parameters): + + """ Parse the member value of a Collection response + and returns the list of object while setting the graph + relationship based on `direction` + + :param obj: PyLD parsed JSON+LD + :param collection: Collection attached to the member property + :param direction: Direction of the member (children, parent) + """ + members = [] + + # Start pagination check here + + for member in obj.get(str(_hyd.member), []): + subcollection = cls.parse(member, metadata_parsed=False, **additional_parameters) + if direction == "children": + subcollection._parents.set({collection}) + members.append(subcollection) + + if "https://www.w3.org/ns/hydra/core#view" not in obj: + collection._parsed[direction] = True + + return members diff --git a/MyCapytain/resources/prototypes/metadata.py b/MyCapytain/resources/prototypes/metadata.py index a7b09094..d4b46c6d 100644 --- a/MyCapytain/resources/prototypes/metadata.py +++ b/MyCapytain/resources/prototypes/metadata.py @@ -65,8 +65,8 @@ def __init__(self, identifier="", *args, **kwargs): ] ) - self.__parent__ = None - self.__children__ = {} + self._parent = None + self._children = {} def __repr__(self): return "%s(%s)" % (self.__class__.__name__, self.id) @@ -168,7 +168,7 @@ def children(self) -> dict: :rtype: dict """ - return self.__children__ + return self._children @property def parents(self) -> List["Collection"]: @@ -189,7 +189,7 @@ def parent(self): :rtype: Collection """ - return self.__parent__ + return self._parent @parent.setter def parent(self, parent): @@ -199,13 +199,13 @@ def parent(self, parent): :type parent: Collection :return: """ - self.__parent__ = parent + self._parent = parent self.graph.add( (self.asNode(), RDF_NAMESPACES.CAPITAINS.parent, parent.asNode()) ) - parent.__add_member__(self) + parent._add_member(self) - def __add_member__(self, member): + def _add_member(self, member): """ Does not add member if it already knows it. .. warning:: It should not be called ! diff --git a/MyCapytain/retrievers/dts/__init__.py b/MyCapytain/retrievers/dts/__init__.py index 94a71282..5ce98b44 100644 --- a/MyCapytain/retrievers/dts/__init__.py +++ b/MyCapytain/retrievers/dts/__init__.py @@ -24,7 +24,7 @@ def __init__(self, endpoint): super(HttpDtsRetriever, self).__init__(endpoint) self._routes = None - def call(self, route, parameters, mimetype="application/ld+json"): + def call(self, route, parameters, mimetype="application/ld+json", defaults=None): """ Call an endpoint given the parameters :param route: Named of the route which is called @@ -35,9 +35,13 @@ def call(self, route, parameters, mimetype="application/ld+json"): :type mimetype: str :rtype: text """ - + if not defaults: + defaults = {} parameters = { - key: str(parameters[key]) for key in parameters if parameters[key] is not None + key: str(parameters[key]) + for key in parameters + if parameters[key] is not None and + parameters[key] != defaults.get(key, None) } parameters.update(self.routes[route].query_dict) @@ -103,6 +107,11 @@ def get_collection(self, collection_id=None, nav="children", page=None): "id": collection_id, "nav": nav, "page": page + }, + defaults={ + "id": None, + "nav": "children", + "page": 1 } ) diff --git a/tests/resolvers/dts/api_v1/__init__.py b/tests/resolvers/dts/api_v1/__init__.py index e78656c0..e69de29b 100644 --- a/tests/resolvers/dts/api_v1/__init__.py +++ b/tests/resolvers/dts/api_v1/__init__.py @@ -1,395 +0,0 @@ -import os.path -import json -import typing -import unittest -import requests_mock -from MyCapytain.resolvers.dts.api_v1 import HttpDtsResolver -from MyCapytain.common.reference import DtsReferenceSet, DtsReference, DtsCitation -from MyCapytain.common.metadata import Metadata -from MyCapytain.common.constants import Mimetypes -from rdflib.term import URIRef -# Set-up for the test classes -_cur_path = os.path.abspath( - os.path.join( - os.path.dirname(__file__) - ) -) - - -def quote(string): - return string.replace(":", "%3A") - - -def _load_mock(*files: str) -> str: - """ Easily load a mock file - - :param endpoint: Endpoint that is being tested - :param example: Example to load - :return: Example data - """ - fname = os.path.abspath( - os.path.join( - _cur_path, - "data", - *files - ) - ) - with open(fname) as fopen: - data = fopen.read() - return data - - -def _load_json_mock(endpoint: str, example: str) -> typing.Union[dict, list]: - return json.loads(_load_mock(endpoint, example)) - - -class TestHttpDtsResolverCollection(unittest.TestCase): - def setUp(self): - self.root_uri = "http://foobar.com/api/dts" - self.resolver = HttpDtsResolver(self.root_uri) - - @requests_mock.mock() - def test_simple_root_access(self, mock_set): - mock_set.get(self.root_uri, text=_load_mock("root.json")) - mock_set.get( - self.root_uri+"/collections?nav=children", - text=_load_mock("collection", "example1.json"), - complete_qs=True - ) - collection = self.resolver.getMetadata() - self.assertEqual( - 3, collection.size, - "There should be 3 collections" - ) - self.assertEqual( - "Cartulaires", str(collection["/cartulaires"].get_label()), - "Titles of subcollection and subcollection should be " - "stored under their IDs" - ) - self.assertEqual( - "Collection Générale de l'École Nationale des Chartes", - str(collection.get_label()), - "Label of the main collection should be correctly parsed" - ) - self.assertEqual( - ["https://viaf.org/viaf/167874585", "École Nationale des Chartes"], - sorted([ - str(obj) - for obj in collection.metadata.get( - URIRef("http://purl.org/dc/terms/publisher") - ) - ]) - ) - - -class TestHttpDtsResolverNavigation(unittest.TestCase): - def setUp(self): - self.root_uri = "http://foobar.com/api/dts" - self.resolver = HttpDtsResolver(self.root_uri) - - @requests_mock.mock() - def test_navigation_simple(self, mock_set): - """ Example 1 of Public Draft. Includes on top of it a citeType=poem""" - _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" - mock_set.get(self.root_uri, text=_load_mock("root.json")) - mock_set.get( - self.root_uri+"/navigation?level=1&groupBy=1&id="+_id, - text=_load_mock("navigation", "example1.json"), - complete_qs=True - ) - reffs = self.resolver.getReffs(_id) - - self.assertEqual( - DtsReferenceSet( - DtsReference("1", type_="poem"), - DtsReference("2", type_="poem"), - DtsReference("3", type_="poem"), - level=1, - citation=DtsCitation("poem") - ), - reffs, - "References are parsed with types and level" - ) - - @requests_mock.mock() - def test_navigation_simple_level(self, mock_set): - """ Test navigation second level """ - _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" - mock_set.get(self.root_uri, text=_load_mock("root.json")) - mock_set.get( - self.root_uri+"/navigation?level=2&groupBy=1&id="+_id, - text=_load_mock("navigation", "example2.json"), - complete_qs=True - ) - reffs = self.resolver.getReffs(_id, level=2) - - self.assertEqual( - DtsReferenceSet( - DtsReference("1.1"), - DtsReference("1.2"), - DtsReference("2.1"), - DtsReference("2.2"), - DtsReference("3.1"), - DtsReference("3.2"), - level=2 - ), - reffs, - "Resolver forwards level=2 parameter" - ) - - @requests_mock.mock() - def test_navigation_simple_ref_deeper_level(self, mock_set): - """ Test navigation second level from single reff """ - _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" - mock_set.get(self.root_uri, text=_load_mock("root.json")) - mock_set.get( - self.root_uri+"/navigation?ref=1&level=1&groupBy=1&id="+_id, - text=_load_mock("navigation", "example3.json"), - complete_qs=True - ) - reffs = self.resolver.getReffs(_id, subreference="1") - - self.assertEqual( - DtsReferenceSet( - DtsReference("1.1"), - DtsReference("1.2"), - level=2 - ), - reffs, - "Resolver forwards subreference parameter" - ) - - @requests_mock.mock() - def test_navigation_simple_level_ref(self, mock_set): - """ Test navigation 2 level deeper than requested reff""" - _id = "urn:cts:latinLit:phi1294.phi001.perseus-lat2" - mock_set.get(self.root_uri, text=_load_mock("root.json")) - mock_set.get( - self.root_uri+"/navigation?ref=1&level=2&groupBy=1&id="+_id, - text=_load_mock("navigation", "example4.json"), - complete_qs=True - ) - reffs = self.resolver.getReffs(_id, subreference="1", level=2) - - self.assertEqual( - DtsReferenceSet( - DtsReference("1.1.1"), - DtsReference("1.1.2"), - DtsReference("1.2.1"), - DtsReference("1.2.2"), - level=3 - ), - reffs, - "Resolver forwards level + subreference parameter correctly" - ) - - @requests_mock.mock() - def test_navigation_ask_in_range(self, mock_set): - """ Test that ranges are correctly requested""" - _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" - mock_set.get(self.root_uri, text=_load_mock("root.json")) - mock_set.get( - self.root_uri+"/navigation?start=1&end=3&level=1&groupBy=1&id="+_id, - text=_load_mock("navigation", "example5.json"), - complete_qs=True - ) - reffs = self.resolver.getReffs(_id, subreference=DtsReference(1, 3)) - - self.assertEqual( - DtsReferenceSet( - DtsReference("1"), - DtsReference("2"), - DtsReference("3"), - level=1 - ), - reffs, - "Resolver handles range reference as subreference parameter" - ) - - @requests_mock.mock() - def test_navigation_ask_in_range_with_level(self, mock_set): - """ Test that ranges are correctly requested""" - _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" - mock_set.get(self.root_uri, text=_load_mock("root.json")) - mock_set.get( - self.root_uri+"/navigation?start=1&end=3&level=2&groupBy=1&id="+_id, - text=_load_mock("navigation", "example6.json"), - complete_qs=True - ) - reffs = self.resolver.getReffs(_id, subreference=DtsReference(1, 3), level=2) - - self.assertEqual( - DtsReferenceSet( - DtsReference("1.1"), - DtsReference("1.2"), - DtsReference("2.1"), - DtsReference("2.2"), - DtsReference("3.1"), - DtsReference("3.2"), - level=2 - ), - reffs, - "Resolver forwards correctly range subreference and level" - ) - - @requests_mock.mock() - def test_navigation_with_level_and_group(self, mock_set): - """ Test that ranges are correctly parsed""" - _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" - mock_set.get(self.root_uri, text=_load_mock("root.json")) - mock_set.get( - self.root_uri+"/navigation?ref=1&level=2&groupBy=2&id="+_id, - text=_load_mock("navigation", "example7.json"), - complete_qs=True - ) - reffs = self.resolver.getReffs( - _id, additional_parameters={"groupBy": 2}, - subreference=DtsReference(1), level=2 - ) - - self.assertEqual( - DtsReferenceSet( - DtsReference("1.1.1", "1.1.2"), - DtsReference("1.2.1", "1.2.2"), - level=3 - ), - reffs, - "groupBy additional parameter is correctly forward + ranges are parsed" - ) - - @requests_mock.mock() - def test_navigation_with_different_types(self, mock_set): - """ Test a navigation where a root type of passage is defined but some are redefined """ - _id = "http://data.bnf.fr/ark:/12148/cb11936111v" - mock_set.get(self.root_uri, text=_load_mock("root.json")) - mock_set.get( - self.root_uri+"/navigation?level=1&groupBy=1&id="+_id, - text=_load_mock("navigation", "example8.json"), - complete_qs=True - ) - reffs = self.resolver.getReffs(_id) - - self.assertEqual( - DtsReferenceSet( - DtsReference("Av", type_="preface"), - DtsReference("Pr", type_="preface"), - DtsReference("1", type_="letter"), - DtsReference("2", type_="letter"), - DtsReference("3", type_="letter"), - level=1, - citation=DtsCitation(name="letter") - ), - reffs, - "Test that default Reference type is overridden when due" - ) - - @requests_mock.mock() - def test_navigation_with_advanced_metadata(self, mock_set): - """ Test references parsing with advanced metadata """ - _id = "http://data.bnf.fr/ark:/12148/cb11936111v" - mock_set.get(self.root_uri, text=_load_mock("root.json")) - mock_set.get( - self.root_uri+"/navigation?level=1&groupBy=1&id="+_id, - text=_load_mock("navigation", "example9.json"), - complete_qs=True - ) - reffs = self.resolver.getReffs(_id) - - self.assertEqual( - DtsReferenceSet( - DtsReference("Av"), - DtsReference("Pr"), - DtsReference("1"), - DtsReference("2"), - DtsReference("3"), - level=1 - ), - reffs, - "Test that default Reference type is overridden when due" - ) - - ref_Av_metadata = Metadata() - ref_Av_metadata.add(URIRef("http://purl.org/dc/terms/title"), "Avertissement de l'Éditeur") - self.assertEqual( - ref_Av_metadata.export(Mimetypes.JSON.Std), - reffs[0].metadata.export(Mimetypes.JSON.Std), - "References metadata should be parsed correctly" - ) - - ref_Pr_metadata = Metadata() - ref_Pr_metadata.add(URIRef("http://purl.org/dc/terms/title"), "Préface") - self.assertEqual( - ref_Pr_metadata.export(Mimetypes.JSON.Std), - reffs[1].metadata.export(Mimetypes.JSON.Std), - "References metadata should be parsed correctly" - ) - - ref_1_metadata = Metadata() - ref_1_metadata.add(URIRef("http://purl.org/dc/terms/title"), "Lettre 1") - ref_1_metadata.add(URIRef("http://foo.bar/ontology#fictionalSender"), "Cécile Volanges") - ref_1_metadata.add(URIRef("http://foo.bar/ontology#fictionalRecipient"), "Sophie Carnay") - self.assertEqual( - ref_1_metadata.export(Mimetypes.JSON.Std), - reffs[2].metadata.export(Mimetypes.JSON.Std), - "References metadata should be parsed correctly" - ) - - ref_2_metadata = Metadata() - ref_2_metadata.add(URIRef("http://purl.org/dc/terms/title"), "Lettre 2") - ref_2_metadata.add(URIRef("http://foo.bar/ontology#fictionalSender"), "La Marquise de Merteuil") - ref_2_metadata.add(URIRef("http://foo.bar/ontology#fictionalRecipient"), "Vicomte de Valmont") - self.assertEqual( - ref_2_metadata.export(Mimetypes.JSON.Std), - reffs[3].metadata.export(Mimetypes.JSON.Std), - "References metadata should be parsed correctly" - ) - - ref_3_metadata = Metadata() - ref_3_metadata.add(URIRef("http://purl.org/dc/terms/title"), "Lettre 3") - ref_3_metadata.add(URIRef("http://foo.bar/ontology#fictionalSender"), "Cécile Volanges") - ref_3_metadata.add(URIRef("http://foo.bar/ontology#fictionalRecipient"), "Sophie Carnay") - self.assertEqual( - ref_3_metadata.export(Mimetypes.JSON.Std), - reffs[4].metadata.export(Mimetypes.JSON.Std), - "References metadata should be parsed correctly" - ) - - @requests_mock.mock() - def test_navigation_paginated(self, mock_set): - """ Check that all pagination is browsed """ - _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" - mock_set.get(self.root_uri, text=_load_mock("root.json")) - mock_set.get( - self.root_uri+"/navigation?level=1&groupBy=1&id="+_id, - text=_load_mock("navigation", "paginated/page1.json"), - complete_qs=True - ) - mock_set.get( - self.root_uri+"/navigation?page=2&level=1&groupBy=1&id="+_id, - text=_load_mock("navigation", "paginated/page2.json"), - complete_qs=True - ) - mock_set.get( - self.root_uri+"/navigation?page=3&level=1&groupBy=1&id="+_id, - text=_load_mock("navigation", "paginated/page3.json"), - complete_qs=True - ) - reffs = self.resolver.getReffs(_id) - - self.assertEqual( - DtsReferenceSet( - DtsReference("1", type_="poem"), - DtsReference("2", type_="poem"), - DtsReference("3", type_="poem"), - DtsReference("4", type_="poem"), - DtsReference("5", type_="poem"), - DtsReference("6", type_="poem"), - DtsReference("7", type_="poem"), - DtsReference("8", type_="poem"), - DtsReference("9", type_="poem"), - level=1, - citation=DtsCitation("poem") - ), - reffs, - "Resolvers follows view property" - ) \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/base.py b/tests/resolvers/dts/api_v1/base.py new file mode 100644 index 00000000..f14d2b5d --- /dev/null +++ b/tests/resolvers/dts/api_v1/base.py @@ -0,0 +1,43 @@ +import os.path +import json +import typing +import unittest +import requests_mock +from MyCapytain.resolvers.dts.api_v1 import HttpDtsResolver +from MyCapytain.common.reference import DtsReferenceSet, DtsReference, DtsCitation +from MyCapytain.common.metadata import Metadata +from MyCapytain.common.constants import Mimetypes +from rdflib.term import URIRef +# Set-up for the test classes +_cur_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__) + ) +) + + +def quote(string): + return string.replace(":", "%3A") + + +def _load_mock(*files: str) -> str: + """ Easily load a mock file + + :param endpoint: Endpoint that is being tested + :param example: Example to load + :return: Example data + """ + fname = os.path.abspath( + os.path.join( + _cur_path, + "data", + *files + ) + ) + with open(fname) as fopen: + data = fopen.read() + return data + + +def _load_json_mock(endpoint: str, example: str) -> typing.Union[dict, list]: + return json.loads(_load_mock(endpoint, example)) diff --git a/tests/resolvers/dts/api_v1/test_collections.py b/tests/resolvers/dts/api_v1/test_collections.py new file mode 100644 index 00000000..ae93c36c --- /dev/null +++ b/tests/resolvers/dts/api_v1/test_collections.py @@ -0,0 +1,41 @@ +from .base import * +from .base import _load_mock, _load_json_mock + + +class TestHttpDtsResolverCollection(unittest.TestCase): + def setUp(self): + self.root_uri = "http://foobar.com/api/dts" + self.resolver = HttpDtsResolver(self.root_uri) + + @requests_mock.mock() + def test_simple_root_access(self, mock_set): + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/collections", + text=_load_mock("collection", "example1.json"), + complete_qs=True + ) + collection = self.resolver.getMetadata() + self.assertEqual( + 3, collection.size, + "There should be 3 collections" + ) + self.assertEqual( + "Cartulaires", str(collection["/cartulaires"].get_label()), + "Titles of subcollection and subcollection should be " + "stored under their IDs" + ) + self.assertEqual( + "Collection Générale de l'École Nationale des Chartes", + str(collection.get_label()), + "Label of the main collection should be correctly parsed" + ) + self.assertEqual( + ["https://viaf.org/viaf/167874585", "École Nationale des Chartes"], + sorted([ + str(obj) + for obj in collection.metadata.get( + URIRef("http://purl.org/dc/terms/publisher") + ) + ]) + ) diff --git a/tests/resolvers/dts/api_v1/test_navigation.py b/tests/resolvers/dts/api_v1/test_navigation.py new file mode 100644 index 00000000..4477667d --- /dev/null +++ b/tests/resolvers/dts/api_v1/test_navigation.py @@ -0,0 +1,315 @@ +from .base import * +from .base import _load_mock, _load_json_mock + + +class TestHttpDtsResolverNavigation(unittest.TestCase): + def setUp(self): + self.root_uri = "http://foobar.com/api/dts" + self.resolver = HttpDtsResolver(self.root_uri) + + @requests_mock.mock() + def test_navigation_simple(self, mock_set): + """ Example 1 of Public Draft. Includes on top of it a citeType=poem""" + _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?level=1&groupBy=1&id="+_id, + text=_load_mock("navigation", "example1.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id) + + self.assertEqual( + DtsReferenceSet( + DtsReference("1", type_="poem"), + DtsReference("2", type_="poem"), + DtsReference("3", type_="poem"), + level=1, + citation=DtsCitation("poem") + ), + reffs, + "References are parsed with types and level" + ) + + @requests_mock.mock() + def test_navigation_simple_level(self, mock_set): + """ Test navigation second level """ + _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?level=2&groupBy=1&id="+_id, + text=_load_mock("navigation", "example2.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id, level=2) + + self.assertEqual( + DtsReferenceSet( + DtsReference("1.1"), + DtsReference("1.2"), + DtsReference("2.1"), + DtsReference("2.2"), + DtsReference("3.1"), + DtsReference("3.2"), + level=2 + ), + reffs, + "Resolver forwards level=2 parameter" + ) + + @requests_mock.mock() + def test_navigation_simple_ref_deeper_level(self, mock_set): + """ Test navigation second level from single reff """ + _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?ref=1&level=1&groupBy=1&id="+_id, + text=_load_mock("navigation", "example3.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id, subreference="1") + + self.assertEqual( + DtsReferenceSet( + DtsReference("1.1"), + DtsReference("1.2"), + level=2 + ), + reffs, + "Resolver forwards subreference parameter" + ) + + @requests_mock.mock() + def test_navigation_simple_level_ref(self, mock_set): + """ Test navigation 2 level deeper than requested reff""" + _id = "urn:cts:latinLit:phi1294.phi001.perseus-lat2" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?ref=1&level=2&groupBy=1&id="+_id, + text=_load_mock("navigation", "example4.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id, subreference="1", level=2) + + self.assertEqual( + DtsReferenceSet( + DtsReference("1.1.1"), + DtsReference("1.1.2"), + DtsReference("1.2.1"), + DtsReference("1.2.2"), + level=3 + ), + reffs, + "Resolver forwards level + subreference parameter correctly" + ) + + @requests_mock.mock() + def test_navigation_ask_in_range(self, mock_set): + """ Test that ranges are correctly requested""" + _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?start=1&end=3&level=1&groupBy=1&id="+_id, + text=_load_mock("navigation", "example5.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id, subreference=DtsReference(1, 3)) + + self.assertEqual( + DtsReferenceSet( + DtsReference("1"), + DtsReference("2"), + DtsReference("3"), + level=1 + ), + reffs, + "Resolver handles range reference as subreference parameter" + ) + + @requests_mock.mock() + def test_navigation_ask_in_range_with_level(self, mock_set): + """ Test that ranges are correctly requested""" + _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?start=1&end=3&level=2&groupBy=1&id="+_id, + text=_load_mock("navigation", "example6.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id, subreference=DtsReference(1, 3), level=2) + + self.assertEqual( + DtsReferenceSet( + DtsReference("1.1"), + DtsReference("1.2"), + DtsReference("2.1"), + DtsReference("2.2"), + DtsReference("3.1"), + DtsReference("3.2"), + level=2 + ), + reffs, + "Resolver forwards correctly range subreference and level" + ) + + @requests_mock.mock() + def test_navigation_with_level_and_group(self, mock_set): + """ Test that ranges are correctly parsed""" + _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?ref=1&level=2&groupBy=2&id="+_id, + text=_load_mock("navigation", "example7.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs( + _id, additional_parameters={"groupBy": 2}, + subreference=DtsReference(1), level=2 + ) + + self.assertEqual( + DtsReferenceSet( + DtsReference("1.1.1", "1.1.2"), + DtsReference("1.2.1", "1.2.2"), + level=3 + ), + reffs, + "groupBy additional parameter is correctly forward + ranges are parsed" + ) + + @requests_mock.mock() + def test_navigation_with_different_types(self, mock_set): + """ Test a navigation where a root type of passage is defined but some are redefined """ + _id = "http://data.bnf.fr/ark:/12148/cb11936111v" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?level=1&groupBy=1&id="+_id, + text=_load_mock("navigation", "example8.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id) + + self.assertEqual( + DtsReferenceSet( + DtsReference("Av", type_="preface"), + DtsReference("Pr", type_="preface"), + DtsReference("1", type_="letter"), + DtsReference("2", type_="letter"), + DtsReference("3", type_="letter"), + level=1, + citation=DtsCitation(name="letter") + ), + reffs, + "Test that default Reference type is overridden when due" + ) + + @requests_mock.mock() + def test_navigation_with_advanced_metadata(self, mock_set): + """ Test references parsing with advanced metadata """ + _id = "http://data.bnf.fr/ark:/12148/cb11936111v" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?level=1&groupBy=1&id="+_id, + text=_load_mock("navigation", "example9.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id) + + self.assertEqual( + DtsReferenceSet( + DtsReference("Av"), + DtsReference("Pr"), + DtsReference("1"), + DtsReference("2"), + DtsReference("3"), + level=1 + ), + reffs, + "Test that default Reference type is overridden when due" + ) + + ref_Av_metadata = Metadata() + ref_Av_metadata.add(URIRef("http://purl.org/dc/terms/title"), "Avertissement de l'Éditeur") + self.assertEqual( + ref_Av_metadata.export(Mimetypes.JSON.Std), + reffs[0].metadata.export(Mimetypes.JSON.Std), + "References metadata should be parsed correctly" + ) + + ref_Pr_metadata = Metadata() + ref_Pr_metadata.add(URIRef("http://purl.org/dc/terms/title"), "Préface") + self.assertEqual( + ref_Pr_metadata.export(Mimetypes.JSON.Std), + reffs[1].metadata.export(Mimetypes.JSON.Std), + "References metadata should be parsed correctly" + ) + + ref_1_metadata = Metadata() + ref_1_metadata.add(URIRef("http://purl.org/dc/terms/title"), "Lettre 1") + ref_1_metadata.add(URIRef("http://foo.bar/ontology#fictionalSender"), "Cécile Volanges") + ref_1_metadata.add(URIRef("http://foo.bar/ontology#fictionalRecipient"), "Sophie Carnay") + self.assertEqual( + ref_1_metadata.export(Mimetypes.JSON.Std), + reffs[2].metadata.export(Mimetypes.JSON.Std), + "References metadata should be parsed correctly" + ) + + ref_2_metadata = Metadata() + ref_2_metadata.add(URIRef("http://purl.org/dc/terms/title"), "Lettre 2") + ref_2_metadata.add(URIRef("http://foo.bar/ontology#fictionalSender"), "La Marquise de Merteuil") + ref_2_metadata.add(URIRef("http://foo.bar/ontology#fictionalRecipient"), "Vicomte de Valmont") + self.assertEqual( + ref_2_metadata.export(Mimetypes.JSON.Std), + reffs[3].metadata.export(Mimetypes.JSON.Std), + "References metadata should be parsed correctly" + ) + + ref_3_metadata = Metadata() + ref_3_metadata.add(URIRef("http://purl.org/dc/terms/title"), "Lettre 3") + ref_3_metadata.add(URIRef("http://foo.bar/ontology#fictionalSender"), "Cécile Volanges") + ref_3_metadata.add(URIRef("http://foo.bar/ontology#fictionalRecipient"), "Sophie Carnay") + self.assertEqual( + ref_3_metadata.export(Mimetypes.JSON.Std), + reffs[4].metadata.export(Mimetypes.JSON.Std), + "References metadata should be parsed correctly" + ) + + @requests_mock.mock() + def test_navigation_paginated(self, mock_set): + """ Check that all pagination is browsed """ + _id = "urn:cts:greekLit:tlg0012.tlg001.opp-grc" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/navigation?level=1&groupBy=1&id="+_id, + text=_load_mock("navigation", "paginated/page1.json"), + complete_qs=True + ) + mock_set.get( + self.root_uri+"/navigation?page=2&level=1&groupBy=1&id="+_id, + text=_load_mock("navigation", "paginated/page2.json"), + complete_qs=True + ) + mock_set.get( + self.root_uri+"/navigation?page=3&level=1&groupBy=1&id="+_id, + text=_load_mock("navigation", "paginated/page3.json"), + complete_qs=True + ) + reffs = self.resolver.getReffs(_id) + + self.assertEqual( + DtsReferenceSet( + DtsReference("1", type_="poem"), + DtsReference("2", type_="poem"), + DtsReference("3", type_="poem"), + DtsReference("4", type_="poem"), + DtsReference("5", type_="poem"), + DtsReference("6", type_="poem"), + DtsReference("7", type_="poem"), + DtsReference("8", type_="poem"), + DtsReference("9", type_="poem"), + level=1, + citation=DtsCitation("poem") + ), + reffs, + "Resolvers follows view property" + ) \ No newline at end of file diff --git a/tests/retrievers/test_dts.py b/tests/retrievers/test_dts.py index c95ae4a5..f6d7448c 100644 --- a/tests/retrievers/test_dts.py +++ b/tests/retrievers/test_dts.py @@ -140,7 +140,7 @@ def test_get_collection_headers_parsing_and_hit(self): _Navigation("18", "20", "500", None, "1") ) - self.assertInCalls(_SERVER_URI+"collections/", {"nav": ["children"]}, ) + self.assertInCalls(_SERVER_URI+"collections/", {}) @responses.activate def test_querystring_type_of_route(self): From 8655100e9b312270c05b80385d591340ccdcb6d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Mon, 15 Oct 2018 15:38:36 +0200 Subject: [PATCH 54/89] (DTS resolver) Unsatisfactory solution to children reparsing... --- MyCapytain/resources/collections/dts/_base.py | 75 +++++++----- .../resources/collections/dts/_resolver.py | 17 +++ .../dts/api_v1/data/collection/example2.json | 20 +--- .../dts/api_v1/data/collection/example3.json | 14 +-- ...inLit:phi1103.phi001.lascivaroma-lat1.json | 44 +++++++ .../resolvers/dts/api_v1/test_collections.py | 41 ------- tests/resolvers/dts/api_v1/test_metadata.py | 107 ++++++++++++++++++ 7 files changed, 227 insertions(+), 91 deletions(-) create mode 100644 tests/resolvers/dts/api_v1/data/collection/id/urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1.json delete mode 100644 tests/resolvers/dts/api_v1/test_collections.py create mode 100644 tests/resolvers/dts/api_v1/test_metadata.py diff --git a/MyCapytain/resources/collections/dts/_base.py b/MyCapytain/resources/collections/dts/_base.py index 61f1efca..1a11bab7 100644 --- a/MyCapytain/resources/collections/dts/_base.py +++ b/MyCapytain/resources/collections/dts/_base.py @@ -78,50 +78,71 @@ def parse(cls, resource, direction="children", **additional_parameters) -> "DtsC :rtype: DtsCollection """ - collection = jsonld.expand(resource) - if len(collection) == 0: + data = jsonld.expand(resource) + if len(data) == 0: raise JsonLdCollectionMissing("Missing collection in JSON") - collection = collection[0] + data = data[0] obj = cls( identifier=resource["@id"], **additional_parameters ) - # We retrieve first the descriptiooon and label that are dependant on Hydra - for val_dict in collection[str(_hyd.title)]: - obj.set_label(val_dict["@value"], None) - - for val_dict in collection["@type"]: - obj.type = val_dict - - # We retrieve the Citation System - _cite_structure_term = str(_dts.term("citeStructure")) - if _cite_structure_term in collection and collection[_cite_structure_term]: - obj.citation = cls.CitationSet.ingest(collection[_cite_structure_term]) - _cite_depth_term = str(_dts.term("citeDepth")) - if _cite_depth_term in collection and collection[_cite_depth_term]: - obj.citation.depth = collection[_cite_depth_term][0]["@value"] + obj._parse_metadata(data) + obj._parse_members(data, direction=direction, **additional_parameters) - for val_dict in collection[str(_hyd.totalItems)]: - obj.metadata.add(_hyd.totalItems, val_dict["@value"], 0) + return obj - for val_dict in collection.get(str(_hyd.description), []): - obj.metadata.add(_hyd.description, val_dict["@value"], None) + def _parse_members(self, data, direction: str="children", **additional_parameters: dict): + """ - parse_metadata(obj.metadata, collection) - members = cls.parse_member( - collection, obj, direction, **additional_parameters + :param data: + :param direction: + :param additional_parameters: + :return: + """ + members = self.parse_member( + data, self, direction, **additional_parameters ) if direction == "children": # ToDo: Should be in a third function ? - obj.children.update({ + self.children.update({ coll.id: coll for coll in members }) else: - obj.parents.add(members) + self.parents.add(members) - return obj + def _parse_metadata(self, data: dict): + + # We retrieve first the descriptiooon and label that are dependant on Hydra + for val_dict in data[str(_hyd.title)]: + self.set_label(val_dict["@value"], None) + for val_dict in data["@type"]: + self.type = val_dict + + # We retrieve the Citation System + _cite_structure_term = str(_dts.term("citeStructure")) + if _cite_structure_term in data and data[_cite_structure_term]: + self.citation = self.CitationSet.ingest(data[_cite_structure_term]) + + _cite_depth_term = str(_dts.term("citeDepth")) + if _cite_depth_term in data and data[_cite_depth_term]: + self.citation.depth = data[_cite_depth_term][0]["@value"] + + for val_dict in data[str(_hyd.totalItems)]: + self.metadata.add(_hyd.totalItems, val_dict["@value"], 0) + + for val_dict in data.get(str(_hyd.description), []): + self.metadata.add(_hyd.description, val_dict["@value"], None) + + parse_metadata(self.metadata, data) + + def retrieve(self) -> bool: + """ If needed, retrieves complete metadata + + :return: Status of retrieval + """ + return True @classmethod def parse_member( diff --git a/MyCapytain/resources/collections/dts/_resolver.py b/MyCapytain/resources/collections/dts/_resolver.py index d3480ddd..5b62a3b5 100644 --- a/MyCapytain/resources/collections/dts/_resolver.py +++ b/MyCapytain/resources/collections/dts/_resolver.py @@ -31,6 +31,14 @@ def __getattr__(self, item): def set(self, value): self._proxied = value + def __iter__(self): + return iter(self._proxied) + + def __getitem__(self, item): + if isinstance(self._proxied, dict): + return self._proxied[item] + raise TypeError("'PaginatedProxy' object is not subscriptable") + class HttpResolverDtsCollection(DtsCollection): def __init__( @@ -114,6 +122,15 @@ def parents(self): self._parse_paginated_members(direction="parents") return super(HttpResolverDtsCollection, self).parents + def retrieve(self): + if not self._metadata_parsed: + query = self._resolver.endpoint.get_collection(self.id) + data = query.json() + if not len(data): + raise Exception("We'll see this one later") # toDo: What error should it be ? + self._parse_metadata(expand(data)[0]) + return True + @classmethod def parse_member( cls, diff --git a/tests/resolvers/dts/api_v1/data/collection/example2.json b/tests/resolvers/dts/api_v1/data/collection/example2.json index bd881b6d..1bfdcb0b 100644 --- a/tests/resolvers/dts/api_v1/data/collection/example2.json +++ b/tests/resolvers/dts/api_v1/data/collection/example2.json @@ -14,32 +14,20 @@ "Thibault Clérice", "http://orcid.org/0000-0003-1852-9204" ], "dc:title" : [ - {"@lang": "la", "@value": "Lasciva Roma"}, + {"@language": "la", "@value": "Lasciva Roma"} ], "dc:description": [ { - "@lang": "en", + "@language": "en", "@value": "Collection of primary sources of interest in the studies of Ancient World's sexuality" } - ], + ] }, "member": [ { "@id" : "urn:cts:latinLit:phi1103.phi001", "title" : "Priapeia", - "dts:dublincore": { - "dc:type": [ - "http://chs.harvard.edu/xmlns/cts#work" - ], - "dc:creator": [ - {"@lang": "en", "@value": "Anonymous"} - ], - "dc:language": ["la", "en"], - "dc:description": [ - { "@lang": "en", "@value": "Anonymous lascivious Poems" } - ], - }, - "@type" : "Collection", + "@type": "Collection", "totalItems": 1 } ] diff --git a/tests/resolvers/dts/api_v1/data/collection/example3.json b/tests/resolvers/dts/api_v1/data/collection/example3.json index adb0474f..19efd3b6 100644 --- a/tests/resolvers/dts/api_v1/data/collection/example3.json +++ b/tests/resolvers/dts/api_v1/data/collection/example3.json @@ -2,7 +2,7 @@ "@context": { "@vocab": "https://www.w3.org/ns/hydra/core#", "dc": "http://purl.org/dc/terms/", - "dts": "https://w3id.org/dts/api#", + "dts": "https://w3id.org/dts/api#" }, "@id": "urn:cts:latinLit:phi1103.phi001", "@type": "Collection", @@ -10,12 +10,12 @@ "dts:dublincore": { "dc:type": ["http://chs.harvard.edu/xmlns/cts#work"], "dc:creator": [ - {"@lang": "en", "@value": "Anonymous"} + {"@language": "en", "@value": "Anonymous"} ], "dc:language": ["la", "en"], - "dc:title": [{"@lang": "la", "@value": "Priapeia"}], + "dc:title": [{"@language": "la", "@value": "Priapeia"}], "dc:description": [{ - "@lang": "en", + "@language": "en", "@value": "Anonymous lascivious Poems " }] }, @@ -28,9 +28,9 @@ "description": "Priapeia based on the edition of Aemilius Baehrens", "totalItems": 0, "dts:dublincore": { - "dc:title": [{"@lang": "la", "@value": "Priapeia"}], + "dc:title": [{"@language": "la", "@value": "Priapeia"}], "dc:description": [{ - "@lang": "en", + "@language": "en", "@value": "Anonymous lascivious Poems " }], "dc:type": [ @@ -40,7 +40,7 @@ "dc:source": ["https://archive.org/details/poetaelatinimino12baeh2"], "dc:dateCopyrighted": 1879, "dc:creator": [ - {"@lang": "en", "@value": "Anonymous"} + {"@language": "en", "@value": "Anonymous"} ], "dc:contributor": ["Aemilius Baehrens"], "dc:language": ["la", "en"] diff --git a/tests/resolvers/dts/api_v1/data/collection/id/urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1.json b/tests/resolvers/dts/api_v1/data/collection/id/urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1.json new file mode 100644 index 00000000..fdd8f6d2 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/collection/id/urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1.json @@ -0,0 +1,44 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id" : "urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", + "@type": "Resource", + "title" : "Priapeia", + "description": "Priapeia based on the edition of Aemilius Baehrens", + "totalItems": 0, + "dts:dublincore": { + "dc:title": [{"@language": "la", "@value": "Priapeia"}], + "dc:description": [{ + "@language": "en", + "@value": "Anonymous lascivious Poems " + }], + "dc:type": [ + "http://chs.harvard.edu/xmlns/cts#edition", + "dc:Text" + ], + "dc:source": ["https://archive.org/details/poetaelatinimino12baeh2"], + "dc:dateCopyrighted": 1879, + "dc:creator": [ + {"@language": "en", "@value": "Anonymous"} + ], + "dc:contributor": ["Aemilius Baehrens"], + "dc:language": ["la", "en"] + }, + "dts:passage": "/api/dts/documents?id=urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", + "dts:references": "/api/dts/navigation?id=urn:cts:latinLit:phi1103.phi001.lascivaroma-lat1", + "dts:download": "https://raw.githubusercontent.com/lascivaroma/priapeia/master/data/phi1103/phi001/phi1103.phi001.lascivaroma-lat1.xml", + "dts:citeDepth": 2, + "dts:citeStructure": [ + { + "dts:citeType": "poem", + "dts:citeStructure": [ + { + "dts:citeType": "line" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/test_collections.py b/tests/resolvers/dts/api_v1/test_collections.py deleted file mode 100644 index ae93c36c..00000000 --- a/tests/resolvers/dts/api_v1/test_collections.py +++ /dev/null @@ -1,41 +0,0 @@ -from .base import * -from .base import _load_mock, _load_json_mock - - -class TestHttpDtsResolverCollection(unittest.TestCase): - def setUp(self): - self.root_uri = "http://foobar.com/api/dts" - self.resolver = HttpDtsResolver(self.root_uri) - - @requests_mock.mock() - def test_simple_root_access(self, mock_set): - mock_set.get(self.root_uri, text=_load_mock("root.json")) - mock_set.get( - self.root_uri+"/collections", - text=_load_mock("collection", "example1.json"), - complete_qs=True - ) - collection = self.resolver.getMetadata() - self.assertEqual( - 3, collection.size, - "There should be 3 collections" - ) - self.assertEqual( - "Cartulaires", str(collection["/cartulaires"].get_label()), - "Titles of subcollection and subcollection should be " - "stored under their IDs" - ) - self.assertEqual( - "Collection Générale de l'École Nationale des Chartes", - str(collection.get_label()), - "Label of the main collection should be correctly parsed" - ) - self.assertEqual( - ["https://viaf.org/viaf/167874585", "École Nationale des Chartes"], - sorted([ - str(obj) - for obj in collection.metadata.get( - URIRef("http://purl.org/dc/terms/publisher") - ) - ]) - ) diff --git a/tests/resolvers/dts/api_v1/test_metadata.py b/tests/resolvers/dts/api_v1/test_metadata.py new file mode 100644 index 00000000..50c5a6de --- /dev/null +++ b/tests/resolvers/dts/api_v1/test_metadata.py @@ -0,0 +1,107 @@ +from .base import * +from .base import _load_mock, _load_json_mock + + +class TestHttpDtsResolverCollection(unittest.TestCase): + def setUp(self): + self.root_uri = "http://foobar.com/api/dts" + self.resolver = HttpDtsResolver(self.root_uri) + + @requests_mock.mock() + def test_simple_root_access(self, mock_set): + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/collections", + text=_load_mock("collection", "example1.json"), + complete_qs=True + ) + collection = self.resolver.getMetadata() + self.assertEqual( + 3, collection.size, + "There should be 3 collections" + ) + self.assertEqual( + "Cartulaires", str(collection["/cartulaires"].get_label()), + "Titles of subcollection and subcollection should be " + "stored under their IDs" + ) + self.assertEqual( + "Collection Générale de l'École Nationale des Chartes", + str(collection.get_label()), + "Label of the main collection should be correctly parsed" + ) + self.assertEqual( + ["https://viaf.org/viaf/167874585", "École Nationale des Chartes"], + sorted([ + str(obj) + for obj in collection.metadata.get( + URIRef("http://purl.org/dc/terms/publisher") + ) + ]) + ) + + @requests_mock.mock() + def test_simple_collection_access(self, mock_set): + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/collections?id=lasciva_roma", + text=_load_mock("collection", "example2.json"), + complete_qs=True + ) + collection = self.resolver.getMetadata("lasciva_roma") + self.assertEqual( + 1, collection.size, + "There should be 3 collections" + ) + self.assertEqual( + "Priapeia", str(collection["urn:cts:latinLit:phi1103.phi001"].get_label()), + "Titles of subcollection and subcollection should be " + "stored under their IDs" + ) + self.assertEqual( + ["Thibault Clérice", "http://orcid.org/0000-0003-1852-9204"], + sorted([ + str(obj) + for obj in collection.metadata.get( + URIRef("http://purl.org/dc/terms/creator") + ) + ]) + ) + + @requests_mock.mock() + def test_simple_collection_child_interaction(self, mock_set): + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/collections?id=lasciva_roma", + text=_load_mock("collection", "example2.json"), + complete_qs=True + ) + + collection_parent = self.resolver.getMetadata("lasciva_roma") + collection = collection_parent.children["urn:cts:latinLit:phi1103.phi001"] + + self.assertEqual( + {key: val for key, val in collection_parent.children.items()}, + {"urn:cts:latinLit:phi1103.phi001": collection}, + "Collections should retrieve children when retrieving metadata" + ) + + self.assertEqual( + str(collection.metadata.get_single("http://purl.org/dc/terms/creator", lang="eng")), + "Anonymous", + "Unfortunately, before it's resolved, " + ) + + mock_set.get( + self.root_uri+"/collections?id=urn:cts:latinLit:phi1103.phi001", + text=_load_mock("collection", "example3.json"), + complete_qs=True + ) + collection.retrieve() + + self.assertEqual(collection.size, 1, "Size is parsed through retrieve") + self.assertEqual( + str(collection.metadata.get_single("http://purl.org/dc/terms/creator", lang="eng")), + "Anonymous", + "Metadata has been retrieved" + ) From 91f18c992ea02ea70837d5a99c49d5627d029db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Mon, 15 Oct 2018 16:25:14 +0200 Subject: [PATCH 55/89] (DTS Resolver) Fixed last push --- tests/resolvers/dts/api_v1/test_metadata.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/resolvers/dts/api_v1/test_metadata.py b/tests/resolvers/dts/api_v1/test_metadata.py index 50c5a6de..cadf640f 100644 --- a/tests/resolvers/dts/api_v1/test_metadata.py +++ b/tests/resolvers/dts/api_v1/test_metadata.py @@ -87,9 +87,9 @@ def test_simple_collection_child_interaction(self, mock_set): ) self.assertEqual( - str(collection.metadata.get_single("http://purl.org/dc/terms/creator", lang="eng")), - "Anonymous", - "Unfortunately, before it's resolved, " + collection.metadata.get_single("http://purl.org/dc/terms/creator", lang="eng"), + None, + "Unfortunately, before it's resolved, this should not be filled." ) mock_set.get( From cc35eda2eadae48574b28ac9781dcfc4e78efa84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Mon, 15 Oct 2018 17:15:19 +0200 Subject: [PATCH 56/89] (DTS resolver) Some cleaning --- tests/resolvers/dts/api_v1/base.py | 5 +---- tests/resolvers/dts/api_v1/test_metadata.py | 5 +++-- tests/resolvers/dts/api_v1/test_navigation.py | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/resolvers/dts/api_v1/base.py b/tests/resolvers/dts/api_v1/base.py index f14d2b5d..a1fdeec5 100644 --- a/tests/resolvers/dts/api_v1/base.py +++ b/tests/resolvers/dts/api_v1/base.py @@ -8,6 +8,7 @@ from MyCapytain.common.metadata import Metadata from MyCapytain.common.constants import Mimetypes from rdflib.term import URIRef + # Set-up for the test classes _cur_path = os.path.abspath( os.path.join( @@ -37,7 +38,3 @@ def _load_mock(*files: str) -> str: with open(fname) as fopen: data = fopen.read() return data - - -def _load_json_mock(endpoint: str, example: str) -> typing.Union[dict, list]: - return json.loads(_load_mock(endpoint, example)) diff --git a/tests/resolvers/dts/api_v1/test_metadata.py b/tests/resolvers/dts/api_v1/test_metadata.py index cadf640f..972e6c15 100644 --- a/tests/resolvers/dts/api_v1/test_metadata.py +++ b/tests/resolvers/dts/api_v1/test_metadata.py @@ -1,5 +1,5 @@ from .base import * -from .base import _load_mock, _load_json_mock +from .base import _load_mock class TestHttpDtsResolverCollection(unittest.TestCase): @@ -92,6 +92,8 @@ def test_simple_collection_child_interaction(self, mock_set): "Unfortunately, before it's resolved, this should not be filled." ) + self.assertEqual(collection.size, 1, "Size is parsed through retrieve") + mock_set.get( self.root_uri+"/collections?id=urn:cts:latinLit:phi1103.phi001", text=_load_mock("collection", "example3.json"), @@ -99,7 +101,6 @@ def test_simple_collection_child_interaction(self, mock_set): ) collection.retrieve() - self.assertEqual(collection.size, 1, "Size is parsed through retrieve") self.assertEqual( str(collection.metadata.get_single("http://purl.org/dc/terms/creator", lang="eng")), "Anonymous", diff --git a/tests/resolvers/dts/api_v1/test_navigation.py b/tests/resolvers/dts/api_v1/test_navigation.py index 4477667d..314f841e 100644 --- a/tests/resolvers/dts/api_v1/test_navigation.py +++ b/tests/resolvers/dts/api_v1/test_navigation.py @@ -1,5 +1,5 @@ from .base import * -from .base import _load_mock, _load_json_mock +from .base import _load_mock class TestHttpDtsResolverNavigation(unittest.TestCase): @@ -312,4 +312,4 @@ def test_navigation_paginated(self, mock_set): ), reffs, "Resolvers follows view property" - ) \ No newline at end of file + ) From 6841d2799965cc67b25e3721dca0cbba9d148697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 17 Oct 2018 17:04:34 +0200 Subject: [PATCH 57/89] (DTS Resolver)(Paginated Collection) Resolved most of the issues with pagineted collections --- .../resources/collections/dts/_resolver.py | 48 +++++++++++++------ .../dts/api_v1/data/collection/example10.json | 0 .../data/collection/paginated/page1.json | 30 ++++++++++++ .../data/collection/paginated/page2.json | 31 ++++++++++++ .../data/collection/paginated/page3.json | 30 ++++++++++++ tests/resolvers/dts/api_v1/test_metadata.py | 45 +++++++++++++++-- 6 files changed, 164 insertions(+), 20 deletions(-) create mode 100644 tests/resolvers/dts/api_v1/data/collection/example10.json create mode 100644 tests/resolvers/dts/api_v1/data/collection/paginated/page1.json create mode 100644 tests/resolvers/dts/api_v1/data/collection/paginated/page2.json create mode 100644 tests/resolvers/dts/api_v1/data/collection/paginated/page3.json diff --git a/MyCapytain/resources/collections/dts/_resolver.py b/MyCapytain/resources/collections/dts/_resolver.py index 5b62a3b5..dbeb6533 100644 --- a/MyCapytain/resources/collections/dts/_resolver.py +++ b/MyCapytain/resources/collections/dts/_resolver.py @@ -11,13 +11,22 @@ class PaginatedProxy: - def __init__(self, proxied, update_lambda, condition_lambda): - self._proxied = proxied + def __init__( + self, + obj, + proxied: str, + update_lambda, + condition_lambda + ): + self._proxied = getattr(obj, proxied) + self._obj = obj + self._attr = proxied self._condition_lambda = condition_lambda self._update_lambda = update_lambda def __getattr__(self, item): if item == "update": + print("called") return self._proxied.update if item == "add": return self._proxied.add @@ -26,16 +35,19 @@ def __getattr__(self, item): else: if not self._condition_lambda(): self._update_lambda() + # Replace the Proxied instance by the actualy proxied value + setattr(self._obj, self._attr, self._proxied) return getattr(self._proxied, item) def set(self, value): self._proxied = value def __iter__(self): - return iter(self._proxied) + if not self._condition_lambda(): + return iter(self._proxied) def __getitem__(self, item): - if isinstance(self._proxied, dict): + if isinstance(self._proxied, (list, dict, set, tuple)): return self._proxied[item] raise TypeError("'PaginatedProxy' object is not subscriptable") @@ -49,12 +61,14 @@ def __init__( super(HttpResolverDtsCollection, self).__init__(identifier, *args, **kwargs) self._children = PaginatedProxy( - self._children, + self, + "_children", lambda: self._parse_paginated_members(direction="children"), lambda: self._parsed["children"] ) self._parents = PaginatedProxy( - self._parents, + self, + "_parents", lambda: self._parse_paginated_members(direction="parents"), lambda: self._parsed["parents"] ) @@ -79,7 +93,10 @@ def _parse_paginated_members(self, direction="children"): page = self._last_page_parsed[direction] if not page: page = 1 + else: + page = int(page) while page: + print(page) if page > 1: response = self._resolver.endpoint.get_collection( collection_id=self.id, @@ -94,32 +111,33 @@ def _parse_paginated_members(self, direction="children"): response.raise_for_status() data = response.json() - data = expand(data) + data = expand(data)[0] - self.parse_member(obj=data, collection=self, direction=direction) + self.children.update({ + o.id: o + for o in type(self).parse_member( + obj=data, collection=self, direction=direction, resolver=self._resolver + ) + }) self._last_page_parsed[direction] = page page = None if "https://www.w3.org/ns/hydra/core#view" in data: if "https://www.w3.org/ns/hydra/core#next" in data["https://www.w3.org/ns/hydra/core#view"][0]: - page = _re_page.findall( + page = int(_re_page.findall( data["https://www.w3.org/ns/hydra/core#view"] [0]["https://www.w3.org/ns/hydra/core#next"] [0]["@value"] - )[0] + )[0]) self._parsed[direction] = True @property def children(self): - if not self._parsed["children"]: - self._parse_paginated_members(direction="children") return super(HttpResolverDtsCollection, self).children @property def parents(self): - if not self._parsed["parents"]: - self._parse_paginated_members(direction="parents") return super(HttpResolverDtsCollection, self).parents def retrieve(self): @@ -135,7 +153,7 @@ def retrieve(self): def parse_member( cls, obj: dict, - collection: "DtsCollection", + collection: "HttpResolverDtsCollection", direction: str, **additional_parameters): diff --git a/tests/resolvers/dts/api_v1/data/collection/example10.json b/tests/resolvers/dts/api_v1/data/collection/example10.json new file mode 100644 index 00000000..e69de29b diff --git a/tests/resolvers/dts/api_v1/data/collection/paginated/page1.json b/tests/resolvers/dts/api_v1/data/collection/paginated/page1.json new file mode 100644 index 00000000..c10685f4 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/collection/paginated/page1.json @@ -0,0 +1,30 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id" : "urn:enc", + "@type" : "Collection", + "title": "Collection Générale de l'École Nationale des Chartes", + "dts:dublincore": { + "dc:publisher": ["École Nationale des Chartes", "https://viaf.org/viaf/167874585"], + "dc:title": [ + {"@language": "fr", "@value" : "Collection Générale de l'École Nationale des Chartes"} + ] + }, + "totalItems" : 3, + "member": [{ + "@id": "urn:enc:membre1", + "title": "Membre 1", + "@type": "Collection", + "totalItems": 1 + }], + "view": { + "@id": "/api/dts/collections/?id=urn:cts&page=1", + "@type": "PartialCollectionView", + "first": "/api/dts/collections/?id=lettres_de_poilus&page=1", + "next": "/api/dts/collections/?id=lettres_de_poilus&page=2", + "last": "/api/dts/collections/?id=lettres_de_poilus&page=3" + } +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/collection/paginated/page2.json b/tests/resolvers/dts/api_v1/data/collection/paginated/page2.json new file mode 100644 index 00000000..1c78f343 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/collection/paginated/page2.json @@ -0,0 +1,31 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id" : "urn:enc", + "@type" : "Collection", + "title": "Collection Générale de l'École Nationale des Chartes", + "dts:dublincore": { + "dc:publisher": ["École Nationale des Chartes", "https://viaf.org/viaf/167874585"], + "dc:title": [ + {"@language": "fr", "@value" : "Collection Générale de l'École Nationale des Chartes"} + ] + }, + "totalItems" : 3, + "member": [{ + "@id": "urn:enc:membre2", + "title": "Membre 2", + "@type": "Collection", + "totalItems": 2 + }], + "view": { + "@id": "/api/dts/collections/?id=urn:cts&page=2", + "@type": "PartialCollectionView", + "first": "/api/dts/collections/?id=urn:enc&page=1", + "previous": "/api/dts/collections/?id=urn:enc&page=1", + "next": "/api/dts/collections/?id=urn:enc&page=3", + "last": "/api/dts/collections/?id=urn:enc&page=3" + } +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/collection/paginated/page3.json b/tests/resolvers/dts/api_v1/data/collection/paginated/page3.json new file mode 100644 index 00000000..7fe18b0f --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/collection/paginated/page3.json @@ -0,0 +1,30 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id" : "urn:enc", + "@type" : "Collection", + "title": "Collection Générale de l'École Nationale des Chartes", + "dts:dublincore": { + "dc:publisher": ["École Nationale des Chartes", "https://viaf.org/viaf/167874585"], + "dc:title": [ + {"@language": "fr", "@value" : "Collection Générale de l'École Nationale des Chartes"} + ] + }, + "totalItems" : 3, + "member": [{ + "@id": "urn:enc:membre3", + "title": "Membre 3", + "@type": "Collection", + "totalItems": 3 + }], + "view": { + "@id": "/api/dts/collections/?id=urn:cts&page=3", + "@type": "PartialCollectionView", + "first": "/api/dts/collections/?id=urn:enc&page=1", + "previous": "/api/dts/collections/?id=urn:enc&page=2", + "last": "/api/dts/collections/?id=urn:enc&page=3" + } +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/test_metadata.py b/tests/resolvers/dts/api_v1/test_metadata.py index 972e6c15..57924263 100644 --- a/tests/resolvers/dts/api_v1/test_metadata.py +++ b/tests/resolvers/dts/api_v1/test_metadata.py @@ -1,5 +1,6 @@ from .base import * from .base import _load_mock +from MyCapytain.resources.collections.dts._resolver import PaginatedProxy class TestHttpDtsResolverCollection(unittest.TestCase): @@ -76,6 +77,11 @@ def test_simple_collection_child_interaction(self, mock_set): text=_load_mock("collection", "example2.json"), complete_qs=True ) + mock_set.get( + self.root_uri+"/collections?id=urn:cts:latinLit:phi1103.phi001", + text=_load_mock("collection", "example3.json"), + complete_qs=True + ) collection_parent = self.resolver.getMetadata("lasciva_roma") collection = collection_parent.children["urn:cts:latinLit:phi1103.phi001"] @@ -94,11 +100,6 @@ def test_simple_collection_child_interaction(self, mock_set): self.assertEqual(collection.size, 1, "Size is parsed through retrieve") - mock_set.get( - self.root_uri+"/collections?id=urn:cts:latinLit:phi1103.phi001", - text=_load_mock("collection", "example3.json"), - complete_qs=True - ) collection.retrieve() self.assertEqual( @@ -106,3 +107,37 @@ def test_simple_collection_child_interaction(self, mock_set): "Anonymous", "Metadata has been retrieved" ) + + @requests_mock.mock() + def test_paginated_member_children(self, mock_set): + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/collections?id=urn:enc", + text=_load_mock("collection", "paginated/page1.json"), + complete_qs=True + ) + mock_set.get( + self.root_uri+"/collections?id=urn:enc&page=2", + text=_load_mock("collection", "paginated/page2.json"), + complete_qs=True + ) + mock_set.get( + self.root_uri+"/collections?id=urn:enc&page=3", + text=_load_mock("collection", "paginated/page3.json"), + complete_qs=True + ) + collection = self.resolver.getMetadata("urn:enc") + # Size is computed pre-reaching pages + self.assertEqual( + 3, collection.size, + "There should be 3 children collection" + ) + self.assertIsInstance(collection.children, PaginatedProxy, "Proxied object is in place") + # Then we test the children + self.assertEqual( + ["urn:enc:membre1", "urn:enc:membre2", "urn:enc:membre3"], + sorted(list(collection.children.keys())), + "Each page should be reached when iteratin over children" + ) + + self.assertIsInstance(collection.children, dict, "Proxied object is replaced") \ No newline at end of file From 5f346ed7f4b69d17e82d5fbd15b69033732796bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Wed, 17 Oct 2018 17:27:49 +0200 Subject: [PATCH 58/89] (DTS Resolver)(Paginated Collection) Added equality magic method + Removed debugging print --- .../resources/collections/dts/_resolver.py | 5 ++- tests/resolvers/dts/api_v1/test_metadata.py | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/MyCapytain/resources/collections/dts/_resolver.py b/MyCapytain/resources/collections/dts/_resolver.py index dbeb6533..e6f8f126 100644 --- a/MyCapytain/resources/collections/dts/_resolver.py +++ b/MyCapytain/resources/collections/dts/_resolver.py @@ -26,7 +26,6 @@ def __init__( def __getattr__(self, item): if item == "update": - print("called") return self._proxied.update if item == "add": return self._proxied.add @@ -51,6 +50,9 @@ def __getitem__(self, item): return self._proxied[item] raise TypeError("'PaginatedProxy' object is not subscriptable") + def __eq__(self, other): + return self._proxied == other + class HttpResolverDtsCollection(DtsCollection): def __init__( @@ -96,7 +98,6 @@ def _parse_paginated_members(self, direction="children"): else: page = int(page) while page: - print(page) if page > 1: response = self._resolver.endpoint.get_collection( collection_id=self.id, diff --git a/tests/resolvers/dts/api_v1/test_metadata.py b/tests/resolvers/dts/api_v1/test_metadata.py index 57924263..3822bf9a 100644 --- a/tests/resolvers/dts/api_v1/test_metadata.py +++ b/tests/resolvers/dts/api_v1/test_metadata.py @@ -108,6 +108,45 @@ def test_simple_collection_child_interaction(self, mock_set): "Metadata has been retrieved" ) + self.assertEqual( + {collection_parent}, collection.parents, + "The collection parents should be pre-defined" + ) + + @requests_mock.mock() + def test_paginated_member_children(self, mock_set): + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/collections?id=urn:enc", + text=_load_mock("collection", "paginated/page1.json"), + complete_qs=True + ) + mock_set.get( + self.root_uri+"/collections?id=urn:enc&page=2", + text=_load_mock("collection", "paginated/page2.json"), + complete_qs=True + ) + mock_set.get( + self.root_uri+"/collections?id=urn:enc&page=3", + text=_load_mock("collection", "paginated/page3.json"), + complete_qs=True + ) + collection = self.resolver.getMetadata("urn:enc") + # Size is computed pre-reaching pages + self.assertEqual( + 3, collection.size, + "There should be 3 children collection" + ) + self.assertIsInstance(collection.children, PaginatedProxy, "Proxied object is in place") + # Then we test the children + self.assertEqual( + ["urn:enc:membre1", "urn:enc:membre2", "urn:enc:membre3"], + sorted(list(collection.children.keys())), + "Each page should be reached when iteratin over children" + ) + + self.assertIsInstance(collection.children, dict, "Proxied object is replaced") + @requests_mock.mock() def test_paginated_member_children(self, mock_set): mock_set.get(self.root_uri, text=_load_mock("root.json")) From a429cbbeeb07dad96102f1dea37eed04b7847159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Thu, 18 Oct 2018 11:03:16 +0200 Subject: [PATCH 59/89] (DTS Resolver) Set of parents is now working as well --- .../resources/collections/dts/_resolver.py | 63 ++++++++++++------- tests/resolvers/dts/api_v1/base.py | 6 ++ .../collection/paginated/parent_root.json | 17 +++++ tests/resolvers/dts/api_v1/test_metadata.py | 44 ++++++++++++- 4 files changed, 108 insertions(+), 22 deletions(-) create mode 100644 tests/resolvers/dts/api_v1/data/collection/paginated/parent_root.json diff --git a/MyCapytain/resources/collections/dts/_resolver.py b/MyCapytain/resources/collections/dts/_resolver.py index e6f8f126..3da96f5d 100644 --- a/MyCapytain/resources/collections/dts/_resolver.py +++ b/MyCapytain/resources/collections/dts/_resolver.py @@ -11,6 +11,11 @@ class PaginatedProxy: + + __UPDATES_CALLABLES__ = { + "update", "add", "extend" + } + def __init__( self, obj, @@ -25,28 +30,36 @@ def __init__( self._update_lambda = update_lambda def __getattr__(self, item): - if item == "update": - return self._proxied.update - if item == "add": - return self._proxied.add + if item in self.__UPDATES_CALLABLES__: + return getattr(self._proxied, item) if item == "set": return self.set - else: - if not self._condition_lambda(): - self._update_lambda() - # Replace the Proxied instance by the actualy proxied value - setattr(self._obj, self._attr, self._proxied) - return getattr(self._proxied, item) + return self._run(item) + + def _run(self, item=None): + if not self._condition_lambda(): + self._update_lambda() + # Replace the Proxied instance by the actualy proxied value + setattr(self._obj, self._attr, self._proxied) + + if item: + return getattr(self._proxied, item) + return self._proxied - def set(self, value): + def set(self, value) -> None: self._proxied = value def __iter__(self): - if not self._condition_lambda(): - return iter(self._proxied) + return iter(self._run()) def __getitem__(self, item): if isinstance(self._proxied, (list, dict, set, tuple)): + # If it is in, we do not update the object + if item in self._proxied: + return self._proxied[item] + # If it not in, and we are still in this object, + # It means we need to crawl : + self._run() return self._proxied[item] raise TypeError("'PaginatedProxy' object is not subscriptable") @@ -114,12 +127,20 @@ def _parse_paginated_members(self, direction="children"): data = response.json() data = expand(data)[0] - self.children.update({ - o.id: o - for o in type(self).parse_member( - obj=data, collection=self, direction=direction, resolver=self._resolver - ) - }) + if direction == "children": + self.children.update({ + o.id: o + for o in type(self).parse_member( + obj=data, collection=self, direction=direction, resolver=self._resolver + ) + }) + else: + self.parents.update({ + o + for o in type(self).parse_member( + obj=data, collection=self, direction=direction, resolver=self._resolver + ) + }) self._last_page_parsed[direction] = page page = None @@ -130,8 +151,8 @@ def _parse_paginated_members(self, direction="children"): [0]["https://www.w3.org/ns/hydra/core#next"] [0]["@value"] )[0]) - - self._parsed[direction] = True + else: + self._parsed[direction] = True @property def children(self): diff --git a/tests/resolvers/dts/api_v1/base.py b/tests/resolvers/dts/api_v1/base.py index a1fdeec5..b1a6a81b 100644 --- a/tests/resolvers/dts/api_v1/base.py +++ b/tests/resolvers/dts/api_v1/base.py @@ -38,3 +38,9 @@ def _load_mock(*files: str) -> str: with open(fname) as fopen: data = fopen.read() return data + +from MyCapytain.common.constants import set_graph, get_graph, bind_graph + + +def reset_graph(): + set_graph(bind_graph()) diff --git a/tests/resolvers/dts/api_v1/data/collection/paginated/parent_root.json b/tests/resolvers/dts/api_v1/data/collection/paginated/parent_root.json new file mode 100644 index 00000000..369d5b3a --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/collection/paginated/parent_root.json @@ -0,0 +1,17 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id" : "urn:enc", + "@type" : "Collection", + "title": "Collection Générale de l'École Nationale des Chartes", + "dts:dublincore": { + "dc:publisher": ["École Nationale des Chartes", "https://viaf.org/viaf/167874585"], + "dc:title": [ + {"@language": "fr", "@value" : "Collection Générale de l'École Nationale des Chartes"} + ] + }, + "totalItems" : 0 +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/test_metadata.py b/tests/resolvers/dts/api_v1/test_metadata.py index 3822bf9a..116f83f1 100644 --- a/tests/resolvers/dts/api_v1/test_metadata.py +++ b/tests/resolvers/dts/api_v1/test_metadata.py @@ -8,6 +8,9 @@ def setUp(self): self.root_uri = "http://foobar.com/api/dts" self.resolver = HttpDtsResolver(self.root_uri) + def tearDown(self): + reset_graph() + @requests_mock.mock() def test_simple_root_access(self, mock_set): mock_set.get(self.root_uri, text=_load_mock("root.json")) @@ -179,4 +182,43 @@ def test_paginated_member_children(self, mock_set): "Each page should be reached when iteratin over children" ) - self.assertIsInstance(collection.children, dict, "Proxied object is replaced") \ No newline at end of file + self.assertIsInstance(collection.children, dict, "Proxied object is replaced") + + @requests_mock.mock() + def test_paginated_member_parents(self, mock_set): + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/collections?id=urn:enc", + text=_load_mock("collection", "paginated/parent_root.json"), + complete_qs=True + ) + mock_set.get( + self.root_uri+"/collections?id=urn:enc&nav=parents", + text=_load_mock("collection", "paginated/page1.json"), + complete_qs=True + ) + mock_set.get( + self.root_uri+"/collections?id=urn:enc&page=2&nav=parents", + text=_load_mock("collection", "paginated/page2.json"), + complete_qs=True + ) + mock_set.get( + self.root_uri+"/collections?id=urn:enc&page=3&nav=parents", + text=_load_mock("collection", "paginated/page3.json"), + complete_qs=True + ) + collection = self.resolver.getMetadata("urn:enc") + # Size is computed pre-reaching pages + self.assertEqual( + 0, collection.size, + "There should be no children collections" + ) + self.assertIsInstance(collection.parents, PaginatedProxy, "Proxied object is in place") + # Then we test the children + self.assertEqual( + ["urn:enc:membre1", "urn:enc:membre2", "urn:enc:membre3"], + sorted([x.id for x in collection.parents]), + "Each page should be reached when iterating over parents" + ) + + self.assertIsInstance(collection.parents, set, "Proxied object is replaced") From edf9522bcbbf85fcc1ac45a38dbec1376f742062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Fri, 19 Oct 2018 08:55:21 +0200 Subject: [PATCH 60/89] (Req) Fixing Responses to 0.8.1 until we find what is going on --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7bfcd45c..2fbdbbfb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ future>=0.16.0 six>=1.10.0 xmlunittest>=0.3.2 rdflib-jsonld>=0.4.0 -responses>=0.8.1 +responses==0.8.1 LinkHeader==0.4.3 pyld==1.0.3 typing From 9ea5d04efa1599bccb390f9ce093137f257f3e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Fri, 19 Oct 2018 10:08:49 +0200 Subject: [PATCH 61/89] Reworked massively prototypes.text with Annotation and better naming schemes --- MyCapytain/common/constants.py | 2 +- MyCapytain/resources/prototypes/text.py | 222 ++++++++---------- .../resources/texts/local/capitains/cts.py | 6 +- MyCapytain/resources/texts/remote/cts.py | 8 +- .../resources/texts/remote/dts/__init__.py | 0 .../texts/remote/dts/_resolver_v1.py | 7 + requirements.txt | 10 +- tests/resolvers/cts/test_api.py | 12 +- tests/resolvers/cts/test_local.py | 12 +- tests/resources/proto/test_text.py | 24 +- 10 files changed, 148 insertions(+), 155 deletions(-) create mode 100644 MyCapytain/resources/texts/remote/dts/__init__.py create mode 100644 MyCapytain/resources/texts/remote/dts/_resolver_v1.py diff --git a/MyCapytain/common/constants.py b/MyCapytain/common/constants.py index 9079c751..79cd00a7 100644 --- a/MyCapytain/common/constants.py +++ b/MyCapytain/common/constants.py @@ -104,7 +104,7 @@ class PYTHON: class MyCapytain: """ MyCapytain Objects - :cvar ReadableText: MyCapytain.resources.prototypes.text.CitableText + :cvar ReadableText: MyCapytain.resources.prototypes.text.CtsText """ TextualElement = "Capitains/TextualElement" diff --git a/MyCapytain/resources/prototypes/text.py b/MyCapytain/resources/prototypes/text.py index dc3ca666..0ae63414 100644 --- a/MyCapytain/resources/prototypes/text.py +++ b/MyCapytain/resources/prototypes/text.py @@ -1,27 +1,29 @@ # -*- coding: utf-8 -*- """ .. module:: MyCapytain.resources.proto.text - :synopsis: Prototypes for CitableText + :synopsis: Prototypes for CtsText .. moduleauthor:: Thibault Clérice """ -from six import text_type +from typing import Union, List, Iterator from rdflib.namespace import DC -from rdflib import BNode, URIRef -from MyCapytain.common.reference import URN, Citation, NodeId +from rdflib import BNode, URIRef, Graph, Literal +from rdflib.term import Identifier +from MyCapytain.common.reference import URN, Citation, NodeId, BaseReference, BaseReferenceSet from MyCapytain.common.metadata import Metadata from MyCapytain.common.constants import Mimetypes, get_graph, RDF_NAMESPACES from MyCapytain.common.base import Exportable from MyCapytain.resources.prototypes.metadata import Collection +from MyCapytain.resources.prototypes.cts.inventory import CtsTextMetadata __all__ = [ "TextualElement", "TextualGraph", "TextualNode", - "CitableText", + "CtsText", "InteractiveTextualNode", "CtsNode" ] @@ -40,56 +42,54 @@ class TextualElement(Exportable): default_exclude = [] - def __init__(self, identifier=None, metadata=None): - self.__graph__ = get_graph() - self.__identifier__ = identifier + def __init__(self, identifier: str=None, metadata: Metadata=None): + self._graph = get_graph() + self._identifier = identifier - self.__node__ = BNode() - self.__metadata__ = metadata or Metadata(node=self.asNode()) + self._node = BNode() + self._metadata = metadata or Metadata(node=self.asNode()) - self.__graph__.addN([ - (self.__node__, RDF_NAMESPACES.DTS.implements, URIRef(identifier), self.__graph__)#, + self._graph.addN([ + (self._node, RDF_NAMESPACES.DTS.implements, URIRef(identifier), self._graph)#, #(self.__node__, RDF_NAMESPACES.DTS.metadata, self.metadata.asNode(), self.__graph__) ]) - def __repr__(self): + def __repr__(self) -> str: return "%s(%s)" % (self.__class__.__name__, self.id) @property - def graph(self): - return self.__graph__ + def graph(self) -> Graph: + return self._graph @property - def text(self): + def text(self) -> str: """ String representation of the text :return: String representation of the text - :rtype: text_type """ return self.export(output=Mimetypes.PLAINTEXT, exclude=self.default_exclude) @property - def id(self): + def id(self) -> str: """ Identifier of the text :return: Identifier of the text - :rtype: text_type """ - return self.__identifier__ + return self._identifier @property - def metadata(self): + def metadata(self) -> Metadata: """ Metadata information about the text :return: Collection object with metadata about the text :rtype: Metadata """ - return self.__metadata__ + return self._metadata - def asNode(self): - return self.__node__ + def asNode(self) -> Identifier: + return self._node - def get_creator(self, lang=None): + def get_creator(self, lang: str=None) -> Literal: """ Get the DC Creator literal value :param lang: Language to retrieve @@ -98,14 +98,15 @@ def get_creator(self, lang=None): """ return self.metadata.get_single(key=DC.creator, lang=lang) - def set_creator(self, value, lang): + def set_creator(self, value: Union[Literal, Identifier, str], lang: str= None): """ Set the DC Creator literal value + :param value: Value of the creator node :param lang: Language in which the value is """ self.metadata.add(key=DC.creator, value=value, lang=lang) - def get_title(self, lang=None): + def get_title(self, lang: str=None) -> Literal: """ Get the title of the object :param lang: Lang to retrieve @@ -114,14 +115,15 @@ def get_title(self, lang=None): """ return self.metadata.get_single(key=DC.title, lang=lang) - def set_title(self, value, lang=None): + def set_title(self, value: Union[Literal, Identifier, str], lang: str= None): """ Set the DC Title literal value + :param value: Value of the title node :param lang: Language in which the value is """ return self.metadata.add(key=DC.title, value=value, lang=lang) - def get_description(self, lang=None): + def get_description(self, lang: str=None) -> Literal: """ Get the description of the object :param lang: Lang to retrieve @@ -130,14 +132,15 @@ def get_description(self, lang=None): """ return self.metadata.get_single(key=DC.description, lang=lang) - def set_description(self, value, lang=None): + def set_description(self, value: Union[Literal, Identifier, str], lang: str= None): """ Set the DC Description literal value + :param value: Value of the title node :param lang: Language in which the value is """ return self.metadata.add(key=DC.description, value=value, lang=lang) - def get_subject(self, lang=None): + def get_subject(self, lang=None) -> Literal: """ Get the subject of the object :param lang: Lang to retrieve @@ -146,14 +149,15 @@ def get_subject(self, lang=None): """ return self.metadata.get_single(key=DC.subject, lang=lang) - def set_subject(self, value, lang=None): + def set_subject(self, value: Union[Literal, Identifier, str], lang: str= None): """ Set the DC Subject literal value + :param value: Value of the subject node :param lang: Language in which the value is """ return self.metadata.add(key=DC.subject, value=value, lang=lang) - def export(self, output=None, exclude=None, **kwargs): + def export(self, output: str=None, exclude: List[str]=None, **kwargs): """ Export the collection item in the Mimetype required. ..note:: If current implementation does not have special mimetypes, reuses default_export method @@ -188,24 +192,23 @@ class TextualNode(TextualElement, NodeId): :cvar default_exclude: Default exclude for exports """ - def __init__(self, identifier=None, citation=None, **kwargs): + def __init__(self, identifier: str=None, citation: Citation=None, **kwargs): super(TextualNode, self).__init__(identifier=identifier, **kwargs) - self.__citation__ = citation or Citation() + self._citation = citation or Citation() @property - def citation(self): - """ XmlCtsCitation Object of the CtsTextMetadata + def citation(self) -> Citation: + """ Citation system of the object - :return: XmlCtsCitation Object of the CtsTextMetadata :rtype: Citation """ - return self.__citation__ + return self._citation @citation.setter - def citation(self, value): + def citation(self, value: Citation): if not isinstance(value, Citation): - raise TypeError("XmlCtsCitation property can only be a XmlCtsCitation object") - self.__citation__ = value + raise TypeError("Citation property can only host a Citation object") + self._citation = value class TextualGraph(TextualNode): @@ -229,15 +232,13 @@ class TextualGraph(TextualNode): :cvar default_exclude: Default exclude for exports """ - def __init__(self, identifier=None, **kwargs): + def __init__(self, identifier: str=None, **kwargs): super(TextualGraph, self).__init__(identifier=identifier, **kwargs) - def getTextualNode(self, subreference): + def getTextualNode(self, subreference: BaseReference) -> "TextualGraph": """ Retrieve a passage and store it in the object :param subreference: CtsReference of the passage to retrieve - :type subreference: str or Node or CtsReference - :rtype: TextualNode :returns: Object representing the passage :raises: *TypeError* when reference is not a list or a CtsReference @@ -245,14 +246,11 @@ def getTextualNode(self, subreference): raise NotImplementedError() - def getReffs(self, level=1, subreference=None): + def getReffs(self, level: int=1, subreference: BaseReference=None) -> BaseReferenceSet: """ CtsReference available at a given level :param level: Depth required. If not set, should retrieve first encountered level (1 based) - :type level: Int - :param passage: Subreference (optional) - :type passage: CtsReference - :rtype: [text_type] + :param subreference: Subreference (optional) :returns: List of levels """ raise NotImplementedError() @@ -279,80 +277,66 @@ class InteractiveTextualNode(TextualGraph): :cvar default_exclude: Default exclude for exports """ - def __init__(self, identifier=None, **kwargs): + def __init__(self, identifier: str=None, **kwargs): super(InteractiveTextualNode, self).__init__(identifier=identifier, **kwargs) - self.__childIds__ = None + self._childIds = None @property - def prev(self): - """ Get Previous CapitainsCtsPassage - - :rtype: Passage + def prev(self) -> "InteractiveTextualNode": + """ Get Previous TextualNode """ if self.prevId is not None: return self.getTextualNode(self.prevId) @property - def next(self): - """ Get Next CapitainsCtsPassage - - :rtype: Passage + def next(self) -> "InteractiveTextualNode": + """ Get Next TextualNode """ if self.nextId is not None: return self.getTextualNode(self.nextId) @property - def children(self): - """ Children Passages + def children(self) -> Iterator["InteractiveTextualNode"]: + """ Children TextualNode - :rtype: iterator(CapitainsCtsPassage) """ for ID in self.childIds: yield self.getTextualNode(ID) @property - def parent(self): - """ Parent CapitainsCtsPassage + def parent(self) -> "InteractiveTextualNode": + """ Parent TextualNode - :rtype: Passage """ return self.getTextualNode(self.parentId) @property - def first(self): - """ First CapitainsCtsPassage - - :rtype: Passage + def first(self) -> TextualNode: + """ First TextualNode """ if self.firstId is not None: return self.getTextualNode(self.firstId) @property - def last(self): + def last(self) -> TextualNode: """ Last CapitainsCtsPassage - - :rtype: Passage """ if self.lastId is not None: return self.getTextualNode(self.lastId) @property - def childIds(self): + def childIds(self) -> BaseReferenceSet: """ Identifiers of children :return: Identifiers of children - :rtype: [str] """ - if self.__childIds__ is None: - self.__childIds__ = self.getReffs() - return self.__childIds__ + if self._childIds is None: + self._childIds = self.getReffs() + return self._childIds @property - def firstId(self): - """ First child of current CapitainsCtsPassage - - :rtype: str - :returns: First passage node Information + def firstId(self) -> BaseReference: + """ First child's id of current TextualNode """ if self.childIds is not None: if len(self.childIds) > 0: @@ -362,11 +346,8 @@ def firstId(self): raise NotImplementedError @property - def lastId(self): - """ Last child of current CapitainsCtsPassage - - :rtype: str - :returns: Last passage Node representation + def lastId(self) -> BaseReference: + """ Last child's id of current TextualNode """ if self.childIds is not None: if len(self.childIds) > 0: @@ -398,52 +379,53 @@ class CtsNode(InteractiveTextualNode): :cvar default_exclude: Default exclude for exports """ - def __init__(self, urn=None, **kwargs): + def __init__(self, urn: Union[URN, str]=None, **kwargs): super(CtsNode, self).__init__(identifier=str(urn), **kwargs) - self.__urn__ = None + self._urn = None if urn is not None: self.urn = urn @property - def urn(self): + def urn(self) -> URN: """ URN Identifier of the object - :rtype: MyCapytain.common.reference._capitains_cts.URN """ - return self.__urn__ + return self._urn @urn.setter - def urn(self, value): + def urn(self, value: Union[URN, str]): """ Set the urn :param value: URN to be saved - :type value: MyCapytain.common.reference._capitains_cts.URN :raises: *TypeError* when the value is not URN compatible """ - if isinstance(value, text_type): + if isinstance(value, str): value = URN(value) elif not isinstance(value, URN): raise TypeError() - self.__urn__ = value + self._urn = value - def get_cts_metadata(self, key, lang=None): + def get_cts_metadata(self, key: str, lang: str=None) -> Literal: + """ Get easily a metadata from the CTS namespace + + :param key: CTS property to retrieve + :param lang: Language in which it should be + :return: Literal value of the CTS graph property + """ return self.metadata.get_single(RDF_NAMESPACES.CTS.term(key), lang) - def getValidReff(self, level=1, reference=None): - """ Given a resource, CitableText will compute valid reffs + def getValidReff(self, level: int=1, reference: BaseReference=None) -> BaseReferenceSet: + """ Given a resource, CtsText will compute valid reffs :param level: Depth required. If not set, should retrieve first encountered level (1 based) - :type level: Int - :param passage: Subreference (optional) - :type passage: CtsReference - :rtype: List.text_type + :param reference: Subreference (optional) :returns: List of levels """ raise NotImplementedError() - def getLabel(self): + def getLabel(self) -> Collection: """ Retrieve metadata about the text :rtype: Collection @@ -451,7 +433,7 @@ def getLabel(self): """ raise NotImplementedError() - def set_metadata_from_collection(self, text_metadata): + def set_metadata_from_collection(self, text_metadata: CtsTextMetadata): """ Set the object metadata using its collections recursively :param text_metadata: Object representing the current text as a collection @@ -483,7 +465,7 @@ def set_metadata_from_collection(self, text_metadata): self.citation = edition.citation -class Passage(CtsNode): +class CtsPassage(CtsNode): """ CapitainsCtsPassage objects possess metadata informations :param urn: A URN identifier @@ -506,26 +488,28 @@ class Passage(CtsNode): """ def __init__(self, **kwargs): - super(Passage, self).__init__(**kwargs) + super(CtsPassage, self).__init__(**kwargs) @property - def reference(self): + def reference(self) -> BaseReference: return self.urn.reference -class CitableText(CtsNode): - """ A CTS CitableText +class CtsText(CtsNode): + """ A CTS CtsText """ def __init__(self, citation=None, metadata=None, **kwargs): - super(CitableText, self).__init__(citation=citation, metadata=metadata, **kwargs) - self.__reffs__ = None + super(CtsText, self).__init__(citation=citation, metadata=metadata, **kwargs) + self._cts = None @property - def reffs(self): - """ Get all valid reffs for every part of the CitableText + def reffs(self) -> BaseReferenceSet: + """ Get all valid reffs for every part of the CtsText :rtype: [str] """ - if not self.__reffs__: - self.__reffs__ = [reff for reffs in [self.getReffs(level=i) for i in range(1, len(self.citation) + 1)] for reff in reffs] - return self.__reffs__ + if not self._cts: + self._cts = BaseReferenceSet( + [reff for reffs in [self.getReffs(level=i) for i in range(1, len(self.citation) + 1)] for reff in reffs] + ) + return self._cts diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index f617c9d8..c409876b 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -293,7 +293,7 @@ def tostring(self, *args, **kwargs): return etree.tostring(self.resource, *args, **kwargs) -class _SimplePassage(_SharedMethods, text.Passage): +class _SimplePassage(_SharedMethods, text.CtsPassage): """ CapitainsCtsPassage for simple and quick parsing of texts :param resource: Element representing the passage @@ -440,7 +440,7 @@ def textObject(self): return self.__text__ -class CapitainsCtsText(_SharedMethods, text.CitableText): +class CapitainsCtsText(_SharedMethods, text.CtsText): """ Implementation of CTS tools for local files :param urn: A URN identifier @@ -485,7 +485,7 @@ def test(self): raise E -class CapitainsCtsPassage(_SharedMethods, text.Passage): +class CapitainsCtsPassage(_SharedMethods, text.CtsPassage): """ CapitainsCtsPassage class for local texts which rebuilds the tree up to the passage. For design purposes, some people would prefer the output of GetPassage to be consistent. ContextPassage rebuilds diff --git a/MyCapytain/resources/texts/remote/cts.py b/MyCapytain/resources/texts/remote/cts.py index 40b39d72..2635ab2e 100644 --- a/MyCapytain/resources/texts/remote/cts.py +++ b/MyCapytain/resources/texts/remote/cts.py @@ -60,7 +60,7 @@ def retriever(self): return self.__retriever__ def getValidReff(self, level=1, reference=None): - """ Given a resource, CitableText will compute valid reffs + """ Given a resource, CtsText will compute valid reffs :param level: Depth required. If not set, should retrieve first encountered level (1 based) :type level: Int @@ -313,7 +313,7 @@ def prevnext(resource): return _prev, _next -class CtsText(_SharedMethod, prototypes.CitableText): +class CtsText(_SharedMethod, prototypes.CtsText): """ API CtsTextMetadata object :param urn: A URN identifier @@ -334,7 +334,7 @@ def __init__(self, urn, retriever, citation=None, **kwargs): @property def reffs(self): - """ Get all valid reffs for every part of the CitableText + """ Get all valid reffs for every part of the CtsText :rtype: MyCapytain.resources.texts.tei.XmlCtsCitation """ @@ -378,7 +378,7 @@ def export(self, output=Mimetypes.PLAINTEXT, exclude=None, **kwargs): return self.getTextualNode().export(output, exclude) -class CtsPassage(_SharedMethod, prototypes.Passage, TeiResource): +class CtsPassage(_SharedMethod, prototypes.CtsPassage, TeiResource): """ CapitainsCtsPassage representing :param urn: diff --git a/MyCapytain/resources/texts/remote/dts/__init__.py b/MyCapytain/resources/texts/remote/dts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/MyCapytain/resources/texts/remote/dts/_resolver_v1.py b/MyCapytain/resources/texts/remote/dts/_resolver_v1.py new file mode 100644 index 00000000..89508041 --- /dev/null +++ b/MyCapytain/resources/texts/remote/dts/_resolver_v1.py @@ -0,0 +1,7 @@ +from ....prototypes.text import InteractiveTextualNode + + +class DtsResolverText(InteractiveTextualNode): + def __init__(self, identifier, resolver, **kwargs): + super(InteractiveTextualNode, self).__init__(identifier=identifier, **kwargs) + self.__childIds__ = None \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2fbdbbfb..02efbcd0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,14 @@ +# Tests +requests_mock==1.5.2 +responses==0.8.1 +xmlunittest==0.5.0 +# Software requests>=2.8.1 lxml>=3.6.4 mock>=2.0.0 future>=0.16.0 six>=1.10.0 -xmlunittest>=0.3.2 rdflib-jsonld>=0.4.0 -responses==0.8.1 LinkHeader==0.4.3 pyld==1.0.3 -typing -requests_mock \ No newline at end of file +typing \ No newline at end of file diff --git a/tests/resolvers/cts/test_api.py b/tests/resolvers/cts/test_api.py index cd9485b9..7b0520f0 100644 --- a/tests/resolvers/cts/test_api.py +++ b/tests/resolvers/cts/test_api.py @@ -2,7 +2,7 @@ from MyCapytain.retrievers.cts5 import HttpCtsRetriever from MyCapytain.common.utils.xml import xmlparser from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes -from MyCapytain.resources.prototypes.text import Passage +from MyCapytain.resources.prototypes.text import CtsPassage from MyCapytain.resources.collections.cts import XmlCtsTextInventoryMetadata, XmlCtsTextgroupMetadata, XmlCtsWorkMetadata, XmlCtsTextMetadata from MyCapytain.resources.prototypes.metadata import Collection @@ -50,7 +50,7 @@ def test_getPassage_full(self): urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2" ) self.assertIsInstance( - passage, Passage, + passage, CtsPassage, "GetPassage should always return passages objects" ) @@ -86,7 +86,7 @@ def test_getPassage_subreference(self): urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1" ) self.assertIsInstance( - passage, Passage, + passage, CtsPassage, "GetPassage should always return passages objects" ) @@ -122,7 +122,7 @@ def test_getPassage_full_metadata(self): urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2" ) self.assertIsInstance( - passage, Passage, + passage, CtsPassage, "GetPassage should always return passages objects" ) self.assertEqual( @@ -183,7 +183,7 @@ def test_getPassage_prevnext(self): urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1" ) self.assertIsInstance( - passage, Passage, + passage, CtsPassage, "GetPassage should always return passages objects" ) self.assertEqual( @@ -235,7 +235,7 @@ def test_getPassage_metadata_prevnext(self): urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1" ) self.assertIsInstance( - passage, Passage, + passage, CtsPassage, "GetPassage should always return passages objects" ) self.assertEqual( diff --git a/tests/resolvers/cts/test_local.py b/tests/resolvers/cts/test_local.py index 98f044f6..c5580428 100644 --- a/tests/resolvers/cts/test_local.py +++ b/tests/resolvers/cts/test_local.py @@ -10,7 +10,7 @@ from MyCapytain.resources.collections.cts import XmlCtsTextInventoryMetadata from MyCapytain.resources.prototypes.cts.inventory import CtsTextgroupMetadata, CtsTextMetadata as TextMetadata, \ CtsTranslationMetadata, CtsTextInventoryMetadata, CtsCommentaryMetadata, CtsTextInventoryCollection -from MyCapytain.resources.prototypes.text import Passage +from MyCapytain.resources.prototypes.text import CtsPassage from MyCapytain.resolvers.utils import CollectionDispatcher from unittest import TestCase @@ -179,7 +179,7 @@ def test_getPassage_full(self): """ Test that we can get a full text """ passage = self.resolver.getTextualNode("urn:cts:latinLit:phi1294.phi002.perseus-lat2") self.assertIsInstance( - passage, Passage, + passage, CtsPassage, "GetPassage should always return passages objects" ) @@ -224,7 +224,7 @@ def test_getPassage_subreference(self): # We check we made a reroute to GetPassage request self.assertIsInstance( - passage, Passage, + passage, CtsPassage, "GetPassage should always return passages objects" ) @@ -257,7 +257,7 @@ def test_getPassage_full_metadata(self): passage = self.resolver.getTextualNode("urn:cts:latinLit:phi1294.phi002.perseus-lat2", metadata=True) self.assertIsInstance( - passage, Passage, + passage, CtsPassage, "GetPassage should always return passages objects" ) self.assertEqual( @@ -315,7 +315,7 @@ def test_getPassage_prevnext(self): ) self.assertIsInstance( - passage, Passage, + passage, CtsPassage, "GetPassage should always return passages objects" ) self.assertEqual( @@ -363,7 +363,7 @@ def test_getPassage_metadata_prevnext(self): "urn:cts:latinLit:phi1294.phi002.perseus-lat2", subreference="1.1", metadata=True, prevnext=True ) self.assertIsInstance( - passage, Passage, + passage, CtsPassage, "GetPassage should always return passages objects" ) self.assertEqual( diff --git a/tests/resources/proto/test_text.py b/tests/resources/proto/test_text.py index e2400e49..3f4abad8 100644 --- a/tests/resources/proto/test_text.py +++ b/tests/resources/proto/test_text.py @@ -42,7 +42,7 @@ def test_urn(self): a.urn = 2 # Test Resource setting works out as well - b = CitableText(urn="urn:cts:latinLit:tg.wk.v") + b = CtsText(urn="urn:cts:latinLit:tg.wk.v") self.assertEqual(str(b.urn), "urn:cts:latinLit:tg.wk.v") @@ -51,20 +51,20 @@ class TestProtoText(unittest.TestCase): def test_init(self): """ Test init works correctly """ - a = CitableText("someId") + a = CtsText("someId") # Test with metadata - a = CitableText("someId") + a = CtsText("someId") self.assertIsInstance(a.metadata, MyCapytain.common.metadata.Metadata) m = MyCapytain.common.metadata.Metadata() m.add(RDF_NAMESPACES.CTS.title, "I am a metadata", "fre") - a = CitableText(metadata=m) + a = CtsText(metadata=m) self.assertEqual(str(a.metadata.get_single(RDF_NAMESPACES.CTS.title, "fre")), "I am a metadata") def test_proto_reff(self): """ Test that getValidReff function are not implemented """ - a = CitableText() + a = CtsText() # It should fail because not implemented with self.assertRaises(NotImplementedError): @@ -77,7 +77,7 @@ def test_proto_reff(self): def test_proto_passage(self): """ Test that getPassage function are not implemented but are consistent""" - a = CitableText() + a = CtsText() # It should fail because not implemented with self.assertRaises(NotImplementedError): @@ -85,7 +85,7 @@ def test_proto_passage(self): def test_get_label(self): """ Test that getLabel function are not implemented but are consistent""" - a = CitableText() + a = CtsText() # It should fail because not implemented with self.assertRaises(NotImplementedError): @@ -93,7 +93,7 @@ def test_get_label(self): def test_urn(self): """ Test setters and getters for urn """ - a = CitableText() + a = CtsText() # Should work with string a.urn = "urn:cts:latinLit:tg.wk.v" self.assertEqual(isinstance(a.urn, MyCapytain.common.reference._capitains_cts.URN), True) @@ -109,12 +109,12 @@ def test_urn(self): a.urn = 2 # Test original setting works out as well - b = CitableText(urn="urn:cts:latinLit:tg.wk.v") + b = CtsText(urn="urn:cts:latinLit:tg.wk.v") self.assertEqual(str(b.urn), "urn:cts:latinLit:tg.wk.v") def test_reffs(self): """ Test property reff, should fail because it supposes validReff is implemented """ - a = CitableText() + a = CtsText() # It should fail because not implemented with self.assertRaises(NotImplementedError): @@ -122,10 +122,10 @@ def test_reffs(self): def test_citation(self): """ Test citation property setter and getter """ - a = CitableText() + a = CtsText() a.citation = MyCapytain.common.reference._capitains_cts.Citation(name="label") self.assertIsInstance(a.citation, MyCapytain.common.reference._capitains_cts.Citation) #On init ? - b = CitableText(citation=MyCapytain.common.reference._capitains_cts.Citation(name="label")) + b = CtsText(citation=MyCapytain.common.reference._capitains_cts.Citation(name="label")) self.assertEqual(b.citation.name, "label") From 08c52f250db0a2f16e4d2193cda2b01dd72b8691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Fri, 19 Oct 2018 10:22:00 +0200 Subject: [PATCH 62/89] (CTS)(Breaking) Prototype CTS texts have been moved From MyCapytain.resources.prototypes.text to MyCapytain.resources.prototypes.cts.text : - CtsNode now PrototypeCtsNode - CtsPassage now PrototypeCtsPassage - CtsText now PrototypeCtsText --- MyCapytain/resources/prototypes/cts/text.py | 174 ++++++++++++++++++ MyCapytain/resources/prototypes/text.py | 165 +---------------- .../resources/texts/local/capitains/cts.py | 10 +- MyCapytain/resources/texts/remote/cts.py | 13 +- tests/resolvers/cts/test_api.py | 12 +- tests/resolvers/cts/test_local.py | 12 +- tests/resources/collections/test_cts.py | 4 +- tests/resources/proto/test_text.py | 30 +-- 8 files changed, 217 insertions(+), 203 deletions(-) create mode 100644 MyCapytain/resources/prototypes/cts/text.py diff --git a/MyCapytain/resources/prototypes/cts/text.py b/MyCapytain/resources/prototypes/cts/text.py new file mode 100644 index 00000000..4fc231a9 --- /dev/null +++ b/MyCapytain/resources/prototypes/cts/text.py @@ -0,0 +1,174 @@ +from typing import Union +from MyCapytain.common.reference import URN, BaseReferenceSet, BaseReference +from MyCapytain.common.constants import RDF_NAMESPACES +from ..metadata import Collection +from rdflib import Literal + +from .inventory import CtsTextMetadata +from ..text import InteractiveTextualNode + + +__all__ = [ + "PrototypeCtsNode", + "PrototypeCtsText", + "PrototypeCtsPassage" +] + + +class PrototypeCtsNode(InteractiveTextualNode): + """ Initiate a Resource object + + :param urn: A URN identifier + :type urn: MyCapytain.common.reference._capitains_cts.URN + :param metadata: Collection Information about the Item + :type metadata: Collection + :param citation: XmlCtsCitation system of the text + :type citation: Citation + :param children: Current node Children's Identifier + :type children: [str] + :param parent: Parent of the current node + :type parent: str + :param siblings: Previous and next node of the current node + :type siblings: str + :param depth: Depth of the node in the global hierarchy of the text tree + :type depth: int + :param resource: Resource used to navigate through the textual graph + + :cvar default_exclude: Default exclude for exports + """ + + def __init__(self, urn: Union[URN, str] = None, **kwargs): + super(PrototypeCtsNode, self).__init__(identifier=str(urn), **kwargs) + self._urn = None + + if urn is not None: + self.urn = urn + + @property + def urn(self) -> URN: + """ URN Identifier of the object + + """ + return self._urn + + @urn.setter + def urn(self, value: Union[URN, str]): + """ Set the urn + + :param value: URN to be saved + :raises: *TypeError* when the value is not URN compatible + + """ + if isinstance(value, str): + value = URN(value) + elif not isinstance(value, URN): + raise TypeError() + self._urn = value + + def get_cts_metadata(self, key: str, lang: str = None) -> Literal: + """ Get easily a metadata from the CTS namespace + + :param key: CTS property to retrieve + :param lang: Language in which it should be + :return: Literal value of the CTS graph property + """ + return self.metadata.get_single(RDF_NAMESPACES.CTS.term(key), lang) + + def getValidReff(self, level: int = 1, reference: BaseReference = None) -> BaseReferenceSet: + """ Given a resource, CtsText will compute valid reffs + + :param level: Depth required. If not set, should retrieve first encountered level (1 based) + :param reference: Subreference (optional) + :returns: List of levels + """ + raise NotImplementedError() + + def getLabel(self) -> Collection: + """ Retrieve metadata about the text + + :rtype: Collection + :returns: Retrieve Label informations in a Collection format + """ + raise NotImplementedError() + + def set_metadata_from_collection(self, text_metadata: CtsTextMetadata): + """ Set the object metadata using its collections recursively + + :param text_metadata: Object representing the current text as a collection + :type text_metadata: CtsEditionMetadata or CtsTranslationMetadata + """ + edition, work, textgroup = tuple(([text_metadata] + text_metadata.parents)[:3]) + + for node in textgroup.metadata.get(RDF_NAMESPACES.CTS.groupname): + lang = node.language + self.metadata.add(RDF_NAMESPACES.CTS.groupname, lang=lang, value=str(node)) + self.set_creator(str(node), lang) + + for node in work.metadata.get(RDF_NAMESPACES.CTS.title): + lang = node.language + self.metadata.add(RDF_NAMESPACES.CTS.title, lang=lang, value=str(node)) + self.set_title(str(node), lang) + + for node in edition.metadata.get(RDF_NAMESPACES.CTS.label): + lang = node.language + self.metadata.add(RDF_NAMESPACES.CTS.label, lang=lang, value=str(node)) + self.set_subject(str(node), lang) + + for node in edition.metadata.get(RDF_NAMESPACES.CTS.description): + lang = node.language + self.metadata.add(RDF_NAMESPACES.CTS.description, lang=lang, value=str(node)) + self.set_description(str(node), lang) + + if not self.citation.is_set() and edition.citation.is_set(): + self.citation = edition.citation + + +class PrototypeCtsPassage(PrototypeCtsNode): + """ CapitainsCtsPassage objects possess metadata informations + + :param urn: A URN identifier + :type urn: MyCapytain.common.reference._capitains_cts.URN + :param metadata: Collection Information about the Item + :type metadata: Collection + :param citation: XmlCtsCitation system of the text + :type citation: Citation + :param children: Current node Children's Identifier + :type children: [str] + :param parent: Parent of the current node + :type parent: str + :param siblings: Previous and next node of the current node + :type siblings: str + :param depth: Depth of the node in the global hierarchy of the text tree + :type depth: int + :param resource: Resource used to navigate through the textual graph + + :cvar default_exclude: Default exclude for exports + """ + + def __init__(self, **kwargs): + super(PrototypeCtsPassage, self).__init__(**kwargs) + + @property + def reference(self) -> BaseReference: + return self.urn.reference + + +class PrototypeCtsText(PrototypeCtsNode): + """ A CTS CtsText + """ + + def __init__(self, citation=None, metadata=None, **kwargs): + super(PrototypeCtsText, self).__init__(citation=citation, metadata=metadata, **kwargs) + self._cts = None + + @property + def reffs(self) -> BaseReferenceSet: + """ Get all valid reffs for every part of the CtsText + + :rtype: [str] + """ + if not self._cts: + self._cts = BaseReferenceSet( + [reff for reffs in [self.getReffs(level=i) for i in range(1, len(self.citation) + 1)] for reff in reffs] + ) + return self._cts diff --git a/MyCapytain/resources/prototypes/text.py b/MyCapytain/resources/prototypes/text.py index 0ae63414..b26e50b8 100644 --- a/MyCapytain/resources/prototypes/text.py +++ b/MyCapytain/resources/prototypes/text.py @@ -11,21 +11,18 @@ from rdflib.namespace import DC from rdflib import BNode, URIRef, Graph, Literal from rdflib.term import Identifier -from MyCapytain.common.reference import URN, Citation, NodeId, BaseReference, BaseReferenceSet +from MyCapytain.common.reference import Citation, NodeId, BaseReference, BaseReferenceSet from MyCapytain.common.metadata import Metadata from MyCapytain.common.constants import Mimetypes, get_graph, RDF_NAMESPACES from MyCapytain.common.base import Exportable from MyCapytain.resources.prototypes.metadata import Collection -from MyCapytain.resources.prototypes.cts.inventory import CtsTextMetadata __all__ = [ "TextualElement", "TextualGraph", "TextualNode", - "CtsText", - "InteractiveTextualNode", - "CtsNode" + "InteractiveTextualNode" ] @@ -355,161 +352,3 @@ def lastId(self) -> BaseReference: return None else: raise NotImplementedError - - -class CtsNode(InteractiveTextualNode): - """ Initiate a Resource object - - :param urn: A URN identifier - :type urn: MyCapytain.common.reference._capitains_cts.URN - :param metadata: Collection Information about the Item - :type metadata: Collection - :param citation: XmlCtsCitation system of the text - :type citation: Citation - :param children: Current node Children's Identifier - :type children: [str] - :param parent: Parent of the current node - :type parent: str - :param siblings: Previous and next node of the current node - :type siblings: str - :param depth: Depth of the node in the global hierarchy of the text tree - :type depth: int - :param resource: Resource used to navigate through the textual graph - - :cvar default_exclude: Default exclude for exports - """ - - def __init__(self, urn: Union[URN, str]=None, **kwargs): - super(CtsNode, self).__init__(identifier=str(urn), **kwargs) - self._urn = None - - if urn is not None: - self.urn = urn - - @property - def urn(self) -> URN: - """ URN Identifier of the object - - """ - return self._urn - - @urn.setter - def urn(self, value: Union[URN, str]): - """ Set the urn - - :param value: URN to be saved - :raises: *TypeError* when the value is not URN compatible - - """ - if isinstance(value, str): - value = URN(value) - elif not isinstance(value, URN): - raise TypeError() - self._urn = value - - def get_cts_metadata(self, key: str, lang: str=None) -> Literal: - """ Get easily a metadata from the CTS namespace - - :param key: CTS property to retrieve - :param lang: Language in which it should be - :return: Literal value of the CTS graph property - """ - return self.metadata.get_single(RDF_NAMESPACES.CTS.term(key), lang) - - def getValidReff(self, level: int=1, reference: BaseReference=None) -> BaseReferenceSet: - """ Given a resource, CtsText will compute valid reffs - - :param level: Depth required. If not set, should retrieve first encountered level (1 based) - :param reference: Subreference (optional) - :returns: List of levels - """ - raise NotImplementedError() - - def getLabel(self) -> Collection: - """ Retrieve metadata about the text - - :rtype: Collection - :returns: Retrieve Label informations in a Collection format - """ - raise NotImplementedError() - - def set_metadata_from_collection(self, text_metadata: CtsTextMetadata): - """ Set the object metadata using its collections recursively - - :param text_metadata: Object representing the current text as a collection - :type text_metadata: CtsEditionMetadata or CtsTranslationMetadata - """ - edition, work, textgroup = tuple(([text_metadata] + text_metadata.parents)[:3]) - - for node in textgroup.metadata.get(RDF_NAMESPACES.CTS.groupname): - lang = node.language - self.metadata.add(RDF_NAMESPACES.CTS.groupname, lang=lang, value=str(node)) - self.set_creator(str(node), lang) - - for node in work.metadata.get(RDF_NAMESPACES.CTS.title): - lang = node.language - self.metadata.add(RDF_NAMESPACES.CTS.title, lang=lang, value=str(node)) - self.set_title(str(node), lang) - - for node in edition.metadata.get(RDF_NAMESPACES.CTS.label): - lang = node.language - self.metadata.add(RDF_NAMESPACES.CTS.label, lang=lang, value=str(node)) - self.set_subject(str(node), lang) - - for node in edition.metadata.get(RDF_NAMESPACES.CTS.description): - lang = node.language - self.metadata.add(RDF_NAMESPACES.CTS.description, lang=lang, value=str(node)) - self.set_description(str(node), lang) - - if not self.citation.is_set() and edition.citation.is_set(): - self.citation = edition.citation - - -class CtsPassage(CtsNode): - """ CapitainsCtsPassage objects possess metadata informations - - :param urn: A URN identifier - :type urn: MyCapytain.common.reference._capitains_cts.URN - :param metadata: Collection Information about the Item - :type metadata: Collection - :param citation: XmlCtsCitation system of the text - :type citation: Citation - :param children: Current node Children's Identifier - :type children: [str] - :param parent: Parent of the current node - :type parent: str - :param siblings: Previous and next node of the current node - :type siblings: str - :param depth: Depth of the node in the global hierarchy of the text tree - :type depth: int - :param resource: Resource used to navigate through the textual graph - - :cvar default_exclude: Default exclude for exports - """ - - def __init__(self, **kwargs): - super(CtsPassage, self).__init__(**kwargs) - - @property - def reference(self) -> BaseReference: - return self.urn.reference - - -class CtsText(CtsNode): - """ A CTS CtsText - """ - def __init__(self, citation=None, metadata=None, **kwargs): - super(CtsText, self).__init__(citation=citation, metadata=metadata, **kwargs) - self._cts = None - - @property - def reffs(self) -> BaseReferenceSet: - """ Get all valid reffs for every part of the CtsText - - :rtype: [str] - """ - if not self._cts: - self._cts = BaseReferenceSet( - [reff for reffs in [self.getReffs(level=i) for i in range(1, len(self.citation) + 1)] for reff in reffs] - ) - return self._cts diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index c409876b..4b3eed2d 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -13,10 +13,10 @@ from MyCapytain.errors import DuplicateReference, MissingAttribute, RefsDeclError, EmptyReference, CitationDepthError, MissingRefsDecl from MyCapytain.common.utils.xml import copyNode, normalizeXpath, passageLoop -from MyCapytain.common.constants import XPATH_NAMESPACES, RDF_NAMESPACES +from MyCapytain.common.constants import XPATH_NAMESPACES from MyCapytain.common.reference import CtsReference, URN, Citation, CtsReferenceSet -from MyCapytain.resources.prototypes import text +from MyCapytain.resources.prototypes.cts.text import PrototypeCtsPassage, PrototypeCtsText from MyCapytain.resources.texts.base.tei import TeiResource from MyCapytain.errors import InvalidSiblingRequest, InvalidURN @@ -293,7 +293,7 @@ def tostring(self, *args, **kwargs): return etree.tostring(self.resource, *args, **kwargs) -class _SimplePassage(_SharedMethods, text.CtsPassage): +class _SimplePassage(_SharedMethods, PrototypeCtsPassage): """ CapitainsCtsPassage for simple and quick parsing of texts :param resource: Element representing the passage @@ -440,7 +440,7 @@ def textObject(self): return self.__text__ -class CapitainsCtsText(_SharedMethods, text.CtsText): +class CapitainsCtsText(_SharedMethods, PrototypeCtsText): """ Implementation of CTS tools for local files :param urn: A URN identifier @@ -485,7 +485,7 @@ def test(self): raise E -class CapitainsCtsPassage(_SharedMethods, text.CtsPassage): +class CapitainsCtsPassage(_SharedMethods, PrototypeCtsPassage): """ CapitainsCtsPassage class for local texts which rebuilds the tree up to the passage. For design purposes, some people would prefer the output of GetPassage to be consistent. ContextPassage rebuilds diff --git a/MyCapytain/resources/texts/remote/cts.py b/MyCapytain/resources/texts/remote/cts.py index 2635ab2e..e13dab01 100644 --- a/MyCapytain/resources/texts/remote/cts.py +++ b/MyCapytain/resources/texts/remote/cts.py @@ -13,9 +13,10 @@ from MyCapytain.common.metadata import Metadata from MyCapytain.common.utils.xml import xmlparser from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes, RDF_NAMESPACES -from MyCapytain.common.reference._capitains_cts import CtsReference, URN +from MyCapytain.common.reference import CtsReference, URN from MyCapytain.resources.collections import cts as CtsCollection -from MyCapytain.resources.prototypes import text as prototypes +from MyCapytain.resources.prototypes.text import InteractiveTextualNode +from MyCapytain.resources.prototypes.cts.text import PrototypeCtsText, PrototypeCtsPassage from MyCapytain.resources.texts.base.tei import TeiResource from MyCapytain.errors import MissingAttribute @@ -26,11 +27,11 @@ ] -class _SharedMethod(prototypes.InteractiveTextualNode): +class _SharedMethod(InteractiveTextualNode): """ Set of methods shared by CtsTextMetadata and CapitainsCtsPassage :param retriever: Retriever used to retrieve other data - :type retriever: MyCapytain.retrievers.prototypes.CitableTextServiceRetriever + :type retriever: MyCapytain.retrievers.PrototypeCitableTextServiceRetriever """ @property @@ -313,7 +314,7 @@ def prevnext(resource): return _prev, _next -class CtsText(_SharedMethod, prototypes.CtsText): +class CtsText(_SharedMethod, PrototypeCtsText): """ API CtsTextMetadata object :param urn: A URN identifier @@ -378,7 +379,7 @@ def export(self, output=Mimetypes.PLAINTEXT, exclude=None, **kwargs): return self.getTextualNode().export(output, exclude) -class CtsPassage(_SharedMethod, prototypes.CtsPassage, TeiResource): +class CtsPassage(_SharedMethod, PrototypeCtsPassage, TeiResource): """ CapitainsCtsPassage representing :param urn: diff --git a/tests/resolvers/cts/test_api.py b/tests/resolvers/cts/test_api.py index 7b0520f0..ccb057e5 100644 --- a/tests/resolvers/cts/test_api.py +++ b/tests/resolvers/cts/test_api.py @@ -2,7 +2,7 @@ from MyCapytain.retrievers.cts5 import HttpCtsRetriever from MyCapytain.common.utils.xml import xmlparser from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes -from MyCapytain.resources.prototypes.text import CtsPassage +from MyCapytain.resources.prototypes.cts.text import PrototypeCtsPassage from MyCapytain.resources.collections.cts import XmlCtsTextInventoryMetadata, XmlCtsTextgroupMetadata, XmlCtsWorkMetadata, XmlCtsTextMetadata from MyCapytain.resources.prototypes.metadata import Collection @@ -50,7 +50,7 @@ def test_getPassage_full(self): urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2" ) self.assertIsInstance( - passage, CtsPassage, + passage, PrototypeCtsPassage, "GetPassage should always return passages objects" ) @@ -86,7 +86,7 @@ def test_getPassage_subreference(self): urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1" ) self.assertIsInstance( - passage, CtsPassage, + passage, PrototypeCtsPassage, "GetPassage should always return passages objects" ) @@ -122,7 +122,7 @@ def test_getPassage_full_metadata(self): urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2" ) self.assertIsInstance( - passage, CtsPassage, + passage, PrototypeCtsPassage, "GetPassage should always return passages objects" ) self.assertEqual( @@ -183,7 +183,7 @@ def test_getPassage_prevnext(self): urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1" ) self.assertIsInstance( - passage, CtsPassage, + passage, PrototypeCtsPassage, "GetPassage should always return passages objects" ) self.assertEqual( @@ -235,7 +235,7 @@ def test_getPassage_metadata_prevnext(self): urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1" ) self.assertIsInstance( - passage, CtsPassage, + passage, PrototypeCtsPassage, "GetPassage should always return passages objects" ) self.assertEqual( diff --git a/tests/resolvers/cts/test_local.py b/tests/resolvers/cts/test_local.py index c5580428..0e674b9c 100644 --- a/tests/resolvers/cts/test_local.py +++ b/tests/resolvers/cts/test_local.py @@ -10,7 +10,7 @@ from MyCapytain.resources.collections.cts import XmlCtsTextInventoryMetadata from MyCapytain.resources.prototypes.cts.inventory import CtsTextgroupMetadata, CtsTextMetadata as TextMetadata, \ CtsTranslationMetadata, CtsTextInventoryMetadata, CtsCommentaryMetadata, CtsTextInventoryCollection -from MyCapytain.resources.prototypes.text import CtsPassage +from MyCapytain.resources.prototypes.cts.text import PrototypeCtsPassage from MyCapytain.resolvers.utils import CollectionDispatcher from unittest import TestCase @@ -179,7 +179,7 @@ def test_getPassage_full(self): """ Test that we can get a full text """ passage = self.resolver.getTextualNode("urn:cts:latinLit:phi1294.phi002.perseus-lat2") self.assertIsInstance( - passage, CtsPassage, + passage, PrototypeCtsPassage, "GetPassage should always return passages objects" ) @@ -224,7 +224,7 @@ def test_getPassage_subreference(self): # We check we made a reroute to GetPassage request self.assertIsInstance( - passage, CtsPassage, + passage, PrototypeCtsPassage, "GetPassage should always return passages objects" ) @@ -257,7 +257,7 @@ def test_getPassage_full_metadata(self): passage = self.resolver.getTextualNode("urn:cts:latinLit:phi1294.phi002.perseus-lat2", metadata=True) self.assertIsInstance( - passage, CtsPassage, + passage, PrototypeCtsPassage, "GetPassage should always return passages objects" ) self.assertEqual( @@ -315,7 +315,7 @@ def test_getPassage_prevnext(self): ) self.assertIsInstance( - passage, CtsPassage, + passage, PrototypeCtsPassage, "GetPassage should always return passages objects" ) self.assertEqual( @@ -363,7 +363,7 @@ def test_getPassage_metadata_prevnext(self): "urn:cts:latinLit:phi1294.phi002.perseus-lat2", subreference="1.1", metadata=True, prevnext=True ) self.assertIsInstance( - passage, CtsPassage, + passage, PrototypeCtsPassage, "GetPassage should always return passages objects" ) self.assertEqual( diff --git a/tests/resources/collections/test_cts.py b/tests/resources/collections/test_cts.py index 2024cf45..ba5cf904 100644 --- a/tests/resources/collections/test_cts.py +++ b/tests/resources/collections/test_cts.py @@ -10,7 +10,7 @@ from MyCapytain.common import constants from MyCapytain.resources.collections.cts import * -from MyCapytain.resources.prototypes.text import CtsNode +from MyCapytain.resources.prototypes.cts.text import PrototypeCtsNode from MyCapytain.common.utils.xml import xmlparser from MyCapytain.common.constants import Mimetypes, RDF_NAMESPACES, XPATH_NAMESPACES from rdflib import URIRef, Literal, XSD @@ -451,7 +451,7 @@ def test_import_to_text(self): TI = XmlCtsTextInventoryMetadata.parse(resource=self.getCapabilities) ti_text = TI["urn:cts:latinLit:phi1294.phi002.perseus-lat2"] - txt_text = CtsNode("urn:cts:latinLit:phi1294.phi002.perseus-lat2") + txt_text = PrototypeCtsNode("urn:cts:latinLit:phi1294.phi002.perseus-lat2") txt_text.set_metadata_from_collection(ti_text) self.assertEqual(str(txt_text.urn), "urn:cts:latinLit:phi1294.phi002.perseus-lat2") self.assertEqual( diff --git a/tests/resources/proto/test_text.py b/tests/resources/proto/test_text.py index 3f4abad8..2278ac11 100644 --- a/tests/resources/proto/test_text.py +++ b/tests/resources/proto/test_text.py @@ -6,7 +6,7 @@ from MyCapytain.common.reference import URN, Citation -from MyCapytain.resources.prototypes.text import * +from MyCapytain.resources.prototypes.cts.text import PrototypeCtsText, PrototypeCtsNode from MyCapytain.common.constants import RDF_NAMESPACES import MyCapytain.common.reference import MyCapytain.common.metadata @@ -15,7 +15,7 @@ class TestProtoResource(unittest.TestCase): """ Test for resource, mother class of CtsTextMetadata and CapitainsCtsPassage """ def test_init(self): - a = CtsNode(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2") + a = PrototypeCtsNode(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2") self.assertEqual(a.id, "urn:cts:latinLit:phi1294.phi002.perseus-lat2") self.assertEqual(a.urn, URN("urn:cts:latinLit:phi1294.phi002.perseus-lat2")) self.assertIsInstance(a.citation, Citation) @@ -26,7 +26,7 @@ def test_init(self): def test_urn(self): """ Test setters and getters for urn """ - a = CtsNode() + a = PrototypeCtsNode() # Should work with string a.urn = "urn:cts:latinLit:tg.wk.v" self.assertEqual(isinstance(a.urn, MyCapytain.common.reference._capitains_cts.URN), True) @@ -42,7 +42,7 @@ def test_urn(self): a.urn = 2 # Test Resource setting works out as well - b = CtsText(urn="urn:cts:latinLit:tg.wk.v") + b = PrototypeCtsText(urn="urn:cts:latinLit:tg.wk.v") self.assertEqual(str(b.urn), "urn:cts:latinLit:tg.wk.v") @@ -51,20 +51,20 @@ class TestProtoText(unittest.TestCase): def test_init(self): """ Test init works correctly """ - a = CtsText("someId") + a = PrototypeCtsText("someId") # Test with metadata - a = CtsText("someId") + a = PrototypeCtsText("someId") self.assertIsInstance(a.metadata, MyCapytain.common.metadata.Metadata) m = MyCapytain.common.metadata.Metadata() m.add(RDF_NAMESPACES.CTS.title, "I am a metadata", "fre") - a = CtsText(metadata=m) + a = PrototypeCtsText(metadata=m) self.assertEqual(str(a.metadata.get_single(RDF_NAMESPACES.CTS.title, "fre")), "I am a metadata") def test_proto_reff(self): """ Test that getValidReff function are not implemented """ - a = CtsText() + a = PrototypeCtsText() # It should fail because not implemented with self.assertRaises(NotImplementedError): @@ -77,7 +77,7 @@ def test_proto_reff(self): def test_proto_passage(self): """ Test that getPassage function are not implemented but are consistent""" - a = CtsText() + a = PrototypeCtsText() # It should fail because not implemented with self.assertRaises(NotImplementedError): @@ -85,7 +85,7 @@ def test_proto_passage(self): def test_get_label(self): """ Test that getLabel function are not implemented but are consistent""" - a = CtsText() + a = PrototypeCtsText() # It should fail because not implemented with self.assertRaises(NotImplementedError): @@ -93,7 +93,7 @@ def test_get_label(self): def test_urn(self): """ Test setters and getters for urn """ - a = CtsText() + a = PrototypeCtsText() # Should work with string a.urn = "urn:cts:latinLit:tg.wk.v" self.assertEqual(isinstance(a.urn, MyCapytain.common.reference._capitains_cts.URN), True) @@ -109,12 +109,12 @@ def test_urn(self): a.urn = 2 # Test original setting works out as well - b = CtsText(urn="urn:cts:latinLit:tg.wk.v") + b = PrototypeCtsText(urn="urn:cts:latinLit:tg.wk.v") self.assertEqual(str(b.urn), "urn:cts:latinLit:tg.wk.v") def test_reffs(self): """ Test property reff, should fail because it supposes validReff is implemented """ - a = CtsText() + a = PrototypeCtsText() # It should fail because not implemented with self.assertRaises(NotImplementedError): @@ -122,10 +122,10 @@ def test_reffs(self): def test_citation(self): """ Test citation property setter and getter """ - a = CtsText() + a = PrototypeCtsText() a.citation = MyCapytain.common.reference._capitains_cts.Citation(name="label") self.assertIsInstance(a.citation, MyCapytain.common.reference._capitains_cts.Citation) #On init ? - b = CtsText(citation=MyCapytain.common.reference._capitains_cts.Citation(name="label")) + b = PrototypeCtsText(citation=MyCapytain.common.reference._capitains_cts.Citation(name="label")) self.assertEqual(b.citation.name, "label") From 73f7770db7dd35cd0f3f7cf59d6e1324ce18e29e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Fri, 19 Oct 2018 18:28:21 +0200 Subject: [PATCH 63/89] (CTS)(Local Texts) Moving from self.__attr__ to self._attr --- MyCapytain/common/reference/_base.py | 79 +++----- MyCapytain/resolvers/dts/api_v1.py | 23 ++- .../resources/texts/local/capitains/cts.py | 185 +++++++++--------- MyCapytain/resources/texts/remote/cts.py | 4 +- .../texts/remote/dts/_resolver_v1.py | 31 ++- tests/resources/texts/local/commonTests.py | 70 +++---- 6 files changed, 217 insertions(+), 175 deletions(-) diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index 90ab3000..946ddf7e 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -2,6 +2,7 @@ from MyCapytain.common.constants import get_graph, Mimetypes, RDF_NAMESPACES from MyCapytain.errors import CitationDepthError from copy import copy +from typing import Tuple from abc import abstractmethod @@ -395,79 +396,63 @@ class NodeId(object): :type depth: int """ def __init__(self, identifier=None, children=None, parent=None, siblings=(None, None), depth=None): - self.__children__ = children or [] - self.__parent__ = parent - self.__prev__, self.__nextId__ = siblings - self.__identifier__ = identifier - self.__depth__ = depth + self._children = children or [] + self._parent = parent + self._prev_id, self._next_id = siblings + self._identifier = identifier + self._depth = depth @property - def depth(self): + def depth(self) -> int: """ Depth of the node in the global hierarchy of the text tree - - :rtype: int """ - return self.__depth__ + return self._depth @property - def childIds(self): - """ Children Node - - :rtype: [str] + def childIds(self) -> BaseReferenceSet: + """ Children Ids """ - return self.__children__ + return self._children @property - def firstId(self): - """ First child Node - - :rtype: str + def firstId(self) -> BaseReference: + """ First child Id """ - if len(self.__children__) == 0: + if len(self._children) == 0: return None - return self.__children__[0] + return self._children[0] @property - def lastId(self): - """ Last child Node - - :rtype: str + def lastId(self) -> BaseReference: + """ Last child id """ - if len(self.__children__) == 0: + if len(self._children) == 0: return None - return self.__children__[-1] + return self._children[-1] @property - def parentId(self): - """ Parent Node - - :rtype: str + def parentId(self) -> BaseReference: + """ Parent Id """ - return self.__parent__ + return self._parent @property - def siblingsId(self): - """ Siblings Node - - :rtype: (str, str) + def siblingsId(self) -> Tuple[BaseReference, BaseReference]: + """ Siblings Id """ - return self.__prev__, self.__nextId__ + return self.prevId, self.nextId @property - def prevId(self): - """ Previous Node (Sibling) - - :rtype: str + def prevId(self) -> BaseReference: + """ Previous Id (Sibling) """ - return self.__prev__ + return self._prev_id @property - def nextId(self): - """ Next Node (Sibling) - - :rtype: str + def nextId(self) -> BaseReference: + """ Next Id """ - return self.__nextId__ + return self._next_id @property def id(self): @@ -475,4 +460,4 @@ def id(self): :rtype: str """ - return self.__identifier__ + return self._identifier diff --git a/MyCapytain/resolvers/dts/api_v1.py b/MyCapytain/resolvers/dts/api_v1.py index 685be0a2..150fab98 100644 --- a/MyCapytain/resolvers/dts/api_v1.py +++ b/MyCapytain/resolvers/dts/api_v1.py @@ -17,8 +17,8 @@ from MyCapytain.retrievers.dts import HttpDtsRetriever from MyCapytain.common.utils.dts import parse_metadata from MyCapytain.resources.collections.dts import HttpResolverDtsCollection - from pyld.jsonld import expand +from MyCapytain.resources.texts.remote.dts._resolver_v1 import DtsResolverText, DtsResolverPassage __all__ = [ @@ -141,3 +141,24 @@ def getReffs( citation=citation ) return reffs + + def getTextualNode( + self, + textId: str, + subreference: Union[str, BaseReference]=None, + prevnext: bool=False, + metadata: bool=False + ) -> Union[DtsResolverText, DtsResolverPassage]: + """ Retrieve a text node from the API + + :param textId: CtsTextMetadata Identifier + :type textId: str + :param subreference: CapitainsCtsPassage Reference + :type subreference: str + :param prevnext: Retrieve graph representing previous and next passage + :type prevnext: boolean + :param metadata: Retrieve metadata about the passage and the text + :type metadata: boolean + :return: CapitainsCtsPassage + :rtype: CapitainsCtsPassage + """ \ No newline at end of file diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index 4b3eed2d..8bfb1838 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -11,6 +11,7 @@ import warnings +from typing import Tuple, Optional from MyCapytain.errors import DuplicateReference, MissingAttribute, RefsDeclError, EmptyReference, CitationDepthError, MissingRefsDecl from MyCapytain.common.utils.xml import copyNode, normalizeXpath, passageLoop from MyCapytain.common.constants import XPATH_NAMESPACES @@ -29,7 +30,7 @@ ] -def _makePassageKwargs(urn, reference): +def _make_passage_kwargs(urn, reference): """ Little helper used by CapitainsCtsPassage here to comply with parents args :param urn: URN String @@ -59,7 +60,6 @@ def getTextualNode(self, subreference=None, simple=False): :rtype: CapitainsCtsPassage, ContextPassage :returns: Asked passage """ - if subreference is None: return self._getSimplePassage() @@ -69,17 +69,17 @@ def getTextualNode(self, subreference=None, simple=False): elif isinstance(subreference, list): subreference = CtsReference(".".join(subreference)) - if not subreference.end: - start = end = subreference.start.list - else: - start, end = subreference.start.list, subreference.end.list - - if len(start) > self.citation.root.depth: + if len(subreference.start) > self.citation.root.depth: raise CitationDepthError("URN is deeper than citation scheme") if simple is True: return self._getSimplePassage(subreference) + if not subreference.is_range(): + start = end = subreference.start.list + else: + start, end = subreference.start.list, subreference.end.list + citation_start = self.citation.root[len(start)-1] citation_end = self.citation.root[len(end)-1] @@ -156,26 +156,22 @@ def textObject(self): text = self return text - def getReffs(self, level=1, subreference=None) -> CtsReferenceSet: + def getReffs(self, level: int=1, subreference: CtsReference=None) -> CtsReferenceSet: """ CtsReference available at a given level :param level: Depth required. If not set, should retrieve first encountered level (1 based) - :type level: Int :param subreference: Subreference (optional) - :type subreference: str - :rtype: List.basestring :returns: List of levels """ - if hasattr(self, "__depth__"): - level = level + self.depth - if not subreference: - if hasattr(self, "reference"): - subreference = self.reference - elif not isinstance(subreference, CtsReference): + + if not subreference and hasattr(self, "reference"): + subreference = self.reference + elif subreference and not isinstance(subreference, CtsReference): subreference = CtsReference(subreference) - return self.getValidReff(level, subreference) - def getValidReff(self, level: int=None, reference: CtsReference=None, _debug: bool=False) -> CtsReferenceSet: + return self.getValidReff(level=level, reference=subreference) + + def getValidReff(self, level: int=1, reference: CtsReference=None, _debug: bool=False) -> CtsReferenceSet: """ Retrieve valid passages directly :param level: Depth required. If not set, should retrieve first encountered level (1 based) @@ -189,35 +185,50 @@ def getValidReff(self, level: int=None, reference: CtsReference=None, _debug: bo .. note:: GetValidReff works for now as a loop using CapitainsCtsPassage, subinstances of CtsTextMetadata, to retrieve the valid \ informations. Maybe something is more powerfull ? """ + depth = 0 xml = self.textObject.xml + if reference: if isinstance(reference, CtsReference): - if reference.end is None: + if not reference.is_range(): passages = [reference.start.list] depth = len(passages[0]) + if level == 0: + level = None + if _debug: + warnings.warn("Using level=0 with a Non-range Reference is invalid. Autocorrected to 1") else: xml = self.getTextualNode(subreference=reference) + common = [] - for index in range(0, len(reference.start)): - if index == (len(common) - 1): - common.append(reference.start.list[index]) + for index, part in enumerate(reference.start.list): + if index <= reference.end.depth: + if part == reference.end.list[index]: + common.append(part) + else: + break else: break passages = [common] depth = len(common) - if not level: - level = len(reference.start.list) + 1 + if level is None: + level = reference.start.depth + depth + elif level == 1: + level = reference.start.depth + 1 + elif level == 0: + level = reference.start.depth else: raise TypeError() else: passages = [[]] - if not level: + if level is None: level = 1 + if level <= len(passages[0]) and reference is not None: level = len(passages[0]) + 1 if level > len(self.citation.root): @@ -311,15 +322,15 @@ def __init__(self, resource, reference, citation, text, urn=None): super(_SimplePassage, self).__init__( resource=resource, citation=citation, - **_makePassageKwargs(urn, reference) + **_make_passage_kwargs(urn, reference) ) - self.__text__ = text - self.__reference__ = reference - self.__children__ = None - self.__depth__ = 0 + self._text = text + self._reference = reference + self._children = None + self._depth = 0 if reference is not None: - self.__depth__ = reference.depth - self.__prevnext__ = None + self._depth = reference.depth + self._prev_next = None @property def reference(self): @@ -328,11 +339,11 @@ def reference(self): :return: CtsReference :rtype: CtsReference """ - return self.__reference__ + return self._reference @reference.setter def reference(self, value): - self.__reference__ = value + self._reference = value @property def childIds(self): @@ -343,20 +354,18 @@ def childIds(self): """ if self.depth >= len(self.citation.root): return [] - elif self.__children__ is not None: - return self.__children__ + elif self._children is not None: + return self._children else: - self.__children__ = self.getReffs() - return self.__children__ + self._children = self.getReffs() + return self._children - def getReffs(self, level=1, subreference=None): + def getReffs(self, level=1, subreference=None) -> CtsReferenceSet: """ Reference available at a given level - :param level: Depth required. If not set, should retrieve first encountered level (1 based) - :type level: Int + :param level: Depth required. If not set, should retrieve first encountered level (1 based). 0 retrieves inside + a range :param subreference: Subreference (optional) - :type subreference: CtsReference - :rtype: List.basestring :returns: List of levels """ level += self.depth @@ -364,7 +373,7 @@ def getReffs(self, level=1, subreference=None): subreference = self.reference return self.textObject.getValidReff(level, reference=subreference) - def getTextualNode(self, subreference=None): + def getTextualNode(self, subreference: CtsReference=None): """ Special GetPassage implementation for SimplePassage (Simple is True by default) :param subreference: @@ -384,7 +393,7 @@ def nextId(self): return self.siblingsId[1] @property - def prevId(self): + def prevId(self) -> Optional[CtsReference]: """ Get the Previous passage reference :returns: Previous passage reference at the same level @@ -393,21 +402,21 @@ def prevId(self): return self.siblingsId[0] @property - def siblingsId(self): + def siblingsId(self) -> Tuple[CtsReference, CtsReference]: """ Siblings Identifiers of the passage :rtype: (str, str) """ - if not self.__text__: + if not self._text: raise MissingAttribute("CapitainsCtsPassage was iniated without CtsTextMetadata object") - if self.__prevnext__ is not None: - return self.__prevnext__ + if self._prev_next is not None: + return self._prev_next - document_references = self.__text__.getReffs(level=self.depth) + document_references = self._text.getReffs(level=self.depth) range_length = 1 - if self.reference.end is not None: + if self.reference.is_range(): range_length = len(self.getReffs()) start = document_references.index(self.reference.start) @@ -428,8 +437,8 @@ def siblingsId(self): else: _next = document_references[start + 1] - self.__prevnext__ = (_prev, _next) - return self.__prevnext__ + self._prev_next = (_prev, _next) + return self._prev_next @property def textObject(self): @@ -437,7 +446,7 @@ def textObject(self): :rtype: CapitainsCtsText """ - return self.__text__ + return self._text class CapitainsCtsText(_SharedMethods, PrototypeCtsText): @@ -458,9 +467,9 @@ def __init__(self, urn=None, citation=None, resource=None): super(CapitainsCtsText, self).__init__(urn=urn, citation=citation, resource=resource) if self.resource is not None: - self.__findCRefPattern(self.resource) + self._findCRefPattern(self.resource) - def __findCRefPattern(self, xml): + def _findCRefPattern(self, xml): """ Find CRefPattern in the text and set object.citation :param xml: Xml Resource :type xml: lxml.etree._Element @@ -537,27 +546,27 @@ def __init__(self, reference, urn=None, citation=None, resource=None, text=None) super(CapitainsCtsPassage, self).__init__( citation=citation, resource=resource, - **_makePassageKwargs(urn, reference) + **_make_passage_kwargs(urn, reference) ) if urn is not None and urn.reference is not None: reference = urn.reference - self.__reference__ = reference - self.__text__ = text - self.__children__ = None - self.__depth__ = self.__depth_2__ = 1 + self._reference = reference + self._text = text + self._children = None + self._depth = self._depth_2 = 1 if self.reference and self.reference.start: - self.__depth_2__ = self.__depth__ = self.reference.start.depth + self._depth_2 = self._depth = self.reference.start.depth if self.reference.is_range() and self.reference.end: - self.__depth_2__ = self.reference.end.depth + self._depth_2 = self.reference.end.depth - self.__prevnext__ = None # For caching purpose + self._prev_next = None # For caching purpose @property def reference(self): """ CtsReference of the object """ - return self.__reference__ + return self._reference @property def childIds(self): @@ -566,14 +575,14 @@ def childIds(self): :rtype: None, CtsReference :returns: Dictionary of chidren, where key are subreferences """ - self.__raiseDepth__() + self._raise_depth() if self.depth >= len(self.citation.root): return [] - elif self.__children__ is not None: - return self.__children__ + elif self._children is not None: + return self._children else: - self.__children__ = self.getReffs() - return self.__children__ + self._children = self.getReffs() + return self._children @property def nextId(self): @@ -593,32 +602,32 @@ def prevId(self): """ return self.siblingsId[0] - def __raiseDepth__(self): + def _raise_depth(self): """ Simple check that raises an exception if the passage cannot run first, last, next or prev See object notes :raise: InvalidSiblingRequest """ - if self.__depth__ != self.__depth_2__: + if self._depth != self._depth_2: raise InvalidSiblingRequest() @property - def siblingsId(self): + def siblingsId(self) -> Tuple[CtsReference, CtsReference]: """ Siblings Identifiers of the passage :rtype: (str, str) """ - self.__raiseDepth__() + self._raise_depth() - if not self.__text__: + if not self._text: raise MissingAttribute("CapitainsCtsPassage was initiated without CtsTextMetadata object") - if self.__prevnext__: - return self.__prevnext__ + if self._prev_next: + return self._prev_next - document_references = self.__text__.getReffs(level=self.depth) + document_references = self._text.getReffs(level=self.depth) - if self.reference.end: + if self.reference.is_range(): start, end = self.reference.start, self.reference.end range_length = len(self.getReffs(level=0)) else: @@ -656,28 +665,28 @@ def siblingsId(self): else: _next = "{}-{}".format(document_references[end+1], document_references[end + range_length]) - self.__prevnext__ = (CtsReference(_prev), CtsReference(_next)) - return self.__prevnext__ + self._prev_next = (CtsReference(_prev), CtsReference(_next)) + return self._prev_next @property def next(self): """ Next CapitainsCtsPassage (Interactive CapitainsCtsPassage) """ if self.nextId is not None: - return _SharedMethods.getTextualNode(self.__text__, self.nextId) + return super(CapitainsCtsPassage, self).getTextualNode(subreference=self.nextId) @property def prev(self): """ Previous CapitainsCtsPassage (Interactive CapitainsCtsPassage) """ if self.prevId is not None: - return _SharedMethods.getTextualNode(self.__text__, self.prevId) + return super(CapitainsCtsPassage, self).getTextualNode(subreference=self.prevId) def getTextualNode(self, subreference=None, *args, **kwargs): if not isinstance(subreference, CtsReference): subreference = CtsReference(subreference) - X = _SharedMethods.getTextualNode(self, subreference) - X.__text__ = self.__text__ + X = super(CapitainsCtsPassage, self).getTextualNode(subreference=subreference) + X._text = self._text return X @property @@ -686,4 +695,4 @@ def textObject(self): :rtype: CapitainsCtsText """ - return self.__text__ + return self._text diff --git a/MyCapytain/resources/texts/remote/cts.py b/MyCapytain/resources/texts/remote/cts.py index e13dab01..2c95f0c5 100644 --- a/MyCapytain/resources/texts/remote/cts.py +++ b/MyCapytain/resources/texts/remote/cts.py @@ -46,7 +46,7 @@ def depth(self): def __init__(self, retriever=None, *args, **kwargs): super(_SharedMethod, self).__init__(*args, **kwargs) - self.__retriever__ = retriever + self._retriever = retriever self.__first__ = False self.__last__ = False if retriever is None: @@ -58,7 +58,7 @@ def retriever(self): :rtype: CitableTextServiceRetriever """ - return self.__retriever__ + return self._retriever def getValidReff(self, level=1, reference=None): """ Given a resource, CtsText will compute valid reffs diff --git a/MyCapytain/resources/texts/remote/dts/_resolver_v1.py b/MyCapytain/resources/texts/remote/dts/_resolver_v1.py index 89508041..e6da6edc 100644 --- a/MyCapytain/resources/texts/remote/dts/_resolver_v1.py +++ b/MyCapytain/resources/texts/remote/dts/_resolver_v1.py @@ -1,7 +1,32 @@ -from ....prototypes.text import InteractiveTextualNode +from typing import TYPE_CHECKING +from MyCapytain.resources.prototypes.text import InteractiveTextualNode +from MyCapytain.common.reference import BaseReference + +if TYPE_CHECKING: # Required for nice and beautiful type checking + from MyCapytain.resolvers.dts.api_v1 import HttpDtsResolver class DtsResolverText(InteractiveTextualNode): - def __init__(self, identifier, resolver, **kwargs): + def __init__(self, identifier: str, resolver: "HttpDtsResolver", resource: str, **kwargs): super(InteractiveTextualNode, self).__init__(identifier=identifier, **kwargs) - self.__childIds__ = None \ No newline at end of file + self._resolver = resolver + self._resource = resource + + @property + def resolver(self) -> "HttpDtsResolver": + return self._resolver + + def getTextualNode(self, subreference: BaseReference) -> "DtsResolverPassage": + return self.resolver.getTextualNode(textId=self.id, subreference=subreference) + + + + +class DtsResolverPassage(DtsResolverText): + def __init__(self, identifier: str, reference: BaseReference, resolver: "HttpDtsResolver", **kwargs): + super(DtsResolverPassage, self).__init__(identifier=identifier, resolver=resolver, **kwargs) + self._reference = reference + + @property + def reference(self) -> BaseReference: + return self._reference diff --git a/tests/resources/texts/local/commonTests.py b/tests/resources/texts/local/commonTests.py index 2663b426..5932e0f6 100644 --- a/tests/resources/texts/local/commonTests.py +++ b/tests/resources/texts/local/commonTests.py @@ -122,52 +122,53 @@ def testValidReffs(self): # Test with reference and level self.assertEqual( - str(self.TEI.getValidReff(reference=CtsReference("2.1"), level=3)[1]), - "2.1.2" + "2.1.2", + str(self.TEI.getValidReff(reference=CtsReference("2.1"), level=3)[1]) ) self.assertEqual( - str(self.TEI.getValidReff(reference=CtsReference("2.1"), level=3)[-1]), - "2.1.12" + "2.1.12", + str(self.TEI.getValidReff(reference=CtsReference("2.1"), level=3)[-1]) + ) self.assertEqual( - self.TEI.getValidReff(reference=CtsReference("2.38-2.39"), level=3), - (CtsReference("2.38.1"), CtsReference("2.38.2"), CtsReference("2.39.1"), CtsReference("2.39.2")) + (CtsReference("2.38.1"), CtsReference("2.38.2"), CtsReference("2.39.1"), CtsReference("2.39.2")), + self.TEI.getValidReff(reference=CtsReference("2.38-2.39"), level=3) ) # Test with reference and level autocorrected because too small self.assertEqual( - str(self.TEI.getValidReff(reference=CtsReference("2.1"), level=0)[-1]), "2.1.12", + str(self.TEI.getValidReff(reference=CtsReference("2.1"), level=0)[-1]), "Level should be autocorrected to len(citation) + 1" ) self.assertEqual( - str(self.TEI.getValidReff(reference=CtsReference("2.1"), level=2)[-1]), "2.1.12", + str(self.TEI.getValidReff(reference=CtsReference("2.1"), level=2)[-1]), "Level should be autocorrected to len(citation) + 1 even if level == len(citation)" ) self.assertEqual( - self.TEI.getValidReff(reference=CtsReference("2.1-2.2")), - CtsReferenceSet(CtsReference(ref) for ref in[ + CtsReferenceSet(*[CtsReference(ref) for ref in [ '2.1.1', '2.1.2', '2.1.3', '2.1.4', '2.1.5', '2.1.6', '2.1.7', '2.1.8', '2.1.9', '2.1.10', '2.1.11', '2.1.12', '2.2.1', '2.2.2', '2.2.3', '2.2.4', '2.2.5', '2.2.6' - ]), + ]], level=3, citation=[c for c in self.TEI.citation][-1]), + self.TEI.getValidReff(reference=CtsReference("2.1-2.2")), "It could be possible to ask for range reffs children") self.assertEqual( - self.TEI.getValidReff(reference=CtsReference("2.1-2.2"), level=2), CtsReferenceSet(CtsReference('2.1'), CtsReference('2.2')), + self.TEI.getValidReff(reference=CtsReference("2.1-2.2"), level=2), "It could be possible to ask for range References reference at the same level in between milestone") self.assertEqual( - self.TEI.getValidReff(reference=CtsReference("1.38-2.2"), level=2), CtsReferenceSet(CtsReference(ref) for ref in ['1.38', '1.39', '2.pr', '2.1', '2.2']), + self.TEI.getValidReff(reference=CtsReference("1.38-2.2"), level=2), "It could be possible to ask for range References reference at the same level in between milestone " "across higher levels") self.assertEqual( - self.TEI.getValidReff(reference=CtsReference("1.1.1-1.1.4"), level=3), CtsReferenceSet(CtsReference(ref) for ref in ['1.1.1', '1.1.2', '1.1.3', '1.1.4']), + self.TEI.getValidReff(reference=CtsReference("1.1.1-1.1.4"), level=3), "It could be possible to ask for range reffs in between at the same level cross higher level") # Test level too deep @@ -750,104 +751,105 @@ def test_errors(self): def test_prevnext_on_first_passage(self): passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("1.pr.1-1.2.1")) self.assertEqual( - str(passage.nextId), "1.2.2-1.5.2", + "1.2.2-1.5.2", str(passage.nextId), "Next reff should be the same length as sibling" ) self.assertEqual( - passage.prevId, None, + None, passage.prevId, "Prev reff should be none if we are on the first passage of the text" ) def test_prevnext_on_close_to_first_passage(self): passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("1.pr.10-1.2.1")) self.assertEqual( - str(passage.nextId), "1.2.2-1.4.1", + "1.2.2-1.4.1", str(passage.nextId), "Next reff should be the same length as sibling" ) self.assertEqual( - str(passage.prevId), "1.pr.1-1.pr.9", + "1.pr.1-1.pr.9", str(passage.prevId), "Prev reff should start at the beginning of the text, no matter the length of the reference" ) def test_prevnext_on_last_passage(self): passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("2.39.2-2.40.8")) self.assertEqual( - passage.nextId, None, + None, passage.nextId, "Next reff should be none if we are on the last passage of the text" ) self.assertEqual( - str(passage.prevId), "2.37.6-2.39.1", + "2.37.6-2.39.1", str(passage.prevId), "Prev reff should be the same length as sibling" ) def test_prevnext_on_close_to_last_passage(self): passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("2.39.2-2.40.5")) self.assertEqual( - str(passage.nextId), "2.40.6-2.40.8", + "2.40.6-2.40.8", str(passage.nextId), "Next reff should finish at the end of the text, no matter the length of the reference" ) self.assertEqual( - str(passage.prevId), "2.37.9-2.39.1", + "2.37.9-2.39.1", str(passage.prevId), "Prev reff should be the same length as sibling" ) def test_prevnext(self): passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("1.pr.5-1.pr.6")) self.assertEqual( - str(passage.nextId), "1.pr.7-1.pr.8", + "1.pr.7-1.pr.8", str(passage.nextId), "Next reff should be the same length as sibling" ) self.assertEqual( - str(passage.prevId), "1.pr.3-1.pr.4", + "1.pr.3-1.pr.4", str(passage.prevId), "Prev reff should be the same length as sibling" ) passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("1.pr.5")) self.assertEqual( - str(passage.nextId), "1.pr.6", + "1.pr.6", str(passage.nextId), "Next reff should be the same length as sibling" ) self.assertEqual( - str(passage.prevId), "1.pr.4", + "1.pr.4", str(passage.prevId), "Prev reff should be the same length as sibling" ) passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("1.pr")) self.assertEqual( - str(passage.nextId), "1.1", + "1.1", str(passage.nextId), "Next reff should be the same length as sibling" ) self.assertEqual( - passage.prevId, None, + None, passage.prevId, "Prev reff should be None when at the start" ) passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("2.40")) self.assertEqual( - str(passage.prevId), "2.39", + "2.39", str(passage.prevId), "Prev reff should be the same length as sibling" ) self.assertEqual( - passage.nextId, None, + None, passage.nextId, "Next reff should be None when at the start" ) def test_first_list(self): passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("2.39")) + print(list(passage.children), str(passage.firstId)) self.assertEqual( - str(passage.firstId), "2.39.1", + "2.39.1", str(passage.firstId), "First reff should be the first" ) self.assertEqual( - str(passage.lastId), "2.39.2", + "2.39.2", str(passage.lastId), "Last reff should be the last" ) passage = self.text.getTextualNode(MyCapytain.common.reference.CtsReference("2.39-2.40")) self.assertEqual( - str(passage.firstId), "2.39.1", + "2.39.1", str(passage.firstId), "First reff should be the first" ) self.assertEqual( - str(passage.lastId), "2.40.8", + "2.40.8", str(passage.lastId), "Last reff should be the last" ) From d1a05d07208649ffc16ffe5787bc37372c9d2e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Fri, 19 Oct 2018 18:31:27 +0200 Subject: [PATCH 64/89] (CTS)(Remote Texts) Moving from self.__attr__ to self._attr --- MyCapytain/resources/texts/remote/cts.py | 58 ++++++++++++------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/MyCapytain/resources/texts/remote/cts.py b/MyCapytain/resources/texts/remote/cts.py index 2c95f0c5..3abdd76b 100644 --- a/MyCapytain/resources/texts/remote/cts.py +++ b/MyCapytain/resources/texts/remote/cts.py @@ -47,8 +47,8 @@ def depth(self): def __init__(self, retriever=None, *args, **kwargs): super(_SharedMethod, self).__init__(*args, **kwargs) self._retriever = retriever - self.__first__ = False - self.__last__ = False + self._first = False + self._last = False if retriever is None: raise MissingAttribute("Object has not retriever") @@ -83,7 +83,7 @@ def getValidReff(self, level=1, reference=None): urn=urn ) xml = xmlparser(xml) - self.__parse_request__(xml.xpath("//ti:request", namespaces=XPATH_NAMESPACES)[0]) + self._parse_request(xml.xpath("//ti:request", namespaces=XPATH_NAMESPACES)[0]) return [ref.split(":")[-1] for ref in xml.xpath("//ti:reply//ti:urn/text()", namespaces=XPATH_NAMESPACES)] @@ -113,7 +113,7 @@ def getTextualNode(self, subreference=None): response = xmlparser(self.retriever.getPassage(urn=urn)) - self.__parse_request__(response.xpath("//ti:request", namespaces=XPATH_NAMESPACES)[0]) + self._parse_request(response.xpath("//ti:request", namespaces=XPATH_NAMESPACES)[0]) return CtsPassage(urn=urn, resource=response, retriever=self.retriever) def getReffs(self, level=1, subreference=None): @@ -148,11 +148,11 @@ def getPassagePlus(self, reference=None): response = xmlparser(self.retriever.getPassagePlus(urn=urn)) passage = CtsPassage(urn=urn, resource=response, retriever=self.retriever) - passage.__parse_request__(response.xpath("//ti:reply/ti:label", namespaces=XPATH_NAMESPACES)[0]) + passage._parse_request(response.xpath("//ti:reply/ti:label", namespaces=XPATH_NAMESPACES)[0]) self.citation = passage.citation return passage - def __parse_request__(self, xml): + def _parse_request(self, xml): """ Parse a request with metadata information :param xml: LXML Object @@ -195,7 +195,7 @@ def getLabel(self): self.retriever.getLabel(urn=str(self.urn)) ) - self.__parse_request__( + self._parse_request( response.xpath("//ti:reply/ti:label", namespaces=XPATH_NAMESPACES)[0] ) @@ -253,10 +253,10 @@ def firstId(self): :rtype: str :returns: First children of the graph. Shortcut to self.graph.children[0] """ - if self.__first__ is False: + if self._first is False: # Request the next urn - self.__first__ = self.getFirstUrn() - return self.__first__ + self._first = self.getFirstUrn() + return self._first @property def lastId(self): @@ -265,10 +265,10 @@ def lastId(self): :rtype: str :returns: First children of the graph. Shortcut to self.graph.children[0] """ - if self.__last__ is False: + if self._last is False: # Request the next urn - self.__last__ = self.childIds[-1] - return self.__last__ + self._last = self.childIds[-1] + return self._last @staticmethod def firstUrn(resource): @@ -395,12 +395,12 @@ def __init__(self, urn, resource, *args, **kwargs): self.urn = urn # Could be set during parsing - self.__nextId__ = False - self.__prev__ = False - self.__first__ = False - self.__last__ = False + self._next_id = False + self._prev_id = False + self._first_id = False + self._last = False - self.__parse__() + self._parse() @property def id(self): @@ -413,10 +413,10 @@ def prevId(self): :rtype: CtsPassage :returns: Previous passage at same level """ - if self.__prev__ is False: + if self._prev_id is False: # Request the next urn - self.__prev__, self.__nextId__ = self.getPrevNextUrn(reference=self.urn.reference) - return self.__prev__ + self._prev_id, self._next_id = self.getPrevNextUrn(reference=self.urn.reference) + return self._prev_id @property def parentId(self): @@ -434,10 +434,10 @@ def nextId(self): :rtype: CtsReference :returns: Following passage reference """ - if self.__nextId__ is False: + if self._next_id is False: # Request the next urn - self.__prev__, self.__nextId__ = self.getPrevNextUrn(reference=self.urn.reference) - return self.__nextId__ + self._prev_id, self._next_id = self.getPrevNextUrn(reference=self.urn.reference) + return self._next_id @property def siblingsId(self): @@ -446,11 +446,11 @@ def siblingsId(self): :rtype: CtsReference :returns: Following passage reference """ - if self.__nextId__ is False or self.__prev__ is False: - self.__prev__, self.__nextId__ = self.getPrevNextUrn(reference=self.urn.reference) - return self.__prev__, self.__nextId__ + if self._next_id is False or self._prev_id is False: + self._prev_id, self._next_id = self.getPrevNextUrn(reference=self.urn.reference) + return self._prev_id, self._next_id - def __parse__(self): + def _parse(self): """ Given self.resource, split information from the CTS API :return: None @@ -458,7 +458,7 @@ def __parse__(self): self.response = self.resource self.resource = self.resource.xpath("//ti:passage/tei:TEI", namespaces=XPATH_NAMESPACES)[0] - self.__prev__, self.__nextId__ = _SharedMethod.prevnext(self.response) + self._prev_id, self._next_id = _SharedMethod.prevnext(self.response) if not self.citation.is_set() and len(self.resource.xpath("//ti:citation", namespaces=XPATH_NAMESPACES)): self.citation = CtsCollection.XmlCtsCitation.ingest( From 423070826d9e3d098f67514a06e18bf3282a4282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Fri, 19 Oct 2018 19:27:06 +0200 Subject: [PATCH 65/89] (DTS)(Resolver : Document) Moving towards working document --- .gitignore | 1 + MyCapytain/common/utils/_http.py | 2 +- MyCapytain/resolvers/dts/api_v1.py | 14 ++++-- .../resources/texts/remote/dts/__init__.py | 1 + .../texts/remote/dts/_resolver_v1.py | 44 ++++++++++++++----- MyCapytain/retrievers/dts/__init__.py | 2 +- 6 files changed, 48 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 752350f4..2afe6b6b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +_test.py *.pkl # Compiled python modules. *.pyc diff --git a/MyCapytain/common/utils/_http.py b/MyCapytain/common/utils/_http.py index c4e731ca..68d212f4 100644 --- a/MyCapytain/common/utils/_http.py +++ b/MyCapytain/common/utils/_http.py @@ -42,4 +42,4 @@ def parse_uri(uri, endpoint_uri): return _Route( urljoin(endpoint_uri, temp_parse.path), parse_qs(temp_parse.query) - ) \ No newline at end of file + ) diff --git a/MyCapytain/resolvers/dts/api_v1.py b/MyCapytain/resolvers/dts/api_v1.py index 150fab98..cd9b097d 100644 --- a/MyCapytain/resolvers/dts/api_v1.py +++ b/MyCapytain/resolvers/dts/api_v1.py @@ -18,7 +18,7 @@ from MyCapytain.common.utils.dts import parse_metadata from MyCapytain.resources.collections.dts import HttpResolverDtsCollection from pyld.jsonld import expand -from MyCapytain.resources.texts.remote.dts._resolver_v1 import DtsResolverText, DtsResolverPassage +from MyCapytain.resources.texts.remote.dts import DtsResolverDocument __all__ = [ @@ -29,7 +29,7 @@ _re_page = re.compile("page=(\d+)") -def _parse_ref(ref_dict, default_type :str =None): +def _parse_ref(ref_dict, default_type: str=None): if "https://w3id.org/dts/api#ref" in ref_dict: refs = ref_dict["https://w3id.org/dts/api#ref"][0]["@value"], elif "https://w3id.org/dts/api#start" in ref_dict and \ @@ -148,7 +148,7 @@ def getTextualNode( subreference: Union[str, BaseReference]=None, prevnext: bool=False, metadata: bool=False - ) -> Union[DtsResolverText, DtsResolverPassage]: + ) -> DtsResolverDocument: """ Retrieve a text node from the API :param textId: CtsTextMetadata Identifier @@ -161,4 +161,10 @@ def getTextualNode( :type metadata: boolean :return: CapitainsCtsPassage :rtype: CapitainsCtsPassage - """ \ No newline at end of file + """ + return DtsResolverDocument.parse( + identifier=textId, + reference=subreference, + resolver=self, + response=self.endpoint.get_document(collection_id=textId, ref=subreference) + ) diff --git a/MyCapytain/resources/texts/remote/dts/__init__.py b/MyCapytain/resources/texts/remote/dts/__init__.py index e69de29b..d8ba9d13 100644 --- a/MyCapytain/resources/texts/remote/dts/__init__.py +++ b/MyCapytain/resources/texts/remote/dts/__init__.py @@ -0,0 +1 @@ +from ._resolver_v1 import DtsResolverDocument diff --git a/MyCapytain/resources/texts/remote/dts/_resolver_v1.py b/MyCapytain/resources/texts/remote/dts/_resolver_v1.py index e6da6edc..f96a1a8e 100644 --- a/MyCapytain/resources/texts/remote/dts/_resolver_v1.py +++ b/MyCapytain/resources/texts/remote/dts/_resolver_v1.py @@ -1,17 +1,33 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from MyCapytain.resources.prototypes.text import InteractiveTextualNode from MyCapytain.common.reference import BaseReference +from MyCapytain.common.utils.xml import xmlparser +from MyCapytain.common.utils import parse_pagination +from MyCapytain.resources.texts.base.tei import TeiResource if TYPE_CHECKING: # Required for nice and beautiful type checking + from requests import Response + from lxml.etree import ElementTree from MyCapytain.resolvers.dts.api_v1 import HttpDtsResolver -class DtsResolverText(InteractiveTextualNode): - def __init__(self, identifier: str, resolver: "HttpDtsResolver", resource: str, **kwargs): - super(InteractiveTextualNode, self).__init__(identifier=identifier, **kwargs) +class DtsResolverDocument(TeiResource): + def __init__( + self, + identifier: str, + resolver: "HttpDtsResolver", + resource: "ElementTree", + reference: Optional[BaseReference]=None, + **kwargs + ): + super(DtsResolverDocument, self).__init__(identifier=identifier, resource=resource, **kwargs) self._resolver = resolver + self._reference = reference self._resource = resource + self._parent = None + self._prev_id, self._next_id = None, None + @property def resolver(self) -> "HttpDtsResolver": return self._resolver @@ -19,14 +35,22 @@ def resolver(self) -> "HttpDtsResolver": def getTextualNode(self, subreference: BaseReference) -> "DtsResolverPassage": return self.resolver.getTextualNode(textId=self.id, subreference=subreference) + def getReffs(self, level: int=1, subreference: BaseReference=None): + return self.resolver.getReffs(textId=self.id, subreference=(subreference or self.reference)) + @classmethod + def parse(cls, identifier: str, reference: BaseReference, resolver: "HttpDtsResolver", response: "Response"): + o = cls( + identifier=identifier, + reference=reference, + resolver=resolver, + resource=xmlparser(response.text) + ) + nav = parse_pagination(response.headers) + print(nav) - -class DtsResolverPassage(DtsResolverText): - def __init__(self, identifier: str, reference: BaseReference, resolver: "HttpDtsResolver", **kwargs): - super(DtsResolverPassage, self).__init__(identifier=identifier, resolver=resolver, **kwargs) - self._reference = reference + return o @property def reference(self) -> BaseReference: - return self._reference + return self._reference \ No newline at end of file diff --git a/MyCapytain/retrievers/dts/__init__.py b/MyCapytain/retrievers/dts/__init__.py index 5ce98b44..19da97e9 100644 --- a/MyCapytain/retrievers/dts/__init__.py +++ b/MyCapytain/retrievers/dts/__init__.py @@ -171,7 +171,7 @@ def get_document( parameters["ref"] = ref return self.call( - "document", + "documents", parameters, mimetype=mimetype ) From f9098efe011b864691beffea2d65881135fa09c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Mon, 22 Oct 2018 16:24:33 +0200 Subject: [PATCH 66/89] (DTS Resolver)(Document) Completely implemented. Need to fix readableDescendants --- .gitignore | 1 + MyCapytain/common/reference/_dts_1.py | 2 +- .../resources/collections/dts/_resolver.py | 4 +- MyCapytain/resources/prototypes/metadata.py | 23 +- MyCapytain/resources/prototypes/text.py | 5 +- .../texts/remote/dts/_resolver_v1.py | 80 ++++++- MyCapytain/retrievers/dts/__init__.py | 23 +- .../example10.json => notebooks/.gitkeep | 0 tests/resolvers/dts/api_v1/base.py | 3 +- .../dts/api_v1/data/document/collection.json | 28 +++ .../dts/api_v1/data/document/example.xml | 9 + .../dts/api_v1/data/document/nav.json | 16 ++ .../data/document/sequence/passage_1.xml | 9 + .../data/document/sequence/passage_2.xml | 9 + .../data/document/sequence/passage_3.xml | 9 + tests/resolvers/dts/api_v1/test_document.py | 226 ++++++++++++++++++ tests/resolvers/dts/api_v1/test_metadata.py | 1 + 17 files changed, 402 insertions(+), 46 deletions(-) rename tests/resolvers/dts/api_v1/data/collection/example10.json => notebooks/.gitkeep (100%) create mode 100644 tests/resolvers/dts/api_v1/data/document/collection.json create mode 100644 tests/resolvers/dts/api_v1/data/document/example.xml create mode 100644 tests/resolvers/dts/api_v1/data/document/nav.json create mode 100644 tests/resolvers/dts/api_v1/data/document/sequence/passage_1.xml create mode 100644 tests/resolvers/dts/api_v1/data/document/sequence/passage_2.xml create mode 100644 tests/resolvers/dts/api_v1/data/document/sequence/passage_3.xml create mode 100644 tests/resolvers/dts/api_v1/test_document.py diff --git a/.gitignore b/.gitignore index 2afe6b6b..166b1c4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.ipynb_checkpoints/ _test.py *.pkl # Compiled python modules. diff --git a/MyCapytain/common/reference/_dts_1.py b/MyCapytain/common/reference/_dts_1.py index 0fa9b9fe..77214864 100644 --- a/MyCapytain/common/reference/_dts_1.py +++ b/MyCapytain/common/reference/_dts_1.py @@ -38,7 +38,7 @@ def __eq__(self, other): self.type == other.type def __repr__(self): - return " [{}]>".format( + return " [{}]>".format( "><".join([str(x) for x in self if x]), self.type ) diff --git a/MyCapytain/resources/collections/dts/_resolver.py b/MyCapytain/resources/collections/dts/_resolver.py index 3da96f5d..765a5309 100644 --- a/MyCapytain/resources/collections/dts/_resolver.py +++ b/MyCapytain/resources/collections/dts/_resolver.py @@ -156,11 +156,11 @@ def _parse_paginated_members(self, direction="children"): @property def children(self): - return super(HttpResolverDtsCollection, self).children + return self._children @property def parents(self): - return super(HttpResolverDtsCollection, self).parents + return self._parents def retrieve(self): if not self._metadata_parsed: diff --git a/MyCapytain/resources/prototypes/metadata.py b/MyCapytain/resources/prototypes/metadata.py index d4b46c6d..d9653beb 100644 --- a/MyCapytain/resources/prototypes/metadata.py +++ b/MyCapytain/resources/prototypes/metadata.py @@ -50,21 +50,14 @@ class Collection(Exportable): def __init__(self, identifier="", *args, **kwargs): super(Collection, self).__init__(identifier, *args, **kwargs) - self.__graph__ = get_graph() + self._graph = get_graph() - self.__node__ = URIRef(identifier) - self.__metadata__ = Metadata(node=self.asNode()) - self.__capabilities__ = Metadata.getOr(self.asNode(), RDF_NAMESPACES.DTS.capabilities) + self._node = URIRef(identifier) + self._metadata = Metadata(node=self.asNode()) self.graph.set((self.asNode(), RDF.type, self.TYPE_URI)) self.graph.set((self.asNode(), RDF_NAMESPACES.DTS.model, self.MODEL_URI)) - self.graph.addN( - [ - (self.asNode(), RDF_NAMESPACES.DTS.capabilities, self.capabilities.asNode(), self.graph) - ] - ) - self._parent = None self._children = {} @@ -113,22 +106,18 @@ def graph(self): :rtype: Graph """ - return self.__graph__ + return self._graph @property def metadata(self): - return self.__metadata__ - - @property - def capabilities(self): - return self.__capabilities__ + return self._metadata def asNode(self): """ Node representation of the collection in the graph :rtype: URIRef """ - return self.__node__ + return self._node @property def id(self): diff --git a/MyCapytain/resources/prototypes/text.py b/MyCapytain/resources/prototypes/text.py index b26e50b8..03fa4bc1 100644 --- a/MyCapytain/resources/prototypes/text.py +++ b/MyCapytain/resources/prototypes/text.py @@ -8,7 +8,7 @@ """ from typing import Union, List, Iterator -from rdflib.namespace import DC +from rdflib.namespace import DC, DCTERMS as DCT from rdflib import BNode, URIRef, Graph, Literal from rdflib.term import Identifier from MyCapytain.common.reference import Citation, NodeId, BaseReference, BaseReferenceSet @@ -110,7 +110,8 @@ def get_title(self, lang: str=None) -> Literal: :return: Title string representation :rtype: Literal """ - return self.metadata.get_single(key=DC.title, lang=lang) + return self.metadata.get_single(key=DC.title, lang=lang) or \ + self.metadata.get_single(key=DCT.title, lang=lang) def set_title(self, value: Union[Literal, Identifier, str], lang: str= None): """ Set the DC Title literal value diff --git a/MyCapytain/resources/texts/remote/dts/_resolver_v1.py b/MyCapytain/resources/texts/remote/dts/_resolver_v1.py index f96a1a8e..19c466f3 100644 --- a/MyCapytain/resources/texts/remote/dts/_resolver_v1.py +++ b/MyCapytain/resources/texts/remote/dts/_resolver_v1.py @@ -1,14 +1,17 @@ from typing import TYPE_CHECKING, Optional -from MyCapytain.resources.prototypes.text import InteractiveTextualNode -from MyCapytain.common.reference import BaseReference +import link_header +from urllib.parse import parse_qs, urlparse + +from MyCapytain.common.reference import BaseReference, DtsReference from MyCapytain.common.utils.xml import xmlparser -from MyCapytain.common.utils import parse_pagination from MyCapytain.resources.texts.base.tei import TeiResource + if TYPE_CHECKING: # Required for nice and beautiful type checking from requests import Response from lxml.etree import ElementTree from MyCapytain.resolvers.dts.api_v1 import HttpDtsResolver + from MyCapytain.resources.collections.dts import HttpResolverDtsCollection class DtsResolverDocument(TeiResource): @@ -17,7 +20,8 @@ def __init__( identifier: str, resolver: "HttpDtsResolver", resource: "ElementTree", - reference: Optional[BaseReference]=None, + reference: Optional[DtsReference]=None, + collection: Optional["HttpResolverDtsCollection"]=None, **kwargs ): super(DtsResolverDocument, self).__init__(identifier=identifier, resource=resource, **kwargs) @@ -27,30 +31,82 @@ def __init__( self._parent = None self._prev_id, self._next_id = None, None + self._parsed_metadata = False + self._collection = collection + self._depth = None + self._metadata = None + + @property + def collection(self): + if not self._collection: + self._collection = self.resolver.getMetadata(self.id) + return self._collection + + @property + def depth(self): + if self._depth is None: + self._depth = self.collection.citation.depth + return self._depth + + @property + def metadata(self): + if not self._metadata: + self._metadata = self.collection.metadata + return self._metadata @property def resolver(self) -> "HttpDtsResolver": return self._resolver - def getTextualNode(self, subreference: BaseReference) -> "DtsResolverPassage": - return self.resolver.getTextualNode(textId=self.id, subreference=subreference) + def getTextualNode(self, subreference: DtsReference) -> "DtsResolverPassage": + obj = self.resolver.getTextualNode(textId=self.id, subreference=subreference) + # We set up the collection with the current one : + # if it has been retrieved, it'll be used + obj._collection = self._collection + return obj - def getReffs(self, level: int=1, subreference: BaseReference=None): + def getReffs(self, level: int=1, subreference: DtsReference=None): return self.resolver.getReffs(textId=self.id, subreference=(subreference or self.reference)) + def _dict_to_ref(self, header_dic): + ref = header_dic.get("ref", [None])[0] + if ref: + return DtsReference(ref) + + s = header_dic.get("start", [None])[0] + e = header_dic.get("end", [None])[0] + if s and e: + return DtsReference(s, e) + @classmethod - def parse(cls, identifier: str, reference: BaseReference, resolver: "HttpDtsResolver", response: "Response"): + def parse(cls, identifier: str, reference: DtsReference, resolver: "HttpDtsResolver", response: "Response"): o = cls( identifier=identifier, reference=reference, resolver=resolver, resource=xmlparser(response.text) ) - nav = parse_pagination(response.headers) - print(nav) + + links = link_header.parse(response.headers.get("Link", "")) + links = { + link.rel: parse_qs(urlparse(link.href).query) + for link in links.links + } + if links.get("next"): + o._next_id = o._dict_to_ref(links.get("next")) + if links.get("prev"): + o._prev_id = o._dict_to_ref(links.get("prev")) + if links.get("parent"): + o._parent = o._dict_to_ref(links.get("up")) + if links.get("first"): + o._first_id = o._dict_to_ref(links.get("first")) + if links.get("parent"): + o._last_id = o._dict_to_ref(links.get("last")) + if links.get("collection"): + o._collection = o._dict_to_ref(links.get("collection")) return o @property - def reference(self) -> BaseReference: - return self._reference \ No newline at end of file + def reference(self) -> DtsReference: + return self._reference diff --git a/MyCapytain/retrievers/dts/__init__.py b/MyCapytain/retrievers/dts/__init__.py index 19da97e9..f3de2995 100644 --- a/MyCapytain/retrievers/dts/__init__.py +++ b/MyCapytain/retrievers/dts/__init__.py @@ -19,6 +19,16 @@ ] +def _parse_ref_parameters(parameters, ref): + if isinstance(ref, BaseReference): + if ref.is_range(): + parameters["start"], parameters["end"] = ref + else: + parameters["ref"] = ref.start + elif ref: + parameters["ref"] = ref + + class HttpDtsRetriever(MyCapytain.retrievers.prototypes.API): def __init__(self, endpoint): super(HttpDtsRetriever, self).__init__(endpoint) @@ -138,13 +148,7 @@ def get_navigation( "exclude": exclude, "page": page } - if isinstance(ref, BaseReference): - if ref.is_range(): - parameters["start"], parameters["end"] = ref - else: - parameters["ref"] = ref.start - elif ref: - parameters["ref"] = ref + _parse_ref_parameters(parameters, ref) return self.call( "navigation", @@ -165,10 +169,7 @@ def get_document( parameters = { "id": collection_id } - if isinstance(ref, tuple): - parameters["start"], parameters["end"] = ref - elif ref: - parameters["ref"] = ref + _parse_ref_parameters(parameters, ref) return self.call( "documents", diff --git a/tests/resolvers/dts/api_v1/data/collection/example10.json b/notebooks/.gitkeep similarity index 100% rename from tests/resolvers/dts/api_v1/data/collection/example10.json rename to notebooks/.gitkeep diff --git a/tests/resolvers/dts/api_v1/base.py b/tests/resolvers/dts/api_v1/base.py index b1a6a81b..ee9ee16f 100644 --- a/tests/resolvers/dts/api_v1/base.py +++ b/tests/resolvers/dts/api_v1/base.py @@ -39,7 +39,8 @@ def _load_mock(*files: str) -> str: data = fopen.read() return data -from MyCapytain.common.constants import set_graph, get_graph, bind_graph + +from MyCapytain.common.constants import set_graph, bind_graph def reset_graph(): diff --git a/tests/resolvers/dts/api_v1/data/document/collection.json b/tests/resolvers/dts/api_v1/data/document/collection.json new file mode 100644 index 00000000..f4aa1791 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/document/collection.json @@ -0,0 +1,28 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id" : "document_id", + "@type": "Resource", + "title" : "A title", + "description": "A description", + "totalItems": 0, + "dts:dublincore": { + "dc:title": [{"@language": "la", "@value": "Titulus"}], + "dc:description": [{ + "@language": "fr", + "@value": "Une description" + }], + "dc:dateCopyrighted": 2018, + "dc:creator": [ + {"@language": "en", "@value": "Anonymous"} + ], + "dc:contributor": ["Aemilius Baehrens"], + "dc:language": ["la", "en"] + }, + "dts:passage": "/api/dts/documents?id=document_id", + "dts:references": "/api/dts/navigation?id=document_id", + "dts:citeDepth": 1 +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/document/example.xml b/tests/resolvers/dts/api_v1/data/document/example.xml new file mode 100644 index 00000000..b15ebfb2 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/document/example.xml @@ -0,0 +1,9 @@ + + + +
+ Full Text +
+ +
+
\ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/document/nav.json b/tests/resolvers/dts/api_v1/data/document/nav.json new file mode 100644 index 00000000..4f0c4c55 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/document/nav.json @@ -0,0 +1,16 @@ +{ + "@context": { + "hydra": "https://www.w3.org/ns/hydra/core#", + "@vocab": "https://w3id.org/dts/api#" + }, + "@id":"/api/dts/navigation/?id=document_id", + "citeDepth" : 1, + "citeType": "psg", + "level": 1, + "hydra:member": [ + {"ref": "1"}, + {"ref": "2"}, + {"ref": "3"} + ], + "passage": "/dts/api/document/?id=document_id{&ref}{&start}{&end}" +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/document/sequence/passage_1.xml b/tests/resolvers/dts/api_v1/data/document/sequence/passage_1.xml new file mode 100644 index 00000000..bf6818b8 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/document/sequence/passage_1.xml @@ -0,0 +1,9 @@ + + + +
+ Passage 1 +
+ +
+
diff --git a/tests/resolvers/dts/api_v1/data/document/sequence/passage_2.xml b/tests/resolvers/dts/api_v1/data/document/sequence/passage_2.xml new file mode 100644 index 00000000..eb59ba2c --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/document/sequence/passage_2.xml @@ -0,0 +1,9 @@ + + + +
+ Passage 2 +
+ +
+
diff --git a/tests/resolvers/dts/api_v1/data/document/sequence/passage_3.xml b/tests/resolvers/dts/api_v1/data/document/sequence/passage_3.xml new file mode 100644 index 00000000..597066d4 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/document/sequence/passage_3.xml @@ -0,0 +1,9 @@ + + + +
+ Passage 3 +
+ +
+
diff --git a/tests/resolvers/dts/api_v1/test_document.py b/tests/resolvers/dts/api_v1/test_document.py new file mode 100644 index 00000000..6be3c828 --- /dev/null +++ b/tests/resolvers/dts/api_v1/test_document.py @@ -0,0 +1,226 @@ +from .base import * +from .base import _load_mock +from link_header import LinkHeader, Link + + +def make_links(root="http://foobar.com/api/dts", **kwargs): + header = LinkHeader( + [ + Link(root+"/"+val, rel=key) + for key, val in kwargs.items() + ] + ) + return { + "Link": str(header) + } + + +class TestHttpDtsResolverDocument(unittest.TestCase): + def setUp(self): + self.root_uri = "http://foobar.com/api/dts" + self.resolver = HttpDtsResolver(self.root_uri) + + @requests_mock.mock() + def test_retrieve_full(self, mock_set): + """ Grab a doc then check its metadata """ + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri + "/documents?id=document_id", + text=_load_mock("document", "example.xml"), + complete_qs=True, + headers=make_links(collection="/collections/?id=document_id") + ) + mock_set.get( + self.root_uri + "/collections?id=document_id", + text=_load_mock("document", "collection.json"), + complete_qs=True + ) + mock_set.get( + self.root_uri + "/navigation?groupby=1&level=1&id=document_id", + text=_load_mock("document", "nav.json"), + complete_qs=True + ) + doc = self.resolver.getTextualNode(textId="document_id") + self.assertIn( + "Full Text", doc.export(Mimetypes.PLAINTEXT), + "Text of the response should be exportable" + ) + self.assertEqual( + "document_id", doc.id, "Document ID should be set" + ) + self.assertEqual( + "Titulus", str(doc.get_title(lang="la")), "Titles are retrieved" + ) + self.assertEqual( + "Aemilius Baehrens", str(doc.metadata.get_single("http://purl.org/dc/terms/contributor")), + "Contributor can be retrieved" + ) + self.assertEqual(1, doc.depth, "Depth should be set up") + + self.assertEqual( + DtsReferenceSet( + DtsReference("1", type_="psg"), + DtsReference("2", type_="psg"), + DtsReference("3", type_="psg"), + level=1, + citation=DtsCitation(name="psg") + ), doc.childIds, "Children IDs should be retrieved" + ) + + @requests_mock.mock() + def test_retrieve_full_then_child(self, mock_set): + """ Grab a doc then grab subpassages """ + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri + "/documents?id=document_id", + text=_load_mock("document", "example.xml"), + complete_qs=True, + headers=make_links(collection=self.root_uri+"/collections?id=document_id") + ) + mock_set.get( + self.root_uri + "/collections?id=document_id", + text=_load_mock("document", "collection.json"), + complete_qs=True + ) + mock_set.get( + self.root_uri + "/navigation?groupby=1&level=1&id=document_id", + text=_load_mock("document", "nav.json"), + complete_qs=True + ) + # Adding child + mock_set.get( + self.root_uri + "/documents?id=document_id&ref=1", + text=_load_mock("document", "sequence/passage_1.xml"), + complete_qs=True, + headers=make_links( + collection=self.root_uri+"/collections?id=document_id", + up=self.root_uri+"/document?id=document_id", + next=self.root_uri+"/document?id=document_id&ref=2" + ) + ) + doc = self.resolver.getTextualNode(textId="document_id") + child = doc.getTextualNode(DtsReference("1")) + self.assertIn( + "Passage 1", child.export(Mimetypes.PLAINTEXT), + "Text of the response should be exportable" + ) + + @requests_mock.mock() + def test_retrieve_child_then_moving(self, mock_set): + """ Retrieve child passage then checkout others by navigation using link headers""" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri + "/documents?id=document_id", + text=_load_mock("document", "example.xml"), + complete_qs=True, + headers=make_links(collection=self.root_uri+"/collections?id=document_id") + ) + # Adding child + mock_set.get( + self.root_uri + "/documents?id=document_id&ref=1", + text=_load_mock("document", "sequence/passage_1.xml"), + complete_qs=True, + headers=make_links( + collection=self.root_uri+"/collections?id=document_id", + up=self.root_uri+"/document?id=document_id", + next=self.root_uri+"/document?id=document_id&ref=2" + ) + ) + # Adding child + mock_set.get( + self.root_uri + "/documents?id=document_id&ref=2", + text=_load_mock("document", "sequence/passage_2.xml"), + complete_qs=True, + headers=make_links( + collection=self.root_uri+"/collections?id=document_id", + up=self.root_uri+"/document?id=document_id", + prev=self.root_uri+"/document?id=document_id&ref=1", + next=self.root_uri+"/document?id=document_id&ref=3" + ) + ) + # Adding child + mock_set.get( + self.root_uri + "/documents?id=document_id&ref=3", + text=_load_mock("document", "sequence/passage_3.xml"), + complete_qs=True, + headers=make_links( + collection=self.root_uri+"/collections?id=document_id", + up=self.root_uri+"/document?id=document_id", + prev=self.root_uri+"/document?id=document_id&ref=2" + ) + ) + doc = self.resolver.getTextualNode(textId="document_id") + child = doc.getTextualNode(DtsReference("1")) + self.assertIn( + "Passage 1", child.export(Mimetypes.PLAINTEXT), + "Text of the response should be exportable" + ) + self.assertEqual( + DtsReference("2"), child.nextId, "Next passage is 2 !" + ) + self.assertIn( + "Passage 2", child.next.export(Mimetypes.PLAINTEXT), + "No specific queries need to be written for the following passage" + ) + self.assertIn( + "Passage 3", child.next.next.export(Mimetypes.PLAINTEXT), + "No specific queries need to be written for the following passage" + ) + + @requests_mock.mock() + def test_retrieve_child_start_end_then_moving(self, mock_set): + """ Retrieve child passage with range ID then checkout others by navigation using link headers""" + mock_set.get(self.root_uri, text=_load_mock("root.json")) + # Adding child + mock_set.get( + self.root_uri + "/documents?id=document_id&start=1&end=2", + text=_load_mock("document", "sequence/passage_1.xml"), + complete_qs=True, + headers=make_links( + collection=self.root_uri+"/collections?id=document_id", + up=self.root_uri+"/document?id=document_id", + next=self.root_uri+"/document?id=document_id&start=3&end=4" + ) + ) + # Adding child + mock_set.get( + self.root_uri + "/documents?id=document_id&start=3&end=4", + text=_load_mock("document", "sequence/passage_2.xml"), + complete_qs=True, + headers=make_links( + collection=self.root_uri+"/collections?id=document_id", + up=self.root_uri+"/document?id=document_id", + prev=self.root_uri+"/document?id=document_id&start=1&end=2", + next=self.root_uri+"/document?id=document_id&start=5&end=6" + ) + ) + # Adding child + mock_set.get( + self.root_uri + "/documents?id=document_id&start=5&end=6", + text=_load_mock("document", "sequence/passage_3.xml"), + complete_qs=True, + headers=make_links( + collection=self.root_uri+"/collections?id=document_id", + up=self.root_uri+"/document?id=document_id", + prev=self.root_uri+"/document?id=document_id&start=3&end=4" + ) + ) + child = self.resolver.getTextualNode(textId="document_id", subreference=DtsReference("1", "2")) + self.assertIn( + "Passage 1", child.export(Mimetypes.PLAINTEXT), + "Text of the response should be exportable" + ) + self.assertEqual( + DtsReference("3", "4"), child.nextId, "Next passage is 3 to 4 !" + ) + self.assertEqual( + DtsReference("1", "2"), child.reference, "Child reference is good" + ) + self.assertIn( + "Passage 2", child.next.export(Mimetypes.PLAINTEXT), + "No specific queries need to be written for the following passage" + ) + self.assertIn( + "Passage 3", child.next.next.export(Mimetypes.PLAINTEXT), + "No specific queries need to be written for the following passage" + ) diff --git a/tests/resolvers/dts/api_v1/test_metadata.py b/tests/resolvers/dts/api_v1/test_metadata.py index 116f83f1..3823a14f 100644 --- a/tests/resolvers/dts/api_v1/test_metadata.py +++ b/tests/resolvers/dts/api_v1/test_metadata.py @@ -5,6 +5,7 @@ class TestHttpDtsResolverCollection(unittest.TestCase): def setUp(self): + reset_graph() self.root_uri = "http://foobar.com/api/dts" self.resolver = HttpDtsResolver(self.root_uri) From 4ad13e0c9af2c8457b2b0ba497a2191c0759235e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 23 Oct 2018 11:30:11 +0200 Subject: [PATCH 67/89] (DTS Resolver) Working. Like completely. Yeah. It's good. --- MyCapytain/resources/collections/dts/_base.py | 5 +- .../resources/collections/dts/_resolver.py | 72 +- MyCapytain/resources/prototypes/metadata.py | 9 +- notebooks/Example DTS Remote Resolver.ipynb | 2097 +++++++++++++++++ .../collection/readableDescendants/coll1.json | 25 + .../readableDescendants/coll1_1.json | 19 + .../readableDescendants/coll1_1_1.json | 11 + .../readableDescendants/coll1_2.json | 25 + .../readableDescendants/coll1_2_1.json | 11 + .../readableDescendants/coll1_2_2.json | 19 + .../readableDescendants/coll1_2_2_1.json | 11 + tests/resolvers/dts/api_v1/test_metadata.py | 37 + .../collections/test_dts_collection.py | 10 +- 13 files changed, 2312 insertions(+), 39 deletions(-) create mode 100644 notebooks/Example DTS Remote Resolver.ipynb create mode 100644 tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1.json create mode 100644 tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_1.json create mode 100644 tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_1_1.json create mode 100644 tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_2.json create mode 100644 tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_2_1.json create mode 100644 tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_2_2.json create mode 100644 tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_2_2_1.json diff --git a/MyCapytain/resources/collections/dts/_base.py b/MyCapytain/resources/collections/dts/_base.py index 1a11bab7..91af7fea 100644 --- a/MyCapytain/resources/collections/dts/_base.py +++ b/MyCapytain/resources/collections/dts/_base.py @@ -41,7 +41,8 @@ def citation(self, citation: CitationSet): @property def size(self): - for value in self.metadata.get_single(RDF_NAMESPACES.HYDRA.totalItems): + value = self.metadata.get_single(RDF_NAMESPACES.HYDRA.totalItems) + if value: return int(value) return 0 @@ -130,7 +131,7 @@ def _parse_metadata(self, data: dict): self.citation.depth = data[_cite_depth_term][0]["@value"] for val_dict in data[str(_hyd.totalItems)]: - self.metadata.add(_hyd.totalItems, val_dict["@value"], 0) + self.metadata.add(_hyd.totalItems, val_dict["@value"]) for val_dict in data.get(str(_hyd.description), []): self.metadata.add(_hyd.description, val_dict["@value"], None) diff --git a/MyCapytain/resources/collections/dts/_resolver.py b/MyCapytain/resources/collections/dts/_resolver.py index 765a5309..a65d42d5 100644 --- a/MyCapytain/resources/collections/dts/_resolver.py +++ b/MyCapytain/resources/collections/dts/_resolver.py @@ -1,6 +1,7 @@ from MyCapytain.common.constants import RDF_NAMESPACES from pyld.jsonld import expand import re +from typing import Optional from ._base import DtsCollection @@ -36,11 +37,9 @@ def __getattr__(self, item): return self.set return self._run(item) - def _run(self, item=None): + def _run(self, item: Optional[str]=None): if not self._condition_lambda(): self._update_lambda() - # Replace the Proxied instance by the actualy proxied value - setattr(self._obj, self._attr, self._proxied) if item: return getattr(self._proxied, item) @@ -75,31 +74,30 @@ def __init__( metadata_parsed=True, *args, **kwargs): super(HttpResolverDtsCollection, self).__init__(identifier, *args, **kwargs) + self._parsed = { + "children": False, + "parents": False, + "metadata": metadata_parsed + } + self._last_page_parsed = { + "children": None, + "parents": None, + } + self._children = PaginatedProxy( self, "_children", - lambda: self._parse_paginated_members(direction="children"), - lambda: self._parsed["children"] + update_lambda=lambda: self._parse_paginated_members(direction="children"), + condition_lambda=lambda: self._parsed["parents"] ) self._parents = PaginatedProxy( self, "_parents", - lambda: self._parse_paginated_members(direction="parents"), - lambda: self._parsed["parents"] + update_lambda=lambda: self._parse_paginated_members(direction="parents"), + condition_lambda=lambda: self._parsed["parents"] ) self._resolver = resolver - self._metadata_parsed = metadata_parsed - - self._parsed = { - "children": False, - "parents": False, - "metadata": False - } - self._last_page_parsed = { - "children": None, - "parents": None, - } def _parse_paginated_members(self, direction="children"): """ Launch parsing of children @@ -151,19 +149,30 @@ def _parse_paginated_members(self, direction="children"): [0]["https://www.w3.org/ns/hydra/core#next"] [0]["@value"] )[0]) - else: - self._parsed[direction] = True + + self._parsed[direction] = True @property def children(self): + if not self._parsed["children"]: + if not self.size == 0: + return self._children + else: + self._parsed["children"] = True + if isinstance(self._children, PaginatedProxy): + + self._children = self._children._proxied + return self._children @property def parents(self): + if self._parsed["parents"] and isinstance(self._parents, PaginatedProxy): + self._parents = self._parents._proxied return self._parents def retrieve(self): - if not self._metadata_parsed: + if not self._parsed["metadata"]: query = self._resolver.endpoint.get_collection(self.id) data = query.json() if not len(data): @@ -190,14 +199,17 @@ def parse_member( members = [] # Start pagination check here - - for member in obj.get(str(_hyd.member), []): - subcollection = cls.parse(member, metadata_parsed=False, **additional_parameters) - if direction == "children": - subcollection._parents.set({collection}) - members.append(subcollection) - - if "https://www.w3.org/ns/hydra/core#view" not in obj: - collection._parsed[direction] = True + hydra_members = obj.get(str(_hyd.member), []) + + if hydra_members: + for member in hydra_members: + subcollection = cls.parse(member, metadata_parsed=False, **additional_parameters) + if direction == "children": + subcollection.parents.set({collection}) + members.append(subcollection) + + if "https://www.w3.org/ns/hydra/core#view" not in obj or \ + (direction == "children" and collection.size == 0): + collection._parsed[direction] = True return members diff --git a/MyCapytain/resources/prototypes/metadata.py b/MyCapytain/resources/prototypes/metadata.py index d9653beb..e5dde4b8 100644 --- a/MyCapytain/resources/prototypes/metadata.py +++ b/MyCapytain/resources/prototypes/metadata.py @@ -62,7 +62,7 @@ def __init__(self, identifier="", *args, **kwargs): self._children = {} def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, self.id) + return "%s(%s)#%s" % (self.__class__.__name__, self.id, id(self)) @property def version(self): @@ -213,7 +213,12 @@ def __getitem__(self, key): :param key: Key of the object to delete :return: Collection identified by the item """ - for obj in self.descendants + [self]: + if key == self.id: + return self + for obj in self.members: + if key == obj.id: + return obj + for obj in self.descendants: if obj.id == key: return obj raise UnknownCollection("%s is not part of this object" % key) diff --git a/notebooks/Example DTS Remote Resolver.ipynb b/notebooks/Example DTS Remote Resolver.ipynb new file mode 100644 index 00000000..a0b92111 --- /dev/null +++ b/notebooks/Example DTS Remote Resolver.ipynb @@ -0,0 +1,2097 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using a DTS Resolver\n", + "==================\n", + "\n", + "*This example is powered up thanks to the API available at Alpheios.net*\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Just importing the parent module, do not change this if you execute the code\n", + "# from the git repository\n", + "import os\n", + "import sys\n", + "nb_dir = os.path.split(os.getcwd())[0]\n", + "if nb_dir not in sys.path:\n", + " sys.path.append(nb_dir)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create the resolver\n", + "\n", + "With the following line we create the resolver :" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from MyCapytain.resolvers.dts.api_v1 import HttpDtsResolver\n", + "\n", + "resolver = HttpDtsResolver(\"http://texts.alpheios.net/api/dts\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Require metadata : let's visit the catalog\n", + "\n", + "The following code is gonna find each text that is readable by Alpheios" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "We found 34 collections that can be parsed\n" + ] + } + ], + "source": [ + "# We get the root collection\n", + "root = resolver.getMetadata()\n", + "# Then we retrieve dynamically all the readableDescendants : it browse automatically the API until\n", + "# it does not have seen any missing texts: be careful with this one on huge repositories\n", + "readable_collections = root.readableDescendants\n", + "print(\"We found %s collections that can be parsed\" % len(readable_collections))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Printing the full tree" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "default\n", + "-- urn:alpheios:arabicLit\n", + "---- urn:cts:arabicLit:perseus201003\n", + "------ urn:cts:arabicLit:perseus201003.perseus0004\n", + "-------- urn:cts:arabicLit:perseus201003.perseus0004.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201003.perseus0002\n", + "-------- urn:cts:arabicLit:perseus201003.perseus0002.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201003.perseus0003\n", + "-------- urn:cts:arabicLit:perseus201003.perseus0003.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201003.perseus0005\n", + "-------- urn:cts:arabicLit:perseus201003.perseus0005.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201003.perseus0001\n", + "-------- urn:cts:arabicLit:perseus201003.perseus0001.alpheios-text-ara1\n", + "---- urn:cts:arabicLit:perseus201001\n", + "------ urn:cts:arabicLit:perseus201001.perseus0004\n", + "-------- urn:cts:arabicLit:perseus201001.perseus0004.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201001.perseus0001\n", + "-------- urn:cts:arabicLit:perseus201001.perseus0001.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201001.perseus0002\n", + "-------- urn:cts:arabicLit:perseus201001.perseus0002.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201001.perseus0003\n", + "-------- urn:cts:arabicLit:perseus201001.perseus0003.alpheios-text-ara1\n", + "---- urn:cts:arabicLit:perseus201002\n", + "------ urn:cts:arabicLit:perseus201002.perseus0010\n", + "-------- urn:cts:arabicLit:perseus201002.perseus0010.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201002.perseus0002\n", + "-------- urn:cts:arabicLit:perseus201002.perseus0002.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201002.perseus0007\n", + "-------- urn:cts:arabicLit:perseus201002.perseus0007.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201002.perseus0006\n", + "-------- urn:cts:arabicLit:perseus201002.perseus0006.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201002.perseus0004\n", + "-------- urn:cts:arabicLit:perseus201002.perseus0004.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201002.perseus0008\n", + "-------- urn:cts:arabicLit:perseus201002.perseus0008.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201002.perseus0011\n", + "-------- urn:cts:arabicLit:perseus201002.perseus0011.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201002.perseus0001\n", + "-------- urn:cts:arabicLit:perseus201002.perseus0001.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201002.perseus0009\n", + "-------- urn:cts:arabicLit:perseus201002.perseus0009.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201002.perseus0005\n", + "-------- urn:cts:arabicLit:perseus201002.perseus0005.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201002.perseus0012\n", + "-------- urn:cts:arabicLit:perseus201002.perseus0012.alpheios-text-ara1\n", + "------ urn:cts:arabicLit:perseus201002.perseus0003\n", + "-------- urn:cts:arabicLit:perseus201002.perseus0003.alpheios-text-ara1\n", + "-- urn:alpheios:latinLit\n", + "---- urn:cts:latinLit:phi0959\n", + "------ urn:cts:latinLit:phi0959.phi006\n", + "-------- urn:cts:latinLit:phi0959.phi006.alpheios-text-lat1\n", + "-- urn:alpheios:greekLit\n", + "---- urn:cts:greekLit:tlg0085\n", + "------ urn:cts:greekLit:tlg0085.tlg006\n", + "-------- urn:cts:greekLit:tlg0085.tlg006.alpheios-text-grc1\n", + "------ urn:cts:greekLit:tlg0085.tlg003\n", + "-------- urn:cts:greekLit:tlg0085.tlg003.alpheios-text-grc1\n", + "------ urn:cts:greekLit:tlg0085.tlg004\n", + "-------- urn:cts:greekLit:tlg0085.tlg004.alpheios-text-grc1\n", + "------ urn:cts:greekLit:tlg0085.tlg005\n", + "-------- urn:cts:greekLit:tlg0085.tlg005.alpheios-text-grc1\n", + "------ urn:cts:greekLit:tlg0085.tlg001\n", + "-------- urn:cts:greekLit:tlg0085.tlg001.alpheios-text-grc1\n", + "------ urn:cts:greekLit:tlg0085.tlg002\n", + "-------- urn:cts:greekLit:tlg0085.tlg002.alpheios-text-grc1\n", + "---- urn:cts:greekLit:tlg0011\n", + "------ urn:cts:greekLit:tlg0011.tlg003\n", + "-------- urn:cts:greekLit:tlg0011.tlg003.alpheios-text-grc1\n", + "---- urn:cts:greekLit:tlg0020\n", + "------ urn:cts:greekLit:tlg0020.tlg002\n", + "-------- urn:cts:greekLit:tlg0020.tlg002.alpheios-text-grc1\n", + "------ urn:cts:greekLit:tlg0020.tlg001\n", + "-------- urn:cts:greekLit:tlg0020.tlg001.alpheios-text-grc1\n", + "------ urn:cts:greekLit:tlg0020.tlg003\n", + "-------- urn:cts:greekLit:tlg0020.tlg003.alpheios-text-grc1\n", + "---- urn:cts:greekLit:tlg0012\n", + "------ urn:cts:greekLit:tlg0012.tlg002\n", + "-------- urn:cts:greekLit:tlg0012.tlg002.alpheios-text-grc1\n", + "------ urn:cts:greekLit:tlg0012.tlg001\n", + "-------- urn:cts:greekLit:tlg0012.tlg001.alpheios-text-grc1\n" + ] + } + ], + "source": [ + "# Note that we could also see and make a tree of the catalog.\n", + "# If you are not familiar with recursivity, the next lines might be a bit complicated\n", + "def show_tree(collection, char_number=1):\n", + " for subcollection_id, subcollection in collection.children.items():\n", + " print(char_number*\"--\" + \" \" + subcollection.id)\n", + " show_tree(subcollection, char_number+1)\n", + "\n", + "print(root.id)\n", + "show_tree(root)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Printing details about a specific one" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Treaing `Arabic Reading Lessons` with id urn:cts:arabicLit:perseus201003.perseus0004.alpheios-text-ara1\n" + ] + } + ], + "source": [ + "# Let's get a random one !\n", + "from random import randint\n", + "# The index needs to be between 0 and the number of collections\n", + "rand_index = randint(0, len(readable_collections))\n", + "collection = readable_collections[rand_index]\n", + "\n", + "# Now let's print information ?\n", + "label = collection.get_label()\n", + "\n", + "text_id = collection.id\n", + "print(\"Treaing `\"+label+\"` with id \" + text_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### What about more detailed informations ? Like the citation scheme ?" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Maximum citation depth : 3\n", + "Citation System\n", + "-- book\n", + "---- section\n", + "------ page\n" + ] + } + ], + "source": [ + "def recursive_printing_citation_scheme(citation, char_number=1):\n", + " for subcitation in citation.children:\n", + " print(char_number*\"--\" + \" \" + subcitation.name)\n", + " recursive_printing_citation_scheme(subcitation, char_number+1)\n", + "\n", + "print(\"Maximum citation depth : \", collection.citation.depth)\n", + "print(\"Citation System\")\n", + "recursive_printing_citation_scheme(collection.citation)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Let's get some references !" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " [book]>, [book]>) level:1, citation:>\n" + ] + } + ], + "source": [ + "reffs = resolver.getReffs(collection.id)\n", + "print(reffs)\n", + "# Nice !" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Let's get some random passage !\n" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "urn:cts:arabicLit:perseus201003.perseus0004.alpheios-text-ara1 [book]>\n", + "SECTION I.\n", + " Miscellaneous Sentences.جُملاَت مُختَلِفَة\n", + "1 اَلدٌّنيَا دَارُ مَمَرٍ لاَ دَارُ مَقَرٍ * سُلطَان بِلاَ عَدلٍ كَنَهرٍ \n", + "بِلاَ مَاءٍ * عَالِم بِلاَ عَمَلٍ كَسَحَابٍ بِلاَ مَطَرٍ * غَنِي بِلاَ سَخَاوةٍ \n", + "كِشجِرٍ بِلاَ ثَمَرٍ * إمرأة بِلاَ حَيَاءٍ كَطَعَامٍ بِلاَ مِلحٍ * لاَ تَستَصغِر \n", + "عَدُوًّا وَإِن ضَعُفَ * قِلَّةُ الأكلِ يَمنَعُ كَثِيرًا مِن أَعلاَلِ الجِسمِ * \n", + "بِالعَمَلِ يُحَصَّلُ الثَّوَابُ لاَ بِالكَسَلِ * \n", + "2 مَنْ رَضيَ عَن نَفسِهِ كَثُرَ السَّاخِطُ عَلَيهِ * إِذَا كُنتَ \n", + "كَذُوبًا فَكُن ذَكُورًا * رَأسُ الُدِينِ المَعرِفَةُ * أَلسَّعِيدُ مَنْ وُعِظَ \n", + "بِغَيرِهِ * أَلصَّبرُ مِفتَاحُ الفَرَحِ * ألصِنَاعَةُ فِي الكَفِّ أَمَان مِنَ \n", + "الفَقْرِ * مَنْ تَسَمَّعَ سَمِعَ مَا يَكرَهُ * قَلبُ الأَحمَقِ فِي فِيهِ \n", + "وَلِسَانُ العَاقِلِ فِي قَلبِهِ * كُنْ قَنِعًا تَكُنْ غَنِيًّا كُن مُتوَكّلاً تَكُنْ\n", + "جُملاَت مُختَلِفَة\n", + "قَوِيًا * حُبٌّ الدٌّنيَا يُفسِدُ العَقلَ وَيُصِمٌّ القَلبَ عَن سَماعِ \n", + "الحِكْمَةِ *\n", + "3 شَرٌّ النَّوَالِ مَا تَقَدَّمَهُ المَطلُ وَتَعَقَّبَهُ المَنٌّ * شَرٌّ \n", + "النَّاسِ مَن يُعِينُ عَلَي المَظلُومِ وَيَنصُرُ الظَّلمَ * شَيآنِ لاَ يُعرَفُ \n", + "فَضلُهُمَا إلاَّ مِن فَقدِهِمَا الشَّبَابُ وَالعَافِيَةُ * الكَسَلُ وَكِثرَةُ \n", + "النَّومِ يُبعِدَانِ مِنَ اللهِ وَيْورِثَانِ الفَقرَ * لَيسَ مِن عَادَةِ \n", + "الكِرَام تَأخِيرُ الأنعَامِ – لَيسَ مِن عَادَةِ الأَشَرافِ تَعجيِلُ \n", + "الإِنتِقَامِ * الصَّدِيقُ الصَّدُوقُ مَن نَصَحَكَ فِي عَيبِكَ وَأَثَرَكَ \n", + "عَلَي نَفْسِهِ * الأَمَلُ كَالسَّرَابِ – يَغُرٌّ مَن رَأَهُ وَيُخلِفُ مَن \n", + "رَجَاهُ *\n", + "4 ثَلاَثَة يُمتَحَنُ بَهنَّ عَقلُ الرِجَالِ – المَالُ وَالوِلاَيَةُ \n", + "والمُصِيبَةُ * إيَّاكَ وَحُبَّ الدٌنيَا – فَإنَّهَا رَأسُ كُّلِ خَطِيَّةٍ \n", + "وَمَعدِنُ كُلّ بَلِيَّةٍ * الحَسَدُ دَآءُ عََيَاء – لاَ يَزُولُ إلاَّ بَهَلكِ الحَاسِدِ \n", + "أَو مَوتِ المَحسُودِ * زِد فِي إصطِنَاع المَعرُوفِ وَأَكثِر مِن \n", + "أَشِدَّآءِ الإحسَانِ – فَإِنَّهُ أَيقَنُ ذُخرٍ وَأَجمَلُ ذُكرٍ * سَل عَنِ \n", + "جُملاَت مُختَلِفَة\n", + "الرَّفِيقِ قَبلَ الطَّرِيقِ – سَل عَنِ الجَارِ قَبلَ الدَّارِ * جَالِس \n", + "أَهلَ العِلم وَالحِكمَةِ وَأَكثِر مُنَافَثَتَهُم – فَإنَّكَ إن كُنتَ جَاهِلاً \n", + "عَلَّمُوكَ وَإِن كُنتَ عَالِمًا إِزدَدْتَّ عِلمًا * \n", + "5 ذُو الشَّرَفِ لاَ تُبطِرُهُ مَنزِلَة نَالَهَا وَإِن عَظُمَت كَالجَبَلِ \n", + "الذَِّي لاَ تُزَعزِعُهُ الرِيّاَحُ – وَالدَّنِيٌّ تُبطِرُهُ أَدنَي مَنزِلةٍ كَالكَلاءِ\n", + "الذَّي يُحَرِكُهُ مَرٌّ النَّسِيم * خَمس يُستَقبِحُ فِي خَمسٍ – كِثرَةُ \n", + "الفُجُورِ في العُلَمَاء وَالحِرصُ في الحُكَمَاءِ والبُخلُ فِي الأغنِيآءِ \n", + "وَالقُبحَةُ في النِسَاءِ وَفِي المَشيَخُ الزِنَاءُ * قَالَ ابنُ المُعتَزّ – \n", + "أَهلُ الدُنياَ كَرُكَّابِ سَفِينَةٍ – يُسَارُ بِهِم وَهمُ نُيَّام * صُن إنمَانَكَ \n", + "مِنَ الشَّكِ – فَإِنَّ الشٌكَّ يُفسِدُ الإيمَانَ كَمَا يُفسِدُ المِلحُ \n", + "العَسَلَ *\n", + "6- طُوبَي لَمَن كَظَمَ غَيظَهُ وَلَمْ يُطلِقْهُ – وَعَصَي إِمرَةَ نَفسِهِ \n", + "وَلَم تُهلِكهُ * قَالَ المَسِيحُ بنُ مَريَمَ (عَلَيهِ السَّلاَمُ) – عَالَجتُ \n", + "الأكَمَهَ وَالأَبرَصَ فَأَبرأتُهُمَا – وَأَعيَانِي عِلاَجُ الأحمَقِ * قَالَ \n", + "ابنُ المُقَفَّعِ – إِذَا حَاجَجْتَ فَلاَ تَغضَب – فَإنَّ الغَضَبَ يَقطَعُ \n", + "جُملاَت مُختَلِفَة\n", + "عَنكَ الحُجَّةَ وَيُظهِرُ عَلَيكَ الخَصمَ * مَثَلُ الأَغنِياء البُخَلاَءِ \n", + "كَمَثَلِ البِغَالِ وَالحَمِيرِ – تَحمَلُ الذَّهَبَ وَالفِضَّةَ وَتعْتَلِفُ \n", + "بِالتِبْنِ وَالشَّعِيرِ * قَالَ أَبُو مُسلِمِ الخُرَاسَانيٌّ – خَاطَرَ مَن رَكِبَ \n", + "البَحرَ – وَأَشَدٌّ مِنهُ مُخَاطَرةً مَن دَاخَلَ المُلُوكَ * \n", + "7 مِثلُ الذَِّي يُعَلِمُ النَّاسَ الخَيرَ وَلاَ يَعمَلُ بَه كَمِثلِ \n", + "أَعمَي بِيَدِهِ سِرَاج – يَستَضِيءُ بِهِ غَيرَهُ وَهُوَ لاَ يَرَاهُ * أَضعَفُ \n", + "النَّاسِ مَن ضَعُفَ عَن كِتمَانِ سِرِهِ – وَأَقَواهُم مِن قَوِيَ عَلَي \n", + "غَضَبِهِ – وَأَصبَرُهُم مَن سَتَرَ فَاقَتَهُ – وَأَغنَاهُم مَن قَنَعَ بَمَا \n", + "تَيَسَّرَ لَهُ * قَالَ أَمِيرُ المُؤمِنيِنَ عَلِيٌّ بنُ أبِي طَالِبِ (كَرَّمَ \n", + "اللهُ وَجْهَهُ) مَنْ عُرِفَ بِالحِكْمَةِ لاَحَظَتهُ العُيُونُ بِالوَقَارِ * قَالَ \n", + "بَعضُ الحُكَمَاءِ – تَحتَاجُ القُلُوبُ إلَي أَقوَاتِهَا مِنَ الحِكمَةِ كَمَا \n", + "تَحتَاجُ الأَجسَامُ إلىَ أَقوَاتِهَا مِنَ الطَّعَامِ * \n", + "8 قَالَ آْفلاَطُونُ – حُبَّكَ لِلشَّيءِ سِتربَينَكَ وَبَيْنَ مَسِاوِيِهِ \n", + "وَبَغضُكَ لَهُ سِتر بَيْنَكَ وَبَيْنَ مَحَاسِنِهِ * مَن مَدَحَكَ بِمَا \n", + "لَيسَ فَيك مِنَ الجَميِلِ وَهُوَ رَاضٍ عَنكَ ذِمَّكَ بِمَا لَيسَ \n", + "جُمْلاَت مُختَلِفَة\n", + "فِيكَ مِنَ القَبِيح وَهُوَ سَاخِط عَليكَ * قَالَ أفلاَطُونُ الحَكِيمُ لاَ \n", + "تَطلُب سُرعَةَ العَمَلِ وَاطلُب تَجوِيدَهَ فَإنَّ النَّاسَ لاَ يَسألوُنَ \n", + "فِي كَم فَرَغَ وَإِ نَّمَا يَنظُرُونَ إلَي إِتقَانِهِ وَجُودَةِ صَنعَتِهِ * وُجِدَ \n", + "عَلَي صَنَم مَكتُوب حَرَام عَلَي النَّفس الخَبِيثَةِ أَن تَخرُجَ مِن \n", + "هذِهِ الدُنيَا حَتَى تُسيِء إلَي مَن أَحسَنَ إلَيهَا * \n", + "9 ثَلاَثَة لاَ يَنفَعُونَ مِن ثَلاَثةٍ شَريف مِن دَنِي وَبَار مِن \n", + "فَاجِرٍ وَحَكِيم مِن جَاهِلٍ * قَالَ عَامِرُ بنُ عَبدِ القَيسِ إذَا \n", + "خَرَجَتِ الكَلِمَةُ مَنَ القَلبِ دَخَلَتِ فِي القَلبِ – وَإذَا خَرَجَت\n", + "مَنَ اللِسَانِ لَم تَتَجَاوَز الآذانَ * قَالَ حَكِيم لِآخَرَ يَا أَخِي ! \n", + "كَيفَ أَصْبَحْتَ ؟ قَالَ أَصبَحتُ – وَبِنَا مِن نِعَم اللَّهِ مَا لاَ نُحصَيهُ \n", + "مَع كَثيِرٍ مَا نَعصِيهُ فَمَا نَدرِي أَيَّهُمَا نَشكُرُ جَمِيلاً مَا يَنشُرُ \n", + "أَو قَبِيحًا مَا يَستُرُ * إِجتَمَعَ حُكَمَاءُ العَرَبِ والعَجَمِ عَلَي أَرَبعِ \n", + "كَلِمَاتٍ – وَهِيَ – لاَ تُحَمِل نَفسَكَ مَا لاَ تُطِيقُ – وَلاَ تَعمَل\n", + "عَمَلاً لاَ يَنفَعُكَ – وَلاَ تَغَتَرَ بِإِمَرأَةٍ وإِن عَفَّت – وَلاَ تَثَق بِمَالٍ \n", + "وَإن كَثرَ * \n", + "جُملاَت مُختَلِفَة\n", + "10 أَلعَالِمُ عَرَفَ الجَاهِلَ لِأَنَّهُ كَانَ جَاهِلاً – وَالجَاهِلُ لاَ يَعرِفُ \n", + "العَالِمَ لأَنَّهُ مَا كَانَ عَالِمًا * لاَ تَحمِل عَلَي يَومِكَ هَمَّ سَنَتِكَ – \n", + "كَفَاكَ كُلٌّ يَومٍ مَا قُدِرَ لَكَ فِيهِ – فَإن تَكُن السَّنَةُ مِن عُمرِكَ \n", + "فَإِنَّ اللهَ سُبحَانَهُ سَيَأتِيكَ فِي كُلٌِّ غَدٍ جَدِيدٍ بِما قُسِمَ لَكَ – \n", + "فَإن لَم تَكُنْ مِنْ عُمرِكَ فَمَا هَمٌّكَ بِمَا لَيسَ لَكَ * فِي \n", + "كِتَابِ كَلِيلَة وَدِمنَهْ – إذَا أَحدَثَ لَكَ العَدْوٌّ صِدَاقَةً لِعِلَّةٍ \n", + "أَلجَأتَهُ إِلَيكَ فَمَعَ ذَهَابِ العِلَةِ رُجُوعُ العَدَاوَة – كَالمَآءِ تُسخِنُهُ \n", + "فَإِذَا أَمسَكتَ نَارًا عَنهُ عَادَ إلَي أَصلِهِ باردًا والشَّجَرَةُ المُرَّةُ \n", + "لَو طَلَيتَهَا بِالعَسَلِ لَم تُثمِر إلاَّ مُرًا* \n", + "11 يَوم وَاحِد لِلعَالِمِ أَخيَرُ مِنَ الحَيَاةِ كُلَِّهَا لِلجَاهِلِ * لاَ \n", + "تُخَاطِبِ الأَحمَقَ وَلاَ تُخَالِطُه فَإنَّهُ مَا يَستَحِي * قَالَ أَمِيرُ \n", + "المُؤمِنِنَ عَلِي (كَرَّمَ اللهُ وَجهَهُ) الأَدَبُ حَلي في الغَنَي – \n", + "كَنز عِندَ الحَاجَةِ – عَون عَلَي المُرُوَّةِ – صَاحِب فِي المَجلِس – \n", + "مُؤنِس فِي الوَحدَةِ تُعمَرُ بِهِ القُلُوبُ الوَاهِيَةُ – وَتُحيَا بِهِ الأَلبَابُ \n", + "المَيِتَةُ – وَتَنفُذُ بِهِ الأَبصَارُ الكَلِيلَةُ – وَيُدرِكُ بِهِ الطَّالِبُونَ \n", + "جُملاَت مُختَلفَة\n", + "مَا حَاوَلُونَ * قَالَ لُقمَانُ لإِبنِهِ يَا بُنَيَّ ! لِتَكُن أَوَّلُ شَيءٍ \n", + "تَكسِبُهُ بَعدَ الإئمَانِ خَليِلاً صَالِحًا – فَإنَّمَا مَثَلُ الخَلِيلِ \n", + "الصَّالِح كَمَثَلِ النَّخلَةِ – إن قَعَدتَ فِي ظِلِهَا أَظَلَّكَ – وَإِن \n", + "إِحتَطَبتَ مِن حَطَبِهَا حَطَبِهَا نَفَعَكَ – وَإن أَكَلتَ مِن ثَمرِهَا وَجَدتَهُ \n", + "طَيِبًا * \n", + "12 مَن تَرَكَ نَفسَهُ بِمَنزِلَةِ العَاقِلِ تَرَكَهُ اللَّهُ وَالنَّاسُ \n", + "بِمَنزِلَةِ الجَاهِلِ * مَن أَحَبَّ أَن يَقوَي عَلَي الحِكمَةِ فَلاَ \n", + "تَملِك نَفسَهُ النِسَاءُ * نَقلُ الشَّرِ عَن شُرُورِهِ أَيسَرُ مِن نَقلِ \n", + "المَخزونِ عَن حُزنِهِ * ثَلاَثَة لاَ يُعرَفُونَ إلاَّ في ثَلاَثَةِ مواضِعَ * \n", + "لاَ يُعرَفُ الشٌّجَاعُ إلاَّ عِندَ الحَربِ – وَلاَ يُعرَفُ الحَكِيمُ إلاَّ عِندَ \n", + "الغَضَبِ – وَلاَ يُعرَفُ الصَّدِيقُ إلاَّ عِندَ الحَاجَةِ إلَيهِ * قَالَ \n", + "رَسُولُ اللهِ (صَلَّي اللَّهُ عَلَيهِ وَسَلَّمَ) – ثَلاَث مُهلِكَات وَثَلاَث \n", + "مُنجِيَات – فَأمَّا المُهلِكَاتُ فَشُح مُطَاع وَهَوَي مُتَّبَع وَإِعجَابُ \n", + "المِرءِ بِنَفسِهِ – وَأَمَّا المُنجِيَاتُ فَخَشِيَّةُ اللهِ فِي السِّرِّ وَالعَلاَنِيَةِ \n", + "وَالقَصدُ فِي الغِنَي والفَقرِ والعَدلُ فِي الرِّضَا والغَضَبِ * \n", + "جُملاَت مُختَلفَة\n", + "أَلكَمَالُ فِي ثَلاَثَةِ أَشيَاءٍ – العِفَّةُ فِي الدِينِ – وَالصَّبرُ \n", + "عِندَ النَّوائِبِ – وَحُسنُ المَعِيشَةِ * أَلظَّالِمُ مَيِّت وَلَو كَانَ فِي \n", + "مَنَازِلِ الأَحيَاء – وَالمُحسِنُ حِيّ وَلُو كَانَ انتَقَلَ إلَي مَنَازِلِ \n", + "المَوتَا * كَمَا البَدَنُ إذَا هُوَ سَقِيم لَا يَنفَعُهُ الطَّعَامُ – كَذَا العَقْلُ \n", + "إذَا غَلَقَهُ حُبٌّ الدٌّنيَا لاَ تَنفَعُهُ المَوَاعِظُ * كُن عَلَي حِذرٍ مِنَ \n", + "الكَرِيمِ اذًا هَوَّنتَهُ – وَمِنَ الأَحمَقِ إذَا مَازَحْتَهُ – وَمِنَ العَاقِلِ \n", + "اذَا غَضًّبتَهُ – وَمِنَ الفَاجِرِ اذَا عَاشرتَهُ * بِسِتَّةِ خِصَالٍ يُعرَفُ \n", + "الأَحمَقُ – بِالغَضَبِ مِن غَيرِ شيءٍ – والكَلاَم فِي غيرِ نَفعٍ – \n", + "والثِقَةِ فِي كُلِّ أَحَدٍ – وَبَدَلِهِ بِغَيرِ مَوضَعِ البَدَلِ – وَسُؤَالِهِ عَن \n", + "مَا لاَ يُعنِيِه – وَبِأَنَهُ مَا يَعرِفُ صَدِيقَهُ مِن عَدُوِهِ * \n", + "14 لاَ يَنبَغِي لِلفَاضِلِ أَن يُخَاطِبَ ذَوِي النَّقضِ – كَمَا لاَ يَنبَغِي \n", + "لَلصَّاحِي أَن يُكَلِمَ السٌّكَاَري * لاَ يَنْبَغِي لِلعَاقِلِ أَن تَسكنُ \n", + "بَلَدًا لَيسَ فِيهِ خَمسَةُ أشياءٍ – سُلطَان حَازِم وَقَاضٍ عَادِل\n", + "وَطَبِيب عَالِم وَنَهر جَارٍ وَسُوق قَائِم * قِيلَ لِلأَحنَفِ بنِ قَيسٍ \n", + "مَا أََحلَمَكَ ! قَالَ لَستُ بِحَلِيمٍ وَلَكِنِي أَتَحَالَمُ – وَاللَّهِ إِنِي \n", + "جُملاَت مُختَلِفَة\n", + "لَأَسمَعُ الكَلِمَةَ فَأَحُمٌّ لَهَا ثَلاَثًا مَا يَمْنَعُنِي مِنَ الجَوَابِ عَنهَا \n", + "إلاَّ خَوف مِن أن أَسمَعُ شَرًّا مِنهَا * قِيلَ لِبَعضِ الحُكَمَاءِ \n", + "مَتَي يُحمَدُ الكِذبُ ؟ قَالَ إذَا جَمَعَ بَيْنَ مُتَقَاطِعِينَ – قِيلَ \n", + "فَمَتَي يُذَمٌّ الصِّدقُ؟ قَالَ إذَا كَانَ غِيبَةً – فَمَتَي يَكُونُ \n", + "الصَّمتُ خَيرًا مِنَ النٌّطقِ؟ قَالَ عِندَ المِرَاءِ * \n", + "Our Lord's Prayer\n", + "أَبَانَا الذَِّي فِي السَّمَوَاتِ * لِيَتَقَدَّسِ اسمُكَ * لِيَأتِ مَلَكُوتُكَ \n", + "لِتَكُن مَشِيَّتُكَ كَمَا فِي السَّمَاءِ كذلِكَ عَلَي الأَرضِ * أَعطِنَا \n", + "خُبزَنَا كِفَاةَ يَومِنَا * واغفِر لَنَا ذُنُوبَنَا كَمَا نَحنُ نَغفِرُ لِمَن \n", + "أخطَأَ إلَينَا * وَلاَ تُدخِلنَا فِي التَّجَارِبِ – لكِن نَجِنّاَ مِنَ الشِرِيرِ * \n", + "آمِينَ * \n", + "SECTION II.\n", + " Fables of Lulcman the Sage\n", + " أَمثاَلُ لُقمَانَ الحَكِيمِ \n", + "1- إنسَان وَأَسوَدَ \n", + "إنسَان مَرَّةً رَأَي رَجُلاً أسَود وَهوَ واقِف فِي الماء يستَحِم * \n", + "فَقَالَ لَهُ يَا أَخِي ! لاَ تُعَكِرِ النَّهرَ – فَإنَّكَ لاَ تَستَطِيعُ البَيَاضَ- \n", + "وَلاَ تَقْدِرُ عَلَيهِ أَبَدَ الدَّهرِ * هذَا مَعنَاهُ – أنَّ المَطبُوعَ لاَ \n", + "يَتَغَيَّرُ طَبعُهُ * \n", + "2- إنسَان وَحَيَّتَانِ \n", + "إنسَان مَرَّةً نَظَرَ حَيَّتَينِ تَقتَتِلاَنِ وَتَتنَاهَشَانِ – وَإَذ بِحَيَّةٍ \n", + "أَُخرَي قَد أَتَت فَأَصلَحَت بَينَهُمَا * فَقَالَ لَهَا الإنسَانُ – \n", + "لَولاَ أَنَّكِ أَشَرٌّ مِنهُمَا لَم تَدخُِلي بَينَهُمَا * هذَا مَعنَاهُ – \n", + "أَنَّ إنسَانَ السَّوء يَصِيرُ إلَي أَبنَاء جِنسِهِ * \n", + "أَمثَالُ لُقمَانَ الحَكِيمِ\n", + "3- ذِئب\n", + "ذِئب مَرَّةً اختَطفَ خِنوصًا صَغِيرًا – وَفِيمَا هُوَ ذَاهِب بِهِ \n", + "لَقِيَهُ الأَسَدُ – فَأّخَذَهُ مِنهُ * فَقَالَ الذِئبُ فِي نَفسِهِ – عَجَبتُ \n", + "أَنَّ شَيئًا اغتَصَبتُهُ كَيفَ لم يَيْبُتَ مَعِي * هذَا مَعنَاهُ – أَنَّ مَا \n", + "يُكسَبُ مِنَ الظُلمِ لاَ يُقِيمُ مَعَ صَاحِبِهِ – وَإن هُوَ أَقَامَ مَعَهُ \n", + "فَلاَ يَتَهَنَّأُ بِهِ * \n", + "4- قِطّ\n", + "قِطٌّ مَرّةً دَخَلَ إلَي دُكَّانِ حَدَّادٍ – فَأَصَابَ المِبرَدَ مُرمِبًا *\n", + "فَأَقبَلَ يَلحَسُهُ بِلِسَانِهِ – ولِسَانُهُ يَسِيلُ مِنهُ الدَّم – وَهوَ يَبلَعُهُ \n", + "وَيَظُنٌّ أَنَّهُ مِنَ المِبرَدِ إلَي أنِ انشَق لِسَانُهُ وَمَاتَ * هذَا \n", + "مَعنَاهُ – مَن يُنفِق مَالَهُ فِي غَيرِ الوَاجبِ ثُمَّ أَنَّهُ لاَ يَحسِبُ \n", + "حَتَّي يُفلِسَ وَهْوَ لاَ يَعلَمُ *\n", + "5- غَزَال\n", + "غَزَال مَرَّةً مَرِضَ – فَكَانَ أَصحَابُهُ مِنَ الوُحُوشِ يَأتُونَ إلَيهِ \n", + "وَيَعُودُونَهُ – وَيَرعَونَ مَا حَولَهُ مِنَ الحَشِيشِ وَالعُشبِ * فَلَمَّا\n", + "أَمثَالُ لٌُقمَانَ الحَكِيِم\n", + "أَفَاقَ مِن مَرَضِهِ التَمَسَ شَيْأً لِيَأكُلَهُ – فَلَم يَجِد فَهَلَكَ جَوعًا * \n", + "هذَا مَعنَاهُ – مَن كَثُرَ أَهلُهُ وإِخوَانُهُ كَيُرَت أَحزَانُهُ \n", + "6- كلاَب وَثَعلَب\n", + "كِلاَب مَرَّةً أصَابُوا جِلدَ سَبُعٍ * فَأقبَلُوا عَلَيهِ يَنهشُونَهُ \n", + "فَنَظَرَهُمُ الثَّعلَُ – فَقَالَ لَهُم – أَمَا لَو أنَه كَانَ حَيَّا – لَرَأَيتُم \n", + "مَخَالِيبَهُ أَحَدَّ مِن أَنيَابِكُم وَأَطوَلَ * هذَا مَعنَاهُ – أَلَّذِينَ \n", + "يَشْمَتُونَ بِقَومٍ أَجلاَّءٍ المِقدَارِ إِذَا هُم تَضَعضَعَت أَحوَالُهُم * \n", + "7- كَلب وَأَرنِب\n", + "كَلب مَرَّةً طُرَدَ أَرنَبًا * فَلَمَّا أَدرَكَهُ قَبَضَ عَلَيهِ وَأَقبَلَ يَعَضَّهُ \n", + "بِأَنيَابِهِ * فَإِذَا جَرَى الدَّمُ لَحِسَهُ بِلِسَانِهِ * فَقَالَ لَهُ الأَرنَبُ -\n", + "أَرَاكَ تَعَضٌّنِي كَأَنِّي عَدوٌّكَ – ثُمَّ تَبُوسُنِي كَأَنَّكَ صَدِيقِي * \n", + "هذَا مَعنَاهُ – مَن يَكُونُ فِي قَلبِهِ غَشّ وَدَغَل وَيُظهِرُ إشفَاقًا \n", + "وَمَحَبَّةً * \n", + "8- غَزَال وَأَسَد\n", + "غَزَال مَرَّةً مِن خَوْفِهِ مِن الصَّيَّادِينَ انْهَزَمَ إلَي مَغَارَةٍ * \n", + "أمثَالُ لُقمَانَ الحَكِيِم\n", + "فَدَخَلَ إلَيَهِ الأسَدُ فَافتَرَسَهُ * فَقَالَ فِي نَفسِهِ – الوَيلُ لِي أَنَا\n", + "الشَّقِيٌّ ! لإِنِي هَرَبتُ مِنَ النَّاسِ – فَوَقَعتُ فِي يَدِ مَن هُوَ أَشَدٌّ \n", + "مِنهُم بَأسًا * هذَا مَعنَاهُ – مَن يَفِرٌ مِن خَوفٍ يسِيرٍ فَيَقَعُ فِي \n", + "بَلاءٍ عظِيمٍ* \n", + "9- غَزَال مَرَّةً عَطِش – فَنَزَلَ إلَي جُبِ ماءٍ فشَرِبَ مِنهُ بِشَرَهٍ *\n", + "ثُمَّ رَامَ الطلُوعَ فَلَم يَقدِر * فَنَظَرهُ الثَّعلَبُ فَقَالَ لَهُ – يَا أَخِي – \n", + "قَد أسَأتَ فِي فَعلِكَ – إذْ لَمْ تُمَيِّز قَبلَ نُزُولِكَ كَيفَ تَطلُعُ \n", + "وَبَعدَ ذلِكَ نَزَلتَ * هذَا مَعنَاهُ – مَن يَنفَرِدُ بِرَأيِ نَفسِهِ بَغْيرِ \n", + "مَشوَرَةٍ *\n", + "10- أََرَانِبُ وَثَعَالِبُ\n", + "أَلنٌّسُورُ وَالأَرَانِبُ مَرَّةً وَقَعَ بَينَهُم حَرب * فَمَضَوا الأَرَانِبُ \n", + "إلَي الثَّعَالِبِ يَسُومُونَ مِنهُمُ الحِلفَ والمُعَاضَدةَ عَلَي النٌّسُورِ *\n", + "فَقَالُوا لَهُم – لَو لاَ عَرَفنَاكُم وَنَعلَمُ لِمَن تَحَارِبُونَ لَفَعَلنَا ذلِك *\n", + "أَمثَالُ لُقْمَانَ الحَكِيِم\n", + "هذَا مَعنَاهُ – أَنَّهُ مَا سَبِيلُ الإنسَانِ أَن يُحارِبَ لِمَنْ هُوَ \n", + "أَشِدٌّ بَأسًا مِنهُ * \n", + "11- أَرنَب وَلَبُؤَة\n", + "أَرنَب مَرَّةً عَبَرَ عَلَي لَبُؤَةٍ قَائِلَةً لَهَا – أَنَا أَنتُجُ فِي كُلِ سَنةٍ \n", + "أَولاَدًا كَثِيَرةً – وَأَنتِ إِنَّمَا تَلِدِينَ في كُلِ عُمرِكِ وَاحِدًا أَوِ \n", + "اثنَينِ * فَقَالت لَهَا اللَّبُؤَةُ – صَدَقْتِ غيرَ أنهُ وَإن كَانَ وَاحِدًا \n", + "فَهَو سَبُعَة * هذَا – أَنَّ وَلدًا وَاحٍدًا مُبَارَكًا خَير مِن \n", + "أَوْلاَدٍ كَثيِرةٍ عَاجِزِينَ * \n", + "12- إمرَأَة وَدَجاجَة\n", + "إِمَرأَة كَانَ لَهَا دَجَاجَة – تَبِيضُ فِي كُلَّ يَومٍ بَيضَةَ فِضَّةٍ * \n", + "فَقَالَتِ الإمرَأَةُ فِي نفسِهَا – إن أَنَا كَثَّرتُ عَلَفَهَا تَبِيضُ فِي \n", + "كُلَّ يَومٍ بَيْضَتَينِ * فَلَمَّا كَثَّرَت عَلَفَهَا انشَقَّت حَوصَلَتُهَا \n", + "فَمَاتَت * هذَا مَعناَهُ – أَنَّ نَاسًا كَثِيرًا بِسَبَبِ رِبحٍ يَسِيرٍ \n", + "يُهلِكُونَ رُؤوسَ أَمَوالِهِم * \n", + "أَمثَالُ لُقمَانَ الحَكِيمِ \n", + "13- بُستَانِي \n", + "بُستَانِيٌ يَومًا كَانَ يَسقِي البَقْلَ * فَقِيلَ لَهُ لَمَاذَا البَقْلُ * فَقِبلَ لَهُ لِمَاذَا البَقْلُ\n", + "البَرِّيٌّ بَهِيٌّ المَنْظَرِ وَهْوَ غَيْرُ مَخْدُومٍ – وَهذَا الجَوِّيٌّ سَرِيعُ \n", + "الذٌّبُولِ وَالعطَطَب؟ قَالَ البُستَانِيٌّ لِأَنَّ البَرِيَّ تُرَبِيهِ أُمٌّهُ وَهذَا \n", + "تُرَبِيهِ امرَأَةُ أَبِيهِ * هذَا مَعناَهُ – أَنَّ تَربِيَةَ الأَّمِ لِأَوّلاَدِ أَفضَلُ \n", + "مِنْ تَربِيِةِ امرأَةِ الأَبِ \n", + "14- إنسَان وَصَنَم\n", + "إنسَان كَانَ لَهُ صَنَم فِي بَيتِهِ يَعْبُدُهُ – وَكَانَ يّذبَحُ لَهُ فِي \n", + "كُلِّ يَومٍ ذَبِيَحةً * فَأَفْنَي جَمِيعَ مَا يَمْلِكُهُ عَلَي ذلِكَ الصَّنَمِ *\n", + "فَشَخَصَ لَهُ قَائلاً – لاَ تلُفنِ مَا لَكَ عَلَيَّ ثُمَّ تَلُومُنِي فِي الآخِرَةِ * \n", + "هذَا مَعنَاهُ – مَن يُنْفِقُ مَالَهُ فِي الخَطِئَةِ ثُمَّ يَحْتَجٌّ أَنَّ اللهَ \n", + "أَفْقَرَهُ * \n", + "15- صَبِيٌ وََعَقْرَب\n", + "صَبِي مَرَّةً كَانَ يَصِيدُ الجَرَادَ * فَنَظَرَ عَقرَبًا – فَظَنَّ أَنَّهَا \n", + "جَرَادَة كَبيِرَة * فَمَدَّ يَدَهُ لِيَأخُذَهَا – ثُمَّ تَبَاعَدَ عَنهَا * فَقَالَتْ \n", + "أمثَالُ لُقمَانَ الحَكِيمِ\n", + "لَهُ – أَمَّا لَو أَنَّكَ قَبَضْتَنِي فِي يَدِكَ – لَتَخَلَّيتَ عَن ضَيدِ الجَرَادِ \n", + "هذَا مَعنَاهُ – أَنَّ سَبِيلَ الإنسَانِ أَن يُمَيِزَ الخَيَر مِن الشَّرِ – \n", + "وَيُدَبِرَ لِكُلّ شَيءٍ تَدبِيرًا عَلَي حَدٌِهِ * \n", + "16- دِيكَانِ\n", + "دِيْكَانِ مَرَّةً اقتَتَلاَ فِي قَاذُورةٍ – فَفَّر أَحَدُهُمَا وَمَضَي وَآختَفَي \n", + "مِن وَقتِهِ فِي بَعضِ الأَمَاكِنِ * فَأَمَّا الدَِّيكُ الَّذِي غَلَبَ صَعِدَ \n", + "فَوْقَ سَطحٍ عالٍ – وَجَعلَ يَصفِقُ بِجَنَاحَيهِ وَيَصِيحُ وَيَفْتَخِرُ*\n", + "فَنَظَرَهُ بَعضُ الجَوَارِحِ – فَاْنقَضَّ عَلَيهِ وَآْخْتَطَفَهُ لِوَقْتِهِ * هذَا \n", + "مَعْنَاهُ – أَنْ لاَ يَجْوزُ للإِنسَانِ أَنْ يَفْتَخِرَ بِقُوَّتِهِ وَشِدَّةِ بَأسِهِ\n", + "17- أَلْوَزُّ وَالْخُطَّافُ\n", + "أَلْوَزُّ وَالْخُطَّافُ آْشْتَرَكا فِي الْمَعِيشَةِ – فَكَانَ مَرْعَي الجَمِيعِ \n", + "فِي مَكَانٍ وَاحِدٍ * وَلَمَّا كَاَن ذَاتَ يَومٍ أَتَوْهُمَا الصَّيَّادُونَ * \n", + "فَأَمَّا الخُطَّافُ فَلِأَجلِ خِفَّتِهِ طَارَ جَمِيعُهُ وَسَلِمَ – وَأَمَّا الوَزٌّ \n", + "فَأَدْرَكُوهُ الصَّيَّادُونَ فَذَبَحُوهُ * هذَا مَعنَاهُ – مَنْ يُعَاشِرُ مَن لاَ\n", + "يُشَاكِلُهُ وَلَيسَ هُوَ ابنُ جِنْسِهِ *\n", + "أَمْثَالُ لُقْمَانَ الحَكِيِم\n", + "18- النِمسُ والدَّجاجُ \n", + "بَلَغَ النِمسَ أَنَّ الدَّجَاجَ مَرضَي * فَقَامَ النِمْسُ فَلَبِسَ \n", + "جِلدَ طَاوسٍ وأَتَي يَزُورُهُنَّ * فَقَالَ لَهُنَّ – السَّلاَمُ عَلَيكُنَّ أَيّهَا \n", + "الدَّجَاجُ – كيفَ أَنتُنَّ وَكَيفَ حالُكُنَّ؛ فَقَالَ لَهُ الدَّجَاجُ – \n", + "مَا نَحنُ إلاَّ بخيرٍ يَومَ لاَ نَرَي وَجهَكَ * هَا معنَاهُ – مَن يُظهِرُ \n", + "المَحَبَّةَ رِيَاءً وَفِي قَلْبَهِ الدَّغَلُ وَالبُغْضُ * \n", + "19- كَلب وَذِئب\n", + "كَلب مََرَّةً كَانَ يَطرَدُ ذِئبًا – وَيَفْتَخِرُ بِقُوَّتِهِ وَخِفَّةِ جَريِهِ \n", + "وَانهِزَامِ الّذِئبِ بَينَ يَدَيْهِ * فَالتَفَتَ إلَيهِ الّذِئبُ قَائلاً لهُ – \n", + "لاَ تَظُنَّ أنَّ خَوفِي مِنكَ – وَإنَمَا خَوفِي مِمَّن هَوَ مَعَكَ \n", + "يَطرَدُنِي * هذَا مَعنَاهُ – أنَّهُ لاَ يفتَخِرُ الإنسَانُ إلاَ بِمَا هُوَ لَهُ – \n", + "وَلاَ يَكُونُ افتِخَارُهُ مِمَا ليسَ لَهُ * \n", + "20 بَعُوضَة وَثَور\n", + "بَعُوضَة (يَعنِي نَامُوسَة) وَقَفَت عَلَي قَرنِ ثَورٍ – وَظَنَّنت أَنَّهَا \n", + "قَد ثَقُلَت عَلَيهِ * فَقَالت لَهُ – إن كُنتُ قَد ثَقُلتُ عَلَيكَ \n", + "أَمثَالُ لُقمَانَ الحَكِيمِ\n", + "فَأَعلِمنِي حَتَّي أَطِيرَ عَنكَ * فَقَالَ الثَّورُ – يا هذِهِ ! مَا حَسِستُ \n", + "بِكِ فِي وَقتِ نُزُولِكِ – ولاَ إذَا أَنتِ طِرتِ أَعلَمُ بِكِ * هذَا \n", + "مَعنَاهُ – مَن يَطلُبُ أن يَجْعَلَ لَهُ ذِكرًا وَمَجدًا وَهوَ ضَعِيف\n", + "حَقِير * \n", + "21- ذِئَاب\n", + "ذِئَاب أصَابُوا جلُودَ بَقَرٍ فِي جَورَةِ مَآءٍ تُبَلٌّ – وَلَيْسَ عِنْدَهَا \n", + "أَحَد * فَاتَّفَقُوا كُلٌّهُم جَمِيعًا عَلَي أَنَّهْم يَشْرِبُونَ المَِاءَ كُلَّهُ حَتَّي \n", + "يَصِلُوا للجُلُودِ ويَأكُلُوهَا * فَمِن كِثرَةِ مَا شَرِبُوا مِنَ المَآءِ انفَلَقُوا \n", + "كُلَّهُم ومَاَتُوا وَلَمْ يَصِلُوا إلَي الجُلُودِ * هذَا مَعنَاهُ – مَن هُوَ قَلِيلُ \n", + "الرَّأيِ ويَعَمَلُ عَمَلاً كَمَا لاَ يَجِبُ عَمَلُهُ \n", + "22- صَبِيّ\n", + "صَبِيّ مَرَّةً رَمَي نَفْسَهُ فِي نَهرِ مَاءٍ – وَلَم يَكُن يَعْرِفُ \n", + "يَسْبَحُ – فَأشْرَفَ عَلَي الغَرْقِ * فَاْسّتَعَانَ بِرجُلٍ عَابِرٍ فِي الطَّرِيقِ * \n", + "فَأَقْبَلَ إِلَيْهِ وَجَعَلَ يُلَوِمُهُ عَلَ نُزُولِهِ إلَي النَّهْرِ * فَقَالَ لَهُ \n", + "الصَّبِيٌ – يَا هذَا خَلِصْنِي أَوَّلاً مِنَ المَوتِ وَبَعدَ ذَلِكَ لَوِمنِي *\n", + "أَمْثَالُ لُقْمَاَن الحَكِيمِ\n", + "هذَا مَعنَاهُ – أَنَّهُ لاَ يَجِبُ أَن يُلاَمَ الإنسَانُ عِندَ وُقُوعِهِ فِي شِدَّةٍ في غَيرِ مَوضِعِ اللَّومِ * \n", + "شِدَّةٍ فِي غَيرِ مَوضِعِ اللَّومِ *\n", + "23 أَسَد وَثَورَان\n", + "أَسَد مَرَّةً خَرَجَ عَلَي ثَوْرَينِ – فَآجْتَمَعَا جَمِيعًا وَكَانَا \n", + "يَنْطَحَانَهِ ب~قُرُونِهشمَا – وَلاَ يُمَكِنَاه مِنَ الدٌّخُولِ بَينهُمَا * فَانفَرَدَ \n", + "بأَحَدِهِمَا وَخَدَعَهُ وَعََدَهُ بِأَنْ لاَ يُعَارِضَهُمَا إن تَخَلَّي عَن \n", + "صَاحِبِهِ * فَلَمَّا آْفتَرَقَا افْتَرسَهُمَا جَمِيعًا * هذَا مَعنَاهُ – أَنَّ\n", + "مَدِينَتَينِ إِذَا آتَّفَقَ رَأيُ أَهْلِهِمَا فَإِنَّهُ لاَ يُمْكَنُ مِنْهُمَا عَدُوٌ – \n", + "فَإَِذا افْتَرَقَاَ هَلِكَا جَمِيعًا * \n", + "24- أَسَد وَثَعْلَب\n", + "أَسَد مَرَّةً اشتَدَّ عَلَيِه حَرٌّ الشَّمْسِ * فَدَخَلَ إلَي بَعضِ \n", + "المَغَايُرِ يَتَظَلَّلُ فِيهَا * فَلَّما رَبَضَ أَتَي إلَيهِ جُرَذ يَمشِي فَوقَ \n", + "ظَهرِهِ * فَوَثَبَ قَائِمًا فَنَظَرَ يَمِينًا وَيَسَارًا وَهُوَ خَائِف مِرعُوب * \n", + "فَنَظَرَهُ الثَعلَبُ فَتَضَحَّكَ عَلَيهِ * فَقَالَ لَهُ الأَسَدُ – لَيسَ مِنَ \n", + "أَمْثَالُ لُقْمَاَن الحَكِيمِ\n", + "آلْجُرَذِ خَوْفِي وَإِنَّمَا كَبُرَ عَلَيَّ آْحْتِقَارُهُ لِي * هذَا مَعنَاهُ – أَنْ \n", + "آلهَوَانَ عَلَي الْعَاقِلِ أَشَدٌّ مِنَ الْمَوْتِ * \n", + "25- حَمَامَة\n", + "حَمَامَة مَرَّةً عَطِشَت – فَأَقْبَلَت تَحُومُ فِي طَلَبِ المَاءِ *\n", + "فَنَظَرتْ عَلَي حَائِطٍ صُورَةَ صَحفَةٍ مَملُؤَةٍ مَآءً * فَطَارَتَ بُسرعَةٍ \n", + "وَضَرَبَت نَفْسَهَا إِلَي تِلْكَ الصٌّوَرةِ – فَآنْشَقَّتْ – حَوْصَلَتُهَا * فَقَالَتِ \n", + "الوَيْلُ لِي – أَنْا الشَّقِيَّةُ ! لِأَنّيِ أَسْرَعتُ فِي طَلَبِ المَآءِ وَأَهْلَكْتُ \n", + "رُوحِي * هَا مَعنَاهُ – أَنَّ التَأخِيرَ وَالتَأَنِي عَلَي آلأَشْيَاء أَخْيَرُ \n", + "مِنَ المُبَادَرَةِ وَالمُسَارَعَةِ إلَيهَا *\n", + "29- سُلَحْفَاة وَأَرْنَب\n", + "سُلَحْفَاة وَأَرْنَب مَرَّةً تَسَابَقَا – وَجَعَلاَ الحَدَّ بَيْنَهُمَا الجَبَلَ \n", + "الطَرِيقِ وَنَامَ * وَأَمَا السٌّلَحفَاةُ فَلِعِلْمِهَا بِثِقَل طَبِيعَتِهَا لَم \n", + "تَكُنْ تِسُتَقِرٌّ وَلاَ تَتَوأَنَي فِي الْجَرْيِ * فَوَصَلَتْ الَي الجَبَلِ عِنْدَ \n", + "أَمْثَالُ لُقْمَاَن الحَكِيمِ\n", + "اْسْتِقَاظِ الأَرْنَبِ مِنْ نَوْمِهِ * هذَا مَعْنَاهُ – أَنَّ طُولَ الٌّروحِ \n", + "وَالْمُدَاوَمَةَ خَيْر مِنَ الْخِفَّةِ والْعَجَلَةِ * \n", + "27- حَدَّاد وَكَلْب\n", + "حَدَّاد مَرَّةً كَاَن َلَهُ كَلْب – وَكَانَ لاَ يَزَالُ نَائِمًا مَا دَامَ \n", + "الحَدَّادُ يَعْمَلُ شُغلاً * فَإذَا رَفَعَ الْعَمَلَ وَجَلَسَ هُوَ وَأَصحَابُهُ \n", + "لِيَأْكُلُوا خُبْزًا اسْتَيْقَظَ آلكَلْبُ * فَقَالَ الحَدَّادُ – يَا كَلْبَ السٌوءِ \n", + "لِأَيّ سَبَبٍ صَوتُ الْمِرْزَبَاتِ الَّذِي يُزَعْزِعُ الأَرضَ لَا يُيَقّظُكَ \n", + "وَصَوْتُ الْمَضْغِ الْخَفِيُّ إذَا سَمِعْتَهُ اسْتَيْقَظْتَ ؟ هذَا \n", + "مَعْنَاهُ – مَنْ يَسْمَعُ مَا يُصْلِحُ شِأنَهُ – وَيَتَغَافَلُ عَمَّا لَيسَ فِيهِ \n", + "مَنْفَعَة * \n", + "28- أَلْبَطْنُ وَاْلرّجْلاَنِ\n", + "أَلْبَطْنُ وَالَرّجْلاَنِ تَخَاصَمْوا فِيمَا بَيْنَهُمْ أَيٌّهُمْ يَحْمِلُ الجِسْمَ * \n", + "قَالَتِ الرِجَْلانِ- نَحْنُ بِقُوَّتِنَا نَحمِلُ آلْجِسمَ جَمِيعًا * قَالَ \n", + "الجَوْفُ – أَنَا إنْ لَمْ أَنَل مِنَ الطَّعَامِ شَيئًا – فَلاَ كُنمُمَا تَسْتَطِيعَانِ \n", + "المَشْيَ – فَضْلاً تَحْملاَنِ شَيئًا * هذَا مَعْنَاهُ – مَنْ يَتَولَّي أَمْرًا \n", + "أَمْثَالُ لُقْمَاَن الحَكِيمِ\n", + "فَإنْ لَمْ يَعْضُدْهُ الَّذِي هُوَ أَرْفَعُ مِنْهُ وَأَشَدَّ مِنهُ – فَمَا لَهُ قُدْرَة \n", + "عَلَي خِدْمَتِهَ وَلاَ مَنفَعَةَ لِرُوحِهَ أَيْضًا * \n", + "29- إنْسَانْ وَالْمَوْتُ\n", + "إِنْسَانْ مَرَّةً حَمَلَ جْزرََةَ حَطبٍ فَثَقُلَتْ عَلَيْهِ * فَلَمّّا أَعْيَي \n", + "وَضَجِرَ مِنْ حَمْلِهَا رَمَي بَهَاعَنْ كَتِفِهِ – وَدَعَا عَلَي رُوحِهِ \n", + "باْلْمَوْتِ * فَشَخَصَ لَهُ المَوْتُ قَائِلاً هُوذَا أََنَا لِمَاذَا دَعَوْتَنِي؟ \n", + "فَقَالَ لَهُ الإنْسَانُ – دَعَوْتُكَ لِتَرْفَعَ هذِهِ جُرْزَةَ الْحَطَبِ عَلَي \n", + "كَتِفي * هذَا مَعْنَاهُ – أَنَّ اْلعَالَمَ بِأَسْرِهِ يُحِبٌ الحَيَاةَ فِي هذِهِ \n", + "الدَّنْيَا- وََمَا يَمَلٌّونَ مِنَ الضُعْفِ وَآْلشَّقَاءِ * \n", + "30- عَزَال\n", + "أَيَّل (يَعْنِي غَزَال) مَرَّةً مَرَّةَ عَطِشَ * فَأتَي إلَي عَينِ مَآءِ يَشْرَبَ * \n", + "فَنَظَرَ خَيَالَهُ فِي المَاءِ – فَحَزِنَ لِدِقَّةِ قَوَائِمِهِ – وَسَرَّوَ آْبْتَهَجَ لِعِظَمِ \n", + "قُرْونَهِ وكِبَرِهَا * وَفِي الحَالِ خَرَجَ عَلَيْهِ آْلصَّيَّادُونَ – فَاْنهْزَمَ \n", + "مِنْهُمْ * فَأَمَّا وَهْوَ فِي السَّهْلِ فَلَمْ يُدْرِكُوهُ – فَلَمَّا دَخَلَ فِي الجَبَلِ \n", + "وَعَبَرَ بَيْنَ الشَّجَرِ فَلَحِقَهُ الصَّيَّادُونَ وَقَتَلُوهُ * فَقَالَ عِنْدَ مَوْتِهِ* \n", + "أَمثَالُ لُقمَانَ الحَكِيمِ\n", + "الوَيْلُ لِي أَنا المِسكِينُ ! الَّذِي آزدَرَيتُ بِهِ هُوَ خَلَّصَنِي – \n", + "وَالذِي رَجَوْتُهُ أَهلَكَنِي * \n", + "36- أَسْوَدُ\n", + "أَسوَدُ فِي يَومٍ ثَلجٍ ثالِجٍ نَزَعَ ثِيَابَهُ وَأَقبِلَ يَأخُذُ الثَّلجَ وَيَعرُكُ \n", + "بِهِ جِسمَهُ * فَقِيلَ لَهُ – لِمَاذَا تعَرُكُ جِسمَكَ بِالثَّلجِ ؟ فَقَالَ \n", + "لَعَلِي أَبيضُ * فَأَجَابَهُ رَجُل حَكِيم قَائِلاً لَهُ – يَا هذَا ! لاَ تُتعِبْ \n", + "نَفْسَكَ – فَقَدْ يُمْكِنُ أَنَّ جِسمَكَ يُسَوّدُ الثَّلجَ وَهوَ لاَ يَزدَادُ \n", + "إلاَّ سَوَادًا * هذَا مَعنَاهُ – أنَّ أَهلَ الشرِ لاَ يَستَطيِعُونَ فِعلَ \n", + "الخيرِ * وَمَعْلُوم أَنَّ الشَّرِيرَ يَقدِرُ أَن يُفسِدَ الخير – وأَمَّا الخَيرُ \n", + "لاَ يَقدِرُ أَحَدُ عَلَي إصلاَح الشَّرِيرِ * \n", + "32- خُنفَسَة ونَحَلَةَ\n", + "خُنفَسَة مَرَّةً قَالَت لِنحلَةِ العَسَلِ – لَو أَخّذتِنِي مَعَكِ \n", + "لَعَمَلتُ عَسلاً مِثلَكِ وَأَكثَرَ * فَأَجابتَهَا النَّحلَةُ إلىَ ذلَكَ* \n", + "فَلَمَّا لَم تَقدِر عَلَى مِثلِ ذلِكَ – ضَرَبَتهَا النَّحلَةُ بِحُمَِتهَا * \n", + "وَفِيمَا هِيَ تَمُوتُ قَالتَ في نَفْسِهَا – لَقَدِ استَوْجَبتُ مَا نَالَنِي\n", + "أَمثَالُ لُقمَانَ الحَكِيمِ\n", + "مِنَ السّوء – فَلَمْ يَكُنْ لِي بَصِيرَة بِعَمَلِ الزِفتِ – لِمَاذَا \n", + "آلتَمَسْتُ عَمَلَ العَسَلِ؟ هذَا مَعْناهُ – مِنْ يَتَحَلَّي بِمَا لَيسَ \n", + "لَه – ويَدَّعِي عَمَلَ مَا يَتّجِهُ لَهُ * \n", + "33- أَسَد وَإنسَان\n", + "أَسِد مَرَّةً وَإنسَان آصطَحَبَا عَلَي الطَّرِيقِ – فَجَعَلاَ يَتَشَاجَرَانِ \n", + "بِالكلاَمِ عَلَي القُوَّةِ وَشِدَّةِ البَأسِ * فَجَعلَ الأَسَدُ يُطْنِبُ في\n", + "شِدَّتِهِ وَبِأسِهِ * فَنَظَرَ الإنسَانُ عَلَي حَائِطٍ صُورَةَ رَجُلٍ – وَهوَ \n", + "يَخْنُقُ سَبُعًا * فَضَحِكَ الإِنسَانُ * فَقَالَ لَهُ الأَسَدُ – لَو أَنَّ السبَاعَ \n", + "مُصَوِروُنَ مِثَلَ بَنِي آدَمَ – لَمَا قَدرَرَ الإِنْسَانُ يَخنُقُ سَبُعًا – \n", + "بَلْ كانَ السَّبُعُ يَخْنُقُ الإنسَانَ * هذَا مَعنَاهُ – أَن مَا يُزَكَّي \n", + "الإنسَانُ بِشَهَادَة أَهلِ بَيتِهِ * \n", + "34- إنسَان وَفَرَس\n", + "إنسَان كَانَ يَركَبُ فَرسًا – وَكَانَتْ حَامِلاً * وَفيِمَا هُوَ فِي \n", + "بَعضِ الطَّرِيقِ أَنْتَجَتِ آبنًا * فَتَبَعَ أُمَّهُ غَيرَ بَعِيدٍ – ثُمَّ وَقَفَ \n", + "وَقَالَ لِصَاحِبِهِ – يَا سَيِدِي ! تَرَانِي صَغِيرًا وَلاَ أَستَطِيعُ المَشْيَ * \n", + "أَمثَالُ لُقمَانَ الحَكِيمِ\n", + "وَإنْ مَضَيَ وتَرَكتَنِي هَا هُنَا فَهَلَكتُ * وَإن أَنتَ أخَذتَّنِي \n", + "مَعَكَ وَرَبَّيْتَنِي إليَ أن أقوَي – فَحَمَلتُكَ عَلَي ظَهرِي – \n", + "وَأَوصَلْتُكَ سَرِيعًا إلَي حَيثَ تِشاءُ * هَذَا مَعْنَاهُ – أَنَّهُ يَجِبُ \n", + "أنْ يُشَدّ المَعرُوفُ لِأَهْلِهِ – وَمُسْتَحِقيهِ وَلاَ يَطرَحُوهُ * \n", + "35- كَلب وَشُوهَة\n", + "كَلب مَرَّةً خَطَفَ بَضعَةَ لَحمٍ مِنَ المَسْلَخِ – ونَزَلَ يَخُوضُ \n", + "فيِ النَّهرِ * فَنَظَرَ خَيَالَهَا فِي المَاء – وَإذَا هِيَ أَكْبَرُ مِنَ الَّتِي \n", + "مَعَهُ * فَرَمَي بَهَا فَانحَدَرَتْ شُوهَة وَأخَذَتْهَا * وَجَعلَ الكَلبُ \n", + "يَجرِي فِي طَلبِ الكَبِيرَةِ فَلَم يَجِد شَيئًا * فَرَجَعَ في طَلَبِ الَّتِي \n", + "كَانَتْ مَعَهُ – فَلَمْ يُصِبْهَا * فَقَالَ مَا أَعرَفُ أَقَلَّ رَأيٍ مِنِي – \n", + "لِأَنَّنِي ضَيَّعْتُ مَا كَانَ مَعِي – وَطَلَبتُ مَا لاَ يَصِحٌ لِي * هذَا \n", + "مَعنَاهُ – مَنْ يَترُكُ شَيئًا قَلِيلاً مِوجُودًا ويَطْلُبُ كَثِيرًا مَفْقُودًا * \n", + "36- أَلشَّمْسُ وَالريَحُ\n", + "أَلشَّمسُ وَالرّيَحُ تَخَاصَمَا فِيمَا بَينَهُمَا مَنْ مِنْهُمَا يَقْدِرُ\n", + "أَمثَالُ لُقمَانَ الحَكِيمِ\n", + "أنْ يُجَردَ الإنسانَ الثِيَابَ * فَآشْتَدَّتَ الرّيحُ بِالهُبُوبِ وَعَصَفَتْ \n", + "جِدًا * فَكَانَ الإنسَانُ إذَا اشتَدَّتْ هُبُوبُ الريحِ – ضَمَّ ثِيَابَهُ \n", + "إِليهِ – وَألتَفَّ بَهَا مِنْ كُلِ جَانِبٍ * فَلَمْ تَقْدِرِ الرّيِحُ عَلَي خَلعِ\n", + "ثِيَابِهِ مِن جَسَدِهِ بِشِدَّةِ عَصْفِهِ * فَلَّمَا أَشرَقَتِ الشَّمسُ \n", + "وَأَرتَفَعَ آلنَّهَارُ واشتَدَّ الحَرٌ وَحَمِيَتِ الرَّمْضَآءُ – فَخَلَعَ الإنسَانُ \n", + "ثَيَابَهُ – وَحَمَلَهَا عَلَي كَتِفِهِ مِنْ شِدَّةِ الحَرِ * هذَا مَعنَاهُ – مِن \n", + "كَانَ مَعَهُ الإِتِضَاعُ وَحُسْنُ الخُلْقِ يَنَالُ مِنْ صاحِبِةِ جَمِيعَ \n", + "مَا يُرِيدُهُ * \n", + "37- أسَد وَثَور\n", + "أَسَد مَرَّةً أَرَادَ يَفْتَرِسُ ثَورًا – فَلَمْ يَجسُرْ عَلَيْهِ لِشِدَّةِ قُوَّتِهِ * \n", + "فَمَضي إليهِ ليِحَتَالَ عَلَيهِ قَائِلاً لَهُ – إعلَمْ أَنَّنِي قَدْ ذَبّحْتُ \n", + "خَرُوفًا سَمينًا – وأشتهِي أنْ تَأكُلَ عِندِي في هذه الليلَةِ \n", + "خُبزًا * فَأَجَابهُ إلَي ذلِكَ * فَلَمَّا وَصَلَ إلَى المَوْضِعِ وَنَظَرَهُ \n", + "وَإذَا قَدِ آسْتَعَدَّ الأَسَدُ حَطَبًا كَثِيرًا وَخَلاَقِينَ كِبَارًا * فَوَلَّي \n", + "أَمثَالُ لُقمَانَ الحَكِيمِ\n", + "الثَّورُ هَارِبًا لَمَّا عَايَنَ ذلَكَ * فَقَالَ لَهُ ألأَسَدُ – لِمَاذَا وَلَّيْتَ\n", + "بَعدَ مَجيِكَ إلَي هَاهُنَا؟ فَقَالَ لَهُ الثَّورُ لأَنِي عَلِمتُ أَنَّ هذَا \n", + "الإستِعدَادَ لَمَا هَوَ أْكَبرُ مِنَ الخَرُوفِ * هذَا مَعنَاهُ – أَنَّ مَا \n", + "سَبِيلُ العَاقِلِ أنْ يُصَدِقَ عَدُوَّهُ وَلاَ يَأنَسُ إلَيهِ * \n", + "38- كَلْبَانِ\n", + "كَلْب مَرَّةً كَانَ فِي دَارِ أَصحَابِهِ دَعوَة – فَخَرَجَ إلَي السٌوقِ \n", + "فَلَقِيَ كَلبًا آخَرَ * فَقَالَ لَهُ آعْلَمْ أَنَّ عِنْدَنَا آليَوْمَ دَعْوَة – \n", + "فَآمضِ بِنَا لِنَقْصِفَ اليَومَ جَمِيعًا * فَمَضي مَعَهُ فَدَخَلَ بِهِ \n", + "إلىَ المَطبَخِ * فَلَمَّا نَظَرُوهُ الخُدّامُ قَبَضَ أَحَدُهُم عَلَي ذَنبِهِ \n", + "وَرَمَي بَهِ مِنَ الحَائِطِ إليَ خَارجِ الدَّار * فَوَقَعَ مَغْشِيًا عَلَيهِ – \n", + "فَلَمَّا أَفَاقَ وانتَفَضَ مِنَ التٌّرَابِ فَرأوهُ أَصحَابُهُ فَقَالُوا لَهُ -\n", + "أَينَ كُنْتَ اليَومَ فَكُنْتَ تَقْصِفُ ؟ فَإنَّنَا نَرَاكَ مَا خَرَجتَ اليَومَ \n", + "تَدْرِي كَيفَ الطَّرِيقُ * هذَا مَعنَاهُ – أَنَّ كَثِيرِينَ يَتَطفَّلُونَ – \n", + "فَيَخرُجُونَ مَطْرُودِينَ بَعْدَ الإسْتِخْفَافِ بِهِم وَالهَوَانِ *\n", + "أَمْثَالُ لُقَمَانَ الحَكِيِم\n", + "39- أَلعَوْسَجُ\n", + "أَلعَوْسَجُ قَالَ مَرَّةً لِلبُستَانِي – لَوْ أَنَّ لِي مَنْ يَهْتَمٌّ بِي \n", + "وَيَنْصُبُنِي في وَسِط البُسْتَانِ – وَيَسْقِيِني وَيَخْدُمِني – لَكَانَ \n", + "الْمُلُوكُ يَشْتَهُونَ يَنْظُرُونَ زَهرِي وَثَمَرِي * فَأَخَذَهُ ونَصَبَهُ \n", + "فِي وَسَطِ البُسْتَانِ فِي أَجْوَدِ الأَرْضِ – وَكَانَ يَسْقِيهِ فِي \n", + "كُلِ يَومٍ دَفْعَتَيْنِ – فَنَشي وَقَويَ شَوْكُهُ – وَتَفرَّعَتْ أَغْصَانُهُ \n", + "عَلَي جَمِيعِ الشَّجَرِ الَّتِي حَولَهُ * فَجَافَتْ وَأَصُلَتْ عُرُوقُهُ فِي \n", + "آلأَرضِ – وَأمَتَلأَ البُستَانُ مِنهُ وَمِنْ كِثَرةِ شَوكِهِ لَمْ يَكُنْ أَحَدْ \n", + "يَسْتَطِيعُ أَنْ يَتَقَدَّمَ إِلْيهِ * هذَا مَعْنَاهُ – مَنْ يُجَاوِرُ إنسَانَ \n", + "سُوءٍ فَإنَّهُ كُلَّمَا أَكَرَمَهُ آشْتَدَّ شَرّهُ وَتَمرُّدُهُ – وَكُلَّمَا أَحسَنَ إِليْهِ \n", + "أَسَاءَ هُوَ الفِعْلَ مَعَهُ * \n", + "40- أَسَد وَثَعْلَب\n", + "أَسَد مَرَّةً شَاخَ وَضَعُفَ وَلمَ يَقْدِر عَلَي كَسرِ شَيءٍ مِنَ \n", + "آلوُحُوشِ * فَأَرَادَ أنْ يَحتَْالَ لِنَفْسِهِ فِي المَعِيشَةِ * فَتَمَارَضَ \n", + "أَمثالُ لُقْمَانَ الحكِيمِ \n", + "وَأَلقَي نَفسَهُ في بَعضِ المَغَائِر* وكَانَ كُلَّما أَتاهُ شيء مِنَ \n", + "الوُحُوشِ ليعُودَهُ أفّتَرَسَهُ داخِلَ المَغَارَةِ وَأَكَلَهُ* فَأَتَي الثَّعلَبُ \n", + "عَائِدًا لهُ – فَوَقَف عَلىَ بابِ المغَارَة مُسلِّمًا عَلَيْهِ قَائِلاً لَهْ \n", + "كَيْفَ حَالُكَ يَا سَيٌِدَ الوُحُوش؟ فَقَالَ لَهُ الأَسَدُ – لِمّاذَا لاَ \n", + "تَدْخُلُ يِا أبَا الحُصضيْنِ ؟ فَقَالَ لَهُ الثَّعْلبُ – يَا سَيِدِي ! قَدْ \n", + "كُنْتُ عَوَّلتُ عَلَي ذَلِكَ غَيرَ أنَّنِي أَرَا عِندِكِ آثَارَ أَقْدَامٍ \n", + "كَثِيرَةٍ قَدْ دَخَلُوا – وَلاَ أرَا أَنْ خَرَجَ مَنْهُمْ وَلاَ وَاحَدْ * هَذَا \n", + "مَعْنَاهُ – أَنَّ مَا سَبِيلُ الإنسَانِ أَنْ يَهْجِمَ عَلَي أَمْرٍ إِلاَّ حَتَّي \n", + "يُمَيِّزَهُ * \n", + "41- إِنْسَان وَخِنْزيرٍ\n", + "إِنْسان مَرَّةً حَمَلَ عَلَي بَهِيمَةٍ كِبْشًا وَعَنزًا وَخِنْزِيرًا – \n", + "وَتَوَجَّهَ إليَ المَدِينَةِ لِيَبِيعَ الجَمِيعَ * فَأَمَّا الكَبْشُ وَالعَنْزُ \n", + "فَلَمْ يَكُونَا يَضطَرِباَنِ عَلَي ألّبَهِيمَةِ – وأَمَّا الخِنْزِيرُ فَإِنَّهُ كَانَ \n", + "يَعَرِضُ دَائِمًا – وَلاَ يَهْدَأ * فَقَالَ لَهُ الإِنْسَانُ – يَا أَشَرَّ الوُحُوشِ\n", + "أَمثَالُ لُقمَانَ الحَكِيمِ\n", + "لِمَاذَا الكَبْشُ وَالعنْزُ سَكُوت؟ لاَ يَضَطرِبَانِ – وَأَنتَ لاَ تَهدَأ \n", + "وَلاَ تَستَقِرٌ * قَالَ لَهْ الخِنزِيرُ كُلٌّ وَاحِدٍ يَعلَمُ دآء نَفْسِهِ \n", + "فَأَنَا أَعلَمُ أَنَّ الكَبشُ لِصُوفِهِ والعَنْزُ يُطلَبُ لِلَبَنِهَا – وَأَنَا الشَّقِيٌ \n", + "لاَ صُوفَ لِي وَلاَ لَبَنَ * وأَنَا عِنْدَ وُصُولِي إلَي المَدِينَةِ أُرْسَلُ \n", + "إلَى المَسلخِ – لاَ مَحَالَةَ * هذَا مَعنَاهُ – أَنَّ الذِينَ يَغْرَقُونَ فِي \n", + "الخطَايَا والذُّنُوبِ الَّتِي قَدَّمَتْ أَيّدِيهِم – يَعْلَمُونَ سوءَ مُنْقَلَبِهِم\n", + "وَماذا تَكُونُ آخِرَتُهُم * \n", + "SECTION III.\n", + " Miscellaneous Anecdotes.\n", + " حِكَايَات مُخْتَلِفَة\n", + "1- أَلحِكَايَة الأولَي\n", + "قِيلَ أَنَّ بَعضَ العُلَمَاء تَخَاصَمَ مَعْ زَوْجَتِهِ – فَعَزَمَ عَلَي \n", + "طَلاَقِهَا * فَقَالَتْ أَذْكُرْ طُولَ الصَّحْبَةِ * فَقَالَ وَاللهِ! مَا لَكِ \n", + "عِندِي ذَنْب سِوَى ذلِكَ * \n", + "2- أَلحِكَايَةُ الثَّانِيَةُ\n", + "قِيلَ أنَّ أَعرَابيَاً وُلِيَ البَحْرَيْنِ * فَجَمَعَ اليَهُودَ وَقَالَ – مَا \n", + "صَنَعْتُمْ بِعِيسي بِنِ مَريَم (عَلَيهِ السَّلامُ) ؟ قَالُوا قَتَلْنَاهُ * قَالَ \n", + "وَاللَّهِ ! لاَ تَخْرْجُوا مِنَ السِجْن حَتَّي تُؤدّوا دِيَتَهُ * فَمَا خَرَجُوا \n", + "حَتَّي أَخذَ مَنهُمِ الدِيَةَ كَامِلَةً * \n", + "حِكَايَات مُختَلِفَة\n", + "3- الّحِكَايَةُ الثَّالِثَةُ \n", + "قَيلَ إجتَازَ بَعْضُ المُغَفَّلِيَن بِمَنَاَرةٍ – وَكَانُوا ثَلاَثَةُ نُفُرٍ * \n", + "فَقَالَ أَحَدُهُمْ – مَا أَطْوَلَ البَنَّائِينَ فِي الزَّمِنِ الأَوَّلِ حَتَّي \n", + "وَصَلُوا إلَي رَأسِ هذِهِ المّنَارَةِ ! فَقَالَ الثَّانِي – يَا أَبّلَهُ ! كُلّ\n", + "يَبْنِيهَا وَلكِنْ يَعمَلُونَهَا عَلَي وَجُهِ الأَرضِ وَيُقِيمُونَهَا * فَقَالَ \n", + "الثَّالثُ – يَا جُهَّالُ ! كَانَتْ هذِهِ بِئرًا فَآنَقَلَبتْ مَنَارَةً * \n", + "4- أَلّحِكَايَةُ آلرَّابِعَةُ\n", + "قَالَ بَعْضُ الحُكَمَاءِ الفُرسِ أَخَذْتُ مِنْ كُلِّ شَيءٍ أَحسَنَ \n", + "مَا فِيهِ * فَقِيلَ لَهُ – فَمَا أَخَذْتَ مَنَ الكَلبِ؟ قَالَ حُبَّهُ لِأَهلِهِ \n", + "وَذَبَّهُ عَن صَاحِبِهِ * قِيلَ فَمَا أَخَذْتَ مِنَ الغُرَابِ ؟ قَالَ \n", + "شِدَّةَ حَذَرِهِ * قِيلَ فَمَا أَخَذْتَ مِنَ آلهِرَّة ؟ قَالَ تَمَلقَهَا عِندَ \n", + "المسَئَلَةِ * \n", + "5- أَلحِكَايَةُ الخَامِسَةُ\n", + "قِيلَ أَنَّ بَعضَ البُخَلاءِ استَأْذَنَ عَلَيهِ ضَيْف وَبِيْنَ يَدَيهِ \n", + "حِكَايَات مُخْتَلِفَة\n", + "خُبز وَقَدَحْ فِيهِ عَسَل * فَرَفَعَ الخُبزَ وَأَرَادَ أَنْ يَرفَعَ العَسَلَ – \n", + "وَظَنَّ البَخِيلُ أَنَّ ضَيفَهُ لاَ يَأكُلُ العَسَلَ بِلاَ خُبْزٍ * فَقَالَ تَرَي \n", + "أَنْ تَأْكُلَ عَسَلاً بِلاَ خُبزٍ ؟ قَالَ نَعَمْ – وَجَعَلَ – يَلْعَقُ لَعقةً بَعْدَ \n", + "لَعْقَةٍ * فَقَالَ لَهُ البَخِيلُ – وَاللَّهِ يَا أَخِي! إنَّهُ يَحْزِقُ القَلْبَ * \n", + "فَقَالَ صَدَقتَ – وَلكِنَّ قَلبَكَ * \n", + "6- أَلْحِكَايَةُ السَّادِسَةُ\n", + "قَيلَ أَنَّ بَعْضَ الأُدَبَاءِ – قَالَ حَضَرَ رَسُولُ مَلِكِ الرُّومِ \n", + "عِنءدَ المُتَوَكِلِ – فَآْجْتَمَعْتُ بِهِ – فَقَالَ لَمَّا أُحْضِرَ الشَرَابُ – \n", + "مَا لَكُمْ مَعَاشِرَ المُسلِمِينَ قَد حُرِمَ عَلَيكُمْ فِي كِتَابِكُم الخَمْرُ \n", + "وَلَحمُ الخِنْزِيرِ فَعَمِلتُمْ بِأَحَدِهِمَا دُونَ الآخِرِ؟ فَقُلْتُ لَهُ أَمَّا \n", + "أَنَا فَلاَ أَشْرِبْ الخَمْرَ فَسَلْ مِنْ يَشْرِبُهَا * فَقَالَ إنْ شِئْتَ \n", + "أَخْبَرْتُكَ * قُلتُ لَهُ قُلْ * فَقَالَ لَمَّا حُّرِمَ عَلِيْكُمْ لَحْمُ الخِنزِيرِ \n", + "وَجّدْتُمْ بَدَلَهُ مَا هَوَ خَيْر مِنهُ لُحُومُ الطُّيُورِ – وَأَمَّا الخَمْرُ فَلَمْ \n", + "تَجِدُوا مَا يُقَارِبُهُ فَلَمْ تَنْتُهوا عَنْهُ * قَالَ فَخَجِلْتُ مِنْهُ وَلَمْ أَدّرِ \n", + "مَا أَقُولُ لَهُ * \n", + "حِكَايَات مُخْتَلَفَة \n", + "7- الحِكَايَةُ السَّابِعَةُ \n", + "سَأَلَ بَعضْ المُلُوكِ وَزِيرَهُ - الأَدَبُ يَغْلِبُ الطَّبْعَ أَمْ الطَّبعُ \n", + "يَغْلِبُ الأَدَبَ ؟ فَقَالَ – الطَّبعُ أغْلَبْ لِأَنَّهُ أَصْل وَالأَدَبُ فَرْع – \n", + "وَكُلٌ فَرعٍ يَرْجِعُ إلَي أَصْلِهِ * ثُمَّ أَنَّ المَلِكَ استَدْعي بالشَّرابِ – \n", + "وَأَحْضَرَ سَنَانِيرَ بِأيدِيهَا الشِمَاعُ فَوَقَفْ حَوْلَهُ * فَقَالَ للِوَزِيرِ \n", + "أُنظُر خَطَاءَكَ في قَوْلِكَ – الطَّبْعُ أَغْلَبُ * فَقَالَ الوَزِيرُ أَمهِلْنِي \n", + "اللَيْلَةَ – قَالَ قَدْ أَمْهَلْتُكَ * فَلَمَّا كَانَ اللَيلَةُ الثَّانِيَةُ – أَخَذَ \n", + "الوَزِيرُ فِي كُمِهِ فَأرَةً وَرَبَطَ فِي رِجلِهِ خَيْطًا وَمَضَي إلي المَلِكِ \n", + "فَلَمَّا قَبَلَتِ السَّنَانيِرُ بِأَيدِيهَا الشِمَاعُ أَخْرَجَ الفَأرَةَ مِنْ كُمِهِ * \n", + "فَلَمَّا رَأَتْهُ السَّنانِيرُ رَمَت بالشِمَاعِ وتَتَبعتِ الفَأرَةَ فَكَادَ البيَتُ \n", + "أَنْ يَحتَرقَ * فقَالَ الوزيرُ أنْظُرْ أَيهَا المَلِكُ ! كَيفَ غَلَبَ الطَّبْعُ \n", + "الأدَبَ – وَرَجَعَ الفَرْعُ إلَي أَصْلِهِ * قَالَ صَدَقْتَ لِلَّهِ دَرُّكَ * \n", + "8- أَلْحِكَايَةُ الثَّامِنَةُ\n", + "قِيلَ لَمَّا تَشَاغَلَ عِبْدُ المَلِكِ ابنِ مَرْوَانَ بِقِتَال مَصْعَبَ \n", + "ابنِ الزُّبَيرِ أُجْتَمَعَ وُجُوهُ الرُّومِ إلَي مَلِكِهِمْ وَقَالُوا – قَدْ أَمْكنَتْكَ \n", + "حِكَايَاتْ مُخْتَلِفَة\n", + "الفُرصَةُ مِنَ العَرَبِ – فَقَدْ تَشَاغَلَ بَعضُهُمْ بِبَعضٍ – وَوَقَعَ بَأْسُهُمْ \n", + "بَيْنَهُمْ – وَالرَّأيُ أنْ تَغزُوهُمْ فِي بِلاَدِهِمْ – فَإِنَّكَ تُذِلّهُمْ وَتَنَالُ \n", + "حَاجَتَكَ مِنْهُمْ * فَنَهَاهُمْ عَنْ ذَلِكَ – فَأَبَوْا عَلَيْهِ إلاَّ أَنْ يَفَعَلَ * \n", + "فَلَمَّا رَأَي ذلِكَ – دَعَا بِكَلْبَيْنِ – فَأَحْرَشَ بَيْنَهُمَا – فَأقتَتَلاَ قِتالاً \n", + "شَدِيدًا * ثُمَّ دَعَا بِذِئبٍ – فَخَّلاَهُ بَيْنَهُمَا – فَلَمَّا رَأَي الكَلْبَانِ \n", + "الذِئبَ تَرَكَا مَا كَانَا بَيْنَهُمَا وَأَقْبَلاَ عَلَي الذِئبِ حَتَّي قَتَلاَهُ * \n", + "فَقَالَ مَلِكُ الرومِ – هكَذَا العَرَبُ – يَقتَتِلُونَ بَيْنَهُمَا – فَإِذَا رَأونَا \n", + "وَهُمْ مُجْتَمِعُونَ تَرَكُوا ذلِكَ وَأَقبَلُوا عَلَيْنَا * فَعَرَفُوا صِدْقَ قَوْلِهِ – \n", + "وَرَجَعُوا عَمَّا كَانُوا عَلَيهِ * \n", + "9- الحِكَايَةُ التَّاسِعَةُ\n", + "قِيلَ أَنَّ المُلُوكِ – كَانَ مُغرَمًا بُجبِ النِسَآءِ * وَكَانَ \n", + "وَزِيرُهُ يَنْهَاهُ عَنْ ذلِكَ * فَرَأَتْهُ بَعْضُ قِيَانِهِ مُتَغَيِرَ الحَالِ عَليهِنَّ * \n", + "فَقَالَتْ يَا مَوْلاَي مَا هذَا ؟ فَقَالَ لَهَا أَنَّ وَزِيرِي فُلاَنَ قَدْ \n", + "نَهَانِي عَنْ مَحَبَّتِكُنَّ * فَقَالَتِ الجَارِيَةُ – هَبْنِي لَهُ أَيُهَا الْمَلِكَ – \n", + "وسَتَرَي مَا أَصْنَعُ بِهِ – فَوَهَبَهَا لَهْ – فَلَمَّا خَلاَ بَهَا تَمَنَّعَتْ مِنْهُ \n", + "حِكَايَات مُخْتَلِفَة\n", + "حَتَّي تَمَكَّنَ حُبُّهَا مِنْ قَلْبِهِ – فَقَالتْ لاَ تَقْرَبُنِي حَتَّي أَرْكَبَكَ \n", + "وَتَمْشِي بِي خَطَوَاتٍ * فَأَجِابَهَا إلي ذلِكَ – فَوَضَعَتْ عَلَيْهِ \n", + "سَرْجًا وَجَعَلَتْ فِي رَأسِهِ لِجَامًا وَرَكَبَتْهُ * وَكَانَتْ قَدْ أَرّسَلَتْ \n", + "إِلَي المَلِكِ بِهذَا الخَبَرِ فَهَجَمَ عَلَيهِ وَهْوَ عَلَي تِلْكَ الحَالَةِ – \n", + "فَقَالَ مَا هذَا أَيُّهَا الوَزِيرُ كُنْتَ تَنْهَانِي عَنْ مَحَبَّتِهنَّ – وَهذِهِ \n", + "حَالَتُكَ مَعْهُنَّ * فَقَالَ أَيهَا المَلِكُ – مِنْ هذَا كُنتُ أَخَافُ \n", + "عَلَيْكَ * فَاسْتَحْسَنَ مِنهُ هذَا الجَوَابَ * \n", + "11- أَلْحِكَايَةُ الّعَاشِرَةُ\n", + "قِيلَ أَنَّ قَيْصَرَ مَلِكَ الشَّامِ وَالروُّمِ – أرسَلَ رِسُولاً إلَي مَلِكِ \n", + "فَارِسٍ كِسرَي أَنْوشِيروَانْ صَاحِبِ الإيوَانِ * فَلَمَّا وَصَلَ وَرَأَي \n", + "عَظَمَةَ الإيوانِ وَعَظَمَةَ مَجْلِسِ كِسْرَي عَلَي كُرْسِيِهِ – وَالمُلُوكِ \n", + "فِي خِدْمَتِهِ – مَيَّزَ الإيوَانَ – فَرَأَي فِي بَعْضِ جَوَانِبِهِ إِعْوِجَاجًا * \n", + "فَسَأَلَ التَّرْجَمَانَ عَنْ ذَلِكَ – فَقِيلَ لَهُ – ذلِكَ بَيْت لِعَجْوزٍ \n", + "كَرِهَتْ بَيْعَهُ عِنْدَ عِمَارَةِ الإِيَوانِ – فَلَمْ يَرَ المَلِكُ إِكرَاهَهَا \n", + "عَلَي البَيَعِ – فَأَبّقَي بَيْتَهَا فَي جَانِبِ الإيْوَانِ فَذلِكَ مَا رَأَيْتَ \n", + "حِكَايَات مُخْتَلِفَة\n", + "وَسَأَلتَ * فَقَالَ الرٌومِيٌّ وَحَقِ دِينِهِ ! إنَّ هذَا الإعْوِجَاجَ أَحْسَنُ \n", + "مِنَ الإسْتِقَامَةِ – وَحَقِ دِينِهِ إنَّ هذَا الذَّي فَعَلَهُ مَلِكُ الزّمَانِ \n", + "لَم يُؤَرَّخ فِيمَا مَضَي لِمَلِكٍ – وَلاَ يُؤَرَّخُ فِيمَا بَقَي لِمَلِكٍ * \n", + "فَأَعجَبَ كِسْرَي كَلاَمَهُ فَأَنْعَمَ عَلَيهِ وَرَدَّهُ مِسرُورًا مَجبُورًا * \n", + "11- الحِكَايَةُ الحَادِيَةُ عَشَرَ\n", + "قِيلَ أَنَّ أَنُوشِيروَانْ – وَضَعَ الْمَوَأيَد لِلناسِ فِي اليومِ \n", + "نَيْرُوزْ وَجَلَسَ – وَدَخَلَ وُجُوهُ مَمْلَكَتِهِ آلْإيوَانَ * فَلَمَّا فَرَغُوا \n", + "مَنَ الطَّعَامِ جَاؤا بِالشَّرَابِ وَأحضِرَتِ الّفَوَاكِهُ وَالمَشمُومُ فِي \n", + "آنِيةٍ مِنَ الذَّهبِ وَالفِضَّةِ * فَلَمَّا رُفِعَتْ آلَةُ المَجْلِسِ –\n", + "أَخَذَ بَعْضُ مَنْ حَضَرَ جَامَ ذَهَبٍ وَزْنُهُ أَلفُ مِثقَالٍ فَخَبَّأَهُ \n", + "تَحتَ ثِيَابِهِ – وَأَنُوشِيرَوانْ يَرَاهُ * فَلَمَّا فَقَدَهُ السَاقِي قَالَ بِصَوْتٍ \n", + "عَالٍ – لاَ يَخْرُجَنَّ أَحَدٌ حَتَّي يُفَتَّشَ * فَقَالَ كِسْرَي وَلِمَ – فَأَخْبَرَهُ \n", + "بِالقِصَّةِ * فَقَالَ قَدْ أَخَذَهُ مَنْ لاَ يَرْدّهُ وَرَأهُ مَنْ لاَ يَنُمَّ عَلَيْهِ فَلاَ \n", + "يُفَتَّشُ أَحَد * فَأَخَذَهُ الرَّجُلُ وَمَضَي فَكَسَرَهُ وَصَاغَ مِنهُ مِنْطَقَةً \n", + "وَحِلْيَةً لِسَيفِهِ وَجَدَّدَ لَهُ كِسوَةً فَاخِرَةً * فَلَمَّا كَانَ فِي مِثْلِ \n", + "حِكَايَات مُخْتَلِفَة\n", + "جُلُوسِ المَلِكِ – دَخَلَ ذلِكَ الرَّجُلُ بِتِلكَ الحِلْيَةِ – فَدَعَاهُ \n", + "كِسرَي وَقَالَ لَهُ – هذَا مِن ذَاكَ * فَقَبَّلَ الأَرضَ وَقَالَ نَعَمْ – \n", + "أصْلَحَكَ اللَّهُ تَعَالَي * \n", + "12- أَلْحِكَايَةُ الثَّانِيَةُ عَشَرَ \n", + "قِيلَ أَنَّ أَبَا دُلاَمَةٍ الشَّاعِرُ – كَانَ وَاقِفًا بَيْنَ يَدَيِ السَّفَّاح \n", + "فِي بَعضِ الأَيَّامِ – فَقَالَ لَهُ سَلْنِي حَاجَتَكَ * فَقَالَ لَهُ أَبُو \n", + "دُلاَمَةٍ – أُرِيدُ كَلْبَ صَيْدٍ * فَقَالَ أَعطُوهُ إيَّاهُ – فَقَالَ وَأُرِيدُ \n", + "دَابَّةً أَتَصَيَّدُ عَلَيهَا * قَالَ أَعْطُوهُ إِيَّاهَا * قَالَ وَغُلاَمًا يَقُودُ \n", + "الْكَلْبَ وَيَصَيدُ بِهِ – قَالَ أَعْطُوهُ غُلاَمًا * قَالَ وَجَارِيَةً تُصْلِحُ \n", + "الصَّيْدَ وَتُطْعِمُنَا مِنْهُ * قَالَ أَعْطُوهُ جَارِيَةً * قَالَ هؤلاءُ يَا أَمِيرُ\n", + "المُؤمِنِينَ – لاَ بُدَّ لَهُمْ مِنْ دَارٍ يَسْكُنُونَهَا * فَقَالَ أَعْطُوهُ دَارًا \n", + "تَجْمَعُهُم * قَالَ وَإن} لَمْ تَكُنْ لَهُمْ ضَنْيعَةْ فَمِنْ أَيّنَ يَعِيشُونَ ؟ \n", + "قَالَ قَدْ أَقَطَعتُكَ عِرَ ضِيَاع عَامِرَةٍ وَعَشْرَ ضِياَعٍ غَامِرَةٍ * \n", + "قَالَ وَمَا الّغَامِرَةُ يَا أَمِيرُ المُؤمِنِينَ ؟ قَالَ مَا لاَ تَبَاتَ فِيهَا * \n", + "قَالَ قَدْ أَقْطَعْتُكَ يَا أَمِيرُ الّمُؤمِنِينَ – مائةً ضَيْعَةً غَامَِرةً مِنْ\n", + "حِكَايَات مُخْتَلِفَة\n", + "فَيَافِي بِني أَسَدٍ * فَضَحِكَ مِنْهُ وَقَالَ إجْعَلُوهَا كُلها عَامِرةً * \n", + "13- الّحِكَايَةُ الثالِثَةُ عَشَرَ\n", + "قَالَ رَسُولُ اللهِ (صَلَّي اللهُ عَلَيهِ وَسَلّمَ) خَمْس مَنْ كُنَّ \n", + "فِيهِ كُنَّ عَلَيهِ * قِيلَ وَمَا هُنَّ يَا رَسُولَ اللهِ؟ قَالَ النَّكْثُ \n", + "والّمَكْرُ وَالبَغْيُ وَالخِدَاعُ وَالظٌلْمُ * فَأَمَّا النَّكْثُ فَقَالَ \n", + "اللهُ تَعَالَي – فَمَنْ نَكَثَ فَإنَّمَا يَنْكُثُ عَلَي نَفْسِهِ * وَأَمَّا \n", + "المَكْرُ فَقَالَ اللهُ تَعَالَي – وَلاَ يَحِيقُ المَكْرُ السَيِءُ إلاَ بِأَهْلِهِ * \n", + "وَأَمّا البَغْيُ فَقَالَ اللهُ تَعَالَي – يَا أَيٌهَا النَّاسُ إِنَّمَا بَغْيُكُمْ عَلَي \n", + "أَنْفُسِكُم * وَأَمَّا الخِدَاعُ فَقَالَ اللهُ تَعَالي- يُخادِعُونَ اللهَ وَالذِينَ \n", + "آمَنُوا وَمَا يُخَادِعُونَ إلا أَنّفُسَهُمْ * وأَمَّا الظْلمُ فَقالَ اللهُ تَعَالَي – \n", + "وَمَا ظَلَمُونَا وَلكِنِّ كَانُوا أَنّفُسَهُمْ يَظْلِمُونَ * وَقَالَ (عَلَيهِ \n", + "السَّلاَمُ) – خَمْسَة مِنْ خَمْسَةٍ مُحَال – الحُرمَةُ مَنَ الفَاسِقِ \n", + "مَحَال – وَالّكِبرُ مَنُ الّفَقِيرِ مُحَال – وَالنّصِيحَةُ مَنَ العَدُوِ \n", + "مُحَال – وَالمَحَبَّةُ مَنَ الحَسُودِ مُحَال – وَالوَفَاءُ مِنَ النِسَآء \n", + "مُحَال – وَقَالَ (عَلَيْهِ السَّلامُ) – إغْتَنِمْ خَمْسًا قَبْلَ خَمْسٍ – \n", + "حِكَايَاتْ مُخْتَلِفَةْ\n", + "شَبَابَكَ قَبْلَ هَرَمِكَ – وَصَحَّتَكَ قَبْلَ سَقَمِكَ – وَغِنَاكَ \n", + "قَبْلَ فَقْرِكَ – وَفرَاغَكَ قَبْلَ شُغْلِكَ وَحَيوتَكَ قَبْلَ مَوْتِكَ * \n", + "14- الحِكَايَةُ الرَّابِعَةُ عِشَرَ\n", + "عَنِ ابنِ الخَرِيفِ – قَالَ حَدَّثَنِي وَالدِي – قَالَ أَعْطَيْتُ \n", + "أَحْمَدَ بْنَ السَّبَّ الدَّلاَّلَ ثَوبًا – وَقُلتُ بِعهُ لِي * وَبَيّنْ هذَا\n", + "العَيْبَ الَّذِي فِيهِ لِمَنْ يَشْتَرِيهِ – وَأَرَيْتُهُ خَرقًا فِي الثَّوبِ * \n", + "فَمَضي وَجَاءَ فِي آخِرِ النَّهَارِ – فَدَفَعَ إلَيَّ ثَمَنَهُ – وَقَالَ بَعتُهُ \n", + "عَلَي رَجُلٍ أَعجَمِيٍ غَرِيبٍ بِهذِهِ الدَّنَانِيرِ * فَقُلتُ لَهُ – وَأَرَيْتَهُ \n", + "العَيْبَ وَأَعْلَمْتَهُ بِهِ؟ فَقَالَ لاَ وَاللَّهِ نَسَيْتُ ذلِكَ – فَقُلْتُ لاَ \n", + "جَزَاكَ اللَّهُ خَيرًا إِمضِ مَعِي إلَيهِ * وَذَهَبْتُ مَعَهُ وَقَصَدْنَا\n", + "مَكَانَهُ فَلَمْ نَجِدْهُ * فَسَأَلْنَا عَنْهُ فَقِيلَ إِنَّهُ رَحَلَ إِلَي مَكَّةَ مَعْ \n", + "قَافِلَةِ الحَاجِ * فَأَخذتُ صِفَةَ الرَّجُلِ مِنَ الدَّلاَّلِ وَاكتَرَيْتُ \n", + "دَابَّةً وَلَحِقتُ القَافِلَةَ وَسَأَلتُ عَنِ الرَّجُلِ فَدُلِلْتُ عَلَيْهِ فَقُلْتُ \n", + "لَهُ – الثَوْبُ الفُلاَنِيٌّ الَّذِي شَرَيتَهُ أَمسِ مِن فُلاَنٍ بِكَذَا وَكَذا \n", + "فِيهِ عَيْب – فَهَاتِهِ وَخُذْ ذَهَبَكَ * فَقَامَ وَأَخرَجَ الثَّوبَ وَطَافَ \n", + "حِكَايَات مُخْتَلِفَة\n", + "عَلَي العَيْبِ حتْي وَجَدَهُ * فَلَمَّا رَأَهُ قَالَ – يَا شَيْخُ أَخْرِجْ \n", + "ذَهَبِي حَتَّي أَرَاهُ وَكُنْتُ لَمَّا قَبَضْتُهُ لَمْ أُمَيِزْ وَلَمْ أَنَتِقدْهُ\n", + "فَأَخْرَجْتُهُ – فَلَمَّا رَأَهُ قَالَ هذَا ذَهَبِي اْنّتَقِدْهُ يَا شَيْخُ * قَالَ \n", + "فَنَظرتُ فَإِذَا هُو مَغشُوش لاَ يُسَاوي شَيئًا * فَأَخَذَهُ وَرَمي بِهِ \n", + "وَقَالَ لِي – قَدْ أَشْتَرَيْتُ مِنْكِ هذَا الثَّوبَ عَلَي عَيْبَهِ بِهذَا \n", + "الذَّهَبِ * وَدَفَعَ إِلَيَّ بِمِقْدَارِ ذلِكَ الذَّهَبِ اْلمَغْشُوشِ ذَهَبًا \n", + "جَيِدًا وَعُدْتُ بِهِ *\n", + "15- الّحِكَاَيةُ الخَامِسَةُ عَشَرَ\n", + "قِيلَ أَنَّ مَلِكَ الصِينَ بَلَغَهُ عَنْ نَقَّاشٍ مَاهِرٍ فِي النّقْشِ \n", + "وَألتَّصْوِيرِ فِي بِلاَدِ الرومِ * فَأَرْسَلَ إِلَيْهِ وأَشْخَصَهُ وَأَمَرَهُ بِعَمَلِ \n", + "شَيءٍ مِمَّا يَقْدِرُ عَلَيْهِ مِنَ النَّقْشِ والتَّصوِيرِ مِثَالاً يُعَلِقُهُ بِبَابِ \n", + "القَصْرِ عَلَي العَادَةِ * فَنَقَشَ لَهُ فِي رُقعَةٍ صُورَةَ سُنْبُلَةِ حِنطَةٍ \n", + "خَضْرَآءَ قَائِمَةً وَعَلَيْهَا عُصْفُور – وَأَتْقَنَ نَقْشَهُ وَهَيبَتَهُ حَتَّي \n", + "إِذَا نَظَرهُ أَحَد لاَ يَشُكٌ فِي أَنَّهُ عُصْفُورْ عَلي سُنْبُلَةٍ خَضْرآءَ \n", + "وَلاَ يُنْكِرُ شَيْئًا مِنْ ذلِكَ غَير النُطقِ وَالحَرَكَةِ * فَأَعجَبَ المّلِكُ \n", + "حِكَايَاتْ مُخْتَلِفَةُ\n", + "ذلِكَ وَأَمَرَهُ بِتَعْلِيقِهِ وَبَادَرَ بِإِدْرَار الرِزّقْ عَلَيهِ إلَي إنقِضَاءِ مُدَّةِ \n", + "التَّعلِيقِ * فَمَضَتْ سَنَةْ إلاَّ بَعْضَ أَيَّامٍ وَلَمْ يَقْدرْ أَحَدً عَلَي\n", + "إِظْهَارِ عَيْبٍ أَوْ خَلَلٍ فِيهِ * فَحَضَرَ شَيْخً مُسِنّ وَنَظَرَ إلَي الْمِثَالِ \n", + "وَقَالَ – هذَا فِيهِ عيبْ * فَأحْضِرَ إِلَي اْلّمَلِكِ وَاَحْضِرَ النَّقَّاشُ \n", + "وَاْلمِثَالُ – وَقَالَ مَاْ الَّذِي فِهِ مِنَ العَيْبِ فَأَخْرِجْ عَمَّا وَقَعْتَ \n", + "فِيهِ بِوَجهٍ ظَاهِرٍ وَدَليلٍ وَإلاَّ حَلَّ بِكَ النَّدَمُ وَاْلتَّنْكِيلُ * فَقَالَ \n", + "الشَّيْخُ أَسْعَدَ اللَّهُ اْلّمَلِكَ وَأَلْهَمَهُ السّدَادَ- مِثَالُ أَيّ شَيءٍ \n", + "هذَا المَوْضُوعُ؟ فَقالَ المّلِكُ مِثَالُ سُنْبُلَةٍ مِنْ حِنْطَةٍ قَائِمَةٍ \n", + "عَلَي سَاقِهاَ وَفَوْقَهَا عُصْفُور * فَقَالَ الشَّيْخُ أَصْلَحَ اللَّهْ المَلِكَ \n", + "أَمَّا العُصْفُورُ فَلَيْسَ بِهِ خَلَلْ وَإِنَّمَا الخَلَلُ فِي وَضْع السٌنْبُلَةِ \n", + "قَالَ المَلِكُ وَمَا الخَلَلُ وَقَدْ إِمْتَزَجَ غَضَبًا عَلَي الشَّيْخ * فَقَالَ \n", + "الّخَلَلُ فِي إِستِقَامَةِ السُّنْبُلَةِ لإِنَّ فِي الُعرفِ أَنَّ العُصَفُورَ إِذَا \n", + "حَطَّ عَلَي سُنْبُلَةٍ أَمَالَهَا لِثِقْل العُصْفُورِ وَضُعْفِ سَاقِ السُّنْبُلَةِ \n", + "وَلَو كَانَتِ السُّنْبُلَةُ مُعْوَجَّةً مَائِلَةً لَكَانَ ذلَكَ نِهَايَةً فِي الوَضعِ \n", + "وَالحِكْمَةِ * فَوَافَقَ المَلِكُ عَلَي ذلِكَ وَسَلَّمَ * \n", + "حِكَايَاتْ مُخْتَلِفَةْ\n", + "16- أَلْحِكَايَةُ السَّادِسَةُ عَشَرَ\n", + "قِيلَ أنَّ عَبدَ المَلِكِ بنُ مَرَوَانِ خَطَبَ يَومًا بِالكُوفةِ *\n", + "فَقَاَم إلَيْهِ رَجْلْ مِنْ آلِ سَمْعَاَن – فَقَالَ مَهلاً يَا أَمِيرَ \n", + "المْؤمِنِينَ إقْضِ لِصَاحِبِي هذَا بَحَقّهِ ثُمَّ اخْطُبْ – فَقَالَ وَمَا \n", + "ذاكَ ؟ فَقَالَ إنَّ النَّاسَ قَالُوا لَهُ مَا يُخَلِصُ ظُلاَمَتَكَ مِنْ \n", + "عَبْدِ المَلِكِ إلاَّ فُلاَن * فَجِئنُ بِهِ إِلَيْكَ لِأَنظُرَ عَدْلَكَ الذَّي \n", + "كُنْتَ تَعِدُنَا بَهِ قَبْلَ أنْ تَتَوَلَّي هِذِهِ المَظَالِمَ * فَطَالَ بَيْنَهُ \n", + "وَبيَنَْهُ الكَلاَمُ – فَقَالَ لَهُ الرَّجُلُ يَا أَمِيرَ المُؤْمِنِينَ إِنَّكُمْ تَأْمُرُونَ\n", + "وَلاَ تَأتَمِرُونَ – وَتَنْهُونَ وَلاَ تَنْتَهُونَ – وَتَعِظُونَ وَلاَ تَتَّعظُونَ \n", + "أَفَنَقْتَدِي بِسِيرَتِكُمْ فِي أَنْفُسِكُمْ أَمْ نَطِيعُ أَمْرَكُمْ بِألْسِنَتِكُمْ ؟ \n", + "وَإنْ قُلُْمْ أَطِيعُوا أَمْرَنَا وأقْبِلُوا نُصْحَنَا – فَكَيْفَ يَنْصَحُ غَيْرَهُ \n", + "مَنْ غَشَّ نَفْسَهُ ؟ وَإنْ قُلْتُمْ خُذُوا الحِكْمَةَ حَيْثَ وَجَدْتُمُوهَا \n", + "وَاقْبِلُوا آلْعِظَةَ مِمَّنْ سَمَعْتُمُوهَا – فَعَلَي مَ قَلَّدْنَاكُمْ أَزِمَّةَ \n", + "أُمُورِنَا وَحَكَّمْنَاكُمْ فِي دِمَائِنَا وَأَموَالِنَا أَو مَا تَعلَمُونَ أَنَّ مَنَّا \n", + "مَنْ هُو أَعرَفُ مَنْكُمْ بَصُنُوفِ اللغَاتِ وَأَبلَغُ فِي العِظَاتِ؟ فَإنْ\n", + "حِكَايَاتْ مُخْتَلِفَةْ\n", + "كَانَتِ الإمَامَةُ قَدْ عَجَزْتُمْ عَنْ إقَامَةِ العَدْلِ فِيهَا فَخَلُّوا سَبِيلَهَا\n", + "وَاْطْلُقُوا عِقَالَهَا يَبْتَدِرُهَا أَهْلُهَا اْلَّذِينَ قَاتلْتُمُوهُمْ فِي الْبِلادِ\n", + "وَشَتَّتَّمْ شَمْلَهُمْ بِكُلّ وَادٍ أَمَّا وَاْاللّّه لَإن بَقِيَتْ فِي يَدِكُمْ إلَي\n", + "بُلُوغِ الغَايَةِ وَاْستِيفَاءِ المُدَّةِ تُضْمَحِلُّ حُقُوقُ الله وَحُقُوقُ \n", + "العِبَادِ؟ فَقَالَ لَهُ كَيْفَ ذلِكَ ؟ فَقَالَ لِأَنَّ مَنْ كَلَّمَكُمْ فيِ حَقّهِ \n", + "زُجِرَ وَمَنْ سَكَتَ قُهِرَ فَلاَ قَوْلُهُ مَسْمُوعْ وَلاَ ظُلْمُهُ مَرْفُوعْ وَلاَ \n", + "مَنْ جَارَ عَلَيْهِ مَرْدُوعْ * وَبَيْنَكَ وَبَيْنَ رَعِيَّتِكَ مَقَامْ تّذُوبُ\n", + "فِيهِ الجِبَالُ حَيْثُ ملَكْكَ هُنَاكَ خَامِلُ وَعِزٌّكَ زَائِلْ وَنَاصِرُكَ\n", + "خَاذِلْ وَالحَاكِمُ عَلَيْكَ عَادِلْ * فَأَكَبَّ عَبْدُ الملِكِ عَلَي وَجْهِهِ \n", + "يَبْكِي – ثُمَّ قَالَ لَهُ – فَمَا حَاجَتُكَ ؟ فَقَالَ عَامِلُكَ بِالسَّمَاوَةِ\n", + "ظَلَمَنِي وَلَيْلُهُ لَهْوْ وَنَهَارُهُ لَغو وَنَظَرُهُ زَهْوْ * فَكَتَبَ إلَيْهِ بإعْطَائِهِ \n", + "ظُلَامَتَهُ ثُمَّ عَزَلَهُ * \n", + "SECTION IV.Extracts from the Qur’anسَتُّ سُوَرٍ مَنَ القُرآنِ\n", + "1- سُورَةُ الفَاتِحَةِ مَكِيّةْ وَهيَ سِبْعُ آيَاتٍ\n", + "بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ * 1 الْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ * \n", + "2 الرَّحْمَنِ الرَّحِيمِ * 3 مَالِكِ يَوْمِ الدِّينِ * 4 إِيَّاكَ نَعْبُدُ \n", + "وَإِيَّاكَ نَسْتَعِينُ * 5 اهْدِنَا الصِّرَاطَ الْمُسْتَقِيمَ * 6 صِرَاطَ الَّذِينَ \n", + "أَنْعَمْتَ عَلَيْهِمْ * 7 غَيْرِ الْمَغْضُوبِ عَلَيْهِمْ وَلَا الضَّالِّينَ *\n", + "2- سُورَةُ التَّغَابُنِ مَكِيَّةْ وَهِيَ ثَمَانِي عَشْرَةَ آيَةً\n", + "بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ *1 يُسَبِّحُ لِلَّهِ مَا فِي السَّمَاوَاتِ \n", + "وَمَا فِي الْأَرْضِ لَهُ الْمُلْكُ وَلَهُ الْحَمْدُ وَهُوَ عَلَى كُلِّ شَيْءٍ\n", + "سَتُّ سُوَرٍ مَنَ القُرآنِ\n", + "قَدِيرٌ * 2 هُوَ الَّذِي خَلَقَكُمْ فَمِنْكُمْ كَافِرٌ وَمِنْكُمْ مُؤْمِنٌ وَاللَّهُ \n", + "بِمَا تَعْمَلُونَ بَصِيرٌ * 3 خَلَقَ السَّمَاوَاتِ وَالْأَرْضَ بِالْحَقِّ \n", + "وَصَوَّرَكُمْ فَأَحْسَنَ صُوَرَكُمْ وَإِلَيْهِ الْمَصِيرُ * 4 يَعْلَمُ مَا فِي السَّمَاوَاتِ \n", + "وَالْأَرْضِ وَيَعْلَمُ مَا تُسِرُّونَ وَمَا تُعْلِنُونَ وَاللَّهُ عَلِيمٌ بِذَاتِ \n", + "الصُّدُورِ * 5 أَلَمْ يَأْتِكُمْ نَبَأُ الَّذِينَ كَفَرُوا مِنْ قَبْلُ فَذَاقُوا \n", + "وَبَالَ أَمْرِهِمْ وَلَهُمْ عَذَابٌ أَلِيمٌ * 6 ذَلِكَ بِأَنَّهُ كَانَتْ تَأْتِيهِمْ \n", + "رُسُلُهُمْ بِالْبَيِّنَاتِ فَقَالُوا أَبَشَرٌ يَهْدُونَنَا فَكَفَرُوا وَتَوَلَّوْا وَاسْتَغْنَى \n", + "اللَّهُ وَاللَّهُ غَنِيٌّ حَمِيدٌ * 7 زَعَمَ الَّذِينَ كَفَرُوا أَنْ لَنْ يُبْعَثُوا \n", + "قُلْ بَلَى وَرَبِّي لَتُبْعَثُنَّ ثُمَّ لَتُنَبَّؤُنَّ بِمَا عَمِلْتُمْ وَذَلِكَ عَلَى \n", + "اللَّهِ يَسِيرٌ * 8 فَآَمِنُوا بِاللَّهِ وَرَسُولِهِ وَالنُّورِ الَّذِي أَنْزَلْنَا وَاللَّهُ بِمَا \n", + "تَعْمَلُونَ خَبِيرٌ * 9 يَوْمَ يَجْمَعُكُمْ لِيَوْمِ الْجَمْعِ ذَلِكَ يَوْمُ التَّغَابُنِ \n", + "وَمَنْ يُؤْمِنْ بِاللَّهِ وَيَعْمَلْ صَالِحًا يُكَفِّرْ عَنْهُ سَيِّئَاتِهِ وَيُدْخِلْهُ \n", + "جَنَّاتٍ تَجْرِي مِنْ تَحْتِهَا الْأَنْهَارُ خَالِدِينَ فِيهَا أَبَدًا ذَلِكَ الْفَوْزُ \n", + "الْعَظِيمُ * 10 وَالَّذِينَ كَفَرُوا وَكَذَّبُوا بِآَيَاتِنَا أُولَئِكَ أَصْحَابُ \n", + "النَّارِ خَالِدِينَ فِيهَا وَبِئْسَ الْمَصِيرُ *11 مَا أَصَابَ مِنْ مُصِيبَةٍ\n", + "سِتُّ سُوَرٍ مَنَ القُرآنِ\n", + "إِلَّا بِإِذْنِ اللَّهِ وَمَنْ يُؤْمِنْ بِاللَّهِ يَهْدِ قَلْبَهُ وَاللَّهُ بِكُلِّ شَيْءٍ \n", + "عَلِيمٌ * 12 وَأَطِيعُوا اللَّهَ وَأَطِيعُوا الرَّسُولَ فَإِنْ تَوَلَّيْتُمْ فَإِنَّمَا \n", + "عَلَى رَسُولِنَا الْبَلَاغُ الْمُبِينُ * 13 اللَّهُ لَا إِلَهَ إِلَّا هُوَ وَعَلَى اللَّهِ \n", + "فَلْيَتَوَكَّلِ الْمُؤْمِنُونَ * 14 يَا أَيُّهَا الَّذِينَ آَمَنُوا إِنَّ مِنْ أَزْوَاجِكُمْ \n", + "وَأَوْلَادِكُمْ عَدُوًّا لَكُمْ فَاحْذَرُوهُمْ وَإِنْ تَعْفُوا وَتَصْفَحُوا وَتَغْفِرُوا \n", + "فَإِنَّ اللَّهَ غَفُورٌ رَحِيمٌ * 15 إِنَّمَا أَمْوَالُكُمْ وَأَوْلَادُكُمْ فِتْنَةٌ وَاللَّهُ \n", + "عِنْدَهُ أَجْرٌ عَظِيمٌ * 16 فَاتَّقُوا اللَّهَ مَا اسْتَطَعْتُمْ وَاسْمَعُوا وَأَطِيعُوا \n", + "وَأَنْفِقُوا خَيْرًا لِأَنْفُسِكُمْ وَمَنْ يُوقَ شُحَّ نَفْسِهِ فَأُولَئِكَ هُمُ \n", + "الْمُفْلِحُونَ * 17 إِنْ تُقْرِضُوا اللَّهَ قَرْضًا حَسَنًا يُضَاعِفْهُ لَكُمْ \n", + "وَيَغْفِرْ لَكُمْ وَاللَّهُ شَكُورٌ حَلِيمٌ * 18 عَالِمُ الْغَيْبِ وَالشَّهَادَةِ الْعَزِيزُ \n", + "الْحَكِيمُ *\n", + "3- سُورَةُ الإنْسَّانِ والدَّهرِ مَكِيَّةْ وَهيَ إحدَ وثَلثَوُنَ آيَةً\n", + "بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ * 1 هَلْ أَتَى عَلَى الْإِنْسَانِ حِينٌ مِنَ الدَّهْرِ لَمْ يَكُنْ شَيْئًا مَذْكُورًا * 2 إِنَّا خَلَقْنَا الْإِنْسَانَ مِنْ\n", + "سِتُّ سُوَرٍ مَنَ القُرآنِ\n", + "نُطْفَةٍ أَمْشَاجٍ نَبْتَلِيهِ فَجَعَلْنَاهُ سَمِيعًا بَصِيرًا * 3 إِنَّا هَدَيْنَاهُ \n", + "السَّبِيلَ إِمَّا شَاكِرًا وَإِمَّا كَفُورًا * 4 إِنَّا أَعْتَدْنَا لِلْكَافِرِينَ سَلَاسِلَ \n", + "وَأَغْلَالًا وَسَعِيرًا * 5 إِنَّ الْأَبْرَارَ يَشْرَبُونَ مِنْ كَأْسٍ كَانَ مِزَاجُهَا \n", + "كَافُورًا * 6 عَيْنًا يَشْرَبُ بِهَا عِبَادُ اللَّهِ يُفَجِّرُونَهَا تَفْجِيرًا *7 \n", + "يُوفُونَ بِالنَّذْرِ وَيَخَافُونَ يَوْمًا كَانَ شَرُّهُ مُسْتَطِيرًا * 8 وَيُطْعِمُونَ \n", + "الطَّعَامَ عَلَى حُبِّهِ مِسْكِينًا وَيَتِيمًا وَأَسِيرًا * 9 إِنَّمَا نُطْعِمُكُمْ \n", + "لِوَجْهِ اللَّهِ لَا نُرِيدُ مِنْكُمْ جَزَاءً وَلَا شُكُورًا * 10 إِنَّا نَخَافُ مِنْ رَبِّنَا \n", + "يَوْمًا عَبُوسًا قَمْطَرِيرًا * 11 فَوَقَاهُمُ اللَّهُ شَرَّ ذَلِكَ الْيَوْمِ وَلَقَّاهُمْ نَضْرَةً \n", + "وَسُرُورًا *12 وَجَزَاهُمْ بِمَا صَبَرُوا جَنَّةً وَحَرِيرًا * 13 مُتَّكِئِينَ \n", + "فِيهَا عَلَى الْأَرَائِكِ لَا يَرَوْنَ فِيهَا شَمْسًا وَلَا زَمْهَرِيرًا * 14 وَدَانِيَةً \n", + "عَلَيْهِمْ ظِلَالُهَا وَذُلِّلَتْ قُطُوفُهَا تَذْلِيلًا * 15 وَيُطَافُ عَلَيْهِمْ بِآَنِيَةٍ \n", + "مِنْ فِضَّةٍ وَأَكْوَابٍ كَانَتْ قَوَارِيرَ * 16 قَوَارِيرَ مِنْ فِضَّةٍ قَدَّرُوهَا \n", + "تَقْدِيرًا * 17 وَيُسْقَوْنَ فِيهَا كَأْسًا كَانَ مِزَاجُهَا زَنْجَبِيلًا * 18 عَيْنًا \n", + "فِيهَا تُسَمَّى سَلْسَبِيلًا * 19 وَيَطُوفُ عَلَيْهِمْ وِلْدَانٌ مُخَلَّدُونَ \n", + "إِذَا رَأَيْتَهُمْ حَسِبْتَهُمْ لُؤْلُؤًا مَنْثُورًا * 20 وَإِذَا رَأَيْتَ ثَمَّ رَأَيْتَ نَعِيمًا\n", + "سِتُّ سُوَرٍ مَنَ القُرآنِ\n", + "وَمُلْكًا كَبِيرًا * 21 عَالِيَهُمْ ثِيَابُ سُنْدُسٍ خُضْرٌ وَإِسْتَبْرَقٌ وَحُلُّوا \n", + "أَسَاوِرَ مِنْ فِضَّةٍ وَسَقَاهُمْ رَبُّهُمْ شَرَابًا طَهُورًا * 22 إِنَّ هَذَا كَانَ \n", + "لَكُمْ جَزَاءً وَكَانَ سَعْيُكُمْ مَشْكُورًا * 23 إِنَّا نَحْنُ نَزَّلْنَا عَلَيْكَ \n", + "الْقُرْآَنَ تَنْزِيلًا فَاصْبِرْ لِحُكْمِ رَبِّكَ وَلَا تُطِعْ مِنْهُمْ آَثِمًا أَوْ كَفُورًا *\n", + "24 وَاذْكُرِ اسْمَ رَبِّكَ بُكْرَةً وَأَصِيلًا * 25 وَمِنَ اللَّيْلِ فَاسْجُدْ لَهُ \n", + "وَسَبِّحْهُ لَيْلًا طَوِيلًا * 26 إِنَّ هَؤُلَاءِ يُحِبُّونَ الْعَاجِلَةَ وَيَذَرُونَ وَرَاءَهُمْ \n", + "يَوْمًا ثَقِيلًا * 27 نَحْنُ خَلَقْنَاهُمْ وَشَدَدْنَا أَسْرَهُمْ وَإِذَا شِئْنَا \n", + "بَدَّلْنَا أَمْثَالَهُمْ تَبْدِيلًا * 28 إِنَّ هَذِهِ تَذْكِرَةٌ فَمَنْ شَاءَ اتَّخَذَ \n", + "إِلَى رَبِّهِ سَبِيلًا * 29 وَمَا تَشَاءُونَ إِلَّا أَنْ يَشَاءَ اللَّهُ إِنَّ اللَّهَ \n", + "كَانَ عَلِيمًا حَكِيمًا * 30 يُدْخِلُ مَنْ يَشَاءُ فِي رَحْمَتِهِ وَالظَّالِمِينَ \n", + "أَعَدَّ لَهُمْ عَذَابًا أَلِيمًا *\n", + "4- سُورَةُ الصَّفِ مَدَنِيَّة وَهِيَ أَربَعَ عَشرَةَ آيَةً\n", + "بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ * 1 سَبَّحَ لِلَّهِ مَا فِي السَّمَاوَاتِ\n", + "سِتُّ سُوَرٍ مِنَ القُرْآنِ\n", + "وَمَا فِي الْأَرْضِ وَهُوَ الْعَزِيزُ الْحَكِيمُ * 1 يَا أَيُّهَا الَّذِينَ آَمَنُوا \n", + "لِمَ تَقُولُونَ مَا لَا تَفْعَلُونَ * 3 كَبُرَ مَقْتًا عِنْدَ اللَّهِ أَنْ تَقُولُوا \n", + "مَا لَا تَفْعَلُونَ * 4 إِنَّ اللَّهَ يُحِبُّ الَّذِينَ يُقَاتِلُونَ فِي سَبِيلِهِ \n", + "صَفًّا كَأَنَّهُمْ بُنْيَانٌ مَرْصُوصٌ * 5 وَإِذْ قَالَ مُوسَى لِقَوْمِهِ يَا قَوْمِ \n", + "لِمَ تُؤْذُونَنِي وَقَدْ تَعْلَمُونَ أَنِّي رَسُولُ اللَّهِ إِلَيْكُمْ فَلَمَّا زَاغُوا \n", + "أَزَاغَ اللَّهُ قُلُوبَهُمْ وَاللَّهُ لَا يَهْدِي الْقَوْمَ الْفَاسِقِينَ * 6 وَإِذْ قَالَ \n", + "عِيسَى ابْنُ مَرْيَمَ يَا بَنِي إِسْرَائِيلَ إِنِّي رَسُولُ اللَّهِ إِلَيْكُمْ \n", + "مُصَدِّقًا لِمَا بَيْنَ يَدَيَّ مِنَ التَّوْرَاةِ وَمُبَشِّرًا بِرَسُولٍ يَأْتِي مِنْ \n", + "بَعْدِي اسْمُهُ أَحْمَدُ فَلَمَّا جَاءَهُمْ بِالْبَيِّنَاتِ قَالُوا هَذَا سِحْرٌ \n", + "مُبِينٌ * 7 وَمَنْ أَظْلَمُ مِمَّنِ افْتَرَى عَلَى اللَّهِ الْكَذِبَ وَهُوَ \n", + "يُدْعَى إِلَى الْإِسْلَامِ وَاللَّهُ لَا يَهْدِي الْقَوْمَ الظَّالِمِينَ * 8 يُرِيدُونَ \n", + "لِيُطْفِئُوا نُورَ اللَّهِ بِأَفْوَاهِهِمْ وَاللَّهُ مُتِمُّ نُورِهِ وَلَوْ كَرِهَ الْكَافِرُونَ *\n", + "9 هُوَ الَّذِي أَرْسَلَ رَسُولَهُ بِالْهُدَى وَدِينِ الْحَقِّ لِيُظْهِرَهُ عَلَى \n", + "الدِّينِ كُلِّهِ وَلَوْ كَرِهَ الْمُشْرِكُونَ * 10 يَا أَيُّهَا الَّذِينَ آَمَنُوا\n", + "سِتُّ سُوَرٍ مِنَ القُرْآنِ\n", + "هَلْ أَدُلُّكُمْ عَلَى تِجَارَةٍ تُنْجِيكُمْ مِنْ عَذَابٍ أَلِيمٍ * 11 تُؤْمِنُونَ \n", + "بِاللَّهِ وَرَسُولِهِ وَتُجَاهِدُونَ فِي سَبِيلِ اللَّهِ بِأَمْوَالِكُمْ وَأَنْفُسِكُمْ \n", + "ذَلِكُمْ خَيْرٌ لَكُمْ إِنْ كُنْتُمْ تَعْلَمُونَ * 12 يَغْفِرْ لَكُمْ ذُنُوبَكُمْ \n", + "وَيُدْخِلْكُمْ جَنَّاتٍ تَجْرِي مِنْ تَحْتِهَا الْأَنْهَارُ وَمَسَاكِنَ طَيِّبَةً \n", + "فِي جَنَّاتِ عَدْنٍ ذَلِكَ الْفَوْزُ الْعَظِيمُ * 13 وَأُخْرَى تُحِبُّونَهَا \n", + "نَصْرٌ مِنَ اللَّهِ وَفَتْحٌ قَرِيبٌ وَبَشِّرِ الْمُؤْمِنِينَ * 14 يَا أَيُّهَا الَّذِينَ \n", + "آَمَنُوا كُونُوا أَنْصَارَ اللَّهِ كَمَا قَالَ عِيسَى ابْنُ مَرْيَمَ لِلْحَوَارِيِّينَ \n", + "مَنْ أَنْصَارِي إِلَى اللَّهِ قَالَ الْحَوَارِيُّونَ نَحْنُ أَنْصَارُ اللَّهِ فَآَمَنَتْ \n", + "طَائِفَةٌ مِنْ بَنِي إِسْرَائِيلَ وَكَفَرَتْ طَائِفَةٌ فَأَيَّدْنَا الَّذِينَ آَمَنُوا \n", + "عَلَى عَدُوِّهِمْ فَأَصْبَحُوا ظَاهِرِينَ *\n", + "5- سُورَةُ لُقْمَانَ مَكِّيَّةْ وَهيَ أَربَعَ وثَلَثُونَ آيَةً\n", + "بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ * 1 الم تِلْكَ آَيَاتُ الْكِتَابِ الْحَكِيمِ * 2 هُدًى وَرَحْمَةً لِلْمُحْسِنِينَ * 3 الَّذِينَ يُقِيمُونَ\n", + "سِتُّ سُوَرٍ مَنَ القُرآنِ\n", + "الصَّلَاةَ وَيُؤْتُونَ الزَّكَاةَ وَهُمْ بِالْآَخِرَةِ هُمْ يُوقِنُونَ * 4 أُولَئِكَ \n", + "عَلَى هُدًى مِنْ رَبِّهِمْ وَأُولَئِكَ هُمُ الْمُفْلِحُونَ * 5 وَمِنَ النَّاسِ \n", + "مَنْ يَشْتَرِي لَهْوَ الْحَدِيثِ لِيُضِلَّ عَنْ سَبِيلِ اللَّهِ بِغَيْرِ عِلْمٍ \n", + "وَيَتَّخِذَهَا هُزُوًا أُولَئِكَ لَهُمْ عَذَابٌ مُهِينٌ * 6 وَإِذَا تُتْلَى عَلَيْهِ \n", + "آَيَاتُنَا وَلَّى مُسْتَكْبِرًا كَأَنْ لَمْ يَسْمَعْهَا كَأَنَّ فِي أُذُنَيْهِ وَقْرًا فَبَشِّرْهُ \n", + "بِعَذَابٍ أَلِيمٍ * 7 إِنَّ الَّذِينَ آَمَنُوا وَعَمِلُوا الصَّالِحَاتِ لَهُمْ \n", + "جَنَّاتُ النَّعِيمِ * 8 خَالِدِينَ فِيهَا وَعْدَ اللَّهِ حَقًّا وَهُوَ الْعَزِيزُ \n", + "الْحَكِيمُ * 9 خَلَقَ السَّمَاوَاتِ بِغَيْرِ عَمَدٍ تَرَوْنَهَا وَأَلْقَى فِي \n", + "الْأَرْضِ رَوَاسِيَ أَنْ تَمِيدَ بِكُمْ وَبَثَّ فِيهَا مِنْ كُلِّ دَابَّةٍ وَأَنْزَلْنَا \n", + "مِنَ السَّمَاءِ مَاءً فَأَنْبَتْنَا فِيهَا مِنْ كُلِّ زَوْجٍ كَرِيمٍ * 10 هَذَا \n", + "خَلْقُ اللَّهِ فَأَرُونِي مَاذَا خَلَقَ الَّذِينَ مِنْ دُونِهِ بَلِ الظَّالِمُونَ \n", + "فِي ضَلَالٍ مُبِينٍ * 11 وَلَقَدْ آَتَيْنَا لُقْمَانَ الْحِكْمَةَ أَنِ اشْكُرْ \n", + "لِلَّهِ وَمَنْ يَشْكُرْ فَإِنَّمَا يَشْكُرُ لِنَفْسِهِ وَمَنْ كَفَرَ فَإِنَّ اللَّهَ غَنِيٌّ \n", + "حَمِيدٌ * 12 وَإِذْ قَالَ لُقْمَانُ لِابْنِهِ وَهُوَ يَعِظُهُ يَا بُنَيَّ لَا تُشْرِكْ \n", + "بِاللَّهِ إِنَّ الشِّرْكَ لَظُلْمٌ عَظِيمٌ * 13 وَوَصَّيْنَا الْإِنْسَانَ بِوَالِدَيْهِ\n", + "سِتُّ سُوَرٍ مَنَ القُرآنِ\n", + "حَمَلَتْهُ أُمُّهُ وَهْنًا عَلَى وَهْنٍ وَفِصَالُهُ فِي عَامَيْنِ أَنِ اشْكُرْ لِي \n", + "وَلِوَالِدَيْكَ إِلَيَّ الْمَصِيرُ * 14 وَإِنْ جَاهَدَاكَ عَلى أَنْ تُشْرِكَ \n", + "بِي مَا لَيْسَ لَكَ بِهِ عِلْمٌ فَلَا تُطِعْهُمَا وَصَاحِبْهُمَا فِي الدُّنْيَا \n", + "مَعْرُوفًا وَاتَّبِعْ سَبِيلَ مَنْ أَنَابَ إِلَيَّ ثُمَّ إِلَيَّ مَرْجِعُكُمْ فَأُنَبِّئُكُمْ \n", + "بِمَا كُنْتُمْ تَعْمَلُونَ * 15 يَا بُنَيَّ إِنَّهَا إِنْ تَكُ مِثْقَالَ حَبَّةٍ مِنْ \n", + "خَرْدَلٍ فَتَكُنْ فِي صَخْرَةٍ أَوْ فِي السَّمَاوَاتِ أَوْ فِي الْأَرْضِ يَأْتِ \n", + "بِهَا اللَّهُ إِنَّ اللَّهَ لَطِيفٌ خَبِيرٌ * 16 يَا بُنَيَّ أَقِمِ الصَّلَاةَ وَأْمُرْ \n", + "بِالْمَعْرُوفِ وَانْهَ عَنِ الْمُنْكَرِ وَاصْبِرْ عَلَى مَا أَصَابَكَ إِنَّ ذَلِكَ \n", + "مِنْ عَزْمِ الْأُمُورِ * 17 وَلَا تُصَعِّرْ خَدَّكَ لِلنَّاسِ وَلَا تَمْشِ فِي \n", + "الْأَرْضِ مَرَحًا إِنَّ اللَّهَ لَا يُحِبُّ كُلَّ مُخْتَالٍ فَخُورٍ * 18 وَاقْصِدْ \n", + "فِي مَشْيِكَ وَاغْضُضْ مِنْ صَوْتِكَ إِنَّ أَنْكَرَ الْأَصْوَاتِ لَصَوْتُ \n", + "الْحَمِيرِ * 19 أَلَمْ تَرَوْا أَنَّ اللَّهَ سَخَّرَ لَكُمْ مَا فِي السَّمَاوَاتِ وَمَا \n", + "فِي الْأَرْضِ وَأَسْبَغَ عَلَيْكُمْ نِعَمَهُ ظَاهِرَةً وَبَاطِنَةً وَمِنَ النَّاسِ \n", + "مَنْ يُجَادِلُ فِي اللَّهِ بِغَيْرِ عِلْمٍ وَلَا هُدًى وَلَا كِتَابٍ مُنِيرٍ *\n", + "20 وَإِذَا قِيلَ لَهُمُ اتَّبِعُوا مَا أَنْزَلَ اللَّهُ قَالُوا بَلْ نَتَّبِعُ مَا وَجَدْنَا\n", + "سِتٌّ سُوَرٍ مَنَ القُرْآنِ\n", + "عَلَيْهِ آَبَاءَنَا أَوَلَوْ كَانَ الشَّيْطَانُ يَدْعُوهُمْ إِلَى عَذَابِ السَّعِيرِ * \n", + "21 وَمَنْ يُسْلِمْ وَجْهَهُ إِلَى اللَّهِ وَهُوَ مُحْسِنٌ فَقَدِ اسْتَمْسَكَ \n", + "بِالْعُرْوَةِ الْوُثْقَى وَإِلَى اللَّهِ عَاقِبَةُ الْأُمُورِ * 22 وَمَنْ كَفَرَ فَلَا \n", + "يَحْزُنْكَ كُفْرُهُ إِلَيْنَا مَرْجِعُهُمْ فَنُنَبِّئُهُمْ بِمَا عَمِلُوا إِنَّ اللَّهَ عَلِيمٌ \n", + "بِذَاتِ الصُّدُورِ * 23 نُمَتِّعُهُمْ قَلِيلًا ثُمَّ نَضْطَرُّهُمْ إِلَى عَذَابٍ \n", + "غَلِيظٍ * 24 وَلَئِنْ سَأَلْتَهُمْ مَنْ خَلَقَ السَّمَاوَاتِ وَالْأَرْضَ لَيَقُولُنَّ \n", + "اللَّهُ قُلِ الْحَمْدُ لِلَّهِ بَلْ أَكْثَرُهُمْ لَا يَعْلَمُونَ * 25 لِلَّهِ مَا فِي \n", + "السَّمَاوَاتِ وَالْأَرْضِ إِنَّ اللَّهَ هُوَ الْغَنِيُّ الْحَمِيدُ * 26 وَلَوْ أَنَّ \n", + "مَا فِي الْأَرْضِ مِنْ شَجَرَةٍ أَقْلَامٌ وَالْبَحْرُ يَمُدُّهُ مِنْ بَعْدِهِ سَبْعَةُ \n", + "أَبْحُرٍ مَا نَفِدَتْ كَلِمَاتُ اللَّهِ إِنَّ اللَّهَ عَزِيزٌ حَكِيمٌ * 27 مَا \n", + "خَلْقُكُمْ وَلَا بَعْثُكُمْ إِلَّا كَنَفْسٍ وَاحِدَةٍ إِنَّ اللَّهَ سَمِيعٌ بَصِيرٌ * \n", + "28 أَلَمْ تَرَ أَنَّ اللَّهَ يُولِجُ اللَّيْلَ فِي النَّهَارِ وَيُولِجُ النَّهَارَ فِي اللَّيْلِ \n", + "وَسَخَّرَ الشَّمْسَ وَالْقَمَرَ كُلٌّ يَجْرِي إِلَى أَجَلٍ مُسَمًّى وَأَنَّ \n", + "اللَّهَ بِمَا تَعْمَلُونَ خَبِيرٌ * 29 ذَلِكَ بِأَنَّ اللَّهَ هُوَ الْحَقُّ وَأَنَّ \n", + "مَا يَدْعُونَ مِنْ دُونِهِ الْبَاطِلُ وَأَنَّ اللَّهَ هُوَ الْعَلِيُّ الْكَبِيرُ * 30\n", + "سِتُّ سُوَرٍ مَنَ القُرْآنِ\n", + "أَلَمْ تَرَ أَنَّ الْفُلْكَ تَجْرِي فِي الْبَحْرِ بِنِعْمَةِ اللَّهِ لِيُرِيَكُمْ مِنْ آَيَاتِهِ \n", + "إِنَّ فِي ذَلِكَ لَآَيَاتٍ لِكُلِّ صَبَّارٍ شَكُورٍ * 31 وَإِذَا غَشِيَهُمْ مَوْجٌ \n", + "كَالظُّلَلِ دَعَوُا اللَّهَ مُخْلِصِينَ لَهُ الدِّينَ فَلَمَّا نَجَّاهُمْ إِلَى الْبَرِّ \n", + "فَمِنْهُمْ مُقْتَصِدٌ وَمَا يَجْحَدُ بِآَيَاتِنَا إِلَّا كُلُّ خَتَّارٍ كَفُورٍ * 32 \n", + "يَا أَيُّهَا النَّاسُ اتَّقُوا رَبَّكُمْ وَاخْشَوْا يَوْمًا لَا يَجْزِي وَالِدٌ عَنْ وَلَدِهِ \n", + "وَلَا مَوْلُودٌ هُوَ جَازٍ عَنْ وَالِدِهِ شَيْئًا * 33 إِنَّ وَعْدَ اللَّهِ حَقٌّ \n", + "فَلَا تَغُرَّنَّكُمُ الْحَيَاةُ الدُّنْيَا وَلَا يَغُرَّنَّكُمْ بِاللَّهِ الْغَرُورُ * 34 إِنَّ اللَّهَ \n", + "عِنْدَهُ عِلْمُ السَّاعَةِ وَيُنَزِّلُ الْغَيْثَ وَيَعْلَمُ مَا فِي الْأَرْحَامِ وَمَا \n", + "تَدْرِي نَفْسٌ مَاذَا تَكْسِبُ غَدًا وَمَا تَدْرِي نَفْسٌ بِأَيِّ أَرْضٍ \n", + "تَمُوتُ إِنَّ اللَّهَ عَلِيمٌ خَبِيرٌ *\n", + "6- سُورَةُ يُوسُفَ مَكِّيَّة وَهِيَ مِائَةْ وإحْدَي عَشَرَةَ آيَةً\n", + "بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ * 1 الر تِلْكَ آَيَاتُ الْكِتَابِ الْمُبِينِ * 2 إِنَّا أَنْزَلْنَاهُ قُرْآَنًا عَرَبِيًّا لَعَلَّكُمْ تَعْقِلُونَ * 3 نَحْنُ \n", + "سِتُّ سُوَرٍ مَنَ القُرْآنِ\n", + "نَقُصُّ عَلَيْكَ أَحْسَنَ الْقَصَصِ بِمَا أَوْحَيْنَا إِلَيْكَ هَذَا الْقُرْآَنَ \n", + "وَإِنْ كُنْتَ مِنْ قَبْلِهِ لَمِنَ الْغَافِلِينَ * 4 إِذْ قَالَ يُوسُفُ لِأَبِيهِ \n", + "يَا أَبَتِ إِنِّي رَأَيْتُ أَحَدَ عَشَرَ كَوْكَبًا وَالشَّمْسَ وَالْقَمَرَ رَأَيْتُهُمْ \n", + "لِي سَاجِدِينَ * 5 قَالَ يَا بُنَيَّ لَا تَقْصُصْ رُؤْيَاكَ عَلَى إِخْوَتِكَ \n", + "فَيَكِيدُوا لَكَ كَيْدًا إِنَّ الشَّيْطَانَ لِلْإِنْسَانِ عَدُوٌّ مُبِينٌ *\n", + "6 وَكَذَلِكَ يَجْتَبِيكَ رَبُّكَ وَيُعَلِّمُكَ مِنْ تَأْوِيلِ الْأَحَادِيثِ \n", + "وَيُتِمُّ نِعْمَتَهُ عَلَيْكَ وَعَلَى آَلِ يَعْقُوبَ كَمَا أَتَمَّهَا عَلَى أَبَوَيْكَ \n", + "مِنْ قَبْلُ إِبْرَاهِيمَ وَإِسْحَاقَ إِنَّ رَبَّكَ عَلِيمٌ حَكِيمٌ * 7 لَقَدْ \n", + "كَانَ فِي يُوسُفَ وَإِخْوَتِهِ آَيَاتٌ لِلسَّائِلِينَ * 8 إِذْ قَالُوا لَيُوسُفُ \n", + "وَأَخُوهُ أَحَبُّ إِلَى أَبِينَا مِنَّا وَنَحْنُ عُصْبَةٌ إِنَّ أَبَانَا لَفِي ضَلَالٍ \n", + "مُبِينٍ * 9 اقْتُلُوا يُوسُفَ أَوِ اطْرَحُوهُ أَرْضًا يَخْلُ لَكُمْ وَجْهُ \n", + "أَبِيكُمْ وَتَكُونُوا مِنْ بَعْدِهِ قَوْمًا صَالِحِينَ * 10 قَالَ قَائِلٌ مِنْهُمْ \n", + "لَا تَقْتُلُوا يُوسُفَ وَأَلْقُوهُ فِي غَيَابَةِ الْجُبِّ يَلْتَقِطْهُ بَعْضُ \n", + "السَّيَّارَةِ إِنْ كُنْتُمْ فَاعِلِينَ * 11 قَالُوا يَا أَبَانَا مَا لَكَ لَا تَأْمَنَّا \n", + "عَلَى يُوسُفَ وَإِنَّا لَهُ لَنَاصِحُونَ * 12 أَرْسِلْهُ مَعَنَا غَدًا يَرْتَعْ\n", + "سِتُّ سُوَر مِنَ القُرآنِ\n", + "وَيَلْعَبْ وَإِنَّا لَهُ لَحَافِظُونَ * 13 قَالَ إِنِّي لَيَحْزُنُنِي أَنْ تَذْهَبُوا \n", + "بِهِ وَأَخَافُ أَنْ يَأْكُلَهُ الذِّئْبُ وَأَنْتُمْ عَنْهُ غَافِلُونَ * 14 قَالُوا \n", + "لَئِنْ أَكَلَهُ الذِّئْبُ وَنَحْنُ عُصْبَةٌ إِنَّا إِذًا لَخَاسِرُونَ * 15 فَلَمَّا \n", + "ذَهَبُوا بِهِ وَأَجْمَعُوا أَنْ يَجْعَلُوهُ فِي غَيَابَةِ الْجُبِّ وَأَوْحَيْنَا \n", + "إِلَيْهِ لَتُنَبِّئَنَّهُمْ بِأَمْرِهِمْ هَذَا وَهُمْ لَا يَشْعُرُونَ * 16 وَجَاءُوا أَبَاهُمْ \n", + "عِشَاءً يَبْكُونَ * 17 قَالُوا يَا أَبَانَا إِنَّا ذَهَبْنَا نَسْتَبِقُ وَتَرَكْنَا \n", + "يُوسُفَ عِنْدَ مَتَاعِنَا فَأَكَلَهُ الذِّئْبُ وَمَا أَنْتَ بِمُؤْمِنٍ لَنَا وَلَوْ \n", + "كُنَّا صَادِقِينَ * 18 وَجَاءُوا عَلَى قَمِيصِهِ بِدَمٍ كَذِبٍ قَالَ بَلْ \n", + "سَوَّلَتْ لَكُمْ أَنْفُسُكُمْ أَمْرًا فَصَبْرٌ جَمِيلٌ وَاللَّهُ الْمُسْتَعَانُ عَلَى \n", + "مَا تَصِفُونَ * 19 وَجَاءَتْ سَيَّارَةٌ فَأَرْسَلُوا وَارِدَهُمْ فَأَدْلَى دَلْوَهُ \n", + "قَالَ يَا بُشْرَى هَذَا غُلَامٌ وَأَسَرُّوهُ بِضَاعَةً وَاللَّهُ عَلِيمٌ بِمَا \n", + "يَعْمَلُونَ * 20 وَشَرَوْهُ بِثَمَنٍ بَخْسٍ دَرَاهِمَ مَعْدُودَةٍ وَكَانُوا فِيهِ \n", + "مِنَ الزَّاهِدِينَ * 21 وَقَالَ الَّذِي اشْتَرَاهُ مِنْ مِصْرَ لِامْرَأَتِهِ \n", + "أَكْرِمِي مَثْوَاهُ عَسَى أَنْ يَنْفَعَنَا أَوْ نَتَّخِذَهُ وَلَدًا وَكَذَلِكَ مَكَّنَّا \n", + "لِيُوسُفَ فِي الْأَرْضِ وَلِنُعَلِّمَهُ مِنْ تَأْوِيلِ الْأَحَادِيثِ وَاللَّهُ غَالِبٌ\n", + "سِتُّ سُوَرٍ مِنَ القُرآنِ\n", + "عَلَى أَمْرِهِ وَلَكِنَّ أَكْثَرَ النَّاسِ لَا يَعْلَمُونَ * 22 وَلَمَّا بَلَغَ أَشُدَّهُ \n", + "آَتَيْنَاهُ حُكْمًا وَعِلْمًا وَكَذَلِكَ نَجْزِي الْمُحْسِنِينَ * 23 وَرَاوَدَتْهُ \n", + "الَّتِي هُوَ فِي بَيْتِهَا عَنْ نَفْسِهِ وَغَلَّقَتِ الْأَبْوَابَ وَقَالَتْ هَيْتَ \n", + "لَكَ قَالَ مَعَاذَ اللَّهِ إِنَّهُ رَبِّي أَحْسَنَ مَثْوَايَ إِنَّهُ لَا يُفْلِحُ \n", + "الظَّالِمُونَ * 23 وَلَقَدْ هَمَّتْ بِهِ وَهَمَّ بِهَا لَوْلَا أَنْ رَأَى بُرْهَانَ \n", + "رَبِّهِ كَذَلِكَ لِنَصْرِفَ عَنْهُ السُّوءَ وَالْفَحْشَاءَ إِنَّهُ مِنْ عِبَادِنَا \n", + "الْمُخْلَصِينَ * 25 وَاسْتَبَقَا الْبَابَ وَقَدَّتْ قَمِيصَهُ مِنْ دُبُرٍ \n", + "وَأَلْفَيَا سَيِّدَهَا لَدَى الْبَابِ قَالَتْ مَا جَزَاءُ مَنْ أَرَادَ بِأَهْلِكَ سُوءًا \n", + "إِلَّا أَنْ يُسْجَنَ أَوْ عَذَابٌ أَلِيمٌ * 26 قَالَ هِيَ رَاوَدَتْنِي عَنْ \n", + "نَفْسِي وَشَهِدَ شَاهِدٌ مِنْ أَهْلِهَا إِنْ كَانَ قَمِيصُهُ قُدَّ مِنْ قُبُلٍ \n", + "فَصَدَقَتْ وَهُوَ مِنَ الْكَاذِبِينَ * 27 وَإِنْ كَانَ قَمِيصُهُ قُدَّ مِنْ \n", + "دُبُرٍ فَكَذَبَتْ وَهُوَ مِنَ الصَّادِقِينَ * 28 فَلَمَّا رَأَى قَمِيصَهُ قُدَّ \n", + "مِنْ دُبُرٍ قَالَ إِنَّهُ مِنْ كَيْدِكُنَّ إِنَّ كَيْدَكُنَّ عَظِيمٌ * 29 يُوسُفُ \n", + "أَعْرِضْ عَنْ هَذَا وَاسْتَغْفِرِي لِذَنْبِكِ إِنَّكِ كُنْتِ مِنَ الْخَاطِئِينَ * \n", + "30 وَقَالَ نِسْوَةٌ فِي الْمَدِينَةِ امْرَأَةُ الْعَزِيزِ تُرَاوِدُ فَتَاهَا عَنْ\n", + "سِتٌّ سُوَرٍ مِنَ القُرآنِ\n", + "نَفْسِهِ قَدْ شَغَفَهَا حُبًّا إِنَّا لَنَرَاهَا فِي ضَلَالٍ مُبِينٍ * 31 فَلَمَّا \n", + "سَمِعَتْ بِمَكْرِهِنَّ أَرْسَلَتْ إِلَيْهِنَّ وَأَعْتَدَتْ لَهُنَّ مُتَّكَأً وَآَتَتْ \n", + "كُلَّ وَاحِدَةٍ مِنْهُنَّ سِكِّينًا وَقَالَتِ اخْرُجْ عَلَيْهِنَّ فَلَمَّا رَأَيْنَهُ \n", + "أَكْبَرْنَهُ وَقَطَّعْنَ أَيْدِيَهُنَّ وَقُلْنَ حَاشَ لِلَّهِ مَا هَذَا بَشَرًا إِنْ \n", + "هَذَا إِلَّا مَلَكٌ كَرِيمٌ * 32 قَالَتْ فَذَلِكُنَّ الَّذِي لُمْتُنَّنِي فِيهِ \n", + "وَلَقَدْ رَاوَدْتُهُ عَنْ نَفْسِهِ فَاسْتَعْصَمَ وَلَئِنْ لَمْ يَفْعَلْ مَا آَمُرُهُ \n", + "لَيُسْجَنَنَّ وَلَيَكُونَنْ مِنَ الصَّاغِرِينَ * 33 قَالَ رَبِّ السِّجْنُ أَحَبُّ \n", + "إِلَيَّ مِمَّا يَدْعُونَنِي إِلَيْهِ وَإِلَّا تَصْرِفْ عَنِّي كَيْدَهُنَّ أَصْبُ \n", + "إِلَيْهِنَّ وَأَكُنْ مِنَ الْجَاهِلِينَ * 34 فَاسْتَجَابَ لَهُ رَبُّهُ فَصَرَفَ \n", + "عَنْهُ كَيْدَهُنَّ إِنَّهُ هُوَ السَّمِيعُ الْعَلِيمُ * 35 ثُمَّ بَدَا لَهُمْ مِنْ بَعْدِ \n", + "مَا رَأَوُا الْآَيَاتِ لَيَسْجُنُنَّهُ حَتَّى حِينٍ * 36 وَدَخَلَ مَعَهُ \n", + "السِّجْنَ فَتَيَانِ قَالَ أَحَدُهُمَا إِنِّي أَرَانِي أَعْصِرُ خَمْرًا وَقَالَ \n", + "الْآَخَرُ إِنِّي أَرَانِي أَحْمِلُ فَوْقَ رَأْسِي خُبْزًا تَأْكُلُ الطَّيْرُ مِنْهُ \n", + "نَبِّئْنَا بِتَأْوِيلِهِ إِنَّا نَرَاكَ مِنَ الْمُحْسِنِينَ * 37 قَالَ لَا يَأْتِيكُمَا \n", + "طَعَامٌ تُرْزَقَانِهِ إِلَّا نَبَّأْتُكُمَا بِتَأْوِيلِهِ قَبْلَ أَنْ يَأْتِيَكُمَا ذَلِكُمَا\n", + "سِتٌ سُوَرٍ مِنَ القُرْآنِ\n", + "مِمَّا عَلَّمَنِي رَبِّي إِنِّي تَرَكْتُ مِلَّةَ قَوْمٍ لَا يُؤْمِنُونَ بِاللَّهِ وَهُمْ \n", + "بِالْآَخِرَةِ هُمْ كَافِرُونَ * 38 وَاتَّبَعْتُ مِلَّةَ آَبَائِي إِبْرَاهِيمَ وَإِسْحَاقَ \n", + "وَيَعْقُوبَ مَا كَانَ لَنَا أَنْ نُشْرِكَ بِاللَّهِ مِنْ شَيْءٍ ذَلِكَ مِنْ \n", + "فَضْلِ اللَّهِ عَلَيْنَا وَعَلَى النَّاسِ وَلَكِنَّ أَكْثَرَ النَّاسِ لَا يَشْكُرُونَ * \n", + "39 يَا صَاحِبَيِ السِّجْنِ أَأَرْبَابٌ مُتَفَرِّقُونَ خَيْرٌ أَمِ اللَّهُ الْوَاحِدُ \n", + "الْقَهَّارُ * 40 مَا تَعْبُدُونَ مِنْ دُونِهِ إِلَّا أَسْمَاءً سَمَّيْتُمُوهَا أَنْتُمْ \n", + "وَآَبَاؤُكُمْ مَا أَنْزَلَ اللَّهُ بِهَا مِنْ سُلْطَانٍ إِنِ الْحُكْمُ إِلَّا لِلَّهِ أَمَرَ \n", + "أَلَّا تَعْبُدُوا إِلَّا إِيَّاهُ ذَلِكَ الدِّينُ الْقَيِّمُ وَلَكِنَّ أَكْثَرَ النَّاسِ لَا \n", + "يَعْلَمُونَ * 41 يَا صَاحِبَيِ السِّجْنِ أَمَّا أَحَدُكُمَا فَيَسْقِي رَبَّهُ \n", + "خَمْرًا وَأَمَّا الْآَخَرُ فَيُصْلَبُ فَتَأْكُلُ الطَّيْرُ مِنْ رَأْسِهِ قُضِيَ الْأَمْرُ \n", + "الَّذِي فِيهِ تَسْتَفْتِيَانِ * 42 وَقَالَ لِلَّذِي ظَنَّ أَنَّهُ نَاجٍ مِنْهُمَا \n", + "اذْكُرْنِي عِنْدَ رَبِّكَ فَأَنْسَاهُ الشَّيْطَانُ ذِكْرَ رَبِّهِ فَلَبِثَ فِي السِّجْنِ \n", + "بِضْعَ سِنِينَ * 43 وَقَالَ الْمَلِكُ إِنِّي أَرَى سَبْعَ بَقَرَاتٍ سِمَانٍ \n", + "يَأْكُلُهُنَّ سَبْعٌ عِجَافٌ وَسَبْعَ سُنْبُلَاتٍ خُضْرٍ وَأُخَرَ يَابِسَاتٍ *\n", + "44 يَا أَيُّهَا الْمَلَأُ أَفْتُونِي فِي رُؤْيَايَ إِنْ كُنْتُمْ لِلرُّؤْيَا تَعْبُرُونَ *\n", + "سِتُّ سُوَرٍ مِنَ القُرْآنِ\n", + "قَالُوا أَضْغَاثُ أَحْلَامٍ وَمَا نَحْنُ بِتَأْوِيلِ الْأَحْلَامِ بِعَالِمِينَ *\n", + "46 وَقَالَ الَّذِي نَجَا مِنْهُمَا وَادَّكَرَ بَعْدَ أُمَّةٍ أَنَا أُنَبِّئُكُمْ بِتَأْوِيلِهِ \n", + "فَأَرْسِلُونِ * 47 يُوسُفُ أَيُّهَا الصِّدِّيقُ أَفْتِنَا فِي سَبْعِ بَقَرَاتٍ \n", + "سِمَانٍ يَأْكُلُهُنَّ سَبْعٌ عِجَافٌ وَسَبْعِ سُنْبُلَاتٍ خُضْرٍ وَأُخَرَ \n", + "يَابِسَاتٍ لَعَلِّي أَرْجِعُ إِلَى النَّاسِ لَعَلَّهُمْ يَعْلَمُونَ * قَالَ \n", + "تَزْرَعُونَ سَبْعَ سِنِينَ دَأَبًا فَمَا حَصَدْتُمْ فَذَرُوهُ فِي سُنْبُلِهِ إِلَّا \n", + "قَلِيلًا مِمَّا تَأْكُلُونَ * 49 ثُمَّ يَأْتِي مِنْ بَعْدِ ذَلِكَ سَبْعٌ شِدَادٌ \n", + "يَأْكُلْنَ مَا قَدَّمْتُمْ لَهُنَّ إِلَّا قَلِيلًا مِمَّا تُحْصِنُونَ * 50 ثُمَّ يَأْتِي \n", + "مِنْ بَعْدِ ذَلِكَ عَامٌ فِيهِ يُغَاثُ النَّاسُ وَفِيهِ يَعْصِرُونَ * 51 \n", + "وَقَالَ الْمَلِكُ ائْتُونِي بِهِ فَلَمَّا جَاءَهُ الرَّسُولُ قَالَ ارْجِعْ إِلَى \n", + "رَبِّكَ فَاسْأَلْهُ مَا بَالُ النِّسْوَةِ اللَّاتِي قَطَّعْنَ أَيْدِيَهُنَّ إِنَّ رَبِّي \n", + "بِكَيْدِهِنَّ عَلِيمٌ * 52 قَالَ مَا خَطْبُكُنَّ إِذْ رَاوَدْتُنَّ يُوسُفَ عَنْ نَفْسِهِ \n", + "قُلْنَ حَاشَ لِلَّهِ مَا عَلِمْنَا عَلَيْهِ مِنْ سُوءٍ قَالَتِ امْرَأَةُ الْعَزِيزِ \n", + "الْآَنَ حَصْحَصَ الْحَقُّ أَنَا رَاوَدْتُهُ عَنْ نَفْسِهِ وَإِنَّهُ لَمِنَ \n", + "الصَّادِقِينَ * 53 ذَلِكَ لِيَعْلَمَ أَنِّي لَمْ أَخُنْهُ بِالْغَيْبِ وَأَنَّ اللَّهَ\n", + "سِتُّ سُوَرٍ مِنَ القُرْآ\n", + "لَا يَهْدِي كَيْدَ الْخَائِنِينَ * 54 وَمَا أُبَرِّئُ نَفْسِي إِنَّ النَّفْسَ \n", + "لَأَمَّارَةٌ بِالسُّوءِ إِلَّا مَا رَحِمَ رَبِّي إِنَّ رَبِّي غَفُورٌ رَحِيمٌ *55 وَقَالَ \n", + "الْمَلِكُ ائْتُونِي بِهِ أَسْتَخْلِصْهُ لِنَفْسِي فَلَمَّا كَلَّمَهُ قَالَ إِنَّكَ \n", + "الْيَوْمَ لَدَيْنَا مَكِينٌ أَمِينٌ * 56 قَالَ اجْعَلْنِي عَلَى خَزَائِنِ الْأَرْضِ \n", + "إِنِّي حَفِيظٌ عَلِيمٌ * 57 وَكَذَلِكَ مَكَّنَّا لِيُوسُفَ فِي الْأَرْضِ يَتَبَوَّأُ \n", + "مِنْهَا حَيْثُ يَشَاءُ نُصِيبُ بِرَحْمَتِنَا مَنْ نَشَاءُ وَلَا نُضِيعُ أَجْرَ \n", + "الْمُحْسِنِينَ * 58 وَلَأَجْرُ الْآَخِرَةِ خَيْرٌ لِلَّذِينَ آَمَنُوا \n", + "وَكَانُوا يَتَّقُونَ * 59 وَجَاءَ إِخْوَةُ يُوسُفَ فَدَخَلُوا عَلَيْهِ فَعَرَفَهُمْ وَهُمْ \n", + "لَهُ مُنْكِرُونَ * 60 وَلَمَّا جَهَّزَهُمْ بِجَهَازِهِمْ قَالَ ائْتُونِي بِأَخٍ \n", + "لَكُمْ مِنْ أَبِيكُمْ أَلَا تَرَوْنَ أَنِّي أُوفِي الْكَيْلَ وَأَنَا خَيْرُ \n", + "الْمُنْزِلِينَ * 61 فَإِنْ لَمْ تَأْتُونِي بِهِ فَلَا كَيْلَ لَكُمْ عِنْدِي \n", + "وَلَا تَقْرَبُونِ * 62 قَالُوا سَنُرَاوِدُ عَنْهُ أَبَاهُ وَإِنَّا لَفَاعِلُونَ * 63 \n", + "وَقَالَ لِفِتْيَانِهِ اجْعَلُوا بِضَاعَتَهُمْ فِي رِحَالِهِمْ لَعَلَّهُمْ يَعْرِفُونَهَا \n", + "إِذَا انْقَلَبُوا إِلَى أَهْلِهِمْ لَعَلَّهُمْ يَرْجِعُونَ * 64 فَلَمَّا رَجَعُوا \n", + "إِلَى أَبِيهِمْ قَالُوا يَا أَبَانَا مُنِعَ مِنَّا الْكَيْلُ فَأَرْسِلْ مَعَنَا أَخَانَا\n", + "سِتُّ سُوَرٍ مِنَ القُرْآ\n", + "نَكْتَلْ وَإِنَّا لَهُ لَحَافِظُونَ * 65 قَالَ هَلْ آَمَنُكُمْ عَلَيْهِ إِلَّا \n", + "كَمَا أَمِنْتُكُمْ عَلَى أَخِيهِ مِنْ قَبْلُ فَاللَّهُ خَيْرٌ حَافِظًا وَهُوَ أَرْحَمُ \n", + "الرَّاحِمِينَ * 66 وَلَمَّا فَتَحُوا مَتَاعَهُمْ وَجَدُوا بِضَاعَتَهُمْ رُدَّتْ \n", + "إِلَيْهِمْ قَالُوا يَا أَبَانَا مَا نَبْغِي هَذِهِ بِضَاعَتُنَا رُدَّتْ إِلَيْنَا وَنَمِيرُ \n", + "أَهْلَنَا وَنَحْفَظُ أَخَانَا وَنَزْدَادُ كَيْلَ بَعِيرٍ ذَلِكَ كَيْلٌ يَسِيرٌ * 67 \n", + "قَالَ لَنْ أُرْسِلَهُ مَعَكُمْ حَتَّى تُؤْتُونِ مَوْثِقًا مِنَ اللَّهِ لَتَأْتُنَّنِي بِهِ \n", + "إِلَّا أَنْ يُحَاطَ بِكُمْ فَلَمَّا آَتَوْهُ مَوْثِقَهُمْ قَالَ اللَّهُ عَلَى مَا نَقُولُ \n", + "وَكِيلٌ * 68 وَقَالَ يَا بَنِيَّ لَا تَدْخُلُوا مِنْ بَابٍ وَاحِدٍ وَادْخُلُوا \n", + "مِنْ أَبْوَابٍ مُتَفَرِّقَةٍ وَمَا أُغْنِي عَنْكُمْ مِنَ اللَّهِ مِنْ شَيْءٍ إِنِ \n", + "الْحُكْمُ إِلَّا لِلَّهِ عَلَيْهِ تَوَكَّلْتُ وَعَلَيْهِ فَلْيَتَوَكَّلِ الْمُتَوَكِّلُونَ * 69 \n", + "وَلَمَّا دَخَلُوا مِنْ حَيْثُ أَمَرَهُمْ أَبُوهُمْ مَا كَانَ يُغْنِي عَنْهُمْ مِنَ \n", + "اللَّهِ مِنْ شَيْءٍ إِلَّا حَاجَةً فِي نَفْسِ يَعْقُوبَ قَضَاهَا وَإِنَّهُ لَذُو \n", + "عِلْمٍ لِمَا عَلَّمْنَاهُ وَلَكِنَّ أَكْثَرَ النَّاسِ لَا يَعْلَمُونَ * 70 وَلَمَّا \n", + "دَخَلُوا عَلَى يُوسُفَ آَوَى إِلَيْهِ أَخَاهُ قَالَ إِنِّي أَنَا أَخُوكَ فَلَا \n", + "تَبْتَئِسْ بِمَا كَانُوا يَعْمَلُونَ * 71 فَلَمَّا جَهَّزَهُمْ بِجَهَازِهِمْ جَعَلَ\n", + "سِتُّ سُوَرٍ مِنَ القُرْآ\n", + "السِّقَايَةَ فِي رَحْلِ أَخِيهِ ثُمَّ أَذَّنَ مُؤَذِّنٌ أَيَّتُهَا الْعِيرُ إِنَّكُمْ \n", + "لَسَارِقُونَ * 72 قَالُوا وَأَقْبَلُوا عَلَيْهِمْ مَاذَا تَفْقِدُونَ * 73 قَالُوا \n", + "نَفْقِدُ صُوَاعَ الْمَلِكِ وَلِمَنْ جَاءَ بِهِ حِمْلُ بَعِيرٍ وَأَنَا بِهِ زَعِيمٌ * \n", + "74 قَالُوا تَاللَّهِ لَقَدْ عَلِمْتُمْ مَا جِئْنَا لِنُفْسِدَ فِي الْأَرْضِ \n", + "وَمَا كُنَّا سَارِقِينَ * 75 قَالُوا فَمَا جَزَاؤُهُ إِنْ كُنْتُمْ كَاذِبِينَ * 76\n", + "قَالُوا جَزَاؤُهُ مَنْ وُجِدَ فِي رَحْلِهِ فَهُوَ جَزَاؤُهُ كَذَلِكَ نَجْزِي \n", + "الظَّالِمِينَ * 77 فَبَدَأَ بِأَوْعِيَتِهِمْ قَبْلَ وِعَاءِ أَخِيهِ ثُمَّ اسْتَخْرَجَهَا \n", + "مِنْ وِعَاءِ أَخِيهِ كَذَلِكَ كِدْنَا لِيُوسُفَ مَا كَانَ لِيَأْخُذَ أَخَاهُ فِي \n", + "دِينِ الْمَلِكِ إِلَّا أَنْ يَشَاءَ اللَّهُ نَرْفَعُ دَرَجَاتٍ مَنْ نَشَاءُ وَفَوْقَ \n", + "كُلِّ ذِي عِلْمٍ عَلِيمٌ * 78 قَالُوا إِنْ يَسْرِقْ فَقَدْ سَرَقَ أَخٌ لَهُ مِنْ \n", + "قَبْلُ فَأَسَرَّهَا يُوسُفُ فِي نَفْسِهِ وَلَمْ يُبْدِهَا لَهُمْ قَالَ أَنْتُمْ شَرٌّ \n", + "مَكَانًا وَاللَّهُ أَعْلَمُ بِمَا تَصِفُونَ * 79 قَالُوا يَا أَيُّهَا الْعَزِيزُ إِنَّ لَهُ \n", + "أَبًا شَيْخًا كَبِيرًا فَخُذْ أَحَدَنَا مَكَانَهُ إِنَّا نَرَاكَ مِنَ الْمُحْسِنِينَ * \n", + "80 قَالَ مَعَاذَ اللَّهِ أَنْ نَأْخُذَ إِلَّا مَنْ وَجَدْنَا مَتَاعَنَا عِنْدَهُ إِنَّا \n", + "إِذًا لَظَالِمُونَ * 81 فَلَمَّا اسْتَيْئَسُوا مِنْهُ خَلَصُوا نَجِيًّا قَالَ كَبِيرُهُمْ\n", + "سِتُّ سُوَرٍ مِنَ القُرْآ\n", + "أَلَمْ تَعْلَمُوا أَنَّ أَبَاكُمْ قَدْ أَخَذَ عَلَيْكُمْ مَوْثِقًا مِنَ اللَّهِ وَمِنْ \n", + "قَبْلُ مَا فَرَّطْتُمْ فِي يُوسُفَ فَلَنْ أَبْرَحَ الأَرْضَ حَتَّى يَأْذَنَ لِي \n", + "أَبِي أَوْ يَحْكُمَ اللَّهُ لِي وَهُوَ خَيْرُ الْحَاكِمِينَ * 81 ارْجِعُوا إِلَى \n", + "أَبِيكُمْ فَقُولُوا يَا أَبَانَا إِنَّ ابْنَكَ سَرَقَ وَمَا شَهِدْنَا إِلا بِمَا \n", + "عَلِمْنَا وَمَا كُنَّا لِلْغَيْبِ حَافِظِينَ * 82 وَاسْأَلِ الْقَرْيَةَ الَّتِي كُنَّا \n", + "فِيهَا وَالْعِيرَ الَّتِي أَقْبَلْنَا فِيهَا وَإِنَّا لَصَادِقُونَ * 83 قَالَ بَلْ \n", + "سَوَّلَتْ لَكُمْ أَنْفُسُكُمْ أَمْرًا فَصَبْرٌ جَمِيلٌ عَسَى اللَّهُ أَنْ يَأْتِيَنِي \n", + "بِهِمْ جَمِيعًا إِنَّهُ هُوَ الْعَلِيمُ الْحَكِيمُ * 85 وَتَوَلَّى عَنْهُمْ وَقَالَ يَا \n", + "أَسَفَى عَلَى يُوسُفَ وَابْيَضَّتْ عَيْنَاهُ مِنَ الْحُزْنِ فَهُوَ كَظِيمٌ * \n", + "86 قَالُوا تَاللَّهِ تَفْتَأُ تَذْكُرُ يُوسُفَ حَتَّى تَكُونَ حَرَضًا أَوْ تَكُونَ \n", + "مِنَ الْهَالِكِينَ * 87 قَالَ إِنَّمَا أَشْكُو بَثِّي وَحُزْنِي إِلَى اللَّهِ وَأَعْلَمُ \n", + "مِنَ اللَّهِ مَا لا تَعْلَمُونَ * 88 يَا بَنِيَّ اذْهَبُوا فَتَحَسَّسُوا مِنْ \n", + "يُوسُفَ وَأَخِيهِ وَلا تَيْئَسُوا مِنْ رَوْحِ اللَّهِ إِنَّهُ لا يَيْئَسُ مِنْ \n", + "رَوْحِ اللَّهِ إِلَّا الْقَوْمُ الْكَافِرُونَ * 89 فَلَمَّا دَخَلُوا عَلَيْهِ قَالُوا \n", + "يَا أَيُّهَا الْعَزِيزُ مَسَّنَا وَأَهْلَنَا الضُّرُّ وَجِئْنَا بِبِضَاعَةٍ مُزْجَاةٍ\n", + "سِتُّ سُوَرٍ مِنَ القُرْآ\n", + "فَأَوْفِ لَنَا الْكَيْلَ وَتَصَدَّقْ عَلَيْنَا إِنَّ اللَّهَ يَجْزِي الْمُتَصَدِّقِينَ * \n", + "90 قَالَ هَلْ عَلِمْتُمْ مَا فَعَلْتُمْ بِيُوسُفَ وَأَخِيهِ إِذْ أَنْتُمْ \n", + "جَاهِلُونَ * 91 قَالُوا أَئِنَّكَ لَأَنْتَ يُوسُفُ قَالَ أَنَا يُوسُفُ \n", + "وَهَذَا أَخِي قَدْ مَنَّ اللَّهُ عَلَيْنَا إِنَّهُ مَنْ يَتَّقِ وَيَصْبِرْ فَإِنَّ اللَّهَ \n", + "لا يُضِيعُ أَجْرَ الْمُحْسِنِينَ * 92 قَالُوا تَاللَّهِ لَقَدْ آَثَرَكَ اللَّهُ \n", + "عَلَيْنَا وَإِنْ كُنَّا لَخَاطِئِينَ * 93 قَالَ لا تَثْرِيبَ عَلَيْكُمُ \n", + "الْيَوْمَ يَغْفِرُ اللَّهُ لَكُمْ وَهُوَ أَرْحَمُ الرَّاحِمِينَ * 93 اذْهَبُوا \n", + "بِقَمِيصِي هَذَا فَأَلْقُوهُ عَلَى وَجْهِ أَبِي يَأْتِ بَصِيرًا وَأْتُونِي \n", + "بِأَهْلِكُمْ أَجْمَعِينَ * 95 وَلَمَّا فَصَلَتِ الْعِيرُ قَالَ أَبُوهُمْ إِنِّي \n", + "لأَجِدُ رِيحَ يُوسُفَ لَوْلَا أَنْ تُفَنِّدُونِ * 96 قَالُوا تَاللَّهِ إِنَّكَ \n", + "لَفِي ضَلالِكَ الْقَدِيمِ * 97 فَلَمَّا أَنْ جَاءَ الْبَشِيرُ أَلْقَاهُ عَلَى \n", + "وَجْهِهِ فَارْتَدَّ بَصِيرًا قَالَ أَلَمْ أَقُلْ لَكُمْ إِنِّي أَعْلَمُ مِنَ اللَّهِ مَا \n", + "لا تَعْلَمُونَ * 98 قَالُوا يَا أَبَانَا اسْتَغْفِرْ لَنَا ذُنُوبَنَا إِنَّا كُنَّا \n", + "خَاطِئِينَ * 99 قَالَ سَوْفَ أَسْتَغْفِرُ لَكُمْ رَبِّي إِنَّهُ هُوَ الْغَفُورُ \n", + "الرَّحِيمُ * 100 فَلَمَّا دَخَلُوا عَلَى يُوسُفَ آَوَى إِلَيْهِ أَبَوَيْهِ وَقَالَ\n", + "سِتُّ سُوَرٍ مِنَ القُرْآ\n", + "ادْخُلُوا مِصْرَ إِنْ شَاءَ اللَّهُ آَمِنِينَ وَرَفَعَ أَبَوَيْهِ عَلَى الْعَرْشِ \n", + "وَخَرُّوا لَهُ سُجَّدًا وَقَالَ يَا أَبَتِ هَذَا تَأْوِيلُ رُؤْيَايَ مِنْ قَبْلُ \n", + "قَدْ جَعَلَهَا رَبِّي حَقًّا وَقَدْ أَحْسَنَ بِي إِذْ أَخْرَجَنِي مِنَ \n", + "السِّجْنِ وَجَاءَ بِكُمْ مِنَ الْبَدْوِ مِنْ بَعْدِ أَنْ نَزَغَ الشَّيْطَانُ \n", + "بَيْنِي وَبَيْنَ إِخْوَتِي إِنَّ رَبِّي لَطِيفٌ لِمَا يَشَاءُ إِنَّهُ هُوَ الْعَلِيمُ \n", + "الْحَكِيمُ * 101 رَبِّ قَدْ آَتَيْتَنِي مِنَ الْمُلْكِ وَعَلَّمْتَنِي مِنْ تَأْوِيلِ \n", + "الأَحَادِيثِ فَاطِرَ السَّمَاوَاتِ وَالأَرْضِ أَنْتَ وَلِيِّي فِي الدُّنْيَا \n", + "وَالآَخِرَةِ تَوَفَّنِي مُسْلِمًا وَأَلْحِقْنِي بِالصَّالِحِينَ * 102 ذَلِكَ مِنْ \n", + "أَنْبَاءِ الْغَيْبِ نُوحِيهِ إِلَيْكَ وَمَا كُنْتَ لَدَيْهِمْ إِذْ أَجْمَعُوا أَمْرَهُمْ \n", + "وَهُمْ يَمْكُرُونَ * 103 وَمَا أَكْثَرُ النَّاسِ وَلَوْ حَرَصْتَ بِمُؤْمِنِينَ * \n", + "104 وَمَا تَسْأَلُهُمْ عَلَيْهِ مِنْ أَجْرٍ إِنْ هُوَ إِلَّا ذِكْرٌ لِلْعَالَمِينَ * \n", + "105 وَكَأَيِّنْ مِنْ آَيَةٍ فِي السَّمَاوَاتِ وَالْأَرْضِ يَمُرُّونَ عَلَيْهَا وَهُمْ \n", + "عَنْهَا مُعْرِضُونَ * 106 وَمَا يُؤْمِنُ أَكْثَرُهُمْ بِاللَّهِ إِلَّا وَهُمْ مُشْرِكُونَ \n", + "107 أَفَأَمِنُوا أَنْ تَأْتِيَهُمْ غَاشِيَةٌ مِنْ عَذَابِ اللَّهِ أَوْ تَأْتِيَهُمُ السَّاعَةُ \n", + "بَغْتَةً وَهُمْ لَا يَشْعُرُونَ * 108 قُلْ هَذِهِ سَبِيلِي أَدْعُو إِلَى اللَّهِ\n", + "سِتُّ سُوَرٍ مِنَ القُرْآ\n", + "عَلَى بَصِيرَةٍ أَنَا وَمَنِ اتَّبَعَنِي وَسُبْحَانَ اللَّهِ وَمَا أَنَا مِنَ \n", + "الْمُشْرِكِينَ * 109 وَلَا أَرْسَلْنَا مِنْ قَبْلِكَ إِلَّا رِجَالًا نُوحِي إِلَيْهِمْ \n", + "مِنْ أَهْلِ الْقُرَى أَفَلَمْ يَسِيرُوا فِي الْأَرْضِ فَيَنْظُرُوا كَيْفَ كَانَ \n", + "عَاقِبَةُ الَّذِينَ مِنْ قَبْلِهِمْ وَلَدَارُ الْآَخِرَةِ خَيْرٌ لِلَّذِينَ اتَّقَوْا أَفَلَا \n", + "تَعْقِلُونَ * 110 حَتَّى إِذَا اسْتَيْئَسَ الرُّسُلُ وَظَنُّوا أَنَّهُمْ قَدْ كُذِبُوا \n", + "جَاءَهُمْ نَصْرُنَا فَنُجِّيَ مَنْ نَشَاءُ وَلَا يُرَدُّ بَأْسُنَا عَنِ الْقَوْمِ \n", + "الْمُجْرِمِينَ * 111 لَقَدْ كَانَ فِي قَصَصِهِمْ عِبْرَةٌ لِأُولِي الْأَلْبَابِ مَا \n", + "كَانَ حَدِيثًا يُفْتَرَى وَلَكِنْ تَصْدِيقَ الَّذِي بَيْنَ يَدَيْهِ وَتَفْصِيلَ \n", + "كُلِّ شَيْءٍ وَهُدًى وَرَحْمَةً لِقَوْمٍ يُؤْمِنُونَ *\n", + "SECTION V.\n", + " Extracts from the Book of a “Thousand Nights and a Night”وَاقِعةُ الاخِ الْحجّامِ السّادِسِ – وَهِيَ مأخُوذَةٌ مِن كتابِ اَلْفِ\n", + "لَيلةٍ وَلَيلَةٍ\n", + "امّا اخي السّادس – فكان فقيرًا بعد ان كان غنيًا ومِن \n", + "اخبارِهِ انّه خرج يومًا يطلبُ شَيئًا يسدُّ بِهِ جوعَهُ * فرأي \n", + "في بعضِ الطرُقِ دارًا حسنًة – لها دهليز واسع وباب مرتفع – \n", + "وعلي البابِ خدمْ وحشمْ وامرْ ونهيْ * فسأل بعضَ الحاضرين \n", + "هُناكَ عن صاحب الدارِ * فقال له – هو رجلْ من البَرامِكة * \n", + "فتقدَم اخي الي الدّاربنة وطلب منهم صدقةً * فقالوا له – \n", + "الباب قُدّامك – ادخل فيه – فانّك تجشد ما تحبّ وتختار * \n", + "فدخل اخي ومَشَي ساعةً – فرأي ساحةً وسيعةً – في وسطها \n", + "من كِتَابِ ألفِ لَيلَةٍ وَلَيلَةٍ\n", + "بستان ما رأي مَثَلَه – فبقى متحيرًا فيما رأى * ثمّ انّه مشي نحو \n", + "مجلسٍ من المجالس * فلما دخله وجد في صدرهِ إنسانًا \n", + "حَسن الوجه جالسًا على بساطٍ مُذْهَب – فقصده * فلما \n", + "راهُ الرّجلُ صاحب المجلس رحّب به وسأله عن حاله * \n", + "فاخبره انه محتاج يُريد شَيئًا في حب الله * فاغتمٌ ذلك \n", + "الرّجل غّمًا شديدًا – وقال يا سُبحان الله ! انا موجود في \n", + "هَذهِ البلدة وانت جائع * ثم وعد اخي بخيرٍ وطيّب خاطِرَه \n", + "وصاح على الخَدَم بأن يأتوا بِطِشت وابريق * \n", + "فلمّا حضر الطشت والابريق – قال لاخي تقدّمْ واغسلْ \n", + "يدك * فقام اخي ليغسلَ يدَه – فما رأى طشتًا ولا ابريقًا * \n", + "فمدّ يده كأنه يغسلها – ثم صاح الرجل يا غِلمان قَدِموا \n", + "المائدة – فلم يَرَ اخي شَيْئًا * ثم قال لاخي تفضَّل كُلْ \n", + "مِن هذا الطّعام ولا تستحي بحيوتي عليك * فمدَّ اخي يدهُ \n", + "وجعل نفسَه كانّه يأكل * فقال الرجل لاخي – بالله كُلْ \n", + "واشبع بطنك – لانّك جائع وانظر الي حسن هذا الخُبْز\n", + "من كِتَابِ ألفِ لَيلَةٍ وَلَيلَةٍ\n", + "وبياضه * فقال له اخي ما رأيتُ أحسن من هذا الطعام \n", + "ولا الذَّ من هذا الخبز * وقال اخي في نفسِه الظاهر انَّ \n", + "هذا رجلٌ يحبُ اللهو والمزاح * ثمّ قال له الرّجل أن هذا \n", + "الخبز خبزّتهُ جارية اشتريتها بخمس مائة دينار * ثم صاح \n", + "باعلي صوتِهِ وقال – يا غلام قَدِم الهريسة وصُبَّ عليها دهنًا \n", + "كثيرًا * والتفَتَ الي اخي وقال له – بالله عليك يا ضيفي \n", + "هَلْ اكلتَ اطيبَ من هذه الهريسة ؟ فقال لا ولا اظنَّ \n", + "السلطانُ أكل مثلها * \n", + "فقال لاخي كُلّ ولا تستحي * وكان اخي يُحرك فمَه \n", + "ويمضغ من غير شيءٍ * والرجل يطلب نوعًا بعد نوع وما \n", + "هُناكَ شيٍ * ويأمُر اخي بالاكل وهو لا يَرَى شيئًا – واستولى \n", + "علي قُواه الضعفُ مِن شِدة الجوع* ثم قال له اخي قد \n", + "اكتفيتُ يا سيدي من الطعام * فصاح الرجل شِيلوا هذا \n", + "وقَدِموا الحلاوات * ثم قال لاخي كُلْ من هذا لوزينج ومن \n", + "هذِه القطائف ومن هذه الكنافة * فقال له اخي ما اطيبَ \n", + "من كِتَابِ ألفِ لَيلَةٍ وَلَيلَةٍ\n", + "هذه الحلاوات وما اسحنَها – وهو يُحرِك فمه واَشداقَهُ \n", + "ثمّ قال له اخي قد اكتفيتُ يا سيّدي وامتلأ بطني انعم \n", + "الله عليك كما انعمتَ عليَّ * فقال له الرَّجل تُريدُ ان \n", + "تشرب ؟ فقال اخي نعم * ثمّ قال اخي في نفسه لاعملن \n", + "معه عملاً يُتَوِبه عن هذه الأفعال * ثم قال الرجل – قَدِموا \n", + "الشراب * فمد اخي يده كأنه يتناوَلُ قدحًا وقرّب يده \n", + "الي فمِهِ كانّه يشربه * فقال له الرّجل هينئًا مريئًا * فقال \n", + "له اخي هنْأَك الله بالعافية * ثمّ انّه جعل نفسه سكران \n", + "وشرع فِي العَربدة * ثمّ شال يده ولطم الرجل لطمةً دَوَّخَتْ \n", + "رأسَه وَاَلْحقه بالثانية * فقال الرجل ما هذا يا سفلة؟ فقال \n", + "اخي يا سيّدي هذا من بخار طعامك اللذيذ وشرابك \n", + "المُفرِح * فلم سمع الرجل كلام اخي ضحك ضحكًا شديدًا \n", + "وقال والله ما رأيتُ مثلك مسخرةً وها انا قد عفوتُ \n", + "فكُنْ نديمي ولا تُفارقني ابدًا * ثم انّه امر له بالطّعام \n", + "والشراب فأَكل اخي وشرب واستراح * \n", + "من كِتَابِ ألفِ لَيلَةٍ وَلَيلَةٍ\n", + "قِصةُ التاجِرِ مع زوجتِهِ – وَهيَ مَأْخُوذَة مِن كِتابِ الفِ لَيلةٍ\n", + "وَلَيلةٍ\n", + "قيل انّه كان تاجِرٌ غَنِيٌ – وله مال ورِجال ومواشي \n", + "وجِمالْ * وله زوجةْ وَأوَلاد وكان مسكَنُه في البريَةِ وهو \n", + "ممتحنْ في الزَرعِ * وكان يفهم لُغةَ البهائم والحيوانات * \n", + "وإذا اَفْشي لِاَحدٍ سِرهُ مات – وكان لا يُظهر لاحد سِرهُ خوفًا\n", + "من المَوتِ * وكن عنده في الَّرَبضِ ثور وحمار وكُل منهما \n", + "مربوط في معلفه * وكانا مُتقارِبَيْنِ احدهما يجنب الآخر * \n", + "فيومًا من الأيام بينما التاجر جالِسْ الي جانِبِهِماَ واولادُه \n", + "يلعبون قُدّامه – سَمِعَ الثور يقول للحِمار – يا ابا اليَقْظان\n", + "هنيًّا لك! فيما أنت فيه من الرَاحة الَخِدْمَةُ لك والكَنْسُ \n", + "والرَّش تحتَك ومَأْكلُكَ الشعيرُ المُغَرْبَلُ وشُرْبُك الماءُ الباردُ * \n", + "وامّا انا يا لِتَعَبي لأنَّهم يأخذوني من نصف الليل ويُشِغلوني \n", + "بالحرث ويُركبون علي رقبتي الفَدّان والمِحراثَ واَبدأ اعمل \n", + "من أول النّهار الي آخر النهار بِشَقّ الارض – ثمَّ اُكَلَّفُ \n", + "من كِتَابِ ألفِ لَيلَةٍ وَلَيلَةٍ\n", + "ما لا طاقَةَ لي به واُقاسِي انواع الاِهانة مثل الضّرب والزّجر \n", + "من الزرّاع القاسي وقد تَهَرَّت اجفاني وانسلختْ رقبتي \n", + "وسيقاني وفي آخر النهار يحبسوني في الدّار ويطرحون لي \n", + "الّتِبْنَ والفُولَ واَبَاتُ طول الليل في ا لنجاسة والروائح الدَّنِسَة * \n", + "وانت لم تزَلْ في المكان المكنوس المرشوش – وفي \n", + "المعلف النّظيف الملآن من التبن النّاعم واقفًا مستريحًا – \n", + "وفي النّادر يعرضُ لصاحبك التّاجر حاجةْ ضروريّة حتّي\n", + "انّه يركبك ويعود بك سريعًا وفيما عدا ذلك من الاوقات \n", + "انت مستريح وانا تعبان وانت نائمُ وانا يقظال وانت \n", + "معزَّز وانا مُهان * \n", + "فلمَّا انتهى كلام الثور قال له الحمارُ يا اَفْطَحُ صدق \n", + "الّذي سَمّاك ثورًا لانَك بليد الي الغاية – ولَيس عندك \n", + "مكر ولا حيلة خُبث – بل انّك تُبْدِي النصح وتبذل \n", + "المجهود قُدّام صاحبك وتَشقي وتقتل نفسك في راحة \n", + "غيرك * أمَا سمعت الشاعر يقول – \n", + "من كِتَابِ ألفِ لَيلَةٍ وَلَيلَةٍ\n", + "اُكَلِفُ نفسي كُلَّ يومٍ وليلةٍ – \n", + "هُموما علي من لا افوز بخيرهِ * \n", + "كما سوَّدَ القَصّارُ بالشمسِ وجهَهُ – \n", + "حريصًا على تبييض اَثوابِ غيرهِ * \n", + "ويُقال في المثلِ – مَنْ عَدِمَ التوفيق ضَلَّ عن الطريق * \n", + "وانت تخرج من صلاة الصُبح – وما تُعاود الاّ المغرب – \n", + "وتُقاسي نهاركَ كُلَّه اصنافَ العذاب تارَةً بالضّرب وتارةً \n", + "بالحَرثِ وتارةً بالنَّهْر * وعند مجيئك يربطك الزرّاعُ علي \n", + "المعلف المُنتن الرائحةِ * فتبقى تخَبِط وتمرح وتنطح\n", + "بقرنك وتلبط برجليك ويظن بك انك فرحان وتصبح كثيرًا – \n", + "وما تُصدق متي يُلقوا لك العلَف – فتسرع في أكلهِ بحرصٍ-\n", + "وتشحن بطنك منه – فلو انّك تنبطح عند مجيئك علي \n", + "قفاك – واذا قَدَّموا لك العلَف لا تأكل منه – وتجعل \n", + "نفسَك – ميتًا كان أوفق لك وكنتَ تلقي من الرّاحة اضعاف \n", + "ما انا فيه * فلمَّا سمع الثورُ كلامَ الحِمار وما أَبدي له\n", + "من كِتَابِ ألفِ لَيلَةٍ وَلَيلَةٍ\n", + "من النصيحة شكره كثيرًا بلسان حاله – ودَعا له وجازاهُ \n", + "خيرًا – وتيقَّن انّه ناصَح له وقال له نِعمَ الرّأيُ يا ابا \n", + "اليقظان ! هذا كُلّهُ يجري والتاجر يسمعه كَوْنَه يعرف لغة \n", + "الحيوانات * \n", + "فلمّا كان ثاني يوم جآء خادمُ التاجر واخذ الثورَ وركَّب \n", + "عليه المِحراث واستعمله كالعادة * فَبدأ الثورُ يُقصِرّ في \n", + "العمل والحرث فضربه الزرّاعُ ضربًا مُوْجِعًا – فكسر المحراثَ \n", + "وهربَ لأنه قَبِلَ وصِيّةَ الحمار * فلحقه الزّراعُ وضربه كثيرًا \n", + "حتي انّه أَيِسَ من الحيوة – فلم يزلْ الثّور يقوم ويقع الي \n", + "ان صار المساء * فجاء به الزّرّ\n", + "المعلف فَبطَّلَ الثورُ الصٌراخَ والمرَح واللَّبط بالرِجْلَيْن * ثَمّ \n", + "انّه تباعَدَ عن العلف – فتعَّجبَ الزَرّاعُ من ذلك * ثمّ انّ \n", + "الزَرّاع اتَاه بالفَول والعلَف فشَمْهُ وتأَخَّر عنه ونام بعيدًا \n", + "منه وبات بغير اكلٍ الي الصباح * فلمّا جاء الزّراعُ ووجدَ \n", + "العلَف والفُول والّتِبن مكانَهُ ولم ينقصْ منه شيءْ ورأَى \n", + "من كِتَابِ ألفِ لَيلَةٍ وَلَيلَةٍ\n", + "الثور قد انتفخ بطنهُ وتكسَّفَت احْوَالهُ ومدّ رجليه حَزِنَ \n", + "عليه وقال في نفسه – واللهِ لقد كان مستضعفًا بالامس فلاِجل \n", + "ذلك كان مقصرًا بالعمل * \n", + "ثم ان الزرّاَع جاء الي التاجر وقال له – يا مولاي – انَّ \n", + "الثورَ لم يأكل العلف في هذه المُدة من يومينِ – ولا ذاق\n", + "منه شيئًا * فعرف التَّاجِرُ الامر بتمامهِ كَوْنه قد سمع ما \n", + "قاله الحمارُ كما مَرَّ سابقًا * ثمَّ قال للزّارعِ اِذهَبْ الي الحمار\n", + "المكارِ وشُدّ عليه المحراث واِجتهِدْ في استعماله حتي \n", + "انَه يحرث مكانَ الثّور * فاخذه الزارعُ وشَدَّ عليه المحراث \n", + "واجتهَد به وكلفه ما لا يُطِيق حتّى انّه حرثَ مكان الثّور – \n", + "ولم يزل الحمار يأكل الضّرب حتي انسلخ جلدُه وتَهرَّت \n", + "اضلاعُه ورقبتُه * فلمّا كان المساء جآء بالحمار الي الدّار \n", + "وهو لا يقدر يجر يَديه ولا رِجليه * واّما الثور فانّه كان ذلك \n", + "النهار كله نائمًا مستريحًا – وقد اكل علفه كلّه بالهنأ والسٌّرور \n", + "والرّاحة – وهو طول نهاره يدعُو للحمار ولم يدرِ ما اصاب \n", + "من كِتَابِ ألفِ لَيلَةٍ وَلَيلَةٍ\n", + "الحمار من اجله * فلمّا اقبَل الليلُ دخل الحمارُ علي \n", + "الثور – فنهض له الثورُ قائمًا وقال له – بُشِرْتَ بالخير يا ابا \n", + "اليقظان ! لانك اَرَحْتَني في هذا اليوم وهَنَّأتَنِي بطعامي * فما \n", + "ردَّ عليه الحمارُ جوابًا من غيظهِ وغضبِه وتعبَه ومن الضَّرب \n", + "الذي اكلَهُ – الاّ أَنه قال في نفسِه – كُلٌ هذا جَرَي عْليَّ من \n", + "سْوء تدبيري ونصيحتي لغيري – كما قيل في المثل – كنت \n", + "قاعِدًا بطُولي ما خَلاٌني فُضولي * ولكن اذا لم اعمل له حيلةً \n", + "واردٌه الي ما كان فيه هلكتُ * ثُمّ انّ الحمار راح الي \n", + "معلفه والثَور يُخَوِرُ ويدعُو له * \n", + "فلما جرى للحِمار مع الثور ما جري خرج التاجر هو \n", + "وزوجته علي السطح ليلةً مقمرةً – والقمَرُ مُبِدر * فاشرَفَ علي\n", + "الثور والحمار من السّطح – فسمع الحمار يقول للثور – اَخْبِرْني\n", + "يا ابا الَّثِيْران! ما الذي تصنعه غدًا * فقال له الثور وما \n", + "الذي اصنعه غير الذي اشرْتَ به عليَّ – وهذا الثَّوْرُ في \n", + "غاية الحُسن وفيه راحة كُلِّية – وما بقيتُ اُفارقه مطلقًا – \n", + "من كِتَابِ ألفِ لَيلَةٍ وَلَيلَةٍ\n", + "واذا قُدِمَ العَلَفْ امكُر فاتمارَضُ واَنضخ بطني* فقال له \n", + "الحمارُ اِيّاكَ ان تفعَلَ ذلك! فقال له لِماذا ؟ فقال له – \n", + "سمعتُ صاحبَنا يقول للِزَّرّاع – اَنْ كان الثور لم ياكلْ علفَه\n", + "ولم ينهض قائمًا فادْعُ الجزار حتى يذبحه – وتصدَّقْ بلحمه \n", + "واجعلْ جلدة نطعًا – وانا خائِف عليكَ من ذلك * ولكن \n", + "اقبل نُصحي قبل أن يُصيبك هذا المصابُ – فإذا قدموا \n", + "لك العلَف فَكُلهُ وانهض وارفس برجليك الارضَ * واذا \n", + "لم تفعل ذلك فانّ صاحبنا يذبحك * فنهض الثور وصاح * \n", + "فلما سمع التاجر هذا المقال نهض علي حَيِلْه وضحك \n", + "ضحكًا عاليًا * فقالت له زوجتهُ وما هو الذي جري حتي \n", + "انك ضحكتَ هذا الضحك الكثير؟ لعلكَ تهزأ بي * فقال \n", + "لها كَلاّ * فقالت له ان كنتَ لم تهزأ بي قُلْ لي ما سبَبْ \n", + "ضِحِككَ * قال لها – لستُ أقدر علي ذلك – واخاف اذا \n", + "بُحْتُ بالسِر اموتُ * فقالت له زوجته – واللهِ انّك تكذب – \n", + "وانما اردتُ اخفاء الكلام عني * ولكن وَحَقِ رَبِ السٌماء ! \n", + "من كِتَابِ ألفِ لَيلَةٍ وَلَيلَةٍ\n", + "اذا لم تقُلْ لي ما سبب ضحكك ما اقعُد عندك من \n", + "الآن * وجَلَسَتْ تبكي * فقال لها زوُجها التاجرُ وَيلَكِ ما \n", + "لَكِ تبكين اِتَّقِي اللهَ وَعّدِي عن سُؤالك ودَعِينا من هذا \n", + "الكلام * فقالت لا بُدْ من ان تقولَ لي ما سبب ضحكك * \n", + "فقال أنني سألتُ ربّي ان يُعَلّمِني لُغَةَ الحيوانات فعلَّمَني – \n", + "ثم اني عاهدتُه ان لا اُعلم بذلك احدًا – وَاِنْ افشيتُ سِري \n", + "فأَمُتْ * فقالت لا بُدَّ من ان تقول لي ما سمعتَ من الثور \n", + "والحمار – ودَعْكَ تموت هذه الساعةَ * فقال لها اِدْعي اهلكِ\n", + "فدعَتْهم * ثمّ اتوا بعض الجيران – فاعلمهم التاجر بأنه قد \n", + "حضرته الوفاةُ * فجلسوا يبكون عليه – ثمّ بكوا عليه اولادُه \n", + "الصَغارُ والكِبارُ والزَّرّاع والغِلمانُ والخُدّامُ وسائل مَنْ يلوذُ به – \n", + "وصار عنده في الدار عزاء عظيم * \n", + "ثمّ انّه دعا بالشٌهود – فلما حضروا اَوْفَي زوجته حقَّها \n", + "وجعل وَصِيًا علي اولاده – واعتق جواريه وودّع اقرباءه واهله \n", + "فتباكوا كلٌهم * ثمٌ بكتِ الشٌّهود واقبلوا علي الامرأة يقولون\n", + "من كِتَابِ ألفِ لَيلَةٍ وَلَيلَةٍ\n", + "لها – ارجعي عن غَيّكِ واعدلي عن هذا الامر – ولو لم يتيقّن \n", + "انه إذا باح بالسِر يموت – ما كان فَعل هذه الفعال وكان \n", + "اَخْبَركِ به * فقالت لهم – والله لم ارجع عنه إذا لم يخبرني \n", + "به * فبكَي الحاضرون بُكاءً شديدًا * \n", + "وكان عنده في البيت خمسُون طيرًا من الدَّجاج ومعها \n", + "دِيكْ * فبينما هو يُودع اهله وعبيده سَمِعَ كلبًا من الكلاب \n", + "يقول للديك بلُغَته – ما اقلَّ عقلك ايَها الدَّيك! واللهِ لقد \n", + "خاب منْ رَبْاك أَفي مثل هذا الوقت تطير من ظهر هذه \n", + "الي ظهر هذه خيَّبك اللهُ تعالي؟ فلما سمع التاجر هذا \n", + "الكلام سكَتَ ولم يتكلَّم – وبقى يسمعُ ما يقول الكلبُ \n", + "والدِيكُ * فقال الدِيك وما في هذا اليوم أيها الكلب؟ \n", + "فقال – أما علمتَ ان سيدي اليوم متهيّأ للموت لان زوجته \n", + "تُريدُ ان يبوح لها بالسِر الذي علّمه الله به؟ واذا باح لها \n", + "بذلك مات من ساعته – وها نحن في حُزْن عليه وانت \n", + "تُصّفِق وتصيح ما تستحي على نفسك * فلمّا سمع الدِيكُ \n", + "مِنْ كِتَابِ ألفِ لَيلَةٍ وَلَيْلَةٍ\n", + "كلامَ الكلب قال له اذا كان سيّدُنا قليل العقل عديم \n", + "التّدبير – ما يقدر علي تدبير امره مع زوجةٍ واحدة – فما \n", + "لبقاء حيوتِه فائدة * \n", + "فقال الكلب وما ذا يصنع سيدُنا ؟ فقال له الديكُ – \n", + "انا عندي خمسون امرأة – اُغضبُ هذه وأرضي هذه \n", + "واُطعم هذه واُجوع هذه بحسن تدبيري – وكُلَّهنّ تحت \n", + "طاعتي * وسيدنا يدَّعي العقلَ والكمال – وعنده امرأةْ \n", + "واحدة – ما عَرفَ تدبيرَ امره معَها * فقال الكلبُ ايّها \n", + "الدّيك اَفِدْنا كيف يصنعُ سيّدُنا حتي يخلص من هذا \n", + "الامر؟ \n", + "فقال الديك – يقوم في هذه الساعة – ويأخذ عصًا بيده – \n", + "ويدخل بها الي بعض المخازن – ويغلق البابَ ويضربها \n", + "حتّي يكسر اضلاعها وظهرها وارجُلَها – ويقول لها انتِ \n", + "تسأَلين عن شيءٍ ما لَكِ فيه غَرض حتٌي تقولَ اتوبُ يا \n", + "سيٌدي – لا اسئلك عن شيءٍ طُولَ عمري – توبةً يا مولاي* \n", + "مِنْ كِتَابِ ألفِ ليلةٍ وَلَيْلَةٍ\n", + "فيوجعها ضربًا شديدًا – فاذا فعَل هذا استراح من الهم \n", + "وعاش * ولكن ما عنده عقل ولا فهم * فلما سمع التاجر \n", + "هذا الكلام من الديك قام مُسرعًا – واخذ الخيزران – \n", + "ودخل الخزانة وامرها بالدَّخول معه * فدخلت وهي فرحانة \n", + "فقام مسرعًا وغلَق البابَ ونزل بالخيزران علي كَتِفَيْها \n", + "وظهرها واضلاعها وايديها وارجلها * وهي تُعّيِط وترتعد \n", + "وتنتفض – وهو يضربها ويقول لها – تسألِيني عن شَيءٍ ما \n", + "لكِ فيه حاجة ؟ فتقول له أنا واللهِ من التّائبين – ولا اسألك \n", + "عن شيءٍ وقد تبتُ توبةً نَصُوحًا * فبعد ذلك فتح لها \n", + "الباب – وخرجت وهي تائبةْ * ففرح الشّهودُ والجيرانُ – واُمّها \n", + "وابوها – وانقلبَ العزاء بالفرح والسرور * وتَعلَّم التاجر حُسنَ \n", + "التّدبير من الديكِ * \n", + "SECTION VI.Extracts from the Ikhwanu – s – Safa.\n", + " فِي بَيَانِ بَدْء العَدَاوَةِ بَيْن الجانِ وبَنِي آدَمَ وَهْوَ مَأْخُوذْ\n", + "مِنْ رِسَالَةِ إخْوان الصّفا\n", + "قال الحكيم اِنّ في قديم الأيام والازمان قبل خَلْقِ اَبي \n", + "البَشَرِ كان سُكاّنْ الارض بني الجانّ وقاطِنُوها* وكانوا قد \n", + "اَطْبَقُوا الارض بَحرًا وبَرًّا سهلاً وجَبَلاً * فطالَتْ اعمارُهم \n", + "وكثرتِ النعمةُ عندهم – وكان فيهم المُلْكُ والنَّبوّة والدّينُ \n", + "والشَريعةُ * فَطَغَتْ وبَغَتْ وتركَتْ وصيّةَ انبِيائِها واكثرَتْ \n", + "في الارض الفسادَ – فضجَت الارضُ ومَنْ عليها من جَورهم * \n", + "فلمّا انقضَي الدّورُ واستأنَفَ القَرْنُ ارسلَ الله جُنْدًا من \n", + "مِنْ رِسَالَةِ إخْوان الصّفا\n", + "الملائكة نزلَتْ من السّماء فسكنَتْ في الارض وطردَتْ \n", + "بَنِي الجانِ الي اطرافِ الارض مُنهَزمةً * واخَذَتْ سَبايا \n", + "كثيرةً منها – وكان فيمَنْ اُخِذَ اسيرًا عزازيلُ ابليسُ اللّعينُ \n", + "فرعونُ آدم وحَوّاء – وهو اِذْ ذاك صَبِي لم يُدْرِكْ * فلمٌا \n", + "نَشِأ مع الملائكة تَعَلَّمَ مِ عِلمها وتشبَّهَ بها في ظاهِر \n", + "الامرِ ورسمُه وجوهرُه غيرُ رُسُومِها وجوهرِها * فلما تطاولَتْ \n", + "الأيام صار رَئيسًا فيها آمِرًا ناهِيًا مَتْبوعًا حِينًا ودَهرًا من \n", + "الزّمان * \n", + "فلمّا انقضَي الدّورُ واستأنف القرنُ اَوْحَي اللهُ الي \n", + "اولَئَك الملائكةِ الذين كانُوا في الارض فقال لهم – إنِي \n", + "جَاعِلْ فِي الأرضِ خَلِيفَةً مِنْ غَيرِكُم وَاَرْفَعُكُم اَلَي السَّمَاءِ * \n", + "فكرِهَتِ الملائكةُ الذين كانوا في الارض مفارقةَ الوَطنِ \n", + "المألوفِ وقالت في مراجعة الجواب – اَتَجْعَلُ فِيهَا مَنْ \n", + "يُفْسِدُ فيهَا ويَسْفِكُ الدِماءَ كَمَا كَانَتْ بَنوُ الجانِ ونَحْنُ \n", + "نُسَبِحُ بِحَمدِكَ ونُقَدِسُ لَكَ؟ قال اِنِي اَعْلَمُ مَا لَا تَعْلَمُون\n", + "مِنْ رِسَالَةِ إخْوان الصّفا\n", + "لأني آلَيْتُ علي نفْسي ان لا اتركُ آخر الامر بعد انقضاء \n", + "دولة آدم وذُرِيَّته علي وجه الارض احدًا من الملائكة ولا \n", + "من الجنَّ ولا من الانس ولا من سائر الحيوانات ولهذه \n", + "اليمين سرُّ قد بَيَّنَّاهُ في موضع آخر * \n", + "فلمّا خلق آدم فسَوَّاه ونفخ فيه من رُوْحهِ وخلق منه \n", + "زوجتَه حَوٌاءَ اَمَرَ الملائكة الذين كانوا في الارض بالسٌجود \n", + "له والطٌاعةِ * فانقادَتْ له الملائكةُ بِاجمعهم غير عِزازيلَ – \n", + "فانٌه اَنِفَ وتكبَّرَ واخذَتْهُ حَمِيَّةُ الجاهِلِيٌة والحسد لمٌا رَأي \n", + "ان رِئاستَهُ قد زالَتْ واحتاج ان يكون تابِعًا بعد اَنْ كانَ \n", + "مَتْبُوعًا ومرؤوسًا بعد اَنْ كان رئيسًا * واَمَر اُولئك الملائكةَ \n", + "اَن اصعَدُوا بآدم الي السٌماء فَادْخِلُوه الجنّة * ثمَّ اَوْحي الله \n", + "تعالي الي آدَمَ (عليه السلام) وقال – يَا آدَمُ اُسْكُنُ اَنْتَ \n", + "وَزَوجُكَ الجَنَّةَ وَكُلاَ مِنَها رَغَدًا حَيْثُ شِئتُمَا وَلاَ تَقرَبَا هذِهِ \n", + "الشَّجَرَةَ فَتَكُونا مَنَ الظَّالِمِين * \n", + "وهذه الجنَّة بُستان بالمشرِق علي رأس جبلِ الياقوت\n", + "مِنْ رِسَالَةِ إخْوان الصّفا\n", + "الذي لا يقدر احدٌ من البشرِ أن يصعَد الي هُناك – وهي \n", + "طيّبة التُربة معتدلُ الهواء صَيفًا وشتاءً وليلاً ونهارًا – كثيرةُ \n", + "الاَنهْار مُخْضَرَّةُ الاشجار مُفنَّنة الفواكه والّثِمارِ والرياضِ \n", + "والرياحين والازهارِ كثيرةُ الحيواناتِ الغير المؤذِية والطيور \n", + "الطيبة الاصواتِ اللذيذة الالحان والنَغَماتِ * وكان علي\n", + "رأس آدم وحَوّاء شعر طويلً مُدْلًي كَاحْسنِ ما يكون\n", + "علي الجواري الاَبْكارِ ويَبْلُغُ قَدَمَيْهما ويَسْتُرُ عَوْرتَيْهما وكان\n", + "دِثارًا لهما وسِتْرًا وزِيْنةً وجَمالاً * وكانا يمشيانِ علي حافّات\n", + "تلك الانهارِ بين الرياحين والاشجارِ – ويأكلان من الوان تلك\n", + "الثمار – ويشربانِ من مياهةِ تلك الانهار بلا تَعَب من الابدان \n", + "ولا عناء من النّفوس * ولا شَفاء من كَدّ الحَرْثِ والزَرْع والسّقي\n", + "والحصدِ والدِياسةِ والطَحْن والعَجْن والخْبز والَغْزل والنَسْجِ \n", + "والغَسْل كما في هذه الأيام اَوْلادُهما مُبْتَلُوْنَ به من شقاوةِ \n", + "اسباب المعاش في هذه الدنيا * \n", + "وكان حُكمُهما في تلك الجنّة كحُكم احَد الحيوانات \n", + "مِنْ رِسَالَةِ إخْوان الصّفا\n", + "التي هناك مستودَعَيْنِ مُسْتَمْتِعَيُنِ مستريحين متلذّذين – \n", + "وكان الله تعالى اَلّهَمَ الي آدمَ اسماء تلك الاشجارِ والثمارِ\n", + "والرّياحينِ واسماءَ تلك الحيوانات التي هناك * فلمّا نطق \n", + "سألَ الملائكةَ عنها فلم يكن عندها جوابٌ – فَقَعَدَ عند \n", + "ذلك آدمُ مُعِلّمًا يُعرِفُها اسماءَها ومنافعها ومضاّرها – فانقادتِ \n", + "الملائكةُ لامره ونهيهِ لِما تَبيَّنَ لها من فضلهِ عليها * ولمٌا \n", + "رأي عزازيلُ ذلك ازداد حسدًا وبُغضًا فَاحتال لهِما المكرَ \n", + "والخديعة والحِيَلَ غدًا وعِشَاءً * ثم اتَاهُما بصورة النّاصح \n", + "فقال لهما – لقد فَضَّلكما الله بما اَنْعَم عليكما به من \n", + "الفصاحة والبيان – ولو اَكَلْتُما من هذه الشّجرة لازدَدْتُما \n", + "عِلْمًا ويقينًا وبَقِيْتُما ههنا خالَدَيْنِ آمنينِ لا تموتانِ \n", + "ابدًا * فاغتَرّا بقولهِ لَمّا حَلَفَ لهما – إنِي لَكُمَا لَمِنَ \n", + "النَّاصِحِين * وحَمَلَهُما الحرصُ فَتسابَقا وتناوَلا ما كانا \n", + "مَنْهِيَّنْنِ عنه * \n", + "فلمّا اَكَلا منها طارت عنهما اَلْبِسَةُ الجنٌةِ وحُلَلُها وحُلِيُّها – \n", + "مِنْ رِسَالَةِ إخْوان الصّفا\n", + "فبدَتْ لهما سَوآتُهما – وطَفِقا يَخْصِفانِ من ورق الجنّة * \n", + "ثم تناثَرَتْ شُعورهما وانكشفت عوراتهما وبَقِيا عُريانَيْن – \n", + "واصابهما حَرٌ الشمس واسودَّت ابدانُهما وتغيَّرت الوانُ \n", + "وجوهِهما * ورأتِ الحيواناتُ حالَهما فانكرَتْهما ونفرَتْ \n", + "منهما واستوحشتْ من سوء حالهما * فامر اللهُ الملائكَة \n", + "ان اَخْرِجُوْهما من هُناك وارمُوْا بهما الي اسفرِ الجبلِ * \n", + "فوقعا في بَرٍ قَفْرٍ لا نَبْتَ فيها ولا ثمرَ وبقيا هناك زمانًا \n", + "طويلاً يبكيانِ ويَنْوحانِ حزنًا واسفًا علي ما فاتهما نادِمَينِ \n", + "علي ما كان منهما * ثمّ انّ رحمة الله تدارَكَتْهما فتاب \n", + "اللهُ عليهما – وارسلَ مَلَكًا يُعَلِمُهما الحرث والزرعَ والحصادَ\n", + "والّدِياسةَ والطّحْنَ والخَبْز والغزل والنسجَ والخياطة وإتْخاذ \n", + "اللْباسِ * ولمّا تَوَالَدُوا وكثرتْ ذُرِيَّتُهما خالطهم اولادُ بني \n", + "الجانّ وعَلْموهم الصنائعَ والحرثَ والغرس والبنيانَ والمنافع\n", + "والمضاَّر وصادقُوهم وتَوَّددوا اليهم وعَاشَرُوهم مدّةً من الزمان \n", + "بالحُسني * \n", + "مِنْ رِسَالَةِ إخْوان الصّفا\n", + "ولكن كلّما ذُكَرَ بنو آدم ما جري علي ابيهم مَن كيدِ \n", + "عَزازيل ابليس اللعينِ وعداوته لهم امتلأت قلوبُ بني آدم\n", + "غيظًا وبُغْضًا وحَنَقًا علي اولاد بني الجانّ * فلمّا قَتَلَ قابيلُ \n", + "هابيلَ اعتقدَ اولادُ هابيلَ انّ ذلك كان من تعليم بني \n", + "الجانَّ – فازدادوا غيظًا وبغضًا وحنقًا علي اولاد بني الجانٌ – \n", + "وطلبوهم كلّ مطلبٍ – واحتالُوْا لَهُمْ بكلّ حيلةٍ من العزائم\n", + "والرٌّقي والمَنادِلِ والحِبْس في القوارير والعذاب باَلْوان\n", + "الاْدخِنَة والبَخوراتِ المُوذِية لاولاد الجانٌ المُنَفّرةِ لهم المُشِتَّةِ \n", + "لامرهم * وكان ذلك دابهم الي اَنْ بعثَ الله تعالي ادريسَ \n", + "النبيّ (علي نبِيِنّاَ وعليه السلام) فاصلح بين بني الجانّ \n", + "وبني آدم بالدّين والشريعة والاسلام والملّة * وتراجعت بنو \n", + "الجانّ الي ديار بني آدَمَ وخالطوهم وعاشوا معهم بخير الي \n", + "ايّام الطوفان الثاني – وبعدها الي ايّام إبراهيمَ خليلِ الّرحمن \n", + "(علي نبيّنا وعليه السّلام) * فلمّا طُرِحَ في النار اِعْتقدَ بنو\n", + "آدم بانّ تعليم المنجنيق كان من بني الجانّ لنمرود\n", + "مِنْ رِسَالةِ إخْوان الصّفا\n", + "الجَبّارِ * ولمّا طَرَحَ اِخْوةُ يوسفَ اَخاهم في البِئر نُسِبَ ذلك \n", + "ايضًا الي نزغات الشيطانِ من اولاد الجانّ * فلمّا بُعُِثَ \n", + "موسي ع اَصْلَح بين بني الجانّ وبني اسرائيلَ بالدّين\n", + "والشريعة – ودخل كثيرْ من الجنّ في دين موسي ع * فلمّا \n", + "كان ايامُ سُلَيمانَ بِن داود (عليهما السلام) وشَيَّد الله مُلْكه \n", + "وسَخّر له الجنَّ والشياطينَ وغلب سليمانُ علي مُلوك الأرضِ \n", + "افتخرتِ الجنُّ علي الانس بأن ذلك من مُعاونة الجنّ \n", + "لسليمانَ – وقالت لولا معاونةُ الجنّ لسليمان لكان حكمهُ \n", + "حكم احدِ ملوك بني آدم – وكانت الجنُّ توهِمُ الانس انّها \n", + "تَعلم الغيبَ * \n", + "ولمّا مات سليمانُ والجُّن كانوا في العذابِ المُهيمن\n", + "ولم يشعروا بموته – فتبيّن للانس انّها لو كانت تَعْلَمُ الغيبَ \n", + "ما لَبِثَتْ في العذاب المُهينِ * وايضًا لمّا جاء الهُدْهُدُ بخبر \n", + "بِلقيس – وقال سليمانُ لِمَلاَء الجنّ والانس ايكم يأتيني \n", + "بعرشها قبل اَن يأتوني مسلمين افتخرت الجنٌ – وقال \n", + "ِمنْ رِسَالَةٍ إخْوان الصَفا\n", + "عفريتْ منها انا آتيك به قبل ان تقوم من مقامك\n", + "اي من مجلسِ الحكم وهو اصطوس بن الايوان * قال \n", + "سليمان اُريدُ اَسرَعَ من ذلك * فقال الذي عنده علمْ من \n", + "الكتاب وهو آصَفُ بنُ بَرْخِيا – انا آتيك به قبل أن يرتدّ \n", + "اليك طرفك * فلّما رَآه مُسْتَقِرًّا عنده خَرَّ سليمانُ ساجِدًا \n", + "لله حين تَبيَّنَ فَضْلُ الانس علي الجنّ – وانقضي المجلسُ \n", + "وانصرفتِ الجنُّ من هناك خَجِلِينَ مُنَكّسِيْنَ رْؤوسهم \n", + "وغَوْغاوُ الانس يُطَقْطِقُون في اَثَرهم – ويُصَفّقُوْن خَلْفَهم\n", + "شامتِنْنَ بهم * \n", + "فلمّا جري ما ذكرتُ هربَتْ طائفة من الجنّ من \n", + "سليمان – وخرج عليه خارجيّ منهم * فوَّجهَ سليمانُ في \n", + "طلبه من جُنوده – وعَلَّمهُمْ كيف يأخذونَهم بالرّقي والعزائم\n", + "والكلماتِ والآياتِ المُنزلات – وكيف يحسبونهم بالمنادِل* \n", + "وَعَمِلَ لذلك كتابًا وُجِدَ في خزانتِه بعد موتِه – واَشْغَلَ \n", + "سليمانُ طُغاة الجنّ بالاعمال الشّاقَة الي اَنْ ماتَ * ولمّا \n", + "ِمنْ رِسَالَةِ إخْوان الصّفا\n", + "اَنْ بُعِثَ المسيح ع ودعا الخلقَ من الجنّ والانس الي اللّه\n", + "تعالي ورغَّبَهم في لقائه وبّيَّن لهم طريق الهُدي وعَلَّمهم \n", + "كيف الصعودُ الي ملكوت السّمواتِ * فدخل في دينه \n", + "طوائفُ من الجنّ وتَرَهَّبَتْ وارتقت الي هُناك – وسمعَتْ \n", + "من الملاء الاعلي الاخباَر واَلْقَتْ الي الكَهَنَةِ * فلمّا بَعث \n", + "اللّه محمّدًا (صلّى الله عليه وآله وسلّم) مُنِعَتْ من استراقِ \n", + "السَّمْع فقاَلتْ لا ندري اَشَرّ اُريْدَ بِمَنْ في الارض اَمْ اَراَد\n", + "بِهِمْ رَبُّهَم رَشِدًا * ودخلَتْ قبائلُ من الجنّ في دينه – وحَسُنَ\n", + "اسلامُها وصَلُحَ الامرُ بين الجانِ وبين المسلمين ومن اولاد \n", + "آدم إلي يومِنا هَذا *\n", + "SECTION VII.\n", + " Historical Extracts.وَاقِعَةُ رِحْلَةِ رَسُولِ اللُهِ صلعم عَلَي مَا ذَكَرَ أبُو الْفِداء\n", + "لمّا قدم رسول الله من حجة الوداع اقام بالمدينة حتي \n", + "خرجت سنة عشر والمحرم من سنة احدى عشرة ومعظم \n", + "صفر وابتدى برسول الله مرضه في اواخر صفر وقيل لليلتين \n", + "بقيتا منه وهو في بيت زينب بنت جحش وكان يدور\n", + "علي نسَاءِهِ حتّي اشتدّ مرضه وهو في بيت ميمونة بنت \n", + "الحارث فجمع نساءه واستأذنهنّ في ان يمرض في بيت \n", + "احديهنّ فاذنَ له ان يمرض في بيت عايشة فانتقل اليها \n", + "وكان قد جهّز جيشًا مع مولاه اسامة بن زيد واكّد في \n", + "مسيره في مرضه وروي عن عايشة رضي الله عنها انها \n", + "ِمنْ كُتُبِ اَلتّوَارِيخِ\n", + "قالت جاء رسول الله وبي صداع وانا اقول وا راساه فقال \n", + "بل انا والله يا عايشة اقول وا راساه ثم قال ما ضرّك لو \n", + "متّ قبلي فقمت عليك وكفنتك وصلّيت عليك ودفنتك \n", + "قالت فقلت كاني بك والله لو فعلت ذلك ورجعت الي \n", + "بيتي وتعزيت ببعض نسايك فتبسّم صلعم *\n", + "وفي اثناء مرضه (وهو في بيت عايشة) خرج بين الفضل \n", + "بن العباس وعلّي بن ابي طالب حتي جلس علي المنبر \n", + "فحمد الله ثم قال ايها الناس من كنت جلدتُ له ظهرًا فهذا \n", + "ظهري فليستقدمنّي ومن كنت شتمت له عِرضًا فهذا عرضي \n", + "فليستقد منه ومن اخذت له مالاً فهذا مالي فلياخذ \n", + "منه ولا يخشى الشحناء من قبلي فانها ليست من شاني \n", + "ثم نزل وصلّى الظهر ثم رجع إلي المنبر فعاد الي مقالته \n", + "فادّعي عليه رجل ثلاثة دراهم فاعطاه عوضها ثم قال الا انّ \n", + "فضوح الدنيا اَهْونَ من فضوح الاخرة ثم صلّي علي اصحاب\n", + "اُحُد واستغفر لهم ثم قال انّ عبدًا خيّره الله بين الدنيا \n", + "ِمنْ كُتُبِ اَلتّوَارِيخِ\n", + "وبين ما عنده فاختار ما عنده فبكي ابو بكر ثم قال فديناك \n", + "بانفسنا ثم اوصي بالانصار * \n", + "فلما اشتدٌ به وجعه قال ايتوني بدواة وبيضاء فاكتب\n", + "لكم كتابًا لا تضلّون بعدي فتنازعوا فقال قوموا عني\n", + "لا ينبغي عند نبي تنازع فقالوا انّ رسول الله يهجر فذهبوا \n", + "يعيدون عليه فقال دعوني فما انا فيه خير مما تدعونني\n", + "اليه وكان في ايام مرضه يصلّي بالناس وانّما انقطع بثلاثة \n", + "ايام فلما اوذن بالصلوة اول ما انقطع فقال مروا ابا بكر \n", + "فليصلّي بالناس وتزايد به مرضه حتي توفّي يوم الاثنين \n", + "ضحوة النهار وقيل نصف النهار قالت عايشة رايت رسول \n", + "الله وهو يموت وعنده قدح فيه ماء يُدْخِل يده في القدح \n", + "ثم يمسح وجهه بالماء ثم يقول اللهمّ اعّني علي سكرات \n", + "الموت قالت وثقل في حجري فذهبت انظر في وجهه \n", + "واذا بصره قد شخص وهو يقول بل الرفيق الاعلي *\n", + "منْ كُتُبِ اَلتّوَارِيخِ\n", + "فِي بَيَانِ خِلافَةِ هَارُونَ الرّشِيدِ وإنْقِضَاء البَرَامَكةِ\n", + "من تَارِيخِ أَبِي اْلفَرَجِ\n", + "لمّا توفّي الهادي بويع الرشيد هارون بالخلافة في الليلة\n", + "التي مات فيها الهادي وكان عمره حين ولي اثنتين \n", + "وعشرين سنة وامّه الخيزران ولمّا مات الهادي خرج الرشيد \n", + "فصلّي عليه بعيساباد ولمّا عاد الرشيد الي بغداد وبلغ \n", + "الجسر دعا الغوّاصين وقال كان ابي قد وهب لي خاتمًا \n", + "شراه ماية الف دينار فاتاني رسول الهادي اخي يطلب \n", + "الخاتم وانا ههنا فالقيته في الما فغاصوا عليه واخرجوه فسرّ \n", + "به ولمّا مات الهادي هجم خزيمة بن حازم تلك الليلة \n", + "علي جعفر بن الهادي فاخذه من فراشه وقال له لتخلعنها \n", + "او لاضربّن عنقك فاجاب الي الخلع واشهد الناس عليه \n", + "فحظي بها خزيمة *\n", + "وقيل لمّا مات الهادي جا يحيي بن خالد البرمكي الي \n", + "ِمنْ كُتُبِ اَلتّوَارِيخِ\n", + "الرشيد فاعلمه بموته فبينا هو يكلّمه اذ اتاه رسول آخر \n", + "يبشّره بمولود فسمّاه عبد الله وهو المامون فقيل في ليلة \n", + "مات خليفة وقام خليفة وولد خليفة وفي هذه السنة ولد \n", + "الامين واسمه محمّد في شوال وكان المامون اكبر منه \n", + "ولمّا ولي الرشيد استوزر يحيي البرمكي وفي سنة اثنتين \n", + "وسبعين وامية بايع الرشيد لعبد الله المامون بولاية العهد \n", + "بعد الامين وولاّه خراسان وما يتّصل بها الي همدان ولقبه \n", + "المامون وسلّمه إلى جعفر بن يحيي الرمكي وفيها حملت \n", + "بنت حاقان الخزر الي الفضل بن يحيي البرمكي فماتت \n", + "ببرذعة فرجع من معها الي ابيها فاخبروه انّها قتلت غيلة \n", + "فتجهّز الي بلاد الاسلام وفيها سلمت الروم عيني ملكهم \n", + "قسطنطين بن لاون واقرّوا امّه ايريني وغزي المسلمون \n", + "الصايفة فبلغوا افسوس مدينة اصحاب الكهف *\n", + "وفي سنة ثلث وثمانين وماية خرج الخزر بسبب ابنة \n", + "خاقان من باب الابواب فاوقعوا بالمسلمين واهل الذمّة\n", + "مِنْ كُتُبِ اَلتّوَارِيخِ\n", + "وسبوا اكثر من ماية الف راس وانتهكوا امرًا عظيمًا لم \n", + "يسمع بمثله في الارض وفي سنة ست وثمانين وماية \n", + "اخذ الرشيد البيعة لقاسم ابنه بولاية العهد بعد المامون \n", + "وسمّاه الموتمن وفي سنة سبع وثمانين وماية خلعت \n", + "الروم ايريني الملكة وملّكت نيقيفور وهو من اولاد جبلة \n", + "فكتب الي الرشيد من نيقيفور ملك الروم الي هارون ملك \n", + "العرب امّا بعد فانّ الملكة ايريني حملت اليك من \n", + "اموالها ما كانت حقيقًا تحمل اضعافه اليها لكن ذلك \n", + "ضعف النسا وحمقهنّ فاذا قرات كتابي هذا فاردد ما \n", + "اخذت والاّ فالسيف بيننا وبينك *\n", + "فلمّا قرا الرشيد الكتاب استفّزه الغضب وكتب في \n", + "ظهر الكتاب من هارون امير المؤمنين الي نيقيفور زعيم \n", + "الروم قد قرأت كتابك والجواب ما تراه دون ما تسمعه \n", + "ثمّ سار من يومه حتي نزل علي هرقلة فاحرق ورجع وفي \n", + "هذه السنة اوقع الرشيد بالبرامكة وقتل جعفر بن يحيي \n", + "ِمنْ كُتُبِ اَلتّوَارِيخِ\n", + "البرمكي وكان سبب ذلك اّن الرشيد كان لا يصبر عن جعفر \n", + "وعن اخته عباسة بنت المهدي وكان يحضرهما اذا جلس \n", + "للشرب فقال لجعفر ازوّجكها ليحلّ لك النظر اليها ولا\n", + "تقربها فاجابه الي ذلك فزوجها منه وكانا يحضران معه ثم \n", + "يقوم عنهما وهما شابّان فجامعها جعفر فحملت منه \n", + "وولدته توامين فعلم ذلك الرشيد فغضب وامر بضرب عنق\n", + "جعفر بن يحيي وحبس اخاه الفضل واباه يحيي بالرقة \n", + "حتى ماتا وكتب الى العمّال في جميع النواحي بالقبض \n", + "على البرامكة واستصفي اموالهم * \n", + "ثم امر بعباسة فجعلت في صندوق وتدلت في بيروهي \n", + "حيّة وامر بابنيها فاحضرا فنظر اليهما مليًا وكانا كلولوتين \n", + "فبكي ثم رمي بهما البيروطمّها عليهما وفي سنة تسعين \n", + "وماية ظهر رافع بن الليث بماورا النهر مخالفًا للرشيد \n", + "بسمرقند وفي سنة اثنتين وتسعين وماية سار الرشيد من \n", + "الرقة إلى بغداد يريد خراسان لحرب رافع ولمّا صار ببعض \n", + "ِمنْ كُتُبِ اَلتّوَارِيخِ\n", + "الطريق ابتدات به العلّة ولمّا بلغ جرجان في صفر اشتدّ\n", + "مرضه وكان معه ابنه المامون فسيّره الي مرو ومعه جماعة \n", + "من القوّاد وسار الرشيد الي طوس واشتدّ به المرض حتي\n", + "ضعف عن الحركة ووصل اليه هناك بشير بن الليث اخو \n", + "رافع اسيرًا فقال له الرشيد والله لو لم يبق من اجلي الا ان \n", + "احرك شفتي بكلمة لقلت اقتلوه ثم دعا بقصّاب فامر به\n", + "ففصل اعضاه فلمّا فرغ منه اغمي عليه ثم مات ودفن\n", + "بطوس سنة ثلث وتسعين وماية وكانت خلافته ثلثًا\n", + "وعشرين سنة وكان عمره سبعًا واربعين سنة وكان جميلاً \n", + "وسيمًا ابيض جعدًا قد وخطه الشيب وكان بعهده ثلثة \n", + "الامين وامّه زبيدة بنت جعفر بن المنصور ثم المامون \n", + "وامّه ام ولد اسمها مراجل ثم الموتمن وامّه امّ ولد قيل\n", + "وكان الرشيد يصلّي كل يوم ماية ركعة الي ان فارق الدنيا \n", + "الا من مرض وكان يتصدّق من صلب ماله كل يوم بالف \n", + "درهم بعد زكاتها *\n", + "ِمنْ كُتُبِ اَلتّوَارِيخِ\n", + "قيل ان الرشيد في بدو خلافته سنة احَدي وسبعين \n", + "وماية مرض من صداع لحقه فقال ليحيي بن خالد بن \n", + "برمك هاولا الاطبّا ليسوا يفهمون شيًا وينبغي ان تطلب \n", + "لي طبيبًا ماهرًا فقال له عن بختيشوع بن جيورجيس فارسل \n", + "البريد في حمله من نيسابور ولمّا كان بعد ايّام ورد ودخل \n", + "علي الرشيد فاكرمه وخلع عليه خلعة سنية ووهب له مالاً \n", + "وافرًا وجعله رييس الاطبّا ولمّا كان في سنة خمس وسبعين \n", + "وماية مرض جعفر بن يحيى بن خالد بن برمك فتقدّم \n", + "الرشيد الي بختيشوع ان يخدمه ولمّا افاق جعفر من مرضه \n", + "قال لبختيشوع اريد ان تختار لي طبيبًا ماهرًا اكرمه واحسن \n", + "اليه قال له بختيشوع لست اعرف في هاولا الاطبّا احذق من \n", + "ابني جبريل فقال له جعفر احضرنيه فلمّا احضره شكا \n", + "اليه مرضًا كان يخفيه فدبّره في مدة ثلثة ايّام وبرا فاحبّه \n", + "جعفر مثل نفسه *\n", + "وفي بعض الايّام تمطّت حظية الرشيد ورفعت يدها \n", + "ِمنْ كُتُبِ اَلتّوَارِيخِ\n", + "فبقيت مبسوطة لا يمكنها ردها والاطبّا يعالجونها بالتمريخ \n", + "والادهان فلا ينفع ذلك شيًا فقال له جعفر عن \n", + "جبريل ومهارته فاحضره وشرح له حال الصبيّة فقال \n", + "جبريل ان لم يسخط امير المومنين عليّ فلها عندي \n", + "حيلة قال له الرشيد ما هي قال تخرج الجارية الي هاهنا\n", + "بحضرة الجميع حتى اعمل ما اريد وتمتهل عليّ ولا تسخط \n", + "عاجلا فامر الرشيد فخرجت وحين رأها جبريل اسرع اليها \n", + "ونكس راسها وامسك ذيلها كانّه يريد ان يكشفها \n", + "فانزعجت الجارية ومن شدّة الحيا والانزعاج استرسلت \n", + "اعضاوها وبسطت يدها الي اسفل وامسكت ذيلها فقال \n", + "جبريل لقد برأت يا امير المومنين فقال الرشيد للجارية \n", + "ابسطي يدك يمنة ويسرة ففعلت فعجت الرشيد وكُلّ \n", + "من حضر وامر لجبريل في الوقت بخمسماية الف درهم\n", + "واحبّه *\n", + "\n" + ] + } + ], + "source": [ + "# Let's get a random one !\n", + "from random import randint\n", + "# The index needs to be between 0 and the number of collections\n", + "rand_index = randint(0, len(reffs)-1)\n", + "reff = reffs[rand_index]\n", + "\n", + "passage = resolver.getTextualNode(collection.id, reff)\n", + "print(passage.id, passage.reference)\n", + "\n", + "# Let's see the XML here\n", + "# For that, we need to get the mimetype right :\n", + "from MyCapytain.common.constants import Mimetypes\n", + "print(passage.export(Mimetypes.XML.TEI))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### (If you are lucky on random) Get sub passage because it's too big\n", + "\n", + "Let's get the first and the last passage of this text shall we ?" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SECTION I.\n", + " Miscellaneous Sentences.جُملاَت مُختَلِفَة\n", + "1 اَلدٌّنيَا دَارُ مَمَرٍ لاَ دَارُ مَقَرٍ * سُلطَان بِلاَ عَدلٍ كَنَهرٍ \n", + "بِلاَ مَاءٍ * عَالِم بِلاَ عَمَلٍ كَسَحَابٍ بِلاَ مَطَرٍ * غَنِي بِلاَ سَخَاوةٍ \n", + "كِشجِرٍ بِلاَ ثَمَرٍ * إمرأة بِلاَ حَيَاءٍ كَطَعَامٍ بِلاَ مِلحٍ * لاَ تَستَصغِر \n", + "عَدُوًّا وَإِن ضَعُفَ * قِلَّةُ الأكلِ يَمنَعُ كَثِيرًا مِن أَعلاَلِ الجِسمِ * \n", + "بِالعَمَلِ يُحَصَّلُ الثَّوَابُ لاَ بِالكَسَلِ * \n", + "2 مَنْ رَضيَ عَن نَفسِهِ كَثُرَ السَّاخِطُ عَلَيهِ * إِذَا كُنتَ \n", + "كَذُوبًا فَكُن ذَكُورًا * رَأسُ الُدِينِ المَعرِفَةُ * أَلسَّعِيدُ مَنْ وُعِظَ \n", + "بِغَيرِهِ * أَلصَّبرُ مِفتَاحُ الفَرَحِ * ألصِنَاعَةُ فِي الكَفِّ أَمَان مِنَ \n", + "الفَقْرِ * مَنْ تَسَمَّعَ سَمِعَ مَا يَكرَهُ * قَلبُ الأَحمَقِ فِي فِيهِ \n", + "وَلِسَانُ العَاقِلِ فِي قَلبِهِ * كُنْ قَنِعًا تَكُنْ غَنِيًّا كُن مُتوَكّلاً تَكُنْ\n", + "جُملاَت مُختَلِفَة\n", + "قَوِيًا * حُبٌّ الدٌّنيَا يُفسِدُ العَقلَ وَيُصِمٌّ القَلبَ عَن سَماعِ \n", + "الحِكْمَةِ *\n", + "3 شَرٌّ النَّوَالِ مَا تَقَدَّمَهُ المَطلُ وَتَعَقَّبَهُ المَنٌّ * شَرٌّ \n", + "النَّاسِ مَن يُعِينُ عَلَي المَظلُومِ وَيَنصُرُ الظَّلمَ * شَيآنِ لاَ يُعرَفُ \n", + "فَضلُهُمَا إلاَّ مِن فَقدِهِمَا الشَّبَابُ وَالعَافِيَةُ * الكَسَلُ وَكِثرَةُ \n", + "النَّومِ يُبعِدَانِ مِنَ اللهِ وَيْورِثَانِ الفَقرَ * لَيسَ مِن عَادَةِ \n", + "الكِرَام تَأخِيرُ الأنعَامِ – لَيسَ مِن عَادَةِ الأَشَرافِ تَعجيِلُ \n", + "الإِنتِقَامِ * الصَّدِيقُ الصَّدُوقُ مَن نَصَحَكَ فِي عَيبِكَ وَأَثَرَكَ \n", + "عَلَي نَفْسِهِ * الأَمَلُ كَالسَّرَابِ – يَغُرٌّ مَن رَأَهُ وَيُخلِفُ مَن \n", + "رَجَاهُ *\n", + "4 ثَلاَثَة يُمتَحَنُ بَهنَّ عَقلُ الرِجَالِ – المَالُ وَالوِلاَيَةُ \n", + "والمُصِيبَةُ * إيَّاكَ وَحُبَّ الدٌنيَا – فَإنَّهَا رَأسُ كُّلِ خَطِيَّةٍ \n", + "وَمَعدِنُ كُلّ بَلِيَّةٍ * الحَسَدُ دَآءُ عََيَاء – لاَ يَزُولُ إلاَّ بَهَلكِ الحَاسِدِ \n", + "أَو مَوتِ المَحسُودِ * زِد فِي إصطِنَاع المَعرُوفِ وَأَكثِر مِن \n", + "أَشِدَّآءِ الإحسَانِ – فَإِنَّهُ أَيقَنُ ذُخرٍ وَأَجمَلُ ذُكرٍ * سَل عَنِ \n", + "جُملاَت مُختَلِفَة\n", + "الرَّفِيقِ قَبلَ الطَّرِيقِ – سَل عَنِ الجَارِ قَبلَ الدَّارِ * جَالِس \n", + "أَهلَ العِلم وَالحِكمَةِ وَأَكثِر مُنَافَثَتَهُم – فَإنَّكَ إن كُنتَ جَاهِلاً \n", + "عَلَّمُوكَ وَإِن كُنتَ عَالِمًا إِزدَدْتَّ عِلمًا * \n", + "5 ذُو الشَّرَفِ لاَ تُبطِرُهُ مَنزِلَة نَالَهَا وَإِن عَظُمَت كَالجَبَلِ \n", + "الذَِّي لاَ تُزَعزِعُهُ الرِيّاَحُ – وَالدَّنِيٌّ تُبطِرُهُ أَدنَي مَنزِلةٍ كَالكَلاءِ\n", + "الذَّي يُحَرِكُهُ مَرٌّ النَّسِيم * خَمس يُستَقبِحُ فِي خَمسٍ – كِثرَةُ \n", + "الفُجُورِ في العُلَمَاء وَالحِرصُ في الحُكَمَاءِ والبُخلُ فِي الأغنِيآءِ \n", + "وَالقُبحَةُ في النِسَاءِ وَفِي المَشيَخُ الزِنَاءُ * قَالَ ابنُ المُعتَزّ – \n", + "أَهلُ الدُنياَ كَرُكَّابِ سَفِينَةٍ – يُسَارُ بِهِم وَهمُ نُيَّام * صُن إنمَانَكَ \n", + "مِنَ الشَّكِ – فَإِنَّ الشٌكَّ يُفسِدُ الإيمَانَ كَمَا يُفسِدُ المِلحُ \n", + "العَسَلَ *\n", + "6- طُوبَي لَمَن كَظَمَ غَيظَهُ وَلَمْ يُطلِقْهُ – وَعَصَي إِمرَةَ نَفسِهِ \n", + "وَلَم تُهلِكهُ * قَالَ المَسِيحُ بنُ مَريَمَ (عَلَيهِ السَّلاَمُ) – عَالَجتُ \n", + "الأكَمَهَ وَالأَبرَصَ فَأَبرأتُهُمَا – وَأَعيَانِي عِلاَجُ الأحمَقِ * قَالَ \n", + "ابنُ المُقَفَّعِ – إِذَا حَاجَجْتَ فَلاَ تَغضَب – فَإنَّ الغَضَبَ يَقطَعُ \n", + "جُملاَت مُختَلِفَة\n", + "عَنكَ الحُجَّةَ وَيُظهِرُ عَلَيكَ الخَصمَ * مَثَلُ الأَغنِياء البُخَلاَءِ \n", + "كَمَثَلِ البِغَالِ وَالحَمِيرِ – تَحمَلُ الذَّهَبَ وَالفِضَّةَ وَتعْتَلِفُ \n", + "بِالتِبْنِ وَالشَّعِيرِ * قَالَ أَبُو مُسلِمِ الخُرَاسَانيٌّ – خَاطَرَ مَن رَكِبَ \n", + "البَحرَ – وَأَشَدٌّ مِنهُ مُخَاطَرةً مَن دَاخَلَ المُلُوكَ * \n", + "7 مِثلُ الذَِّي يُعَلِمُ النَّاسَ الخَيرَ وَلاَ يَعمَلُ بَه كَمِثلِ \n", + "أَعمَي بِيَدِهِ سِرَاج – يَستَضِيءُ بِهِ غَيرَهُ وَهُوَ لاَ يَرَاهُ * أَضعَفُ \n", + "النَّاسِ مَن ضَعُفَ عَن كِتمَانِ سِرِهِ – وَأَقَواهُم مِن قَوِيَ عَلَي \n", + "غَضَبِهِ – وَأَصبَرُهُم مَن سَتَرَ فَاقَتَهُ – وَأَغنَاهُم مَن قَنَعَ بَمَا \n", + "تَيَسَّرَ لَهُ * قَالَ أَمِيرُ المُؤمِنيِنَ عَلِيٌّ بنُ أبِي طَالِبِ (كَرَّمَ \n", + "اللهُ وَجْهَهُ) مَنْ عُرِفَ بِالحِكْمَةِ لاَحَظَتهُ العُيُونُ بِالوَقَارِ * قَالَ \n", + "بَعضُ الحُكَمَاءِ – تَحتَاجُ القُلُوبُ إلَي أَقوَاتِهَا مِنَ الحِكمَةِ كَمَا \n", + "تَحتَاجُ الأَجسَامُ إلىَ أَقوَاتِهَا مِنَ الطَّعَامِ * \n", + "8 قَالَ آْفلاَطُونُ – حُبَّكَ لِلشَّيءِ سِتربَينَكَ وَبَيْنَ مَسِاوِيِهِ \n", + "وَبَغضُكَ لَهُ سِتر بَيْنَكَ وَبَيْنَ مَحَاسِنِهِ * مَن مَدَحَكَ بِمَا \n", + "لَيسَ فَيك مِنَ الجَميِلِ وَهُوَ رَاضٍ عَنكَ ذِمَّكَ بِمَا لَيسَ \n", + "جُمْلاَت مُختَلِفَة\n", + "فِيكَ مِنَ القَبِيح وَهُوَ سَاخِط عَليكَ * قَالَ أفلاَطُونُ الحَكِيمُ لاَ \n", + "تَطلُب سُرعَةَ العَمَلِ وَاطلُب تَجوِيدَهَ فَإنَّ النَّاسَ لاَ يَسألوُنَ \n", + "فِي كَم فَرَغَ وَإِ نَّمَا يَنظُرُونَ إلَي إِتقَانِهِ وَجُودَةِ صَنعَتِهِ * وُجِدَ \n", + "عَلَي صَنَم مَكتُوب حَرَام عَلَي النَّفس الخَبِيثَةِ أَن تَخرُجَ مِن \n", + "هذِهِ الدُنيَا حَتَى تُسيِء إلَي مَن أَحسَنَ إلَيهَا * \n", + "9 ثَلاَثَة لاَ يَنفَعُونَ مِن ثَلاَثةٍ شَريف مِن دَنِي وَبَار مِن \n", + "فَاجِرٍ وَحَكِيم مِن جَاهِلٍ * قَالَ عَامِرُ بنُ عَبدِ القَيسِ إذَا \n", + "خَرَجَتِ الكَلِمَةُ مَنَ القَلبِ دَخَلَتِ فِي القَلبِ – وَإذَا خَرَجَت\n", + "مَنَ اللِسَانِ لَم تَتَجَاوَز الآذانَ * قَالَ حَكِيم لِآخَرَ يَا أَخِي ! \n", + "كَيفَ أَصْبَحْتَ ؟ قَالَ أَصبَحتُ – وَبِنَا مِن نِعَم اللَّهِ مَا لاَ نُحصَيهُ \n", + "مَع كَثيِرٍ مَا نَعصِيهُ فَمَا نَدرِي أَيَّهُمَا نَشكُرُ جَمِيلاً مَا يَنشُرُ \n", + "أَو قَبِيحًا مَا يَستُرُ * إِجتَمَعَ حُكَمَاءُ العَرَبِ والعَجَمِ عَلَي أَرَبعِ \n", + "كَلِمَاتٍ – وَهِيَ – لاَ تُحَمِل نَفسَكَ مَا لاَ تُطِيقُ – وَلاَ تَعمَل\n", + "عَمَلاً لاَ يَنفَعُكَ – وَلاَ تَغَتَرَ بِإِمَرأَةٍ وإِن عَفَّت – وَلاَ تَثَق بِمَالٍ \n", + "وَإن كَثرَ * \n", + "جُملاَت مُختَلِفَة\n", + "10 أَلعَالِمُ عَرَفَ الجَاهِلَ لِأَنَّهُ كَانَ جَاهِلاً – وَالجَاهِلُ لاَ يَعرِفُ \n", + "العَالِمَ لأَنَّهُ مَا كَانَ عَالِمًا * لاَ تَحمِل عَلَي يَومِكَ هَمَّ سَنَتِكَ – \n", + "كَفَاكَ كُلٌّ يَومٍ مَا قُدِرَ لَكَ فِيهِ – فَإن تَكُن السَّنَةُ مِن عُمرِكَ \n", + "فَإِنَّ اللهَ سُبحَانَهُ سَيَأتِيكَ فِي كُلٌِّ غَدٍ جَدِيدٍ بِما قُسِمَ لَكَ – \n", + "فَإن لَم تَكُنْ مِنْ عُمرِكَ فَمَا هَمٌّكَ بِمَا لَيسَ لَكَ * فِي \n", + "كِتَابِ كَلِيلَة وَدِمنَهْ – إذَا أَحدَثَ لَكَ العَدْوٌّ صِدَاقَةً لِعِلَّةٍ \n", + "أَلجَأتَهُ إِلَيكَ فَمَعَ ذَهَابِ العِلَةِ رُجُوعُ العَدَاوَة – كَالمَآءِ تُسخِنُهُ \n", + "فَإِذَا أَمسَكتَ نَارًا عَنهُ عَادَ إلَي أَصلِهِ باردًا والشَّجَرَةُ المُرَّةُ \n", + "لَو طَلَيتَهَا بِالعَسَلِ لَم تُثمِر إلاَّ مُرًا* \n", + "11 يَوم وَاحِد لِلعَالِمِ أَخيَرُ مِنَ الحَيَاةِ كُلَِّهَا لِلجَاهِلِ * لاَ \n", + "تُخَاطِبِ الأَحمَقَ وَلاَ تُخَالِطُه فَإنَّهُ مَا يَستَحِي * قَالَ أَمِيرُ \n", + "المُؤمِنِنَ عَلِي (كَرَّمَ اللهُ وَجهَهُ) الأَدَبُ حَلي في الغَنَي – \n", + "كَنز عِندَ الحَاجَةِ – عَون عَلَي المُرُوَّةِ – صَاحِب فِي المَجلِس – \n", + "مُؤنِس فِي الوَحدَةِ تُعمَرُ بِهِ القُلُوبُ الوَاهِيَةُ – وَتُحيَا بِهِ الأَلبَابُ \n", + "المَيِتَةُ – وَتَنفُذُ بِهِ الأَبصَارُ الكَلِيلَةُ – وَيُدرِكُ بِهِ الطَّالِبُونَ \n", + "جُملاَت مُختَلفَة\n", + "مَا حَاوَلُونَ * قَالَ لُقمَانُ لإِبنِهِ يَا بُنَيَّ ! لِتَكُن أَوَّلُ شَيءٍ \n", + "تَكسِبُهُ بَعدَ الإئمَانِ خَليِلاً صَالِحًا – فَإنَّمَا مَثَلُ الخَلِيلِ \n", + "الصَّالِح كَمَثَلِ النَّخلَةِ – إن قَعَدتَ فِي ظِلِهَا أَظَلَّكَ – وَإِن \n", + "إِحتَطَبتَ مِن حَطَبِهَا حَطَبِهَا نَفَعَكَ – وَإن أَكَلتَ مِن ثَمرِهَا وَجَدتَهُ \n", + "طَيِبًا * \n", + "12 مَن تَرَكَ نَفسَهُ بِمَنزِلَةِ العَاقِلِ تَرَكَهُ اللَّهُ وَالنَّاسُ \n", + "بِمَنزِلَةِ الجَاهِلِ * مَن أَحَبَّ أَن يَقوَي عَلَي الحِكمَةِ فَلاَ \n", + "تَملِك نَفسَهُ النِسَاءُ * نَقلُ الشَّرِ عَن شُرُورِهِ أَيسَرُ مِن نَقلِ \n", + "المَخزونِ عَن حُزنِهِ * ثَلاَثَة لاَ يُعرَفُونَ إلاَّ في ثَلاَثَةِ مواضِعَ * \n", + "لاَ يُعرَفُ الشٌّجَاعُ إلاَّ عِندَ الحَربِ – وَلاَ يُعرَفُ الحَكِيمُ إلاَّ عِندَ \n", + "الغَضَبِ – وَلاَ يُعرَفُ الصَّدِيقُ إلاَّ عِندَ الحَاجَةِ إلَيهِ * قَالَ \n", + "رَسُولُ اللهِ (صَلَّي اللَّهُ عَلَيهِ وَسَلَّمَ) – ثَلاَث مُهلِكَات وَثَلاَث \n", + "مُنجِيَات – فَأمَّا المُهلِكَاتُ فَشُح مُطَاع وَهَوَي مُتَّبَع وَإِعجَابُ \n", + "المِرءِ بِنَفسِهِ – وَأَمَّا المُنجِيَاتُ فَخَشِيَّةُ اللهِ فِي السِّرِّ وَالعَلاَنِيَةِ \n", + "وَالقَصدُ فِي الغِنَي والفَقرِ والعَدلُ فِي الرِّضَا والغَضَبِ * \n", + "جُملاَت مُختَلفَة\n", + "أَلكَمَالُ فِي ثَلاَثَةِ أَشيَاءٍ – العِفَّةُ فِي الدِينِ – وَالصَّبرُ \n", + "عِندَ النَّوائِبِ – وَحُسنُ المَعِيشَةِ * أَلظَّالِمُ مَيِّت وَلَو كَانَ فِي \n", + "مَنَازِلِ الأَحيَاء – وَالمُحسِنُ حِيّ وَلُو كَانَ انتَقَلَ إلَي مَنَازِلِ \n", + "المَوتَا * كَمَا البَدَنُ إذَا هُوَ سَقِيم لَا يَنفَعُهُ الطَّعَامُ – كَذَا العَقْلُ \n", + "إذَا غَلَقَهُ حُبٌّ الدٌّنيَا لاَ تَنفَعُهُ المَوَاعِظُ * كُن عَلَي حِذرٍ مِنَ \n", + "الكَرِيمِ اذًا هَوَّنتَهُ – وَمِنَ الأَحمَقِ إذَا مَازَحْتَهُ – وَمِنَ العَاقِلِ \n", + "اذَا غَضًّبتَهُ – وَمِنَ الفَاجِرِ اذَا عَاشرتَهُ * بِسِتَّةِ خِصَالٍ يُعرَفُ \n", + "الأَحمَقُ – بِالغَضَبِ مِن غَيرِ شيءٍ – والكَلاَم فِي غيرِ نَفعٍ – \n", + "والثِقَةِ فِي كُلِّ أَحَدٍ – وَبَدَلِهِ بِغَيرِ مَوضَعِ البَدَلِ – وَسُؤَالِهِ عَن \n", + "مَا لاَ يُعنِيِه – وَبِأَنَهُ مَا يَعرِفُ صَدِيقَهُ مِن عَدُوِهِ * \n", + "14 لاَ يَنبَغِي لِلفَاضِلِ أَن يُخَاطِبَ ذَوِي النَّقضِ – كَمَا لاَ يَنبَغِي \n", + "لَلصَّاحِي أَن يُكَلِمَ السٌّكَاَري * لاَ يَنْبَغِي لِلعَاقِلِ أَن تَسكنُ \n", + "بَلَدًا لَيسَ فِيهِ خَمسَةُ أشياءٍ – سُلطَان حَازِم وَقَاضٍ عَادِل\n", + "وَطَبِيب عَالِم وَنَهر جَارٍ وَسُوق قَائِم * قِيلَ لِلأَحنَفِ بنِ قَيسٍ \n", + "مَا أََحلَمَكَ ! قَالَ لَستُ بِحَلِيمٍ وَلَكِنِي أَتَحَالَمُ – وَاللَّهِ إِنِي \n", + "جُملاَت مُختَلِفَة\n", + "لَأَسمَعُ الكَلِمَةَ فَأَحُمٌّ لَهَا ثَلاَثًا مَا يَمْنَعُنِي مِنَ الجَوَابِ عَنهَا \n", + "إلاَّ خَوف مِن أن أَسمَعُ شَرًّا مِنهَا * قِيلَ لِبَعضِ الحُكَمَاءِ \n", + "مَتَي يُحمَدُ الكِذبُ ؟ قَالَ إذَا جَمَعَ بَيْنَ مُتَقَاطِعِينَ – قِيلَ \n", + "فَمَتَي يُذَمٌّ الصِّدقُ؟ قَالَ إذَا كَانَ غِيبَةً – فَمَتَي يَكُونُ \n", + "الصَّمتُ خَيرًا مِنَ النٌّطقِ؟ قَالَ عِندَ المِرَاءِ * \n", + "Our Lord's Prayer\n", + "أَبَانَا الذَِّي فِي السَّمَوَاتِ * لِيَتَقَدَّسِ اسمُكَ * لِيَأتِ مَلَكُوتُكَ \n", + "لِتَكُن مَشِيَّتُكَ كَمَا فِي السَّمَاءِ كذلِكَ عَلَي الأَرضِ * أَعطِنَا \n", + "خُبزَنَا كِفَاةَ يَومِنَا * واغفِر لَنَا ذُنُوبَنَا كَمَا نَحنُ نَغفِرُ لِمَن \n", + "أخطَأَ إلَينَا * وَلاَ تُدخِلنَا فِي التَّجَارِبِ – لكِن نَجِنّاَ مِنَ الشِرِيرِ * \n", + "آمِينَ * \n", + "\n" + ] + } + ], + "source": [ + "first_passage = passage.first\n", + "print(first_passage.export(Mimetypes.XML.TEI))" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "جُملاَت مُختَلِفَة\n", + "لَأَسمَعُ الكَلِمَةَ فَأَحُمٌّ لَهَا ثَلاَثًا مَا يَمْنَعُنِي مِنَ الجَوَابِ عَنهَا \n", + "إلاَّ خَوف مِن أن أَسمَعُ شَرًّا مِنهَا * قِيلَ لِبَعضِ الحُكَمَاءِ \n", + "مَتَي يُحمَدُ الكِذبُ ؟ قَالَ إذَا جَمَعَ بَيْنَ مُتَقَاطِعِينَ – قِيلَ \n", + "فَمَتَي يُذَمٌّ الصِّدقُ؟ قَالَ إذَا كَانَ غِيبَةً – فَمَتَي يَكُونُ \n", + "الصَّمتُ خَيرًا مِنَ النٌّطقِ؟ قَالَ عِندَ المِرَاءِ * \n", + "Our Lord's Prayer\n", + "أَبَانَا الذَِّي فِي السَّمَوَاتِ * لِيَتَقَدَّسِ اسمُكَ * لِيَأتِ مَلَكُوتُكَ \n", + "لِتَكُن مَشِيَّتُكَ كَمَا فِي السَّمَاءِ كذلِكَ عَلَي الأَرضِ * أَعطِنَا \n", + "خُبزَنَا كِفَاةَ يَومِنَا * واغفِر لَنَا ذُنُوبَنَا كَمَا نَحنُ نَغفِرُ لِمَن \n", + "أخطَأَ إلَينَا * وَلاَ تُدخِلنَا فِي التَّجَارِبِ – لكِن نَجِنّاَ مِنَ الشِرِيرِ * \n", + "آمِينَ * \n", + "\n" + ] + } + ], + "source": [ + "# Still too big ? Then let's get the child of the child passage\n", + "first_passage_last_child = first_passage.last\n", + "print(first_passage_last_child.export(Mimetypes.XML.TEI))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1.json b/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1.json new file mode 100644 index 00000000..0d934be3 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1.json @@ -0,0 +1,25 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id": "/coll1", + "@type": "Collection", + "totalItems": 2, + "title": "Collection 1", + "member": [ + { + "@id" : "/coll1_1", + "title" : "Collection 1.1", + "@type" : "Collection", + "totalItems" : 1 + }, + { + "@id" : "/coll1_2", + "title" : "Collection 1.2", + "@type" : "Collection", + "totalItems" : 2 + } + ] +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_1.json b/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_1.json new file mode 100644 index 00000000..5ce7a015 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_1.json @@ -0,0 +1,19 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id": "/coll1_1", + "@type": "Collection", + "totalItems": 1, + "title": "Collection 1.1", + "member": [ + { + "@id" : "/coll1_1_1", + "title" : "Collection 1.1.1", + "@type" : "Resource", + "totalItems" : 0 + } + ] +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_1_1.json b/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_1_1.json new file mode 100644 index 00000000..6d6a95be --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_1_1.json @@ -0,0 +1,11 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id": "/coll1_1_1", + "@type": "Resource", + "totalItems": 0, + "title": "Collection 1.2.1" +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_2.json b/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_2.json new file mode 100644 index 00000000..dd9335b1 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_2.json @@ -0,0 +1,25 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id": "/coll1_2", + "@type": "Collection", + "totalItems": 2, + "title": "Collection 1.2", + "member": [ + { + "@id" : "/coll1_2_1", + "title" : "Collection 1.2.1", + "@type" : "Resource", + "totalItems" : 0 + }, + { + "@id" : "/coll1_2_2", + "title" : "Collection 1.2.2", + "@type" : "Collection", + "totalItems" : 1 + } + ] +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_2_1.json b/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_2_1.json new file mode 100644 index 00000000..35ef0bee --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_2_1.json @@ -0,0 +1,11 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id": "/coll1_2_1", + "@type": "Resource", + "totalItems": 0, + "title": "Collection 1.2.1" +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_2_2.json b/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_2_2.json new file mode 100644 index 00000000..5f4d90b9 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_2_2.json @@ -0,0 +1,19 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id": "/coll1_2_2", + "@type": "Collection", + "totalItems": 1, + "title": "Collection 1.2.2", + "member": [ + { + "@id" : "/coll1_2_2_1", + "title" : "Collection 1.2.2.1", + "@type" : "Resource", + "totalItems" : 0 + } + ] +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_2_2_1.json b/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_2_2_1.json new file mode 100644 index 00000000..3df9bdb0 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_2_2_1.json @@ -0,0 +1,11 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/hydra/core#", + "dc": "http://purl.org/dc/terms/", + "dts": "https://w3id.org/dts/api#" + }, + "@id": "/coll1_2_2_1", + "@type": "Resource", + "totalItems": 0, + "title": "Collection 1.2.2.1" +} \ No newline at end of file diff --git a/tests/resolvers/dts/api_v1/test_metadata.py b/tests/resolvers/dts/api_v1/test_metadata.py index 3823a14f..34cab98b 100644 --- a/tests/resolvers/dts/api_v1/test_metadata.py +++ b/tests/resolvers/dts/api_v1/test_metadata.py @@ -223,3 +223,40 @@ def test_paginated_member_parents(self, mock_set): ) self.assertIsInstance(collection.parents, set, "Proxied object is replaced") + + @requests_mock.mock() + def test_readable_descendants(self, mock_set): + mock_set.get(self.root_uri, text=_load_mock("root.json")) + mock_set.get( + self.root_uri+"/collections?", + text=_load_mock("collection", "readableDescendants/coll1.json"), + complete_qs=True + ) + + def add_mock(mocks, id_): + mocks.get( + self.root_uri+"/collections?id=%2Fcoll"+id_, + text=_load_mock("collection", "readableDescendants/coll"+id_+".json"), + complete_qs=True + ) + add_mock(mock_set, "1_1") + add_mock(mock_set, "1_1_1") + add_mock(mock_set, "1_2") + add_mock(mock_set, "1_2_1") + add_mock(mock_set, "1_2_2") + add_mock(mock_set, "1_2_2_1") + + root = self.resolver.getMetadata() + + self.assertEqual( + ["/coll1_1_1", "/coll1_2_1", "/coll1_2_2_1"], + sorted([str(c.id) for c in root.readableDescendants]), + "Collections should be retrieved automatically" + ) + history = [history.url for history in mock_set.request_history] + print(history) + self.assertNotIn( + self.root_uri+"/collections?id=%2Fcoll1_2_2_1", + history, + "Resource should not be parsed" + ) diff --git a/tests/resources/collections/test_dts_collection.py b/tests/resources/collections/test_dts_collection.py index 4bcddef3..659e3330 100644 --- a/tests/resources/collections/test_dts_collection.py +++ b/tests/resources/collections/test_dts_collection.py @@ -38,9 +38,8 @@ def test_simple_collection(self): coll = self.get_collection(1) parsed = DtsCollection.parse(coll) exported = self.reorder_orderable(parsed.export(Mimetypes.JSON.DTS.Std)) - + self.maxDiff = 1555555 self.assertEqual( - exported, { '@context': { 'dct': 'http://purl.org/dc/terms/', @@ -52,7 +51,7 @@ def test_simple_collection(self): 'member': [ {'@id': '/cartulaires', '@type': 'Collection', - 'totalItems': 1, + 'totalItems': 10, 'description': 'Collection de cartulaires ' "d'Île-de-France et de ses " 'environs', @@ -67,7 +66,7 @@ def test_simple_collection(self): 'title': 'Lasciva Roma'}, {'@id': '/lettres_de_poilus', '@type': 'Collection', - 'totalItems': 1, + 'totalItems': 10000, 'description': 'Collection de lettres de ' 'poilus entre 1917 et 1918', 'title': 'Correspondance des poilus'}], @@ -81,7 +80,8 @@ def test_simple_collection(self): 'Nationale des Chartes'}]}, 'dts:extensions': {'skos:prefLabel': "Collection Générale de l'École " 'Nationale des Chartes'} - } + }, + exported ) def test_collection_single_member_with_types(self): From f532e7d60afd9026b4c7ec34fec055c4074afd01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 6 Nov 2018 10:59:45 +0100 Subject: [PATCH 68/89] Answers to @sonofmun review --- MyCapytain/common/metadata.py | 11 ++++++++--- MyCapytain/errors.py | 8 +++++++- MyCapytain/resolvers/dts/api_v1.py | 10 ++++++---- MyCapytain/resources/collections/dts/_resolver.py | 9 ++++++--- MyCapytain/resources/prototypes/cts/text.py | 2 +- MyCapytain/resources/prototypes/metadata.py | 3 +-- 6 files changed, 29 insertions(+), 14 deletions(-) diff --git a/MyCapytain/common/metadata.py b/MyCapytain/common/metadata.py index 6b9f39fc..a149a74a 100644 --- a/MyCapytain/common/metadata.py +++ b/MyCapytain/common/metadata.py @@ -7,12 +7,11 @@ """ -from __future__ import unicode_literals from MyCapytain.common.utils.xml import make_xml_node from MyCapytain.common.constants import Mimetypes, get_graph from MyCapytain.common.base import Exportable from rdflib import BNode, Literal, Graph, URIRef, term -from typing import Union +from typing import Union, Optional __all__ = [ @@ -53,7 +52,13 @@ def graph(self): """ return self.__graph__ - def set(self, key: URIRef, value: Union[Literal, BNode, URIRef, str, int], lang: str=None): + def set(self, key: URIRef, value: Union[Literal, BNode, URIRef, str, int], lang: Optional[str]=None): + """ Set the VALUE for KEY predicate in the Metadata Graph + + :param key: Predicate to be set (eg. DCT.creator) + :param value: Value to be stored (eg. "Cicero") + :param lang: [Optional] Language of the value (eg. "la") + """ if not isinstance(value, Literal) and lang is not None: value = Literal(value, lang=lang) elif not isinstance(value, (BNode, URIRef)): diff --git a/MyCapytain/errors.py b/MyCapytain/errors.py index f3da71ea..7c6fd551 100644 --- a/MyCapytain/errors.py +++ b/MyCapytain/errors.py @@ -66,8 +66,9 @@ class UnknownCollection(KeyError, MyCapytainException): """ A collection is unknown to its ancestor """ + class EmptyReference(SyntaxWarning, MyCapytainException): - """ Error generated when a duplicate is found in CtsReference + """ Error generated when a CtsReference is wrong """ @@ -81,6 +82,11 @@ class MissingRefsDecl(Exception, MyCapytainException): """ +class PaginationBrowsingError(MyCapytainException): + """ When contacting a remote service and some part of the pages where not reachable or parsable + """ + + class CapitainsXPathError(Exception): def __init__(self, message): super(CapitainsXPathError, self).__init__() diff --git a/MyCapytain/resolvers/dts/api_v1.py b/MyCapytain/resolvers/dts/api_v1.py index cd9b097d..e405863a 100644 --- a/MyCapytain/resolvers/dts/api_v1.py +++ b/MyCapytain/resolvers/dts/api_v1.py @@ -10,6 +10,7 @@ from typing import Union, Optional, Any, Dict import re +from pyld.jsonld import expand from MyCapytain.resolvers.prototypes import Resolver from MyCapytain.common.reference import BaseReference, BaseReferenceSet, \ @@ -17,9 +18,8 @@ from MyCapytain.retrievers.dts import HttpDtsRetriever from MyCapytain.common.utils.dts import parse_metadata from MyCapytain.resources.collections.dts import HttpResolverDtsCollection -from pyld.jsonld import expand from MyCapytain.resources.texts.remote.dts import DtsResolverDocument - +from MyCapytain.errors import EmptyReference, PaginationBrowsingError __all__ = [ "HttpDtsResolver" @@ -39,7 +39,7 @@ def _parse_ref(ref_dict, default_type: str=None): ref_dict["https://w3id.org/dts/api#end"][0]["@value"] ) else: - return None # Maybe Raise ? + raise EmptyReference("A reference is either empty or malformed") type_ = default_type if "https://w3id.org/dts/api#citeType" in ref_dict: type_ = ref_dict["https://w3id.org/dts/api#citeType"][0]["@value"] @@ -110,7 +110,9 @@ def getReffs( data = response.json() data = expand(data) if not len(data): - raise Exception("We'll see this one later") # toDo: What error should it be ? + raise PaginationBrowsingError( + "The contacted endpoint seems to not have any data about collection %s " % self.id + ) data = data[0] level_ = data.get("https://w3id.org/dts/api#level", _empty)[0]["@value"] diff --git a/MyCapytain/resources/collections/dts/_resolver.py b/MyCapytain/resources/collections/dts/_resolver.py index a65d42d5..cb07adda 100644 --- a/MyCapytain/resources/collections/dts/_resolver.py +++ b/MyCapytain/resources/collections/dts/_resolver.py @@ -1,8 +1,9 @@ -from MyCapytain.common.constants import RDF_NAMESPACES -from pyld.jsonld import expand import re +from pyld.jsonld import expand from typing import Optional +from MyCapytain.common.constants import RDF_NAMESPACES +from MyCapytain.errors import UnknownCollection from ._base import DtsCollection @@ -176,7 +177,9 @@ def retrieve(self): query = self._resolver.endpoint.get_collection(self.id) data = query.json() if not len(data): - raise Exception("We'll see this one later") # toDo: What error should it be ? + raise UnknownCollection( + "The contacted endpoint seems to not have any data about collection %s " % self.id + ) self._parse_metadata(expand(data)[0]) return True diff --git a/MyCapytain/resources/prototypes/cts/text.py b/MyCapytain/resources/prototypes/cts/text.py index 4fc231a9..7eefedc2 100644 --- a/MyCapytain/resources/prototypes/cts/text.py +++ b/MyCapytain/resources/prototypes/cts/text.py @@ -62,7 +62,7 @@ def urn(self, value: Union[URN, str]): if isinstance(value, str): value = URN(value) elif not isinstance(value, URN): - raise TypeError() + raise TypeError("New urn must be string or {} instead of {}".format(type(URN), type(value))) self._urn = value def get_cts_metadata(self, key: str, lang: str = None) -> Literal: diff --git a/MyCapytain/resources/prototypes/metadata.py b/MyCapytain/resources/prototypes/metadata.py index e5dde4b8..7209e902 100644 --- a/MyCapytain/resources/prototypes/metadata.py +++ b/MyCapytain/resources/prototypes/metadata.py @@ -9,8 +9,7 @@ from MyCapytain.common.metadata import Metadata from MyCapytain.errors import UnknownCollection -from MyCapytain.common.utils import literal_to_dict -from MyCapytain.common.utils import Subgraph +from MyCapytain.common.utils import literal_to_dict, Subgraph from MyCapytain.common.constants import RDF_NAMESPACES, RDFLIB_MAPPING, Mimetypes, get_graph from MyCapytain.common.base import Exportable from MyCapytain.common.reference import BaseCitationSet From 3fba98654451453d582c3176f7eaaa32bfea36bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Mon, 1 Jul 2019 10:00:17 +0100 Subject: [PATCH 69/89] Change logs --- CHANGES.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 75409148..e5166124 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,38 @@ +### 2019-07-01 3.0.0 @ponteineptique + +- Added DTS ! +- New Generic / Prototype CitationSetObject + - New .match(passageId) function +- New Generic / Prototype Citation Object + - New .children Property + - New .depth property (Not ported to CTS Citation object yet) + - New meaning of .__len__() magic method +- New .root and is_root properties to link back to the citation set (for match implementations) +- Guidelines / CTS Citation Object + - Implementation of the .match(passageId) method +- CitationSet.is_root -> CitationSet.is_root() # Keep in check with other practices +- BaseCitation.is_empty() now checks if the object has children +- BaseCitation.is_set() checks if objects have their properties set +- cts.Citation.isEmpty() old behaviour is now in .is_set() and has been reversed in meaning + - Old system would check if the CTS Citation was not set, old code such as + - if citation.isEmpty() should be moved to if citation.is_set() +- Guidelines / CTS Citation Object +- New Generic / Prototype Citation Object + - RetroPorted .start and .end Properties (see below for breaking change) + - New .is_range property + +From MyCapytain.resources.prototypes.text to MyCapytain.resources.prototypes.cts.text : + +- `CtsNode` now `PrototypeCtsNode` +- `CtsPassage` now `PrototypeCtsPassage` +- `CtsText` now `PrototypeCtsText` + +Texts class: +- `self.__attr__` to `self._attr` + +CTS Citation: +- `CTSCitation.isEmpty()` to `CTSCitation.is_empty()` + ### 2019-04-10 2.0.10 @sonofmun - Corrected JSON.Std export for metadata to include all objects for a predicate From 930e6e1d0fa14fa5f1307b9369c1741804bcb7bb Mon Sep 17 00:00:00 2001 From: Ralph Giles Date: Fri, 19 Jul 2019 12:04:51 -0700 Subject: [PATCH 70/89] Replace quadratic algorithm in cts.GetValidReff. When the _debug parameter is set to True, as it is when called by hooktest, the GetValidReff method for CapitainsCtsText files used a quadratic algorithm to search for duplicate passage reference strings. When a large number of passage references is generated for a document this caused a significant processing delay. Instead, use a more verbose but O(N) algorithm. --- MyCapytain/resources/texts/local/capitains/cts.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/MyCapytain/resources/texts/local/capitains/cts.py b/MyCapytain/resources/texts/local/capitains/cts.py index 8dc10dfc..786f6812 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -264,7 +264,13 @@ def getValidReff(self, level: int=1, reference: CtsReference=None, _debug: bool= passages = [".".join(passage) for passage in passages] if _debug: - duplicates = set([n for n in passages if passages.count(n) > 1]) + duplicates = set() + seen = set() + for n in passages: + if n in seen: + duplicates.add(n) + else: + seen.add(n) if len(duplicates) > 0: message = ", ".join(duplicates) warnings.warn(message, DuplicateReference) From 91a97e5c87560f0eb5ccc05fdb5517a9b4688ec1 Mon Sep 17 00:00:00 2001 From: Matthew Munson Date: Mon, 26 Aug 2019 11:14:37 +0200 Subject: [PATCH 71/89] Fix problem with get_cts_property See #193 --- MyCapytain/resources/prototypes/cts/inventory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MyCapytain/resources/prototypes/cts/inventory.py b/MyCapytain/resources/prototypes/cts/inventory.py index 316d6748..d8003e40 100644 --- a/MyCapytain/resources/prototypes/cts/inventory.py +++ b/MyCapytain/resources/prototypes/cts/inventory.py @@ -80,7 +80,7 @@ def get_cts_property(self, prop, lang=None): if lang is not None: if lang in x: return x[lang] - return next(x.values()) + return next(iter(x.values())) return x def set_cts_property(self, prop, value, lang=None): From 1dbfaeccd5fe1ab275b71f0b51f4fbac04fe4bae Mon Sep 17 00:00:00 2001 From: Matthew Munson Date: Mon, 26 Aug 2019 11:49:24 +0200 Subject: [PATCH 72/89] Added a test to make sure that get_cts_property with non-existent language works --- tests/resources/collections/test_cts.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/resources/collections/test_cts.py b/tests/resources/collections/test_cts.py index ba5cf904..3bd85c90 100644 --- a/tests/resources/collections/test_cts.py +++ b/tests/resources/collections/test_cts.py @@ -367,14 +367,23 @@ def test_Inventory_metadata(self): self.assertEqual(str(TI["urn:cts:latinLit:phi1294"].get_cts_property("groupname", "lat")), "Martialis") self.assertEqual(str(TI["urn:cts:latinLit:phi1294.phi002"].get_cts_property("title", "eng")), "Epigrammata") self.assertEqual(str(TI["urn:cts:latinLit:phi1294.phi002"].get_cts_property("title", "fre")), "Epigrammes") + self.assertRegex(str(TI["urn:cts:latinLit:phi1294.phi002"].get_cts_property("title", "per")), + "Epigrammata|Epigrammes", + "Requesting a CTS title with a language that does not exist should return one of the existing properties.") self.assertEqual(str(TI["urn:cts:latinLit:phi1294.phi002.perseus-lat2"].get_cts_property("label", "eng")), "Epigrammata Label") self.assertEqual(str(TI["urn:cts:latinLit:phi1294.phi002.perseus-lat2"].get_cts_property("label", "fre")), "Epigrammes Label") + self.assertRegex(str(TI["urn:cts:latinLit:phi1294.phi002.perseus-lat2"].get_cts_property("label", "per")), + "Epigrammes Label|Epigrammata Label", + "Requesting a CTS label with a language that does not exist should return one of the existing properties.") self.assertEqual(str(TI["urn:cts:latinLit:phi1294.phi002.perseus-lat2"].get_cts_property("description", "fre")), "G. Heraeus") self.assertEqual(str(TI["urn:cts:latinLit:phi1294.phi002.perseus-lat2"].get_cts_property("description", "eng")), "W. Heraeus") + self.assertRegex(str(TI["urn:cts:latinLit:phi1294.phi002.perseus-lat2"].get_cts_property("description", "per")), + "W. Heraeus|G. Heraeus", + "Requesting a CTS description with a language that does not exist should return one of the existing properties.") self.assertCountEqual( list([str(o) for o in TI["urn:cts:latinLit:phi1294.phi002.perseus-eng3"].get_link(constants.RDF_NAMESPACES.CTS.term("about"))]), ["urn:cts:latinLit:phi1294.phi002.perseus-eng2", "urn:cts:latinLit:phi1294.phi002.perseus-lat2"]) From 67eb4c97a0b4b132a6b1beb08ae87899606d7b2b Mon Sep 17 00:00:00 2001 From: Matthew Munson Date: Mon, 26 Aug 2019 11:52:08 +0200 Subject: [PATCH 73/89] Removed 3.4.5 from Travis tests (pending @balmas approval) --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0241cc30..07f65fd0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: - - "3.4.5" - "3.5" - "3.6" From e3e1f5ff8b4d3fbb498a2db76eb1af095df0f42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 15 Oct 2019 11:10:49 +0200 Subject: [PATCH 74/89] (PR 3.0.0) Comment 1 of @sonofmun review : Use of Max in depth explained --- MyCapytain/common/reference/_base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index 946ddf7e..a8e39dba 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -90,7 +90,9 @@ def match(self, passageId): @property def depth(self): - """ Depth of the citation scheme + """ Depth of the citation scheme: if multiple scheme are available, + the deepest one from the current node is chosen (if there is two possibilities, + a->((b->c)(d->e->f)) 4 is the depth .. example:: If we have a Book, Poem, Line system, and the citation we are looking at is Poem, depth is 2 From ecc585c9bafd3f84f2b082ccaeb1247447176450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 15 Oct 2019 11:14:54 +0200 Subject: [PATCH 75/89] (PR 3.0.0) Comment 2 of @sonofmun review : Clear-up BaseReference.level vs BaseCitation.depth --- MyCapytain/common/reference/_base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py index a8e39dba..674042a9 100644 --- a/MyCapytain/common/reference/_base.py +++ b/MyCapytain/common/reference/_base.py @@ -372,6 +372,10 @@ def citation(self) -> BaseCitationSet: @property def level(self) -> int: + """ Level represents the depth at which lies the reference. + + eg. depth is the number of floors, level is the actual floor number. + """ return self._level def __repr__(self): From 71c24840b9e637743c55fb3460fe200c4be3ad72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 15 Oct 2019 11:17:26 +0200 Subject: [PATCH 76/89] (PR 3.0.0) Comment 4 of @sonofmun review : Fix docstring of fromScopeXpathToRefsDecl --- MyCapytain/common/reference/_capitains_cts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index 04cf7145..a6b7fa30 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -786,7 +786,7 @@ def _parseXpathScope(self): return REFSDECL_REPLACER.sub("?", "".join(matches[0:-1])), REFSDECL_REPLACER.sub("?", matches[-1]) def _fromScopeXpathToRefsDecl(self, scope, xpath): - """ Update xpath and scope property when refsDecl is updated + """ Update the refsDecl value if xpath and scope property are to be updated """ if scope is not None and xpath is not None: From 7165528fcc6a0a82e7474ed7bb593724552d5c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 15 Oct 2019 11:19:41 +0200 Subject: [PATCH 77/89] (PR 3.0.0) Comment 5 of @sonofmun review : Fix Citation.is_set docstring --- MyCapytain/common/reference/_capitains_cts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MyCapytain/common/reference/_capitains_cts.py b/MyCapytain/common/reference/_capitains_cts.py index a6b7fa30..0353b074 100644 --- a/MyCapytain/common/reference/_capitains_cts.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -871,10 +871,10 @@ def fill(self, passage=None, xpath=None): self.refsDecl ) - def is_set(self): - """ Check if the citation has not been set + def is_set(self) -> bool: + """ Check if the citation has been set - :return: True if nothing was setup + :return: True if set up, False if not :rtype: bool """ return self.refsDecl is not None From 037c17c69e1e8e0a2d7f1b5a41b5cafe4a12f82a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 15 Oct 2019 12:18:57 +0200 Subject: [PATCH 78/89] (PR 3.0.0) Comment 6 of @sonofmun review : DtsCitationSet docstring completed --- MyCapytain/common/reference/_dts_1.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MyCapytain/common/reference/_dts_1.py b/MyCapytain/common/reference/_dts_1.py index 77214864..60743134 100644 --- a/MyCapytain/common/reference/_dts_1.py +++ b/MyCapytain/common/reference/_dts_1.py @@ -100,7 +100,8 @@ def match(self, passageId): class DtsCitationSet(BaseCitationSet): - """ Set of citation that are supposed + """ Set of citations following the DTS model (Unlike CTS, one citation + can have two or more children) """ From f6ebffe25c0c5aa41a497cc3a51bc9101864c38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 15 Oct 2019 12:21:42 +0200 Subject: [PATCH 79/89] (PR 3.0.0) Comment 7 of @sonofmun review : Example on JSONLdCollection --- MyCapytain/errors.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MyCapytain/errors.py b/MyCapytain/errors.py index 7c6fd551..2d6852b6 100644 --- a/MyCapytain/errors.py +++ b/MyCapytain/errors.py @@ -15,7 +15,10 @@ class MyCapytainException(BaseException): class JsonLdCollectionMissing(MyCapytainException): - """ Error thrown when a JSON LD has now first ressource + """ Error thrown when a JSON LD has now first resource + + Raised when a json supposed to contain collection is parsed + but nothing is found """ From 2fdae24bbc2d544bcca31f517e2f5c59ffab81bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 15 Oct 2019 12:23:09 +0200 Subject: [PATCH 80/89] (PR 3.0.0) Comment 8 of @sonofmun review : Better description of EmptyReference --- MyCapytain/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MyCapytain/errors.py b/MyCapytain/errors.py index 2d6852b6..7eedc8f5 100644 --- a/MyCapytain/errors.py +++ b/MyCapytain/errors.py @@ -71,7 +71,7 @@ class UnknownCollection(KeyError, MyCapytainException): class EmptyReference(SyntaxWarning, MyCapytainException): - """ Error generated when a CtsReference is wrong + """ Error generated when a CtsReference does not exist or is invalid """ From 713e0db92fcb5cc180859fae3c97a811b1d5f2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 15 Oct 2019 12:25:59 +0200 Subject: [PATCH 81/89] (PR 3.0.0) Comment 9/10 of @sonofmun review : Used _parse_wrapper in resolvers.cts.local --- MyCapytain/resolvers/cts/local.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MyCapytain/resolvers/cts/local.py b/MyCapytain/resolvers/cts/local.py index 9a5296d8..0b224288 100644 --- a/MyCapytain/resolvers/cts/local.py +++ b/MyCapytain/resolvers/cts/local.py @@ -286,14 +286,14 @@ def parse(self, resource): for folder in resource: cts_files = glob("{base_folder}/data/*/__cts__.xml".format(base_folder=folder)) for cts_file in cts_files: - textgroup, cts_file = self._parse_textgroup(cts_file) + textgroup, cts_file = self._parse_textgroup_wrapper(cts_file) textgroups.append((textgroup, cts_file)) for textgroup, cts_textgroup_file in textgroups: cts_work_files = glob("{parent}/*/__cts__.xml".format(parent=os.path.dirname(cts_textgroup_file))) for cts_work_file in cts_work_files: - _, parsed_texts, directory = self._parse_work(cts_work_file, textgroup) + _, parsed_texts, directory = self._parse_work_wrapper(cts_work_file, textgroup) texts.extend([(text, directory) for text in parsed_texts]) for text, directory in texts: From 45bc00db4d08eebc46100be63eea335fa3c476b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 15 Oct 2019 14:00:29 +0200 Subject: [PATCH 82/89] (PR 3.0.0) Comment 12 of @sonofmun review : Clean up of _cls_dict in some docstrings --- MyCapytain/resources/collections/cts.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/MyCapytain/resources/collections/cts.py b/MyCapytain/resources/collections/cts.py index 67bcc95b..d4092182 100644 --- a/MyCapytain/resources/collections/cts.py +++ b/MyCapytain/resources/collections/cts.py @@ -257,10 +257,8 @@ def parse(cls, resource, parent=None, _with_children=False): """ Parse a resource :param resource: Element rerpresenting a work - :param type: basestring, etree._Element :param parent: Parent of the object :type parent: XmlCtsTextgroupMetadata - :param _cls_dict: Dictionary of classes to generate subclasses """ xml = xmlparser(resource) o = cls(urn=xml.get("urn"), parent=parent) @@ -307,7 +305,6 @@ def parse(cls, resource, parent=None): :param resource: Element representing the textgroup :param parent: Parent of the textgroup - :param _cls_dict: Dictionary of classes to generate subclasses """ xml = xmlparser(resource) o = cls(urn=xml.get("urn"), parent=parent) @@ -334,7 +331,6 @@ def parse(cls, resource): """ Parse a resource :param resource: Element representing the text inventory - :param _cls_dict: Dictionary of classes to generate subclasses """ xml = xmlparser(resource) o = cls(name=xml.xpath("//ti:TextInventory", namespaces=XPATH_NAMESPACES)[0].get("tiid") or "") From b4a5c0d75692c1c818c9478e8f159e491ac4dcbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 15 Oct 2019 14:01:50 +0200 Subject: [PATCH 83/89] (PR 3.0.0) Comment 13 of @sonofmun review : Fix docstring for type in prototypes.cts.text.set_metadata... --- MyCapytain/resources/prototypes/cts/text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MyCapytain/resources/prototypes/cts/text.py b/MyCapytain/resources/prototypes/cts/text.py index 7eefedc2..0584a19f 100644 --- a/MyCapytain/resources/prototypes/cts/text.py +++ b/MyCapytain/resources/prototypes/cts/text.py @@ -95,7 +95,7 @@ def set_metadata_from_collection(self, text_metadata: CtsTextMetadata): """ Set the object metadata using its collections recursively :param text_metadata: Object representing the current text as a collection - :type text_metadata: CtsEditionMetadata or CtsTranslationMetadata + :type text_metadata: CtsTextMetadata """ edition, work, textgroup = tuple(([text_metadata] + text_metadata.parents)[:3]) From 5c54ac01b48ae282c7fe503d0154cb50603d30cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 15 Oct 2019 14:04:54 +0200 Subject: [PATCH 84/89] (PR 3.0.0) Comment 14 of @sonofmun review : Fix docstring for retrievers and resolvers --- MyCapytain/resolvers/dts/api_v1.py | 4 +++- MyCapytain/retrievers/dts/__init__.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/MyCapytain/resolvers/dts/api_v1.py b/MyCapytain/resolvers/dts/api_v1.py index e405863a..1defc8a5 100644 --- a/MyCapytain/resolvers/dts/api_v1.py +++ b/MyCapytain/resolvers/dts/api_v1.py @@ -70,6 +70,7 @@ def endpoint(self) -> HttpDtsRetriever: return self._endpoint def getMetadata(self, objectId: str=None, **filters) -> HttpResolverDtsCollection: + """ Retrieves metadata calling the Collections Endpoint """ req = self.endpoint.get_collection(objectId) req.raise_for_status() @@ -87,6 +88,7 @@ def getReffs( include_descendants: bool=False, additional_parameters: Optional[Dict[str, Any]]=None ) -> DtsReferenceSet: + """ Retrieve references by calling the Navigation API """ if not additional_parameters: additional_parameters = {} @@ -151,7 +153,7 @@ def getTextualNode( prevnext: bool=False, metadata: bool=False ) -> DtsResolverDocument: - """ Retrieve a text node from the API + """ Retrieve a text node from the API via the Document Endpoint :param textId: CtsTextMetadata Identifier :type textId: str diff --git a/MyCapytain/retrievers/dts/__init__.py b/MyCapytain/retrievers/dts/__init__.py index f3de2995..cf833bfb 100644 --- a/MyCapytain/retrievers/dts/__init__.py +++ b/MyCapytain/retrievers/dts/__init__.py @@ -158,7 +158,7 @@ def get_navigation( def get_document( self, collection_id, ref=None, mimetype="application/tei+xml, application/xml"): - """ Make a navigation request on the DTS API + """ Make a document request on the DTS API :param collection_id: Id of the collection :param ref: If ref is a tuple, it is treated as a range. String or int are treated as single ref From 1470a55428432df26d9ca841a406cdf16a35c608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 15 Oct 2019 14:06:44 +0200 Subject: [PATCH 85/89] (PR 3.0.0) Comment 16/17 of @sonofmun review : Fix error message in test_citation_dts --- tests/common/test_reference/test_citation_dts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/common/test_reference/test_citation_dts.py b/tests/common/test_reference/test_citation_dts.py index e2083bce..169d874d 100644 --- a/tests/common/test_reference/test_citation_dts.py +++ b/tests/common/test_reference/test_citation_dts.py @@ -112,14 +112,14 @@ def test_ingest_simple_line(self): cite = DtsCitationSet.ingest(_context(_ex_2)) children = {c.name: c for c in cite} - self.assertEqual(2, cite.depth, "There should be 3 levels of citation") - self.assertEqual(2, len(cite), "There should be 5 children") + self.assertEqual(2, cite.depth, "There should be 2 levels of citation") + self.assertEqual(2, len(cite), "There should be 2 children") self.assertEqual(list(cite[-1]), [children["line"]], "Last level should contain line only") self.assertEqual(list(cite[-1]), list(cite[1]), "-1 level == level 1") self.assertCountEqual(list(cite[-2]), [children["poem"]], "-2 level == level 0") - self.assertCountEqual(list(cite[-2]), list(cite[0]), "-32 level == level 0") + self.assertCountEqual(list(cite[-2]), list(cite[0]), "-2 level == level 0") self.assertIsInstance(cite, DtsCitationSet, "Root should be a DtsCitationSet") self.assertEqual([type(child) for child in cite.children], [DtsCitation], "Children should be DtsCitation") From 12c3433591ccf67f3a9050e63b0cf619bb320cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 15 Oct 2019 14:09:01 +0200 Subject: [PATCH 86/89] (PR 3.0.0) Comment 18 of @sonofmun review : Fix next/previous reference in test_local of cts --- tests/resolvers/cts/test_local.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/resolvers/cts/test_local.py b/tests/resolvers/cts/test_local.py index 0e674b9c..e6f22212 100644 --- a/tests/resolvers/cts/test_local.py +++ b/tests/resolvers/cts/test_local.py @@ -544,11 +544,11 @@ def test_getSiblings(self): ) self.assertEqual( previous, CtsReference("1.pr"), - "Previous should be well computed" + "Previous reference should be well computed" ) self.assertEqual( nextious, CtsReference("1.2"), - "Previous should be well computed" + "Next reference should be well computed" ) def test_getSiblings_nextOnly(self): @@ -558,11 +558,11 @@ def test_getSiblings_nextOnly(self): ) self.assertEqual( previous, None, - "Previous Should not exist" + "Previous reference should not exist" ) self.assertEqual( nextious, CtsReference("1.1"), - "Next should be well computed" + "Next reference should be well computed" ) def test_getSiblings_prevOnly(self): @@ -572,11 +572,11 @@ def test_getSiblings_prevOnly(self): ) self.assertEqual( previous, CtsReference("14.222"), - "Previous should be well computed" + "Previous reference should be well computed" ) self.assertEqual( nextious, None, - "Next should not exist" + "Next reference should not exist" ) def test_getReffs_full(self): From 76da910b90f9291b6c985c2847edba59958f96bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 15 Oct 2019 14:11:25 +0200 Subject: [PATCH 87/89] (PR 3.0.0) Comment 19 of @sonofmun review : correct title of col.1.1.1 --- .../api_v1/data/collection/readableDescendants/coll1_1_1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_1_1.json b/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_1_1.json index 6d6a95be..1a084e88 100644 --- a/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_1_1.json +++ b/tests/resolvers/dts/api_v1/data/collection/readableDescendants/coll1_1_1.json @@ -7,5 +7,5 @@ "@id": "/coll1_1_1", "@type": "Resource", "totalItems": 0, - "title": "Collection 1.2.1" + "title": "Collection 1.1.1" } \ No newline at end of file From 6813cbecd9920ff9e86c6193a234660557a4b682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 15 Oct 2019 14:12:30 +0200 Subject: [PATCH 88/89] (PR 3.0.0) Comment 20 of @sonofmun review : Remove trailing print --- tests/resolvers/dts/api_v1/test_metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/resolvers/dts/api_v1/test_metadata.py b/tests/resolvers/dts/api_v1/test_metadata.py index 34cab98b..cc3aa107 100644 --- a/tests/resolvers/dts/api_v1/test_metadata.py +++ b/tests/resolvers/dts/api_v1/test_metadata.py @@ -254,7 +254,7 @@ def add_mock(mocks, id_): "Collections should be retrieved automatically" ) history = [history.url for history in mock_set.request_history] - print(history) + self.assertNotIn( self.root_uri+"/collections?id=%2Fcoll1_2_2_1", history, From 3860683fffb96ec68299e85b00d54af73edc5fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Tue, 15 Oct 2019 18:47:14 +0200 Subject: [PATCH 89/89] Fixed typo not/now --- MyCapytain/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MyCapytain/errors.py b/MyCapytain/errors.py index 7eedc8f5..d05c5a5f 100644 --- a/MyCapytain/errors.py +++ b/MyCapytain/errors.py @@ -15,7 +15,7 @@ class MyCapytainException(BaseException): class JsonLdCollectionMissing(MyCapytainException): - """ Error thrown when a JSON LD has now first resource + """ Error thrown when a JSON LD contains no principle collection Raised when a json supposed to contain collection is parsed but nothing is found