diff --git a/.gitignore b/.gitignore index 752350f4..166b1c4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.ipynb_checkpoints/ +_test.py *.pkl # Compiled python modules. *.pyc diff --git a/.travis.yml b/.travis.yml index 85a6b55b..07f65fd0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ language: python python: - - "2.7" - - "3.4.5" - "3.5" + - "3.6" install: - pip install -r requirements.txt @@ -12,10 +11,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 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 diff --git a/MyCapytain/__init__.py b/MyCapytain/__init__.py index 92a21757..3c44848d 100644 --- a/MyCapytain/__init__.py +++ b/MyCapytain/__init__.py @@ -9,4 +9,4 @@ """ -__version__ = "2.0.10" +__version__ = "3.0.0" \ No newline at end of file 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 c3f63d4a..79cd00a7 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", @@ -26,9 +37,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: @@ -92,20 +104,37 @@ class PYTHON: class MyCapytain: """ MyCapytain Objects - :cvar ReadableText: MyCapytain.resources.prototypes.text.CitableText + :cvar ReadableText: MyCapytain.resources.prototypes.text.CtsText """ TextualElement = "Capitains/TextualElement" 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/metadata.py b/MyCapytain/common/metadata.py index 40d652ff..20fc6ce0 100644 --- a/MyCapytain/common/metadata.py +++ b/MyCapytain/common/metadata.py @@ -7,11 +7,16 @@ """ -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 +from typing import Union, Optional + + +__all__ = [ + "Metadata" +] class Metadata(Exportable): @@ -24,7 +29,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): @@ -46,6 +52,23 @@ def graph(self): """ return self.__graph__ + 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)): + 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 @@ -85,6 +108,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/common/reference/__init__.py b/MyCapytain/common/reference/__init__.py new file mode 100644 index 00000000..1fcc0d40 --- /dev/null +++ b/MyCapytain/common/reference/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +.. module:: MyCapytain.common.reference + :synopsis: URN related objects + +.. moduleauthor:: Thibault Clérice + +""" +from ._base import NodeId, BaseCitationSet, BaseReference, BaseReferenceSet +from ._capitains_cts import Citation, CtsReference, CtsReferenceSet, URN +from ._dts_1 import DtsCitation, DtsCitationSet, DtsReference, DtsReferenceSet diff --git a/MyCapytain/common/reference/_base.py b/MyCapytain/common/reference/_base.py new file mode 100644 index 00000000..674042a9 --- /dev/null +++ b/MyCapytain/common/reference/_base.py @@ -0,0 +1,469 @@ +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 typing import Tuple +from abc import abstractmethod + + +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 = [] + + if children: + self.children = children + + @property + def children(self): + """ Children of a citation + + :rtype: [BaseCitation] + """ + return self._children or [] + + @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: + 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): + """ Adds a child to the CitationSet + + :param child: Child citation to add + :return: + """ + if isinstance(child, BaseCitation): + self._children.append(child) + + def __iter__(self): + """ Iteration method + + Loop over the citation children + + :Example: + >>> 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], + + """ + 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 + + :param passageId: Passage Identifier + :return: Citation that matches the passageId + :rtype: BaseCitation + """ + + @property + def depth(self): + """ 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 + + + :rtype: int + :return: Depth of the citation scheme + """ + if len(self.children): + return 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 + + """ + 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) -> bool: + """ Check if the citation has not been set + + :return: True if nothing was setup + :rtype: bool + """ + return len(self.children) == 0 + + 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): + 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 + ] + + 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): + """ + + :return: String representation of the object + :rtype: str + """ + return "<{} name({})>".format(type(self).__name__, self.name) + + def __init__(self, name: str=None, children: list=None, root: BaseCitationSet=None): + """ Initialize a BaseCitation object + + :param name: Name of the citation level + :type name: str + :param children: list of children + :type children: [BaseCitation] + :param root: Root of the citation group + :type root: BaseCitationSet + """ + super(BaseCitation, self).__init__() + + self._name = None + self._root = None + + self.name = name + self.children = children + self.root = root + + 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) -> bool: + """ Checks that the current object is set + + :rtype: bool + """ + return self.name is not None + + @property + def root(self) -> BaseCitationSet: + """ Returns the root of the citation set + + :return: Root of the Citation set + :rtype: BaseCitationSet + """ + if self._root is None: + return self + 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: BaseCitationSet + :return: + """ + self._root = value + + @property + def name(self) -> str: + """ 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 + + 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 + + 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 + + @property + 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 + + + :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 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 + + obj = tuple.__new__(cls, refs) + + return obj + + def is_range(self) -> int: + 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): + """ 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] + obj = tuple.__new__(cls, refs) + obj._citation = None + obj._level = level + + if citation is not None: + obj._citation = citation + return obj + + @property + def citation(self) -> BaseCitationSet: + return self._citation + + @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): + 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 + + :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_id, self._next_id = siblings + self._identifier = identifier + self._depth = depth + + @property + def depth(self) -> int: + """ Depth of the node in the global hierarchy of the text tree + """ + return self._depth + + @property + def childIds(self) -> BaseReferenceSet: + """ Children Ids + """ + return self._children + + @property + def firstId(self) -> BaseReference: + """ First child Id + """ + if len(self._children) == 0: + return None + return self._children[0] + + @property + def lastId(self) -> BaseReference: + """ Last child id + """ + if len(self._children) == 0: + return None + return self._children[-1] + + @property + def parentId(self) -> BaseReference: + """ Parent Id + """ + return self._parent + + @property + def siblingsId(self) -> Tuple[BaseReference, BaseReference]: + """ Siblings Id + """ + return self.prevId, self.nextId + + @property + def prevId(self) -> BaseReference: + """ Previous Id (Sibling) + """ + return self._prev_id + + @property + def nextId(self) -> BaseReference: + """ Next Id + """ + return self._next_id + + @property + def id(self): + """Current object identifier + + :rtype: str + """ + return self._identifier diff --git a/MyCapytain/common/reference.py b/MyCapytain/common/reference/_capitains_cts.py similarity index 64% rename from MyCapytain/common/reference.py rename to MyCapytain/common/reference/_capitains_cts.py index 2059a6bd..0353b074 100644 --- a/MyCapytain/common/reference.py +++ b/MyCapytain/common/reference/_capitains_cts.py @@ -1,26 +1,19 @@ -# -*- 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 typing import Optional, List, Union, Tuple 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.utils import make_xml_node + +from MyCapytain.common.constants import Mimetypes, get_graph, RDF_NAMESPACES, XPATH_NAMESPACES +from MyCapytain.common.utils.xml import make_xml_node + +from ._base import BaseCitation, BaseReference, BaseReferenceSet 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): + +def _child_or_none(liste): """ Used to parse resources in XmlCtsCitation :param liste: List of item @@ -32,66 +25,185 @@ def __childOrNone__(liste): return None -class Reference(object): - """ A reference object giving information +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]) + + if counter: + counter = int(counter) + else: + counter = 0 + + obj = str.__new__(cls, "@"+word_reference) + obj.counter = counter + obj.word = word + + return obj + + def tuple(self) -> Tuple[str, int]: + return self.word, self.counter + + def __iter__(self): + return iter([self.word, self.counter]) + + +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) + + # 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 + return obj + + @property + def list(self) -> List[str]: + return list(iter(self)) + + @property + def subreference(self) -> Optional[CtsWordReference]: + subref = self.split("@") + if len(subref) == 2: + return CtsWordReference(subref[1]) - :param reference: CapitainsCtsPassage Reference part of a Urn - :type reference: basestring + def __iter__(self) -> List[str]: + subref = self.split("@")[0] + yield from subref.split(".") + + def __len__(self) -> int: + return self.count(".") + 1 + + @property + def depth(self) -> int: + return self.count(".") + 1 + + +class CtsReference(BaseReference): + """ A reference object giving information :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("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(). :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:: - 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 = CtsReference('1.2.3') """ - def __init__(self, reference=""): - self.reference = reference - if reference == "": - self.parsed = (self.__model__(), self.__model__()) - else: - self.parsed = self.__parse__(reference) + def __new__(cls, *references): + # pickle.load will try to feed the tuple back ! + 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 not references: + return None + elif isinstance(references, tuple): + if references[1]: + o = BaseReference.__new__( + CtsReference, + CtsSinglePassageId(references[0]), + CtsSinglePassageId(references[1]) + ) + else: + o = BaseReference.__new__( + CtsReference, + CtsSinglePassageId(references[0]) + ) + o._str_repr = "-".join([r for r in references if r]) + + 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 + + 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: Reference + :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: - return Reference("{0}{1}".format( - ".".join(list(self.parsed[0][1])[0:-1]), - self.parsed[0][3] or "" + if self.start.depth > 1 and (self.end is None or self.end.depth == 0): + return CtsReference("{0}{1}".format( + ".".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 Reference(".".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 Reference("{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 "" + return CtsReference("{0}{1}-{2}{3}".format( + ".".join(_start), + self.start.subreference or "", + ".".join(_end), + self.end.subreference or "" )) @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 @@ -99,10 +211,10 @@ def highest(self): .. note:: By default, this property returns the start level - :rtype: Reference + :rtype: CtsReference """ if not self.end: - return 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): @@ -111,33 +223,20 @@ def highest(self): return self.start @property - def start(self): + def start(self) -> CtsSinglePassageId: """ Quick access property for start list - :rtype: Reference + :rtype: str """ - if self.parsed[0][0] and len(self.parsed[0][0]): - return Reference(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: Reference - """ - if self.parsed[1][0] and len(self.parsed[1][0]): - return Reference(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] + :rtype: str """ - if not self.end: - return self.parsed[0][1] + return super(CtsReference, self).end @property def subreference(self): @@ -149,9 +248,10 @@ 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): + @property + 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 @@ -174,100 +274,27 @@ 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("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" """ - return self.reference + 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): + """ A CTS version of the BaseReferenceSet - :Example: - >>> a = Reference(reference="1.1@Achiles[1]-1.2@Zeus[1]") - >>> b = Reference(reference="1.1") - >>> c = Reference(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) - """ + """ + def __contains__(self, item: str) -> bool: + return BaseReferenceSet.__contains__(self, item) or \ + BaseReferenceSet.__contains__(self, CtsReference(item)) - 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 + 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): @@ -295,7 +322,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 """ @@ -388,7 +415,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"] @@ -396,10 +423,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 @@ -413,7 +440,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 +484,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 @@ -471,7 +498,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 @@ -481,12 +508,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 +540,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 = [ @@ -615,7 +642,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(".") @@ -630,9 +657,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,62 +691,45 @@ class Citation(Exportable): 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 + super(Citation, self).__init__(name=name, children=[child]) + self.__refsDecl = None - self.__child = None self.name = name - self.scope = scope - self.xpath = xpath - 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 + if scope and xpath: + self._fromScopeXpathToRefsDecl(scope, xpath) + else: + self.refsDecl = refsDecl @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="?"] """ - 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): """ 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.__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): @@ -729,12 +739,11 @@ 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: self.__refsDecl = val - self.__upXpathScope() @property def child(self): @@ -743,12 +752,19 @@ 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] + if self.is_root(): + val.root = self + else: + val.root = self.root + else: + self.children = [] @property def attribute(self): @@ -760,48 +776,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 _fromScopeXpathToRefsDecl(self, scope, xpath): + """ Update the refsDecl value if xpath and scope property are to be updated - def __upRefsDecl(self): - """ 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 - - 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 + self.refsDecl = _xpath def __getitem__(self, item): if not isinstance(item, int) or item > len(self)-1: @@ -814,13 +810,26 @@ 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): + """ Given a passageId matches a citation level + + :param passageId: A passage to match + :return: + """ + if not isinstance(passageId, CtsReference): + passageId = CtsReference(passageId) + + if self.is_root(): + return self[passageId.depth-1] + return self.root.match(passageId) 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 @@ -833,7 +842,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'] @@ -844,13 +853,13 @@ 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) else: - if isinstance(passage, Reference): - passage = passage.list or passage.start.list + if isinstance(passage, CtsReference): + passage = passage.start.list elif passage is None: return REFERENCE_REPLACER.sub( r"\1", @@ -858,28 +867,17 @@ 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 ) - def __getstate__(self): - """ Pickling method - - :return: dict - """ - return copy(self.__dict__) - - def __setstate__(self, dic): - self.__dict__ = dic - return self + def is_set(self) -> bool: + """ Check if the citation has been set - def isEmpty(self): - """ Check if the citation has not been set - - :return: True if nothing was setup + :return: True if set up, False if not :rtype: bool """ - return self.refsDecl is None and self.scope is None and self.xpath is None + return self.refsDecl is not 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 @@ -937,21 +940,23 @@ 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=_child_or_none(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): +def _ref_replacer(match, passage): """ Helper to replace xpath/scope/refsDecl on iteration with passage value :param match: A RegExp match @@ -967,102 +972,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/common/reference/_dts_1.py b/MyCapytain/common/reference/_dts_1.py new file mode 100644 index 00000000..60743134 --- /dev/null +++ b/MyCapytain/common/reference/_dts_1.py @@ -0,0 +1,145 @@ +from ._base import BaseCitationSet, BaseCitation, BaseReference, BaseReferenceSet +from ..metadata import Metadata +from MyCapytain.common.constants import RDF_NAMESPACES + + +_dts = RDF_NAMESPACES.DTS +_cite_type_term = str(_dts.term("citeType")) +_cite_structure_term = str(_dts.term("citeStructure")) + + +class DtsReference(BaseReference): + 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 + 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: BaseCitationSet + :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 DtsCitationSet(BaseCitationSet): + """ Set of citations following the DTS model (Unlike CTS, one citation + can have two or more children) + + """ + + 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) + + CitationClass = DtsCitation + + def match(self, passageId): + raise NotImplementedError("Passage Match is not part of the DTS Standard Citation") + + @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 + :return: Citation Graph + """ + _set = cls() + for data in resource: + _set.add_child( + cls.CitationClass.ingest(data, root=_set) + ) + return _set 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/_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..68d212f4 --- /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) + ) diff --git a/MyCapytain/common/utils/_json_ld.py b/MyCapytain/common/utils/_json_ld.py new file mode 100644 index 00000000..ffcdeb67 --- /dev/null +++ b/MyCapytain/common/utils/_json_ld.py @@ -0,0 +1,28 @@ +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): + """ Transforms a JSON+LD PyLD dictionary into + an RDFLib object""" + 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/dts.py b/MyCapytain/common/utils/dts.py new file mode 100644 index 00000000..8173c60c --- /dev/null +++ b/MyCapytain/common/utils/dts.py @@ -0,0 +1,26 @@ +from ._json_ld import dict_to_literal +from rdflib import URIRef +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 + + :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/common/utils.py b/MyCapytain/common/utils/xml.py similarity index 63% rename from MyCapytain/common/utils.py rename to MyCapytain/common/utils/xml.py index 26ef9e0c..8b9d15d8 100644 --- a/MyCapytain/common/utils.py +++ b/MyCapytain/common/utils/xml.py @@ -1,30 +1,27 @@ -# -*- coding: utf-8 -*- -""" -.. module:: MyCapytain.common.reference - :synopsis: Common useful tools and constants - -.. moduleauthor:: Thibault Clérice - - -""" -from __future__ import unicode_literals - -import re -from collections import OrderedDict, defaultdict 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 MyCapytain.common.constants import XPATH_NAMESPACES -__strip = re.compile("([ ]{2,})+") -__parser__ = etree.XMLParser(collect_ids=False, resolve_entities=False) + +__all__ = [ + "make_xml_node", + "xmlparser", + "normalizeXpath", + "xmliter", + "performXpath", + "copyNode", + "passageLoop" +] + + +_parser = etree.XMLParser(collect_ids=False, resolve_entities=False) def make_xml_node(graph, name, close=False, attributes=None, text="", complete=False, innerXML=""): @@ -70,86 +67,6 @@ def make_xml_node(graph, name, close=False, attributes=None, text="", complete=F return "<{}>".format(name) -def LiteralToDict(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), "@lang": value.language} - return value.toPython() - elif isinstance(value, URIRef): - return {"@id": str(value)} - elif value is None: - return None - return str(value) - - -class Subgraph(object): - """ Utility class to generate subgraph around one or more items - - :param - """ - def __init__(self, namespace_manager): - self.graph = Graph() - self.graph.namespace_manager = namespace_manager - 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 @@ -162,23 +79,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 @@ -402,63 +302,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 \ No newline at end of file diff --git a/MyCapytain/errors.py b/MyCapytain/errors.py index 82bf5389..d05c5a5f 100644 --- a/MyCapytain/errors.py +++ b/MyCapytain/errors.py @@ -14,8 +14,16 @@ class MyCapytainException(BaseException): """ +class JsonLdCollectionMissing(MyCapytainException): + """ Error thrown when a JSON LD contains no principle collection + + Raised when a json supposed to contain collection is parsed + but nothing is found + """ + + class DuplicateReference(SyntaxWarning, MyCapytainException): - """ Error generated when a duplicate is found in Reference + """ Error generated when a duplicate is found in CtsReference """ @@ -63,7 +71,7 @@ class UnknownCollection(KeyError, MyCapytainException): class EmptyReference(SyntaxWarning, MyCapytainException): - """ Error generated when a duplicate is found in Reference + """ Error generated when a CtsReference does not exist or is invalid """ @@ -75,3 +83,17 @@ class CitationDepthError(UnknownObjectError, MyCapytainException): class MissingRefsDecl(Exception, MyCapytainException): """ A text has no properly encoded refsDecl """ + + +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__() + self.message = message + + def __repr__(self): + return "CapitainsXPathError("+self.message+")" \ No newline at end of file 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 ba76d12f..0b224288 100644 --- a/MyCapytain/resolvers/cts/local.py +++ b/MyCapytain/resolvers/cts/local.py @@ -7,17 +7,25 @@ from glob import glob from math import ceil -from MyCapytain.common.reference import URN, Reference -from MyCapytain.common.utils import xmlparser +from MyCapytain.common.reference._capitains_cts import CtsReference, URN +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 -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 +__all__ = [ + "CtsCapitainsLocalResolver" +] + + class CtsCapitainsLocalResolver(Resolver): """ XML Folder Based resolver. CtsTextMetadata and metadata resolver based on local directories @@ -35,25 +43,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 +96,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 +108,208 @@ 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__ + ), 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, + _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 not text_metadata.citation.is_set(): + 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_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_wrapper(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 +326,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 +339,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 +441,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 +452,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] @@ -323,7 +474,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 @@ -333,25 +484,27 @@ def getTextualNode(self, textId, subreference=None, prevnext=False, metadata=Fal :rtype: CapitainsCtsPassage """ text, text_metadata = self.__getText__(textId) - if subreference is not None: - subreference = Reference(subreference) + 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 :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)) + if not isinstance(subreference, CtsReference): + subreference = CtsReference(subreference) + passage = text.getTextualNode(subreference) return passage.siblingsId def getReffs(self, textId, level=1, subreference=None): @@ -361,7 +514,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/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..1defc8a5 --- /dev/null +++ b/MyCapytain/resolvers/dts/api_v1.py @@ -0,0 +1,174 @@ +# -*- 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 +import re +from pyld.jsonld import expand + +from MyCapytain.resolvers.prototypes import Resolver +from MyCapytain.common.reference import BaseReference, BaseReferenceSet, \ + DtsReference, DtsReferenceSet, DtsCitation +from MyCapytain.retrievers.dts import HttpDtsRetriever +from MyCapytain.common.utils.dts import parse_metadata +from MyCapytain.resources.collections.dts import HttpResolverDtsCollection +from MyCapytain.resources.texts.remote.dts import DtsResolverDocument +from MyCapytain.errors import EmptyReference, PaginationBrowsingError + +__all__ = [ + "HttpDtsResolver" +] + +_empty = [{"@value": None}] +_re_page = re.compile("page=(\d+)") + + +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 \ + "https://w3id.org/dts/api#end" in ref_dict: + refs = ( + ref_dict["https://w3id.org/dts/api#start"][0]["@value"], + ref_dict["https://w3id.org/dts/api#end"][0]["@value"] + ) + else: + 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"] + + obj = DtsReference(*refs, type_=type_) + parse_metadata(obj.metadata, ref_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 getMetadata(self, objectId: str=None, **filters) -> HttpResolverDtsCollection: + """ Retrieves metadata calling the Collections Endpoint """ + req = self.endpoint.get_collection(objectId) + req.raise_for_status() + + collection = HttpResolverDtsCollection.parse(req.json(), resolver=self) + # Pagination is not completed upon first query. + # Pagination will be treated direction in the HttpResolverDtsCollection + + return collection + + 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: + """ Retrieve references by calling the Navigation API """ + if not additional_parameters: + additional_parameters = {} + + 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 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"] + 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( + *references, + level=level_, + citation=citation + ) + return reffs + + def getTextualNode( + self, + textId: str, + subreference: Union[str, BaseReference]=None, + prevnext: bool=False, + metadata: bool=False + ) -> DtsResolverDocument: + """ Retrieve a text node from the API via the Document Endpoint + + :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 + """ + return DtsResolverDocument.parse( + identifier=textId, + reference=subreference, + resolver=self, + response=self.endpoint.get_document(collection_id=textId, ref=subreference) + ) diff --git a/MyCapytain/resolvers/prototypes.py b/MyCapytain/resolvers/prototypes.py index 9f3d0b88..eaeec6d3 100644 --- a/MyCapytain/resolvers/prototypes.py +++ b/MyCapytain/resolvers/prototypes.py @@ -7,6 +7,16 @@ """ +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 + + +__all__ = [ + "Resolver" +] + class Resolver(object): """ Resolver provide a native python API which returns python objects. @@ -14,7 +24,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 +35,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 +57,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[BaseReference, BaseReference]: """ Retrieve the siblings of a textual node :param textId: CtsTextMetadata Identifier @@ -53,7 +69,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 +85,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/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 9a048271..d4092182 100644 --- a/MyCapytain/resources/collections/cts.py +++ b/MyCapytain/resources/collections/cts.py @@ -14,18 +14,33 @@ 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.utils import xmlparser, expand_namespace +from MyCapytain.common.reference._capitains_cts import Citation as CitationPrototype +from MyCapytain.common.utils import expand_namespace +from MyCapytain.common.utils.xml import xmlparser from MyCapytain.common.constants import XPATH_NAMESPACES, Mimetypes, RDF_NAMESPACES +__all__ = [ + "XmlCtsCitation", + "XmlCtsWorkMetadata", + "XmlCtsCommentaryMetadata", + "XmlCtsTranslationMetadata", + "XmlCtsEditionMetadata", + "XmlCtsTextgroupMetadata", + "XmlCtsTextInventoryMetadata", + "XmlCtsTextMetadata" +] + +_CLASSES_DICT = {} + + class XmlCtsCitation(CitationPrototype): """ XmlCtsCitation XML implementation for CtsTextInventoryMetadata """ - @staticmethod - def ingest(resource, element=None, xpath="ti:citation"): + @classmethod + def ingest(cls, resource, element=None, xpath="ti:citation"): """ Ingest xml to create a citation :param resource: XML on which to do xpath @@ -37,21 +52,21 @@ def ingest(resource, element=None, xpath="ti:citation"): # Reuse of of find citation results = resource.xpath(xpath, namespaces=XPATH_NAMESPACES) if len(results) > 0: - citation = XmlCtsCitation( + citation = cls( name=results[0].get("label"), xpath=results[0].get("xpath"), scope=results[0].get("scope") ) - if isinstance(element, XmlCtsCitation): + if isinstance(element, cls): element.child = citation - XmlCtsCitation.ingest( + cls.ingest( resource=results[0], element=element.child ) else: element = citation - XmlCtsCitation.ingest( + cls.ingest( resource=results[0], element=element ) @@ -61,7 +76,7 @@ def ingest(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 @@ -75,15 +90,17 @@ 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): +def _parse_structured_metadata(obj, xml): """ Parse an XML object for structured metadata :param obj: Object whose metadata are parsed @@ -125,6 +142,7 @@ class XmlCtsTextMetadata(cts.CtsTextMetadata): """ DEFAULT_EXPORT = Mimetypes.PYTHON.ETREE + CLASS_CITATION = XmlCtsCitation @staticmethod def __findCitations(obj, xml, xpath="ti:citation"): @@ -134,8 +152,8 @@ def __findCitations(obj, xml, xpath="ti:citation"): :param xpath: Xpath to use to retrieve the xml node """ - @staticmethod - def parse_metadata(obj, xml): + @classmethod + def parse_metadata(cls, obj, xml): """ Parse a resource to feed the object :param obj: Obj to set metadata of @@ -154,13 +172,13 @@ 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.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): 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) @@ -173,64 +191,77 @@ 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): + @classmethod + def parse(cls, resource, parent=None): xml = xmlparser(resource) - o = XmlCtsEditionMetadata(urn=xml.get("urn"), parent=parent) - XmlCtsEditionMetadata.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): + @classmethod + def parse(cls, resource, parent=None): xml = xmlparser(resource) lang = xml.get("{http://www.w3.org/XML/1998/namespace}lang") - o = XmlCtsTranslationMetadata(urn=xml.get("urn"), parent=parent) + o = cls(urn=xml.get("urn"), parent=parent) if lang is not None: o.lang = lang - XmlCtsTranslationMetadata.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): + @classmethod + def parse(cls, resource, parent=None): xml = xmlparser(resource) lang = xml.get("{http://www.w3.org/XML/1998/namespace}lang") - o = XmlCtsCommentaryMetadata(urn=xml.get("urn"), parent=parent) + o = cls(urn=xml.get("urn"), parent=parent) if lang is not None: o.lang = lang - XmlCtsCommentaryMetadata.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): + @classmethod + 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 """ xml = xmlparser(resource) - o = 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: @@ -242,29 +273,41 @@ 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) - # Added for commentary - xpathDict(xml=xml, xpath='ti:commentary', cls=XmlCtsCommentaryMetadata, parent=o) - - __parse_structured_metadata__(o, xml) - + children = [] + children.extend(_xpathDict( + xml=xml, xpath='ti:edition', + cls=cls.CLASS_EDITION, parent=o + )) + children.extend(_xpathDict( + xml=xml, xpath='ti:translation', + cls=cls.CLASS_TRANSLATION, parent=o + )) + children.extend(_xpathDict( + xml=xml, xpath='ti:commentary', + cls=cls.CLASS_COMMENTARY, parent=o + )) + + _parse_structured_metadata(o, xml) + + if _with_children: + return o, children return o class XmlCtsTextgroupMetadata(cts.CtsTextgroupMetadata): """ Represents a CTS Textgroup in XML """ + CLASS_WORK = XmlCtsWorkMetadata - @staticmethod - def parse(resource, parent=None): + @classmethod + def parse(cls, resource, parent=None): """ Parse a textgroup resource :param resource: Element representing the textgroup :param parent: Parent of the textgroup """ xml = xmlparser(resource) - o = 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") @@ -272,25 +315,25 @@ 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.CLASS_WORK, parent=o) - __parse_structured_metadata__(o, xml) + _parse_structured_metadata(o, xml) return o class XmlCtsTextInventoryMetadata(cts.CtsTextInventoryMetadata): """ Represents a CTS Inventory file """ + CLASS_TEXTGROUP = XmlCtsTextgroupMetadata - @staticmethod - def parse(resource): - """ Parse a resource + @classmethod + def parse(cls, resource): + """ Parse a resource :param resource: Element representing the text inventory - :param type: basestring, etree._Element """ xml = xmlparser(resource) - o = 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=XmlCtsTextgroupMetadata, 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 deleted file mode 100644 index 9af80bf0..00000000 --- a/MyCapytain/resources/collections/dts.py +++ /dev/null @@ -1,63 +0,0 @@ -from MyCapytain.resources.prototypes.metadata import Collection -from rdflib import URIRef - - -class DTSCollection(Collection): - @staticmethod - def parse(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.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(): - 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 - - return 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 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/_base.py b/MyCapytain/resources/collections/dts/_base.py new file mode 100644 index 00000000..91af7fea --- /dev/null +++ b/MyCapytain/resources/collections/dts/_base.py @@ -0,0 +1,171 @@ +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.dts import parse_metadata + + +from typing import List +from pyld import jsonld + + +__all__ = [ + "DtsCollection" +] + + +_hyd = RDF_NAMESPACES.HYDRA +_dts = RDF_NAMESPACES.DTS +_cap = RDF_NAMESPACES.CAPITAINS +_tei = RDF_NAMESPACES.TEI +_empty_extensions = [{}] + + +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() + self._parents = set() + + @property + def citation(self): + return self._citation + + @citation.setter + def citation(self, citation: CitationSet): + self._citation = citation + + @property + def size(self): + value = self.metadata.get_single(RDF_NAMESPACES.HYDRA.totalItems) + if value: + return int(value) + return 0 + + @property + def readable(self): + if self.type == RDF_NAMESPACES.HYDRA.Resource: + 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", **additional_parameters) -> "DtsCollection": + """ Given a dict representation of a json object, generate a DTS Collection + + :param resource: + :type resource: dict + :param direction: Direction of the hydra:members value + :return: DTSCollection parsed + :rtype: DtsCollection + """ + + data = jsonld.expand(resource) + if len(data) == 0: + raise JsonLdCollectionMissing("Missing collection in JSON") + data = data[0] + + obj = cls( + identifier=resource["@id"], + **additional_parameters + ) + + obj._parse_metadata(data) + obj._parse_members(data, direction=direction, **additional_parameters) + + return obj + + def _parse_members(self, data, direction: str="children", **additional_parameters: dict): + """ + + :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 ? + self.children.update({ + coll.id: coll + for coll in members + }) + else: + self.parents.add(members) + + 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"]) + + 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( + cls, + obj: dict, + collection: "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` + + :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, **additional_parameters) + if direction == "children": + 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..cb07adda --- /dev/null +++ b/MyCapytain/resources/collections/dts/_resolver.py @@ -0,0 +1,218 @@ +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 + + +_hyd = RDF_NAMESPACES.HYDRA +_empty = [{"@value": None}] +_re_page = re.compile("page=(\d+)") + + +class PaginatedProxy: + + __UPDATES_CALLABLES__ = { + "update", "add", "extend" + } + + 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 in self.__UPDATES_CALLABLES__: + return getattr(self._proxied, item) + if item == "set": + return self.set + return self._run(item) + + def _run(self, item: Optional[str]=None): + if not self._condition_lambda(): + self._update_lambda() + + if item: + return getattr(self._proxied, item) + return self._proxied + + def set(self, value) -> None: + self._proxied = value + + def __iter__(self): + 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") + + def __eq__(self, other): + return self._proxied == other + + +class HttpResolverDtsCollection(DtsCollection): + def __init__( + self, + identifier: str, + resolver: "HttpDtsResolver", + 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", + update_lambda=lambda: self._parse_paginated_members(direction="children"), + condition_lambda=lambda: self._parsed["parents"] + ) + self._parents = PaginatedProxy( + self, + "_parents", + update_lambda=lambda: self._parse_paginated_members(direction="parents"), + condition_lambda=lambda: self._parsed["parents"] + ) + + self._resolver = resolver + + def _parse_paginated_members(self, direction="children"): + """ Launch parsing of children + """ + + page = self._last_page_parsed[direction] + if not page: + page = 1 + else: + page = int(page) + 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)[0] + + 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 + 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 = 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]) + + 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._parsed["metadata"]: + query = self._resolver.endpoint.get_collection(self.id) + data = query.json() + if not len(data): + raise UnknownCollection( + "The contacted endpoint seems to not have any data about collection %s " % self.id + ) + self._parse_metadata(expand(data)[0]) + return True + + @classmethod + def parse_member( + cls, + obj: dict, + collection: "HttpResolverDtsCollection", + 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 + 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/cts/inventory.py b/MyCapytain/resources/prototypes/cts/inventory.py index 519d4eaa..d8003e40 100644 --- a/MyCapytain/resources/prototypes/cts/inventory.py +++ b/MyCapytain/resources/prototypes/cts/inventory.py @@ -10,8 +10,8 @@ from six import text_type from MyCapytain.resources.prototypes.metadata import Collection, ResourceCollection -from MyCapytain.common.reference import URN -from MyCapytain.common.utils import make_xml_node, xmlparser +from MyCapytain.common.reference._capitains_cts import URN +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 @@ -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 @@ -28,7 +40,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 +50,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 @@ -73,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): @@ -135,14 +142,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 +172,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 +455,7 @@ def texts(self): :return: Dictionary of texts :rtype: defaultdict(:class:`PrototypeTexts`) """ - return self.__children__ + return self.children @property def lang(self): @@ -480,7 +490,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 +576,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 +656,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 @@ -733,6 +743,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) diff --git a/MyCapytain/resources/prototypes/cts/text.py b/MyCapytain/resources/prototypes/cts/text.py new file mode 100644 index 00000000..0584a19f --- /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("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: + """ 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: CtsTextMetadata + """ + 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/metadata.py b/MyCapytain/resources/prototypes/metadata.py index 9983aedc..7209e902 100644 --- a/MyCapytain/resources/prototypes/metadata.py +++ b/MyCapytain/resources/prototypes/metadata.py @@ -9,11 +9,28 @@ from MyCapytain.common.metadata import Metadata from MyCapytain.errors import UnknownCollection -from MyCapytain.common.utils import Subgraph, LiteralToDict +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 rdflib import URIRef, RDF, Literal, Graph, RDFS -from rdflib.namespace import SKOS, DC +from rdflib.namespace import SKOS, DC, DCTERMS +from typing import List + + +__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_dct_str = str(DCTERMS) +_ns_cap_str = str(RDF_NAMESPACES.CAPITAINS) +_ns_rdf_str = str(RDF) +_ns_rdfs_str = str(RDFS) class Collection(Exportable): @@ -32,26 +49,19 @@ 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__ = {} + self._parent = None + 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): @@ -95,22 +105,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): @@ -145,15 +151,15 @@ def set_label(self, label, lang): ]) @property - def children(self): + def children(self) -> dict: """ Dictionary of childrens {Identifier: Collection} :rtype: dict """ - return self.__children__ + 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`] @@ -171,7 +177,7 @@ def parent(self): :rtype: Collection """ - return self.__parent__ + return self._parent @parent.setter def parent(self, parent): @@ -181,13 +187,13 @@ def parent(self, parent): :type parent: Collection :return: """ - self.__parent__ = parent - self.graph.set( - (self.asNode(), RDF_NAMESPACES.DTS.parent, parent.asNode()) + 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 ! @@ -206,7 +212,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) @@ -291,70 +302,130 @@ def __namespaces_header__(self, cpt=None): return bindings - def __export__(self, output=None, domain=""): + @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 + :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.HYDRA.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, 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 """ if output == Mimetypes.JSON.DTS.Std: - nm = self.graph.namespace_manager + + # Set-up a derived Namespace Manager + 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(self.graph.predicates()): - prefix, namespace, name = nm.compute_qname(predicate) - bindings[prefix] = str(URIRef(namespace)) + for predicate in set(graph.predicates()): + prefix, namespace, name = nsm.compute_qname(predicate) + if prefix not in bindings and str(namespace) not in ignore_ns_for_bindings: + bindings[prefix] = str(URIRef(namespace)) - 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 = {} + # Builds the specific Store data + extensions = {} + dublincore = {} + 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: - k = self.graph.qname(predicate) + 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)) + 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) - 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 - } - } - version = self.version - if version is not None: - o["@graph"]["version"] = str(version) - if len(self.members): - 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 - } + 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 + + if dublincore: + o[graph.qname(RDF_NAMESPACES.DTS.dublincore)] = dublincore + + if self.size: + o[graph.qname(RDF_NAMESPACES.HYDRA.member)] = [ + self.export_base_dts(self.graph, member, nsm) for member in self.members ] - if self.parent: - o["@graph"][self.graph.qname(RDF_NAMESPACES.DTS.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 - ] + + # If the system handles citation structure + if hasattr(self, "citation") and \ + 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 elif output == Mimetypes.JSON.LD\ diff --git a/MyCapytain/resources/prototypes/text.py b/MyCapytain/resources/prototypes/text.py index d9e8125d..03fa4bc1 100644 --- a/MyCapytain/resources/prototypes/text.py +++ b/MyCapytain/resources/prototypes/text.py @@ -1,22 +1,31 @@ # -*- 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 rdflib.namespace import DC -from rdflib import BNode, URIRef -from MyCapytain.common.reference import URN, Citation, NodeId +from typing import Union, List, Iterator +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 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", + "InteractiveTextualNode" +] + + class TextualElement(Exportable): """ Node representing a text passage. @@ -30,56 +39,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 @@ -88,30 +95,33 @@ 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 :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, 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 @@ -120,14 +130,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 @@ -136,14 +147,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 @@ -178,24 +190,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): @@ -219,30 +230,25 @@ 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: Reference of the passage to retrieve - :type subreference: str or Node or Reference - :rtype: TextualNode + :param subreference: CtsReference of the passage to retrieve :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 + 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: Reference - :rtype: [text_type] + :param subreference: Subreference (optional) :returns: List of levels """ raise NotImplementedError() @@ -269,80 +275,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: @@ -352,11 +344,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: @@ -364,158 +353,3 @@ def lastId(self): return None else: raise NotImplementedError - - -class CtsNode(InteractiveTextualNode): - """ Initiate a Resource object - - :param urn: A URN identifier - :type urn: 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=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 Identifier of the object - - :rtype: URN - """ - return self.__urn__ - - @urn.setter - def urn(self, value): - """ Set the urn - - :param value: URN to be saved - :type value: URN - :raises: *TypeError* when the value is not URN compatible - - """ - if isinstance(value, text_type): - value = URN(value) - elif not isinstance(value, URN): - raise TypeError() - self.__urn__ = value - - def get_cts_metadata(self, key, lang=None): - 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 - - :param level: Depth required. If not set, should retrieve first encountered level (1 based) - :type level: Int - :param passage: Subreference (optional) - :type passage: Reference - :rtype: List.text_type - :returns: List of levels - """ - raise NotImplementedError() - - def getLabel(self): - """ 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): - """ 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 self.citation.isEmpty() and not edition.citation.isEmpty(): - self.citation = edition.citation - - -class Passage(CtsNode): - """ CapitainsCtsPassage objects possess metadata informations - - :param urn: A URN identifier - :type urn: 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(Passage, self).__init__(**kwargs) - - @property - def reference(self): - return self.urn.reference - - -class CitableText(CtsNode): - """ A CTS CitableText - """ - def __init__(self, citation=None, metadata=None, **kwargs): - super(CitableText, self).__init__(citation=citation, metadata=metadata, **kwargs) - self.__reffs__ = None - - @property - def reffs(self): - """ Get all valid reffs for every part of the CitableText - - :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__ diff --git a/MyCapytain/resources/texts/base/tei.py b/MyCapytain/resources/texts/base/tei.py index 0cfcdf2e..96bdd680 100644 --- a/MyCapytain/resources/texts/base/tei.py +++ b/MyCapytain/resources/texts/base/tei.py @@ -9,11 +9,17 @@ 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 -class TEIResource(InteractiveTextualNode): +__all__ = [ + "TeiResource" +] + + +class TeiResource(InteractiveTextualNode): """ TEI Encoded Resource :param resource: XML Resource that needs to be parsed into a CapitainsCtsPassage/CtsTextMetadata @@ -28,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): @@ -47,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 @@ -102,7 +108,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 eeb62e21..786f6812 100644 --- a/MyCapytain/resources/texts/local/capitains/cts.py +++ b/MyCapytain/resources/texts/local/capitains/cts.py @@ -11,19 +11,26 @@ import warnings +from typing import Tuple, Optional 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 import URN, Citation, Reference +from MyCapytain.common.utils.xml import copyNode, normalizeXpath, passageLoop +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.texts.base.tei import TEIResource +from MyCapytain.resources.prototypes.cts.text import PrototypeCtsPassage, PrototypeCtsText +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 _make_passage_kwargs(urn, reference): """ Little helper used by CapitainsCtsPassage here to comply with parents args :param urn: URN String @@ -39,7 +46,7 @@ def __makePassageKwargs__(urn, reference): return kwargs -class __SharedMethods__: +class _SharedMethods(TeiResource): """ Set of shared methods between objects in local TEI. Avoid recoding functions """ @@ -47,34 +54,35 @@ 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 :returns: Asked passage """ - 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 = subreference.start.list, subreference.start.list - else: - start, end = subreference.start.list, subreference.end.list + if not isinstance(subreference, CtsReference): + if isinstance(subreference, str): + subreference = CtsReference(subreference) + elif isinstance(subreference, list): + subreference = CtsReference(".".join(subreference)) - if len(start) > len(self.citation): + if len(subreference.start) > self.citation.root.depth: raise CitationDepthError("URN is deeper than citation scheme") 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] + 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] + start, end = citation_start.fill(passage=start), citation_end.fill(passage=end) start, end = normalizeXpath(start.split("/")[2:]), normalizeXpath(end.split("/")[2:]) @@ -88,14 +96,15 @@ 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, resource=root, text=self, - citation=self.citation, + citation=citation_start, reference=subreference ) @@ -110,27 +119,28 @@ def _getSimplePassage(self, reference=None): :rtype: CapitainsCtsPassage """ if reference is None: - return __SimplePassage__( + return _SimplePassage( 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[len(reference)-1].fill(reference), + subcitation.fill(reference), namespaces=XPATH_NAMESPACES ) if len(resource) != 1: raise InvalidURN - return __SimplePassage__( + return _SimplePassage( resource[0], reference=reference, urn=self.urn, - citation=self.citation, + citation=subcitation, text=self.textObject ) @@ -146,76 +156,87 @@ def textObject(self): text = self return text - def getReffs(self, level=1, subreference=None): - """ Reference available at a given level + 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 - else: - subreference = Reference(subreference) - return self.getValidReff(level, subreference) - def getValidReff(self, level=None, reference=None, _debug=False): + if not subreference and hasattr(self, "reference"): + subreference = self.reference + elif subreference and not isinstance(subreference, CtsReference): + subreference = CtsReference(subreference) + + 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) :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 - :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 ? """ + depth = 0 xml = self.textObject.xml + if reference: - if isinstance(reference, Reference): - if reference.end is None: - passages = [reference.list] + if isinstance(reference, CtsReference): + 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.list)): - 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): - return [] + 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 = [ @@ -243,7 +264,13 @@ def getValidReff(self, level=None, reference=None, _debug=False): 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) @@ -253,7 +280,12 @@ def getValidReff(self, level=None, reference=None, _debug=False): message = '{} empty reference(s) at citation level {}'.format(len(empties), level) warnings.warn(message, EmptyReference) - return passages + references = CtsReferenceSet( + [CtsReference(reff) for reff in passages], + citation=self.citation.root[level-1], + level=level + ) + return references def xpath(self, *args, **kwargs): """ Perform XPath on the passage XML @@ -277,13 +309,13 @@ def tostring(self, *args, **kwargs): return etree.tostring(self.resource, *args, **kwargs) -class __SimplePassage__(__SharedMethods__, TEIResource, text.Passage): +class _SimplePassage(_SharedMethods, PrototypeCtsPassage): """ 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 @@ -292,55 +324,53 @@ 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) + **_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__ = len(reference) - self.__prevnext__ = None + self._depth = reference.depth + self._prev_next = None @property def reference(self): - """ URN CapitainsCtsPassage Reference + """ URN CapitainsCtsPassage CtsReference - :return: Reference - :rtype: Reference + :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): """ Children of the passage - :rtype: None, Reference + :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__ + 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: Reference - :rtype: List.basestring :returns: List of levels """ level += self.depth @@ -348,14 +378,14 @@ 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: :return: """ - if not isinstance(subreference, Reference): - subreference = Reference(subreference) + if not isinstance(subreference, CtsReference): + subreference = CtsReference(subreference) return self.textObject.getTextualNode(subreference) @property @@ -363,57 +393,57 @@ def nextId(self): """ Next passage :returns: Next passage at same level - :rtype: None, Reference + :rtype: None, CtsReference """ 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 - :rtype: None, Reference + :rtype: None, CtsReference """ 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 = 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: + if self.reference.is_range(): 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 = Reference(document_references[0]) + _prev = document_references[0] else: - _prev = Reference(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 = Reference(document_references[-1]) + _next = document_references[-1] else: - _next = Reference(document_references[start + 1]) + _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): @@ -421,14 +451,14 @@ def textObject(self): :rtype: CapitainsCtsText """ - return self.__text__ + return self._text -class CapitainsCtsText(__SharedMethods__, TEIResource, text.CitableText): +class CapitainsCtsText(_SharedMethods, PrototypeCtsText): """ 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 @@ -442,15 +472,15 @@ 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 :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") @@ -469,7 +499,7 @@ def test(self): raise E -class CapitainsCtsPassage(__SharedMethods__, TEIResource, text.Passage): +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 @@ -501,7 +531,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 @@ -521,50 +551,50 @@ 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.start: - self.__depth_2__ = self.__depth__ = len(self.reference.start) - if self.reference and self.reference.end: - self.__depth_2__ = len(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 + self._prev_next = None # For caching purpose @property def reference(self): - """ Reference of the object + """ CtsReference of the object """ - return self.__reference__ + return self._reference @property def childIds(self): """ Children of the passage - :rtype: None, Reference + :rtype: None, CtsReference :returns: Dictionary of chidren, where key are subreferences """ - self.__raiseDepth__() - if self.depth >= len(self.citation): + 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): """ Next passage :returns: Next passage at same level - :rtype: None, Reference + :rtype: None, CtsReference """ return self.siblingsId[1] @@ -573,40 +603,40 @@ 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] - 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 = list(map(str, self.__text__.getReffs(level=self.depth))) + document_references = self._text.getReffs(level=self.depth) - if self.reference.end: - start, end = str(self.reference.start), str(self.reference.end) + if self.reference.is_range(): + 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) @@ -640,28 +670,28 @@ def siblingsId(self): else: _next = "{}-{}".format(document_references[end+1], document_references[end + range_length]) - self.__prevnext__ = (_prev, _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, Reference): - subreference = Reference(subreference) - X = __SharedMethods__.getTextualNode(self, subreference) - X.__text__ = self.__text__ + if not isinstance(subreference, CtsReference): + subreference = CtsReference(subreference) + X = super(CapitainsCtsPassage, self).getTextualNode(subreference=subreference) + X._text = self._text return X @property @@ -670,4 +700,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 931ee816..3abdd76b 100644 --- a/MyCapytain/resources/texts/remote/cts.py +++ b/MyCapytain/resources/texts/remote/cts.py @@ -11,20 +11,27 @@ 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 import URN, Reference +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.texts.base.tei import TEIResource +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 -class __SharedMethod__(prototypes.InteractiveTextualNode): +__all__ = [ + "CtsPassage", + "CtsText" +] + + +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 @@ -35,13 +42,13 @@ 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) - self.__retriever__ = retriever - self.__first__ = False - self.__last__ = False + super(_SharedMethod, self).__init__(*args, **kwargs) + self._retriever = retriever + self._first = False + self._last = False if retriever is None: raise MissingAttribute("Object has not retriever") @@ -51,15 +58,15 @@ def retriever(self): :rtype: CitableTextServiceRetriever """ - return self.__retriever__ + 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 :param reference: CapitainsCtsPassage reference - :type reference: Reference + :type reference: CtsReference :rtype: list(str) :returns: List of levels """ @@ -76,23 +83,23 @@ 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)] 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: @@ -106,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): @@ -115,7 +122,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 +135,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 @@ -141,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 @@ -172,7 +179,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)]" @@ -188,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] ) @@ -197,11 +204,11 @@ 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( + _prev, _next = _SharedMethod.prevnext( self.retriever.getPrevNextUrn( urn="{}:{}".format( str( @@ -217,8 +224,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 """ @@ -232,7 +239,7 @@ def getFirstUrn(self, reference=None): ) else: urn = str(self.urn) - _first = __SharedMethod__.firstUrn( + _first = _SharedMethod.firstUrn( self.retriever.getFirstUrn( urn ) @@ -246,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): @@ -258,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): @@ -307,7 +314,7 @@ def prevnext(resource): return _prev, _next -class CtsText(__SharedMethod__, prototypes.CitableText): +class CtsText(_SharedMethod, PrototypeCtsText): """ API CtsTextMetadata object :param urn: A URN identifier @@ -328,11 +335,11 @@ 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 """ - 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 @@ -372,7 +379,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, PrototypeCtsPassage, TeiResource): """ CapitainsCtsPassage representing :param urn: @@ -388,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): @@ -406,16 +413,16 @@ 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): """ Shortcut for getting the parent passage identifier - :rtype: Reference + :rtype: CtsReference :returns: Following passage reference """ return str(self.urn.reference.parent) @@ -424,26 +431,26 @@ 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: + 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): """ 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: - 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 @@ -451,9 +458,9 @@ 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 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/MyCapytain/resources/texts/remote/dts/__init__.py b/MyCapytain/resources/texts/remote/dts/__init__.py new file mode 100644 index 00000000..d8ba9d13 --- /dev/null +++ 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 new file mode 100644 index 00000000..19c466f3 --- /dev/null +++ b/MyCapytain/resources/texts/remote/dts/_resolver_v1.py @@ -0,0 +1,112 @@ +from typing import TYPE_CHECKING, Optional +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.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): + def __init__( + self, + identifier: str, + resolver: "HttpDtsResolver", + resource: "ElementTree", + reference: Optional[DtsReference]=None, + collection: Optional["HttpResolverDtsCollection"]=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 + 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: 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: 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: DtsReference, resolver: "HttpDtsResolver", response: "Response"): + o = cls( + identifier=identifier, + reference=reference, + resolver=resolver, + resource=xmlparser(response.text) + ) + + 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) -> DtsReference: + return self._reference diff --git a/MyCapytain/retrievers/cts5.py b/MyCapytain/retrievers/cts5.py index 45c82832..ac35030b 100644 --- a/MyCapytain/retrievers/cts5.py +++ b/MyCapytain/retrievers/cts5.py @@ -8,10 +8,15 @@ """ import MyCapytain.retrievers.prototypes -from MyCapytain.common.reference import Reference +from MyCapytain.common.reference._capitains_cts import CtsReference import requests +__all__ = [ + "HttpCtsRetriever" +] + + class HttpCtsRetriever(MyCapytain.retrievers.prototypes.CtsRetriever): """ Basic integration of the MyCapytain.retrievers.proto.CTS abstraction """ @@ -224,10 +229,10 @@ def getReffs(self, textId, level=1, subreference=None): if subreference: textId = "{}:{}".format(textId, subreference) if subreference: - if isinstance(subreference, Reference): - depth += len(subreference) + if isinstance(subreference, CtsReference): + depth += subreference.depth else: - depth += len(Reference(subreference)) + depth += (CtsReference(subreference)).depth if level: level = max(depth, level) return self.getValidReff(urn=textId, level=level) diff --git a/MyCapytain/retrievers/dts/__init__.py b/MyCapytain/retrievers/dts/__init__.py new file mode 100644 index 00000000..cf833bfb --- /dev/null +++ b/MyCapytain/retrievers/dts/__init__.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +""" +.. module:: MyCapytain.retrievers.dts + :synopsis: DTS endpoint implementation + +.. moduleauthor:: Thibault Clérice + + +""" +import MyCapytain.retrievers.prototypes +from MyCapytain import __version__ +from MyCapytain.common.reference import BaseReference +import requests +from MyCapytain.common.utils import parse_uri + + +__all__ = [ + "HttpDtsRetriever" +] + + +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) + self._routes = None + + 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 + :type route: str + :param parameters: Dictionary of parameters + :type parameters: dict + :param mimetype: Mimetype to require + :type mimetype: str + :rtype: text + """ + if not defaults: + defaults = {} + parameters = { + 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) + + 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 + + @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 + :rtype: requests.Response + """ + return self.call( + "collections", + { + "id": collection_id, + "nav": nav, + "page": page + }, + defaults={ + "id": None, + "nav": "children", + "page": 1 + } + ) + + def get_navigation( + 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_by: Size of the ranges the server should produce + :param max_: Maximum number of results + :param exclude: Exclude specific metadata. + :param page: Page + :return: Response + :rtype: requests.Response + """ + parameters = { + "id": collection_id, + "level": level, + "groupBy": group_by, + "max": max_, + "exclude": exclude, + "page": page + } + _parse_ref_parameters(parameters, ref) + + return self.call( + "navigation", + parameters + ) + + def get_document( + self, + collection_id, ref=None, mimetype="application/tei+xml, application/xml"): + """ 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 + :param mimetype: Media type to request + :return: Response + :rtype: requests.Response + """ + parameters = { + "id": collection_id + } + _parse_ref_parameters(parameters, ref) + + return self.call( + "documents", + parameters, + mimetype=mimetype + ) 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/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 diff --git a/doc/MyCapytain.api.rst b/doc/MyCapytain.api.rst index 26cb8820..ae63f437 100644 --- a/doc/MyCapytain.api.rst +++ b/doc/MyCapytain.api.rst @@ -23,17 +23,28 @@ Constants URN, References and Citations ***************************** -.. autoclass:: MyCapytain.common.reference.NodeId - :members: +MyCapytain Base Objects ++++++++++++++++++++++++ -.. autoclass:: MyCapytain.common.reference.URN - :members: +.. autoclass:: MyCapytain.common.reference.NodeId +.. autoclass:: MyCapytain.common.reference.BaseCitationSet +.. autoclass:: MyCapytain.common.reference.BaseReference +.. autoclass:: MyCapytain.common.reference.BaseReferenceSet -.. autoclass:: MyCapytain.common.reference.Reference - :members: +Canonical Text Services Objects ++++++++++++++++++++++++++++++++ .. autoclass:: MyCapytain.common.reference.Citation - :members: fill, __iter__, __len__ +.. 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 ******************* diff --git a/notebooks/.gitkeep b/notebooks/.gitkeep new file mode 100644 index 00000000..e69de29b 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/requirements.txt b/requirements.txt index 9c0ffb03..02efbcd0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +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 \ No newline at end of file diff --git a/setup.py b/setup.py index 5a44e6e8..d4b00f73 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,10 @@ "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", + "typing" ], tests_require=[ "mock>=2.0.0", diff --git a/tests/common/test_metadata.py b/tests/common/test_metadata.py index c1e91a22..eeb36d88 100644 --- a/tests/common/test_metadata.py +++ b/tests/common/test_metadata.py @@ -126,14 +126,14 @@ 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'} } ) 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 71% rename from tests/common/test_reference.py rename to tests/common/test_reference/test_capitains_cts.py index b0b647fe..e2c4adbd 100644 --- a/tests/common/test_reference.py +++ b/tests/common/test_reference/test_capitains_cts.py @@ -1,9 +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 import URN, Reference, Citation, NodeId + +from six import text_type as str +from MyCapytain.common.utils.xml import xmlparser +from MyCapytain.common.reference import CtsReference, URN, Citation class TestReferenceImplementation(unittest.TestCase): @@ -11,78 +10,79 @@ 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]") - self.assertEqual(len(a), 2) - a = Reference("1.1.1") - self.assertEqual(len(a), 3) + def test_depth_ref(self): + a = CtsReference("1.1@Achilles[0]-1.10@Atreus[3]") + self.assertEqual(a.depth, 2) + a = CtsReference("1.1.1") + self.assertEqual(a.depth, 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]") - self.assertEqual(str(a.start), "1.1@Achilles") + a = CtsReference("1.1@Achilles-1.10@Atreus[3]") + self.assertEqual(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.start.subreference.word, "Achilles") + self.assertEqual(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.end.subreference.counter, 3) + self.assertEqual(a.end.subreference.tuple(), ("Atreus", 3)) def test_Unicode_Support(self): - a = Reference("1.1@καὶ[0]-1.10@Ἀλκιβιάδου[3]") - self.assertEqual(str(a.start), "1.1@καὶ[0]") + a = CtsReference("1.1@καὶ[0]-1.10@Ἀλκιβιάδου[3]") + self.assertEqual(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.start.subreference.word, "καὶ") + self.assertEqual(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.end.subreference.counter, 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(a.start.subreference[0], "") - self.assertEqual(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(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(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") - - self.assertEqual(str(a.parent), "1") + 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(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) @@ -105,7 +105,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]") @@ -159,7 +159,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") @@ -195,7 +195,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): @@ -209,9 +209,9 @@ 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.start, Reference("1")) - self.assertEqual(a.reference.end, Reference("2")) + self.assertEqual(a.reference, CtsReference("1-2")) + self.assertEqual(a.reference.start, "1") + self.assertEqual(a.reference.end, "2") self.assertIsNone(a.version) def test_warning_on_empty(self): @@ -237,7 +237,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") @@ -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): @@ -369,8 +370,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']") @@ -378,30 +379,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/common/test_reference/test_citation_dts.py b/tests/common/test_reference/test_citation_dts.py new file mode 100644 index 00000000..169d874d --- /dev/null +++ b/tests/common/test_reference/test_citation_dts.py @@ -0,0 +1,131 @@ +from MyCapytain.common.reference import DtsCitationSet, DtsCitation +from MyCapytain.common.constants import RDF_NAMESPACES +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 = 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") + 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 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") + 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): + """ Test a simple ingest """ + 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") + 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 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 = DtsCitationSet.ingest(_context(_ex_2)) + children = {c.name: c for c in cite} + + 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]), "-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") + + 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/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 e2448233..ccb057e5 100644 --- a/tests/resolvers/cts/test_api.py +++ b/tests/resolvers/cts/test_api.py @@ -1,8 +1,8 @@ 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.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, Passage, + 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, Passage, + 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, Passage, + 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, Passage, + 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, Passage, + passage, PrototypeCtsPassage, "GetPassage should always return passages objects" ) self.assertEqual( @@ -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)["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)["@graph"]["dts:members"]], + [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 fb85c9ee..e6f22212 100644 --- a/tests/resolvers/cts/test_local.py +++ b/tests/resolvers/cts/test_local.py @@ -4,13 +4,13 @@ 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 CtsReference, URN from MyCapytain.errors import InvalidURN, UnknownObjectError, UndispatchedTextError from MyCapytain.resources.prototypes.metadata import Collection 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.cts.text import PrototypeCtsPassage from MyCapytain.resolvers.utils import CollectionDispatcher from unittest import TestCase @@ -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" ) @@ -179,15 +179,15 @@ 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, PrototypeCtsPassage, "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" ) @@ -224,14 +224,14 @@ def test_getPassage_subreference(self): # We check we made a reroute to GetPassage request self.assertIsInstance( - passage, Passage, + passage, PrototypeCtsPassage, "GetPassage should always return passages objects" ) children = list(passage.getReffs()) self.assertEqual( - children[0], '1.1.1', + str(children[0]), '1.1.1', "Resource should be string identifiers" ) @@ -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, PrototypeCtsPassage, "GetPassage should always return passages objects" ) self.assertEqual( @@ -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" ) @@ -315,15 +315,15 @@ def test_getPassage_prevnext(self): ) self.assertIsInstance( - passage, Passage, + passage, PrototypeCtsPassage, "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" ) @@ -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, PrototypeCtsPassage, "GetPassage should always return passages objects" ) self.assertEqual( @@ -384,19 +384,23 @@ 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( - passage.prevId, "1.pr", + len(passage.citation.root), 3, + "Local Inventory Files should be parsed and aggregated correctly" + ) + self.assertEqual( + 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 +418,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" ) @@ -459,7 +463,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)["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 +506,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)["member"]], ["urn:cts:latinLit:phi1294.phi002.opp-eng3", "urn:cts:latinLit:phi1294.phi002.perseus-lat2"], "There should be two members in DTS JSON" ) @@ -539,12 +543,12 @@ def test_getSiblings(self): textId="urn:cts:latinLit:phi1294.phi002.perseus-lat2", subreference="1.1" ) self.assertEqual( - previous, "1.pr", - "Previous should be well computed" + previous, CtsReference("1.pr"), + "Previous reference should be well computed" ) self.assertEqual( - nextious, "1.2", - "Previous should be well computed" + nextious, CtsReference("1.2"), + "Next reference should be well computed" ) def test_getSiblings_nextOnly(self): @@ -554,11 +558,11 @@ def test_getSiblings_nextOnly(self): ) self.assertEqual( previous, None, - "Previous Should not exist" + "Previous reference should not exist" ) self.assertEqual( - nextious, "1.1", - "Next should be well computed" + nextious, CtsReference("1.1"), + "Next reference should be well computed" ) def test_getSiblings_prevOnly(self): @@ -567,12 +571,12 @@ def test_getSiblings_prevOnly(self): textId="urn:cts:latinLit:phi1294.phi002.perseus-lat2", subreference="14.223" ) self.assertEqual( - previous, "14.222", - "Previous should be well computed" + previous, CtsReference("14.222"), + "Previous reference should be well computed" ) self.assertEqual( nextious, None, - "Next should not exist" + "Next reference should not exist" ) def test_getReffs_full(self): @@ -583,7 +587,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 +596,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 +609,7 @@ def test_getReffs_full(self): "There should be 6 references" ) self.assertEqual( - reffs[0], "1.1.1" + reffs[0], CtsReference("1.1.1") ) @@ -771,3 +775,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 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..e69de29b diff --git a/tests/resolvers/dts/api_v1/base.py b/tests/resolvers/dts/api_v1/base.py new file mode 100644 index 00000000..ee9ee16f --- /dev/null +++ b/tests/resolvers/dts/api_v1/base.py @@ -0,0 +1,47 @@ +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 + + +from MyCapytain.common.constants import set_graph, bind_graph + + +def reset_graph(): + set_graph(bind_graph()) 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/collection/example2.json b/tests/resolvers/dts/api_v1/data/collection/example2.json new file mode 100644 index 00000000..1bfdcb0b --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/collection/example2.json @@ -0,0 +1,34 @@ +{ + "@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" : [ + {"@language": "la", "@value": "Lasciva Roma"} + ], + "dc:description": [ + { + "@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", + "@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..19efd3b6 --- /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": [ + {"@language": "en", "@value": "Anonymous"} + ], + "dc:language": ["la", "en"], + "dc:title": [{"@language": "la", "@value": "Priapeia"}], + "dc:description": [{ + "@language": "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": [{"@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/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/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/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/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..1a084e88 --- /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.1.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/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/data/navigation/example1.json b/tests/resolvers/dts/api_v1/data/navigation/example1.json new file mode 100644 index 00000000..98bf17a3 --- /dev/null +++ b/tests/resolvers/dts/api_v1/data/navigation/example1.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=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}" +} \ No newline at end of file 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 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 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..9497072a --- /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/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 new file mode 100644 index 00000000..cc3aa107 --- /dev/null +++ b/tests/resolvers/dts/api_v1/test_metadata.py @@ -0,0 +1,262 @@ +from .base import * +from .base import _load_mock +from MyCapytain.resources.collections.dts._resolver import PaginatedProxy + + +class TestHttpDtsResolverCollection(unittest.TestCase): + def setUp(self): + reset_graph() + 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")) + 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 + ) + 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"] + + 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( + collection.metadata.get_single("http://purl.org/dc/terms/creator", lang="eng"), + None, + "Unfortunately, before it's resolved, this should not be filled." + ) + + self.assertEqual(collection.size, 1, "Size is parsed through retrieve") + + collection.retrieve() + + self.assertEqual( + str(collection.metadata.get_single("http://purl.org/dc/terms/creator", lang="eng")), + "Anonymous", + "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")) + 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_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") + + @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] + + self.assertNotIn( + self.root_uri+"/collections?id=%2Fcoll1_2_2_1", + history, + "Resource should not be parsed" + ) 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..314f841e --- /dev/null +++ b/tests/resolvers/dts/api_v1/test_navigation.py @@ -0,0 +1,315 @@ +from .base import * +from .base import _load_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" + ) diff --git a/tests/resources/collections/test_cts.py b/tests/resources/collections/test_cts.py index 4f56f598..3bd85c90 100644 --- a/tests/resources/collections/test_cts.py +++ b/tests/resources/collections/test_cts.py @@ -5,14 +5,15 @@ from io import open, StringIO from operator import attrgetter -from rdflib import Literal, URIRef - import lxml.etree as etree import xmlunittest -from MyCapytain.resources.collections.cts import * -from MyCapytain.resources.prototypes.text import CtsNode from MyCapytain.common import constants +from MyCapytain.resources.collections.cts import * +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 class XML_Compare(object): @@ -366,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"]) @@ -450,7 +460,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( @@ -662,3 +672,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") diff --git a/tests/resources/collections/test_dts_collection.py b/tests/resources/collections/test_dts_collection.py new file mode 100644 index 00000000..659e3330 --- /dev/null +++ b/tests/resources/collections/test_dts_collection.py @@ -0,0 +1,199 @@ +from MyCapytain.resources.collections.dts import DtsCollection +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 + """ + with open("tests/testing_data/dts/collection_{}.json".format(number)) as f: + collection = json.load(f) + return collection + + def reorder_orderable(self, exported): + """ Reorded orderable keys + + :param exported: Exported Collection to DTS + :return: Sorted exported collection + """ + if "member" in exported: + exported["member"] = sorted(exported["member"], key=lambda x: x["@id"]) + 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): + 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 = self.reorder_orderable(parsed.export(Mimetypes.JSON.DTS.Std)) + self.maxDiff = 1555555 + self.assertEqual( + { + '@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#'}, + '@id': 'general', + '@type': 'Collection', + 'member': [ + {'@id': '/cartulaires', + '@type': 'Collection', + 'totalItems': 10, + 'description': 'Collection de cartulaires ' + "d'Île-de-France et de ses " + 'environs', + 'title': 'Cartulaires'}, + {'@id': '/lasciva_roma', + '@type': 'Collection', + 'totalItems': 1, + 'description': 'Collection of primary ' + 'sources of interest in the ' + "studies of Ancient World's " + 'sexuality', + 'title': 'Lasciva Roma'}, + {'@id': '/lettres_de_poilus', + '@type': 'Collection', + 'totalItems': 10000, + 'description': 'Collection de lettres de ' + 'poilus entre 1917 et 1918', + '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'], + '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'} + }, + exported + ) + + 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#', + '@vocab': 'https://www.w3.org/ns/hydra/core#', + 'skos': 'http://www.w3.org/2004/02/skos/core#' + }, + "@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": { + "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'}, + "member": [ + { + "@id": "urn:cts:latinLit:phi1103.phi001", + "title": "Priapeia", + "@type": "Collection", + "totalItems": 1 + } + ] + } + ) + + 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)) + self.assertEqual( + exported, + { + "@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#' + }, + "@id": "urn:cts:latinLit:phi1103.phi001", + "@type": "Collection", + "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'}, + "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 + }] + } + ) + + # 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 + ) + + 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/resources/proto/test_text.py b/tests/resources/proto/test_text.py index 08ce11b1..2278ac11 100644 --- a/tests/resources/proto/test_text.py +++ b/tests/resources/proto/test_text.py @@ -4,7 +4,10 @@ import unittest -from MyCapytain.resources.prototypes.text import * +from MyCapytain.common.reference import URN, Citation + +from MyCapytain.resources.prototypes.cts.text import PrototypeCtsText, PrototypeCtsNode +from MyCapytain.common.constants import RDF_NAMESPACES import MyCapytain.common.reference import MyCapytain.common.metadata @@ -12,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) @@ -23,15 +26,15 @@ 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.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 @@ -39,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 = PrototypeCtsText(urn="urn:cts:latinLit:tg.wk.v") self.assertEqual(str(b.urn), "urn:cts:latinLit:tg.wk.v") @@ -48,20 +51,20 @@ class TestProtoText(unittest.TestCase): def test_init(self): """ Test init works correctly """ - a = CitableText("someId") + a = PrototypeCtsText("someId") # Test with metadata - a = CitableText("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 = CitableText(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 = CitableText() + a = PrototypeCtsText() # It should fail because not implemented with self.assertRaises(NotImplementedError): @@ -74,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 = PrototypeCtsText() # It should fail because not implemented with self.assertRaises(NotImplementedError): @@ -82,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 = PrototypeCtsText() # It should fail because not implemented with self.assertRaises(NotImplementedError): @@ -90,15 +93,15 @@ def test_get_label(self): def test_urn(self): """ Test setters and getters for urn """ - a = CitableText() + a = PrototypeCtsText() # 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 @@ -106,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 = 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 = CitableText() + a = PrototypeCtsText() # It should fail because not implemented with self.assertRaises(NotImplementedError): @@ -119,10 +122,10 @@ 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 = PrototypeCtsText() + 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 = PrototypeCtsText(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..e9772002 100644 --- a/tests/resources/texts/base/test_tei.py +++ b/tests/resources/texts/base/test_tei.py @@ -3,10 +3,10 @@ import unittest -from MyCapytain.common.reference import Reference, Citation -from MyCapytain.resources.texts.base.tei import TEIResource +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): @@ -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\']" ) @@ -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 ) diff --git a/tests/resources/texts/local/commonTests.py b/tests/resources/texts/local/commonTests.py index 98c3fbac..5932e0f6 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 CtsReference, URN, Citation, CtsReferenceSet from MyCapytain.resources.texts.local.capitains.cts import CapitainsCtsText @@ -122,64 +122,63 @@ def testValidReffs(self): # Test with reference and level self.assertEqual( - str(self.TEI.getValidReff(reference=Reference("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=Reference("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=Reference("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")), + 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=Reference("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=Reference("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=Reference("2.1-2.2")), - [ + 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=Reference("2.1-2.2"), level=2), - ['2.1', '2.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=Reference("1.38-2.2"), level=2), - ['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']), + 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=Reference("1.1.1-1.1.4"), level=3), - ['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']), + 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 when already too deep - self.assertEqual( - self.TEI.getValidReff(reference=Reference("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): - 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 """ @@ -264,11 +263,11 @@ def test_xml_with_xml_id(self): "Word should be there !" ) self.assertEqual( - text.getReffs(level=2), [ + 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." ) @@ -294,12 +293,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 +306,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 +334,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 +347,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 +377,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 +435,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 +450,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 +458,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 +479,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 +492,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 +501,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 +509,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 +521,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(), @@ -579,7 +578,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 @@ -697,28 +696,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 !") @@ -742,7 +741,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,106 +749,107 @@ 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", + "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.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", + "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.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, + 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.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", + "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.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", + "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.Reference("1.pr.5")) + 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.Reference("1.pr")) + 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.Reference("2.40")) + 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.Reference("2.39")) + 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.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", + "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" ) diff --git a/tests/resources/texts/local/test_capitains_xml_default.py b/tests/resources/texts/local/test_capitains_xml_default.py index 623930cf..63dabfb7 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.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 fd6d2ca8..a325dfb4 100644 --- a/tests/resources/texts/local/test_capitains_xml_notObjectified.py +++ b/tests/resources/texts/local/test_capitains_xml_notObjectified.py @@ -8,10 +8,11 @@ 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 -from MyCapytain.common.utils import xmlparser +from MyCapytain.common.utils.xml import xmlparser from tests.resources.texts.local.commonTests import CapitainsXmlTextTest, CapitainsXmlPassageTests, CapitainsXMLRangePassageTests @@ -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.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 049940b0..3249998d 100644 --- a/tests/resources/texts/remote/test_cts.py +++ b/tests/resources/texts/remote/test_cts.py @@ -7,9 +7,9 @@ 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 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 @@ -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={ diff --git a/tests/retrievers/test_dts.py b/tests/retrievers/test_dts.py new file mode 100644 index 00000000..f6d7448c --- /dev/null +++ b/tests/retrievers/test_dts.py @@ -0,0 +1,175 @@ +import unittest + +import responses + +from MyCapytain.retrievers.dts import HttpDtsRetriever +from MyCapytain.common.utils._http import _Navigation, parse_pagination +from urllib.parse import parse_qs, urlparse, urljoin + +_SERVER_URI = "http://domainname.com/api/dts/" +patch_args = ("MyCapytain.retrievers.dts.requests.get", ) + + +class TestDtsParsing(unittest.TestCase): + """ Test Cts5 Endpoint request making """ + + def setUp(self): + self.cli = HttpDtsRetriever(_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() + + req = self.cli.get_collection() + response, pagination = req.text, parse_pagination(req.headers) + self.assertEqual( + pagination, + _Navigation("18", "20", "500", None, "1") + ) + + self.assertInCalls(_SERVER_URI+"collections/", {}) + + @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" + ) + req = self.cli.get_collection( + collection_id="Hello", + nav="parents", + page=19 + ) + response, pagination = req.text, parse_pagination(req.headers) + + 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 + ) diff --git a/tests/testing_data/dts/collection_1.json b/tests/testing_data/dts/collection_1.json new file mode 100644 index 00000000..7b536456 --- /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": 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": "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 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..7ff5f703 --- /dev/null +++ b/tests/testing_data/dts/collection_3.json @@ -0,0 +1,62 @@ +{ + "@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", + "dts:citeStructure": [ + { + "dts:citeType": "poem", + "dts:citeStructure": { + "dts:citeType": "line" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/testing_data/dts/collection_4.json b/tests/testing_data/dts/collection_4.json new file mode 100644 index 00000000..9ea0ab82 --- /dev/null +++ b/tests/testing_data/dts/collection_4.json @@ -0,0 +1,43 @@ +{ + "@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:citeDepth": 2, + "dts:citeStructure": [ + { + "dts:citeType": "poem", + "dts:citeStructure": [{ + "dts:citeType": "line" + }] + } + ] +} \ No newline at end of file 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