diff --git a/.gitignore b/.gitignore index c3471e3f..a1605054 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ *.pyc .idea build +env +src +env2 # Setuptools distribution folder. /dist/ diff --git a/.travis.yml b/.travis.yml index a8b4366b..c6ea9872 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,15 +2,26 @@ language: python python: - "2.7" - "3.4" -# command to install dependencies -install: + - "pypy" + - "pypy3" + +install: - pip install -r requirements.txt - pip install coveralls + # command to run tests -script: +script: - coverage run --source=MyCapytain setup.py test + +matrix: + allow_failures: + - python: pypy + - python: pypy3 + after_success: - if [[ $TRAVIS_PYTHON_VERSION == 3* ]]; then coveralls; fi + branches: only: - - master \ No newline at end of file + - master + - 0.1.0dev diff --git a/MyCapytain/__init__.py b/MyCapytain/__init__.py index dd37a494..7fba3b05 100644 --- a/MyCapytain/__init__.py +++ b/MyCapytain/__init__.py @@ -10,5 +10,5 @@ """ __name__ = "MyCapytain" -__version__ = "0.0.9" +__version__ = "0.1.0" __all__ = ["common", "endpoints", "resources"] diff --git a/MyCapytain/common/metadata.py b/MyCapytain/common/metadata.py index e506dc3c..b231f07f 100644 --- a/MyCapytain/common/metadata.py +++ b/MyCapytain/common/metadata.py @@ -13,18 +13,17 @@ from collections import defaultdict, OrderedDict from past.builtins import basestring from builtins import range, object -from copy import copy class Metadatum(object): - """ Metadatum object represent a single field of metadata - + """ Metadatum object represent a single field of metadata + :param name: Name of the field :type name: basestring :param children: List of tuples, where first element is the key, and second the value :type children: List - :Example: + :Example: >>> a = Metadatum(name="label", [("lat", "Amores"), ("fre", "Les Amours")]) >>> print(a["lat"]) # == "Amores" @@ -48,9 +47,9 @@ def __init__(self, name, children=None): def __getitem__(self, key): """ Add an iterable access method - Int typed key access to the *n* th registered key in the instance. + Int typed key access to the *n* th registered key in the instance. If string based key does not exist, see for a default. - + :param key: Key of wished value :type key: basestring, tuple, int :returns: An element of children whose index is key @@ -61,7 +60,7 @@ def __getitem__(self, key): >>> a = Metadatum(name="label", [("lat", "Amores"), ("fre", "Les Amours")]) >>> print(a["lat"]) # Amores >>> print(a[("lat", "fre")]) # Amores, Les Amours - >>> print(a[0]) # Amores + >>> print(a[0]) # Amores >>> print(a["dut"]) # Amores """ @@ -84,7 +83,7 @@ def __getitem__(self, key): def __setitem__(self, key, value): """ Register index key and value for the instance - + :param key: Index key(s) for the metadata :type key: basestring, list, tuple :param value: Values for the metadata @@ -130,7 +129,7 @@ def setDefault(self, key): :returns: Default key :raises: `ValueError` If key is not registered - :Example: + :Example: >>> a = Metadatum(name="label", [("lat", "Amores"), ("fre", "Les Amours")]) >>> a.setDefault("fre") >>> print(a["eng"]) # == "Les Amours" @@ -178,23 +177,21 @@ def __getstate__(self): default=self.default ) - @staticmethod - def __setstate__(dic): + def __setstate__(self, dic): """ Unpickling method :param value: :return: """ - self = Metadatum(name=dic["name"]) + self.name = dic["name"] self.children = OrderedDict(dic["langs"]) self.default = dic["default"] return self - class Metadata(object): - """ + """ A metadatum aggregation object provided to centralize metadata - + :param key: A metadata field name :type key: List. @@ -217,7 +214,7 @@ def __init__(self, keys=None): def __getitem__(self, key): """ Add a quick access system through getitem on the instance - + :param key: Index key representing a set of metadatum :type key: basestring, int, tuple :returns: An element of children whose index is key @@ -250,7 +247,7 @@ def __getitem__(self, key): def __setitem__(self, key, value): """ Set a new metadata field - + :param key: Name of metadatum field :type key: basestring, tuple :param value: Metadum dictionary @@ -316,7 +313,8 @@ def __add__(self, other): >>> b = Metadata(name="title") >>> a + b == Metadata(name=["label", "title"]) """ - result = copy(self) + from copy import deepcopy + result = deepcopy(self) for metadata_key, metadatum in other: if metadata_key in self.__keys: for key, value in metadatum: @@ -354,11 +352,13 @@ def __getstate__(self): def __setstate__(self, dic): """ Unpickling method - :param value: + :param dic: Dictionary with request valied :return: """ + self.metadata = defaultdict(Metadatum) - self.metadata = { - key: Metadatum.__setstate__(value) for key, value in dic.items() - } - self.__keys = list(dic.keys()) + self.__keys = [] + for key, value in dic.items(): + self.__keys.append(key) + self.metadata[key] = getattr(Metadatum(name=value["name"]), "__setstate__")(value) + return self diff --git a/MyCapytain/common/reference.py b/MyCapytain/common/reference.py index b40b8e6b..fd7ac744 100644 --- a/MyCapytain/common/reference.py +++ b/MyCapytain/common/reference.py @@ -5,14 +5,17 @@ .. moduleauthor:: Thibault Clérice ->>> from MyCapytain.common.reference import (URN, Reference, Citation) +>>> from MyCapytain.common.reference import URN, Reference, Citation """ from __future__ import unicode_literals from collections import defaultdict from past.builtins import basestring -from builtins import range, object +from six import text_type as str +from builtins import \ + range, object +from copy import copy import re @@ -23,74 +26,151 @@ class Reference(object): - """ A reference object giving informations :param reference: Passage Reference part of a Urn :type reference: basestring + :ivar parent: Parent Reference + :type parent: Reference + :ivar highest: List representation of the range member which is the highest in the hierarchy (If equal, start is returned) + :type highest: Reference + :ivar start: First part of the range + :type start: Reference + :ivar end: Second part of the range + :type end: Reference + :ivar list: List representation of the range. Not available for range + :type list: list + :ivar subreference: Word and Word counter ("Achiles", 1) representing the subreference. Not available for range + :type subreference: (str, int) :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"] - .. automethod:: __str__ - .. automethod:: __eq__ - .. automethod:: __getitem__ - .. automethod:: __setitem__ - """ + Reference object supports the following magic methods : len(), str() and eq(). - def __init__(self, reference): - self.reference = reference - self.parsed = self.__parse(reference) + :Example: + >>> len(a) == 2 && len(b) == 1 + >>> str(a) == "1.1@Achiles[1]-1.2@Zeus[1]" + >>> b == Reference("1.1") && b != a - 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 + .. note:: + While Reference(...).subreference and .list are not available for range, Reference(..).start.subreference and Reference(..).end.subreference as well as .list are available + """ - :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, self.__class__) - and self.reference == str(other)) + def __init__(self, reference=""): + self.reference = reference + if reference == "": + self.parsed = (self.__model(), self.__model()) + else: + self.parsed = self.__parse(reference) @property def parent(self): - """ + """ Parent of the actual URN, for example, 1.1 for 1.1.1 - :return: + :rtype: Reference """ if len(self.parsed[0][1]) == 1 and len(self.parsed[1][1]) <= 1: return None else: if len(self.parsed[0][1]) > 1 and len(self.parsed[1][1]) == 0: - return "{0}{1}".format( + return Reference("{0}{1}".format( ".".join(list(self.parsed[0][1])[0:-1]), self.parsed[0][3] 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 ".".join(first) + return Reference(".".join(first)) else: - return "{0}{1}-{2}{3}".format( + 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 "" - ) + )) + + @property + def highest(self): + """ 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 + hierarchy. In this case, the highest level would be 1.1. The function would return ["1", "1"] + + .. note:: By default, this property returns the start level + + :rtype: Reference + """ + if not self.end: + return self + elif len(self.start) < len(self.end) and len(self.start): + return self.start + elif len(self.start) > len(self.end) and len(self.end): + return self.end + elif len(self.start): + return self.start + return self + + @property + def start(self): + """ Quick access property for start list + """ + if self.parsed[0][0] and len(self.parsed[0][0]): + return Reference(self.parsed[0][0]) + + @property + def end(self): + """ Quick access property for reference end list + """ + 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] + """ + if not self.end: + return self.parsed[0][1] + + @property + def subreference(self): + """ Return the subreference of a single node reference + + .. note:: Access to start and end subreference should be done through obj.start.subreference + and obj.end.subreference + + :rtype: (str, int) + """ + if not self.end: + return Reference.convert_subreference(*self.parsed[0][2]) + + def __len__(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 + know the depth of the reference to access the right XPath for example. This property returns the depth of the + highest node + + :example: + - len(1.1) == 2 + - len(1.2.8-1.3) == 2 + - len(1-1.2) == 1 + + :rtype: int + """ + return len(self.highest.list) def __str__(self): """ Return full reference in string format - + :rtype: basestring :returns: String representation of Reference Object @@ -102,54 +182,32 @@ def __str__(self): """ return self.reference - def __getitem__(self, key): - """ Return part of or full passage reference - - Available keys : - - *1 | start* : First part of the reference - - *2 | start_list* : Reference start parsed into a list - - *3 | start_sub* : Subreference start parsed into a tuple - - *4 | end* : Last part of the reference - - *5 | end_list* : Reference start parsed into a list - - *6 | end_sub* : Subreference end parsed into a tuple - - *default* : full string reference - - :param key: Identifier of the part to return - :type key: basestring or int - :rtype: basestring or List. or None or Tuple. - :returns: Desired part of the passage reference + 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 :Example: >>> a = Reference(reference="1.1@Achiles[1]-1.2@Zeus[1]") - >>> print(a[1]) # "1.1@Achiles[1]" - >>> print(a["start_list"]) # ("1", "1") - >>> print(a[6]) # ("Zeus", "1") - >>> print(a[7]) # "1.1@Achiles[1]-1.2@Zeus[1]" + >>> b = Reference(reference="1.1") + >>> c = Reference(reference="1.1") + >>> (a == b) == False + >>> (c == b) == True """ - if key == 1 or key == "start": - return self.parsed[0][0] - elif key == 4 or key == "end": - return self.parsed[1][0] - elif key == 2 or key == "start_list": - return self.parsed[0][1] - elif key == 5 or key == "end_list": - return self.parsed[1][1] - elif key == 3 or key == "start_sub": - return self.parsed[0][2] - elif key == 6 or key == "end_sub": - return self.parsed[1][2] - else: - return self.reference + return (isinstance(other, self.__class__) + and self.reference == str(other)) def __model(self): """ 3-Tuple model for references - First element is full text reference, - Second is list of passage identifiers - Third is subreference + First element is full text reference, + Second is list of passage identifiers + Third is subreference - :rtype: Tuple - :returns: An empty tuple to model data + :returns: An empty list to model data + :rtype: list """ return [None, [], None, None] @@ -182,40 +240,131 @@ def __parse(self, reference): element[i] = tuple(element[i]) return tuple(element) + @staticmethod + def convert_subreference(word, counter): + if len(counter) and word: + return str(word), int(counter) + elif len(counter) == 0 and word: + return str(word), 0 + else: + return "", 0 + class URN(object): """ A URN object giving all useful sections :param urn: A CTS URN - :type urn: basestring + :type urn: str + :ivar urn_namespace: Namespace of the URN + :type urn_namespace: str + :ivar namespace: CTS Namespace + :type namespace: str + :ivar textgroup: CTS Textgroup + :type textgroup: str + :ivar work: CTS Work + :type work: str + :ivar version: CTS Version + :type version: str + :ivar reference: CTS Reference + :type reference: Reference + :cvar NAMESPACE: Constant representing the URN until its namespace + :cvar TEXTGROUP: Constant representing the URN until its textgroup + :cvar WORK: Constant representing the URN until its work + :cvar VERSION: Constant representing the URN until its version + :cvar PASSAGE: Constant representing the URN until its full passage + :cvar PASSAGE_START: Constant representing the URN until its passage (end excluded) + :cvar PASSAGE_END: Constant representing the URN until its passage (start excluded) + :cvar NO_PASSAGE: Constant representing the URN until its passage excluding its passage + :cvar COMPLETE: Constant representing the complete URN :Example: >>> a = URN(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1") - .. automethod:: __len__ - .. automethod:: __gt__ - .. automethod:: __lt__ - .. automethod:: __eq__ - .. automethod:: __str__ - .. automethod:: __getitem__ + URN object supports the following magic methods : len(), str() and eq(), gt() and lt(). + + :Example: + >>> b = URN("urn:cts:latinLit:phi1294.phi002") + >>> 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(b) == 4 + + .. exclude-members:: all + .. automethod:: upTo """ - - __order = [ - "full", - "urn_namespace", - "cts_namespace", - "textgroup", - "work", - "text", - "passage", - "reference" # Reference is a more complex object - ] + NAMESPACE = 0 + TEXTGROUP = 1 + WORK = 2 + VERSION = 3 + PASSAGE = 4 + PASSAGE_START = 5 + PASSAGE_END = 6 + NO_PASSAGE = 10 + COMPLETE = 100 def __init__(self, urn): - self.urn = urn - self.parsed = self.__parse(self.urn) + self.__urn = None + self.__parsed = self.__parse(urn) + + @property + def urn_namespace(self): + return self.__parsed["urn_namespace"] + + @urn_namespace.setter + def urn_namespace(self, value): + self.__urn = None + self.__parsed["urn_namespace"] = value + + @property + def namespace(self): + return self.__parsed["cts_namespace"] + + @namespace.setter + def namespace(self, value): + self.__urn = None + self.__parsed["cts_namespace"] = value + + @property + def textgroup(self): + return self.__parsed["textgroup"] + + @textgroup.setter + def textgroup(self, value): + self.__urn = None + self.__parsed["textgroup"] = value + + @property + def work(self): + return self.__parsed["work"] + + @work.setter + def work(self, value): + self.__urn = None + self.__parsed["work"] = value + + @property + def version(self): + return self.__parsed["version"] + + @version.setter + def version(self, value): + self.__urn = None + self.__parsed["version"] = value + + @property + def reference(self): + return self.__parsed["reference"] + + @reference.setter + def reference(self, value): + self.__urn = None + if isinstance(value, Reference): + self.__parsed["reference"] = value + else: + self.__parsed["reference"] = Reference(value) def __len__(self): """ Returns the len of the URN @@ -227,12 +376,14 @@ def __len__(self): :Example: >>> a = URN(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1") - >>> print(len(a)) # - - + >>> print(len(a)) """ - items = [key for key in self.parsed if key not in ["passage", "reference", "full"] ] + items = [ + key + for key, value in self.__parsed.items() + if key not in ["reference"] and value is not None + ] return len(items) def __gt__(self, other): @@ -283,7 +434,7 @@ def __eq__(self, other): >>> (b == a) == False # """ return (isinstance(other, self.__class__) - and self.parsed["full"] == other["full"]) + and self.__str__() == str(other)) def __str__(self): """ Return full initial urn @@ -295,178 +446,128 @@ def __str__(self): >>> a = URN(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1") >>> str(a) == "urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1" """ - return self.urn - - def __getitem__(self, key): - """ Returns the urn (int) level or up to (str) level. - - - Available keys : - - *0* : URN - - *full* : URN - - *1* : Namespace of the urn (cts) - - *urn_namespace* : URN until the Namespace of the urn - - *2* : CTS Namespace of the URN (e.g. latinLit) - - *cts_namespace* : URN until the CTS Namespace - - *3* : Textgroup of the URN - - *textgroup* : URN until the Textgroup - - *4* : Work of the URN - - *work* : URN until the Work - - *5* : Text of the URN - - *text* : URN until the Text - - *6* or *passage*: Passage of URN - - :param key: Identifier of the wished resource - :type key: int or basestring - :rtype: basestring or Reference - :returns: Part or complete URN - :warning: *urn:* is not counted as an element ! + if self.__urn is None: + urn = "urn:" + self.__parsed["urn_namespace"] + if self.namespace: + urn += ":" + self.namespace + if self.textgroup: + urn += ":" + self.textgroup + if self.work: + urn += "." + self.work + if self.version: + urn += "." + self.version + if self.reference: + urn += ":" + str(self.reference) + self.__urn = urn + return self.__urn + + def upTo(self, key): + """ Returns the urn up to given level using URN Constants + + :param key: Identifier of the wished resource using URN constants + :type key: int + :returns: String representation of the partial URN requested + :rtype: str :Example: >>> a = URN(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1") - >>> a["textgroup"] == "urn:cts:latinLit:phi1294" - >>> a[3] == "phi1294 + >>> a.upTo(URN.TEXTGROUP) == "urn:cts:latinLit:phi1294" """ - if isinstance(key, int) and key < len(URN.__order): - return self.parsed[URN.__order[key]] - elif key == "urn_namespace": - return ":".join(["urn", self.parsed["urn_namespace"]]) - elif key == "cts_namespace": + middle = [ + component + for component in [self.__parsed["textgroup"], self.__parsed["work"], self.__parsed["version"]] + if component is not None + ] + + if key == URN.COMPLETE: + return self.__str__() + elif key == URN.NAMESPACE: return ":".join([ "urn", - self.parsed["urn_namespace"], - self.parsed["cts_namespace"]]) - elif key == "textgroup" and "textgroup" in self.parsed: + self.__parsed["urn_namespace"], + self.__parsed["cts_namespace"]]) + elif key == URN.TEXTGROUP and self.__parsed["textgroup"]: return ":".join([ "urn", - self.parsed["urn_namespace"], - self.parsed["cts_namespace"], - self.parsed["textgroup"] + self.__parsed["urn_namespace"], + self.__parsed["cts_namespace"], + self.__parsed["textgroup"] ]) - elif key == "work" and "work" in self.parsed: + elif key == URN.WORK and self.__parsed["work"]: return ":".join([ "urn", - self.parsed["urn_namespace"], - self.parsed["cts_namespace"], - ".".join( - [ - self.parsed["textgroup"], - self.parsed["work"] - ]) + self.__parsed["urn_namespace"], + self.__parsed["cts_namespace"], + ".".join([self.__parsed["textgroup"], self.__parsed["work"]]) ]) - elif key == "text" and "text" in self.parsed: + elif key == URN.VERSION and self.__parsed["version"]: return ":".join([ "urn", - self.parsed["urn_namespace"], - self.parsed["cts_namespace"], - ".".join( - [ - self.parsed["textgroup"], - self.parsed["work"], - self.parsed["text"] - ]) + self.__parsed["urn_namespace"], + self.__parsed["cts_namespace"], + ".".join(middle) + ]) + elif key == URN.NO_PASSAGE and self.__parsed["work"]: + return ":".join([ + "urn", + self.__parsed["urn_namespace"], + self.__parsed["cts_namespace"], + ".".join(middle) + ]) + elif key == URN.PASSAGE and self.__parsed["reference"]: + return ":".join([ + "urn", + self.__parsed["urn_namespace"], + self.__parsed["cts_namespace"], + ".".join(middle), + str(self.reference) + ]) + elif key == URN.PASSAGE_START and self.__parsed["reference"]: + return ":".join([ + "urn", + self.__parsed["urn_namespace"], + self.__parsed["cts_namespace"], + ".".join(middle), + str(self.reference.start) + ]) + elif key == URN.PASSAGE_END and self.__parsed["reference"] and self.reference.end is not None: + return ":".join([ + "urn", + self.__parsed["urn_namespace"], + self.__parsed["cts_namespace"], + ".".join(middle), + str(self.reference.end) ]) - elif key == "passage" and "passage" in self.parsed: - if "text" in self.parsed: - return ":".join([ - "urn", - self.parsed["urn_namespace"], - self.parsed["cts_namespace"], - ".".join( - [ - self.parsed["textgroup"], - self.parsed["work"], - self.parsed["text"] - ]), - self.parsed["passage"] - ]) - else: - return ":".join([ - "urn", - self.parsed["urn_namespace"], - self.parsed["cts_namespace"], - ".".join( - [ - self.parsed["textgroup"], - self.parsed["work"] - ]), - self.parsed["passage"] - ]) - elif key == "start" and "passage" in self.parsed: - if "text" in self.parsed: - return ":".join([ - "urn", - self.parsed["urn_namespace"], - self.parsed["cts_namespace"], - ".".join( - [ - self.parsed["textgroup"], - self.parsed["work"], - self.parsed["text"] - ]), - self.parsed["reference"]["start"] - ]) - else: - return ":".join([ - "urn", - self.parsed["urn_namespace"], - self.parsed["cts_namespace"], - ".".join( - [ - self.parsed["textgroup"], - self.parsed["work"] - ]), - self.parsed["reference"]["start"] - ]) - elif key == "end" and "passage" in self.parsed and self.parsed["reference"]["end"] is not None: - if "text" in self.parsed: - return ":".join([ - "urn", - self.parsed["urn_namespace"], - self.parsed["cts_namespace"], - ".".join([ - self.parsed["textgroup"], - self.parsed["work"], - self.parsed["text"] - ]), - self.parsed["reference"]["end"] - ]) - else: - return ":".join([ - "urn", - self.parsed["urn_namespace"], - self.parsed["cts_namespace"], - ".".join([ - self.parsed["textgroup"], - self.parsed["work"] - ]), - self.parsed["reference"]["end"] - ]) - elif key == "full": - return self.parsed["full"] - elif key == "reference" and "reference" in self.parsed: - return self.parsed["reference"] else: - return None + raise KeyError("Provided key is not recognized.") + + @staticmethod + def model(): + return { + "urn_namespace": None, + "cts_namespace": None, + "textgroup": None, + "work": None, + "version": None, + "reference": None + } def __parse(self, urn): """ Parse a URN - :param urn: A URN:CTS :type urn: basestring :rtype: defaultdict.basestring :returns: Dictionary representation """ - parsed = defaultdict(str) - parsed["full"] = urn.split("#")[0] - urn = parsed["full"].split(":") + parsed = URN.model() + self.__urn = urn.split("#")[0] + urn = self.__urn.split(":") if isinstance(urn, list) and len(urn) > 2: parsed["urn_namespace"] = urn[1] parsed["cts_namespace"] = urn[2] if len(urn) == 5: - parsed["passage"] = urn[4] parsed["reference"] = Reference(urn[4]) if len(urn) >= 4: @@ -476,7 +577,7 @@ def __parse(self, urn): if len(urn) >= 2: parsed["work"] = urn[1] if len(urn) >= 3: - parsed["text"] = urn[2] + parsed["version"] = urn[2] else: raise ValueError("URN is empty") return parsed @@ -495,9 +596,17 @@ class Citation(object): :type refsDecl: basestring :param child: A citation :type child: Citation + :ivar name: Name of the citation (e.g. "book") + :type name: basestring + :ivar xpath: Xpath of the citation (As described by CTS norm) + :type xpath: basestring + :ivar scope: Scope of the citation (As described by CTS norm) + :type xpath: basestring + :ivar refsDecl: refsDecl version + :type refsDecl: basestring + :ivar child: A citation + :type child: Citation - .. automethod:: __iter__ - .. automethod:: __len__ """ def __init__(self, name=None, xpath=None, scope=None, refsDecl=None, child=None): @@ -644,13 +753,27 @@ def fill(self, passage=None, xpath=None): """ Fill the xpath with given informations :param passage: Passage reference - :type passage: Reference or lsit + :type passage: Reference 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 :returns: Xpath to find the passage + + .. code-block:: python + + citation = Citation(name="line", scope="/TEI/text/body/div/div[@n=\"?\"]",xpath="//l[@n=\"?\"]") + print(citation.fill(["1", 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")) + # /TEI/text/body/div/div[@n='1']//l[@n='1'] + print(citation.fill("1", xpath=True) + # //l[@n='1'] + + """ - if xpath is True: # Then passage is a string or None + if xpath is True: # Then passage is a string or None xpath = self.xpath if passage is None: @@ -661,13 +784,30 @@ def fill(self, passage=None, xpath=None): return REFERENCE_REPLACER.sub(replacement, xpath) else: if isinstance(passage, Reference): - passage = passage[2] - passage = iter(passage) + passage = passage.list or passage.start.list + elif passage is None: + return REFERENCE_REPLACER.sub( + r"\1", + self.refsDecl + ) + passage = iter(passage) return REFERENCE_REPLACER.sub( lambda m: REF_REPLACER(m, passage), self.refsDecl ) + def __getstate__(self): + """ Pickling method + + :return: + """ + return copy(self.__dict__) + + def __setstate__(self, dic): + self.__dict__ = dic + return self + + def REF_REPLACER(match, passage): """ Helper to replace xpath/scope/refsDecl on iteration with passage value @@ -684,4 +824,4 @@ def REF_REPLACER(match, passage): if ref is None: return groups[0] else: - return "{1}='{0}'".format(ref, groups[0]) \ No newline at end of file + return "{1}='{0}'".format(ref, groups[0]) diff --git a/MyCapytain/common/utils.py b/MyCapytain/common/utils.py index 863f1a1c..f2cd70ed 100644 --- a/MyCapytain/common/utils.py +++ b/MyCapytain/common/utils.py @@ -14,6 +14,7 @@ from io import IOBase, StringIO from past.builtins import basestring import re +from copy import copy __strip = re.compile("([ ]{2,})+") @@ -62,3 +63,190 @@ def xmlparser(xml): xml.close() return parsed + +def formatXpath(xpath): + """ + + :param xpath: + :return: + """ + if len(xpath) > 1: + current, queue = xpath[0], xpath[1:] + current = "./{}[./{}]".format( + current, + "/".join(queue) + ) + else: + current, queue = "./{}".format(xpath[0]), [] + + return current, queue + + +def performXpath(parent, xpath): + """ + + :param parent: + :param xpath: + :return: (Result, Loop Indicator) + """ + loop = False + if xpath.startswith(".//"): + result = parent.xpath( + xpath.replace(".//", "./"), + namespaces=NS + ) + if len(result) == 0: + result = parent.xpath( + "*[{}]".format(xpath), + namespaces=NS + ) + loop = True + else: + result = parent.xpath( + xpath, + namespaces=NS + ) + return result[0], loop + + +def copyNode(node, children=False, parent=False): + """ + + :param node: + :param children: + :param parent: + :return: + """ + if parent is not False: + element = etree.SubElement( + parent, + node.tag, + attrib=node.attrib, + nsmap={None: "http://www.tei-c.org/ns/1.0"} + ) + else: + element = etree.Element( + node.tag, + attrib=node.attrib, + nsmap={None: "http://www.tei-c.org/ns/1.0"} + ) + if children: + element.text = node.text + for child in node: + element.append(copy(child)) + return element + + +def normalizeXpath(xpath): + """ Normalize XPATH splitted around slashes + + :param xpath: List of xpath elements + :type xpath: [str] + :return: List of refined xpath + :rtype: [str] + """ + new_xpath = [] + for x in range(0, len(xpath)): + if x > 0 and len(xpath[x-1]) == 0: + new_xpath.append("/"+xpath[x]) + elif len(xpath[x]) > 0: + new_xpath.append(xpath[x]) + return new_xpath + + +def passageLoop(parent, new_tree, xpath1, xpath2=None, preceding_siblings=False, following_siblings=False): + """ + + :param parent: Parent on which to perform xpath + :param new_tree: Parent on which to add nodes + :param xpath: List of xpath elements + :type xpath: [str] + :return: + """ + + current_1, queue_1 = formatXpath(xpath1) + if xpath2 is None: # In case we need what is following or preceding our node + result_1, loop = performXpath(parent, current_1) + if loop is True: + queue_1 = xpath1 + siblings = list(parent) + index_1 = siblings.index(result_1) + children = len(queue_1) == 0 + + # We fill the gaps using the list option of LXML + if preceding_siblings: + [ + copyNode(child, parent=new_tree, children=True) + for child in siblings + if index_1 > siblings.index(child) + ] + child = copyNode(result_1, children=children, parent=new_tree) + elif following_siblings: + child = copyNode(result_1, children=children, parent=new_tree) + [ + copyNode(child, parent=new_tree, children=True) + for child in siblings + if index_1 < siblings.index(child) + ] + + if not children: + child = passageLoop( + result_1, + child, + queue_1, + None, + preceding_siblings=preceding_siblings, + following_siblings=following_siblings + ) + else: + + result_1, loop = performXpath(parent, current_1) + if loop is True: + queue_1 = xpath1 + if xpath2 == xpath1: + current_2, queue_2 = current_1, queue_1 + else: + current_2, queue_2 = formatXpath(xpath2) + else: + current_2, queue_2 = formatXpath(xpath2) + + if xpath1 != xpath2: + result_2, loop = performXpath(parent, current_2) + if loop is True: + queue_2 = xpath2 + else: + result_2 = result_1 + + if result_1 == result_2: + children = len(queue_1) == 0 + child = copyNode(result_1, children=children, parent=new_tree) + if not children: + child = passageLoop( + result_1, + child, + queue_1, + queue_2 + ) + else: + children = list(parent) + index_1 = children.index(result_1) + index_2 = children.index(result_2) + # Appends the starting passage + children_1 = len(queue_1) == 0 + child_1 = copyNode(result_1, children=children_1, parent=new_tree) + if not children_1: + passageLoop(result_1, child_1, queue_1, None, following_siblings=True) + # Appends what's in between + nodes = [ + copyNode(child, parent=new_tree, children=True) + for child in children + if index_1 < children.index(child) < index_2 + ] + # Appends the Ending passage + children_2 = len(queue_2) == 0 + child_2 = copyNode(result_2, children=children_2, parent=new_tree) + + if not children_2: + passageLoop(result_2, child_2, queue_2, None, preceding_siblings=True) + + return new_tree diff --git a/MyCapytain/endpoints/cts5.py b/MyCapytain/endpoints/cts5.py index e9ad448d..7af3a901 100644 --- a/MyCapytain/endpoints/cts5.py +++ b/MyCapytain/endpoints/cts5.py @@ -71,6 +71,7 @@ def getValidReff(self, urn, inventory=None, level=None): :type inventory: text :param level: Depth of references expected :type level: int + :return: XML Response from the API as string :rtype: str """ return self.call({ diff --git a/MyCapytain/errors.py b/MyCapytain/errors.py index 00ac1697..f7353ac9 100644 --- a/MyCapytain/errors.py +++ b/MyCapytain/errors.py @@ -8,3 +8,10 @@ class RefsDeclError(Exception): """ Error issued when an the refsDecl does not succeed in xpath (no results) """ pass + + +class InvalidSiblingRequest(Exception): + """ This error is thrown when one attempts to get previous or next passage on a passage with a range of different + depth, ex. : 1-2.25 + """ + pass \ No newline at end of file diff --git a/MyCapytain/resources/inventory.py b/MyCapytain/resources/inventory.py index 711efddb..e0e660a7 100644 --- a/MyCapytain/resources/inventory.py +++ b/MyCapytain/resources/inventory.py @@ -10,9 +10,9 @@ from __future__ import unicode_literals from MyCapytain.resources.proto import inventory, text -from MyCapytain.common.reference import Citation as CitationPrototype +from MyCapytain.common.reference import Citation as CitationPrototype, URN from MyCapytain.common.utils import xmlparser, NS - +import re from six import text_type as str import collections @@ -21,6 +21,9 @@ class Citation(CitationPrototype): """ Citation XML implementation for TextInventory """ + + escape = re.compile('(")') + def __str__(self): """ Returns a string text inventory version of the object @@ -39,10 +42,10 @@ def __str__(self): if self.name is not None: label = self.name - return "{child}".format( + return """{child}""".format( child=child, - xpath=self.xpath, - scope=self.scope, + xpath=re.sub(Citation.escape, "'", self.xpath), + scope=re.sub(Citation.escape, "'", self.scope), label=label ) @@ -82,6 +85,7 @@ def ingest(resource, element=None, xpath="ti:citation"): return None + def xpathDict(xml, xpath, children, parents, **kwargs): """ Returns a default Dict given certain informations @@ -138,7 +142,7 @@ def __str__(self): "".format( tag_start, self.urn, - self.urn["work"] + self.urn.upTo(URN.WORK) ) ) else: @@ -212,7 +216,7 @@ def export(self, output="xml", **kwargs): elif issubclass(output, text.Text): complete_metadata = self.metadata for parent in self.parents: - if isinstance(parent, inventory.Resource): + if isinstance(parent, inventory.Resource) and hasattr(parent, "metadata"): complete_metadata = complete_metadata + parent.metadata return output(urn=self.urn, citation=self.citation, metadata=complete_metadata, **kwargs) @@ -224,7 +228,6 @@ def __findCitations(self, xml, xpath="ti:citation"): """ self.citation = Citation.ingest(xml, self.citation, xpath) - def parse(self, resource): """ Parse a resource to feed the object @@ -299,7 +302,7 @@ def __str__(self): if self.urn is not None: strings.append( "".format( - self.urn, self.urn["textgroup"]) + self.urn, self.urn.upTo(URN.TEXTGROUP)) ) else: if len(self.parents) > 0 and hasattr(self.parents[0], "urn") is True: @@ -308,7 +311,6 @@ def __str__(self): ) else: strings.append("") - for tag, metadatum in self.metadata: for lang, value in metadatum: strings.append("{value}".format(tag=tag, lang=lang, value=value)) @@ -352,13 +354,13 @@ def parse(self, resource): xml=self.xml, xpath='ti:edition', children=Edition, - parents=tuple([self]) + self.parents + parents=[self] + self.parents ) self.__translations = xpathDict( xml=self.xml, xpath='ti:translation', children=Translation, - parents=tuple([self]) + self.parents + parents=[self] + self.parents ) self.texts = collections.defaultdict(Text) @@ -429,7 +431,7 @@ def parse(self, resource): xml=self.xml, xpath='ti:work', children=Work, - parents=(self, self.parents) + parents=[self] + self.parents ) return self.works @@ -483,6 +485,6 @@ def parse(self, resource): xml=self.xml, xpath='//ti:textgroup', children=TextGroup, - parents=self + parents=[self] ) return self.textgroups diff --git a/MyCapytain/resources/proto/inventory.py b/MyCapytain/resources/proto/inventory.py index 6a98a244..91a69288 100644 --- a/MyCapytain/resources/proto/inventory.py +++ b/MyCapytain/resources/proto/inventory.py @@ -8,10 +8,13 @@ """ -from MyCapytain.common.reference import URN, Reference +from MyCapytain.common.reference import URN, Reference, Citation from MyCapytain.common.metadata import Metadata from past.builtins import basestring from collections import defaultdict +from copy import copy +from lxml import etree +from six import text_type as str class Resource(object): @@ -49,8 +52,8 @@ def __eq__(self, other): return False elif self.resource is None: # Not totally true - return (hasattr(self, "urn") and hasattr(other, "urn") and self.urn == other.urn) - return (hasattr(self, "urn") and hasattr(other, "urn") and self.urn == other.urn) and self.resource == other.resource + return hasattr(self, "urn") and hasattr(other, "urn") and self.urn == other.urn + return hasattr(self, "urn") and hasattr(other, "urn") and self.urn == other.urn and self.resource == other.resource def __str__(self): raise NotImplementedError() @@ -78,12 +81,14 @@ def __urnitem__(self, key): elif isinstance(self, Work): children = self.texts - order = ["", "", "textgroup", "work", "text"] - + order = ["", "", URN.TEXTGROUP, URN.WORK, URN.VERSION] while i <= len(urn) - 1: - children = children[urn[order[i]]] - if not hasattr(children, "urn") or str(children.urn) != urn[order[i]]: - raise ValueError("Unrecognized urn at level " + order[i]) + children = children[urn.upTo(order[i])] + if not hasattr(children, "urn") or str(children.urn) != urn.upTo(order[i]): + error = "Unrecognized urn at " + [ + "URN namespace", "CTS Namespace", "URN Textgroup", "URN Work", "URN Version" + ][i] + raise ValueError(error) i += 1 return children @@ -108,6 +113,34 @@ def parse(self, resource): """ raise NotImplementedError() + def __getstate__(self, children=True): + """ Pickling method to be called upon dumping object + + :return: + """ + + dic = copy(self.__dict__) + if "xml" in dic: + dic["xml"] = etree.tostring(dic["xml"], encoding=str) + if "resource" in dic: + del dic["resource"] + """ The resource is unecessary in later than parsing state + if "resource" in dic: + dic["resource"] = str(dic["resource"]) + """ + return dic + + def __setstate__(self, dic): + """ + + :param dic: + :return: + """ + self.__dict__ = dic + if "xml" in dic: + self.xml = etree.fromstring(dic["xml"]) + return self + class Text(Resource): """ Represents a CTS Text @@ -124,11 +157,10 @@ def __init__(self, resource=None, urn=None, parents=None, subtype="Edition"): self.lang = None self.urn = None self.docname = None - self.parents = () + self.parents = list() self.subtype = subtype self.validate = None self.metadata = Metadata(keys=["label", "description", "namespaceMapping"]) - # self.citations = () if urn is not None: self.urn = URN(urn) @@ -140,29 +172,26 @@ def __init__(self, resource=None, urn=None, parents=None, subtype="Edition"): if resource is not None: self.setResource(resource) - if self.subtype == "Edition": - self.translations = lambda key=None: self.parents[0].getLang(key) - elif self.subtype == "Translation": - self.editions = lambda: [ - self.parents[0].texts[urn] - for urn in self.parents[0].texts - if self.parents[0].texts[urn].subtype == "Edition" - ] + def translations(self, key=None): + """ Get translations in given language - def __getstate__(self): - """ Pickling method to be called upon dumping object + :param key: Language ISO Code to filter on + :return: + """ + return self.parents[0].getLang(key) - :return: Dictionary Representation + def editions(self): + """ Get all editions of the texts + + :return: List of editions + :rtype: [Text] """ + return [ + self.parents[0].texts[urn] + for urn in self.parents[0].texts + if self.parents[0].texts[urn].subtype == "Edition" + ] - return dict( - metadata=getattr(self.metadata, "__getstate__")(), - urn=str(self.urn), - lang=self.lang, - subtype=self.subtype, - parents=[getattr(item, "__getstate__")(children=False) for item in self.parents], - citations=[getattr(value, "__getstate__")() for value in self.citation] - ) def Edition(resource=None, urn=None, parents=None): return Text(resource=resource, urn=urn, parents=parents, subtype="Edition") @@ -188,7 +217,7 @@ def __init__(self, resource=None, urn=None, parents=None): self.lang = None self.urn = None self.texts = defaultdict(Text) - self.parents = () + self.parents = list() self.metadata = Metadata(keys=["title"]) if urn is not None: @@ -213,23 +242,6 @@ def getLang(self, key=None): else: return [self.texts[urn] for urn in self.texts if self.texts[urn].subtype == "Translation"] - def __getstate__(self, children=False): - """ Pickling method to be called upon dumping object - - :return: Dictionary Representation - """ - __dict__ = dict( - metadata=getattr(self.metadata, "__getstate__")(), - urn=str(self.urn), - parents=[getattr(item, "__getstate__")(children=False) for item in self.parents] - ) - if children: - __dict__["texts"] = { - key: getattr(value, "__getstate__")() for key, value in self.texts.items() - } - - return __dict__ - class TextGroup(Resource): """ Represents a CTS Textgroup @@ -246,32 +258,18 @@ def __init__(self, resource=None, urn=None, parents=None): """ self.urn = None self.works = defaultdict(Work) - self.parents = () + self.parents = list() self.metadata = Metadata(keys=["groupname"]) if urn is not None: self.urn = URN(urn) if parents: - self.parents = [parents] + self.parents = parents if resource is not None: self.setResource(resource) - def __getstate__(self, children=True): - """ Pickling method to be called upon dumping object - - :return: - """ - __dict__ = dict( - metadata=getattr(self.metadata, "__getstate__")(), - urn=str(self.urn) - ) - if children: - __dict__["works"] = { - key: getattr(value, "__getstate__")() for key, value in self.textgroups.items() - } - return __dict__ class TextInventory(Resource): """ Represents a CTS Inventory file @@ -286,18 +284,18 @@ def __init__(self, resource=None, id=None): """ self.textgroups = defaultdict(TextGroup) self.id = id - self.parents = () + self.parents = list() if resource is not None: self.setResource(resource) - def __getstate__(self): - """ Pickling method to be called upon dumping object + def __len__(self): + """ - :return: + :return: Number of texts available in the inventory """ - return { - "textgroups": { - key: getattr(value, "__getstate__")() for key, value in self.textgroups.items() - }, - "id": self.id - } \ No newline at end of file + return len([ + text + for tg in self.textgroups.values() + for work in tg.works.values() + for text in work.texts.values() + ]) diff --git a/MyCapytain/resources/texts/api.py b/MyCapytain/resources/texts/api.py index 8a207937..52e10e87 100644 --- a/MyCapytain/resources/texts/api.py +++ b/MyCapytain/resources/texts/api.py @@ -1,4 +1,7 @@ +# -*- coding: utf-8 -*- + from __future__ import unicode_literals +from past.builtins import basestring from six import text_type as str import MyCapytain.resources.proto.text @@ -7,6 +10,7 @@ import MyCapytain.endpoints.proto import MyCapytain.common.metadata import MyCapytain.common.utils +import MyCapytain.common.reference class Text(MyCapytain.resources.proto.text.Text): @@ -50,9 +54,9 @@ def getValidReff(self, level=1, reference=None): :param level: Depth required. If not set, should retrieve first encountered level (1 based) :type level: Int - :param reference: Subreference (optional) + :param reference: Passage reference :type reference: Reference - :rtype: List.basestring + :rtype: list(str) :returns: List of levels """ if reference: @@ -82,13 +86,19 @@ def getPassage(self, reference=None): """ Retrieve a passage and store it in the object :param reference: Reference of the passage - :type reference: MyCapytain.common.reference.Reference or List of basestring + :type reference: MyCapytain.common.reference.Reference, or MyCapytain.common.reference.URN, or str or list(str) :rtype: Passage :returns: Object representing the passage :raises: *TypeError* when reference is not a list or a Reference """ - if reference: + if isinstance(reference, MyCapytain.common.reference.URN): + urn = str(reference) + elif isinstance(reference, MyCapytain.common.reference.Reference): + urn = "{0}:{1}".format(self.urn, str(reference)) + elif isinstance(reference, str): urn = "{0}:{1}".format(self.urn, reference) + elif isinstance(reference, list): + urn = "{0}:{1}".format(self.urn, ".".join(reference)) else: urn = str(self.urn) @@ -157,6 +167,49 @@ def getLabel(self): return self.metadata + def getPrevNextUrn(self, reference): + """ Get the previous URN of a reference of the text + + :param reference: Reference from which to find siblings + :type reference: Reference + :return: (Previous Passage Reference,Next Passage Reference) + """ + _prev, _next = Passage.prevnext( + self.resource.getPrevNextUrn( + urn="{}:{}".format( + str( + MyCapytain.common.reference.URN( + str(self.urn)).upTo(MyCapytain.common.reference.URN.NO_PASSAGE) + ), + str(reference) + ) + ) + ) + return _prev, _next + + 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 + :return: Children URN + :rtype: URN + """ + if reference: + urn = "{}:{}".format( + str(MyCapytain.common.reference.URN(str(self.urn)).upTo(MyCapytain.common.reference.URN.NO_PASSAGE)), + str(reference) + ) + else: + urn = self.urn + + _first = Passage.firstUrn( + self.resource.getFirstUrn( + urn + ) + ) + return _first + @property def reffs(self): """ Get all valid reffs for every part of the Text @@ -182,29 +235,24 @@ def __init__(self, urn, resource, *args, **kwargs): self.urn = urn # Could be set during parsing - self._next = None - self._prev = None - self.__first = None - self.__last = None + self.__next = False + self.__prev = False + self.__first = False + self.__last = False self.__parse() @property - def next(self): - """ Following passage + def first(self): + """ Children passage :rtype: Passage - :returns: Following passage at same level + :returns: Previous passage at same level """ - if self._next is not None: - _next = self._next - else: + if self.__first is False: # Request the next urn - _prev, _next = Passage.prevnext( - self.parent.resource.getPrevNextUrn(urn=str(self.urn)) - ) - - self.parent.resource.getPassage(urn=_next) + self.__first = self.parent.getFirstUrn(reference=str(self.urn.reference)) + return self.__first @property def prev(self): @@ -213,15 +261,49 @@ def prev(self): :rtype: Passage :returns: Previous passage at same level """ - if self._prev is not None: - _prev = self._prev - else: + if self.__prev is False: # Request the next urn - _prev, _next = Passage.prevnext( - self.parent.resource.getPrevNextUrn(urn=str(self.urn)) - ) + self.__prev, self.__next = self.parent.getPrevNextUrn(reference=self.urn.reference) + return self.__prev - return self.parent.resource.getPassage(urn=_prev) + @property + def next(self): + """ Shortcut for getting the following passage + + :rtype: MyCapytain.common.reference.Reference + :returns: Following passage reference + """ + if self.__next is False: + # Request the next urn + self.__prev, self.__next = self.parent.getPrevNextUrn(reference=self.urn.reference) + return self.__next + + def getNext(self): + """ Shortcut for getting the following passage + + :rtype: Passage + :returns: Following passage at same level + """ + if self.next: + return self.parent.getPassage(reference=self.next) + + def getPrev(self): + """ Shortcut for getting the preceding passage + + :rtype: Passage + :returns: Previous passage at same level + """ + if self.prev: + return self.parent.getPassage(reference=self.prev) + + def getFirst(self): + """ Shortcut for getting the first child passage + + :rtype: Passage + :returns: Previous passage at same level + """ + if self.first: + return self.parent.getPassage(reference=self.first) def __parse(self): """ Given self.resource, split informations from the CTS API @@ -230,7 +312,7 @@ def __parse(self): """ self.resource = self.resource.xpath("//ti:passage/tei:TEI", namespaces=MyCapytain.common.utils.NS)[0] - self._prev, self._next = Passage.prevnext(self.resource) + self.__prev, self.__next = Passage.prevnext(self.resource) @staticmethod def prevnext(resource): @@ -239,22 +321,41 @@ def prevnext(resource): :param resource: XML Resource :type resource: etree._Element :return: Tuple representing previous and next urn - :rtype: (str, str) + :rtype: (URN, URN) """ - _prev, _next = None, None + _prev, _next = False, False resource = MyCapytain.common.utils.xmlparser(resource) prevnext = resource.xpath("//ti:prevnext", namespaces=MyCapytain.common.utils.NS) if len(prevnext) > 0: + _next, _prev = None, None prevnext = prevnext[0] - _next_xpath = prevnext.xpath("ti:next/ti:urn/text()", namespaces=MyCapytain.common.utils.NS) - _prev_xpath = prevnext.xpath("ti:prev/ti:urn/text()", namespaces=MyCapytain.common.utils.NS) + _next_xpath = prevnext.xpath("ti:next/ti:urn/text()", namespaces=MyCapytain.common.utils.NS, smart_strings=False) + _prev_xpath = prevnext.xpath("ti:prev/ti:urn/text()", namespaces=MyCapytain.common.utils.NS, smart_strings=False) if len(_next_xpath): - _next = _next_xpath[0] + _next = MyCapytain.common.reference.URN(_next_xpath[0]) if len(_prev_xpath): - _prev = _prev_xpath[0] + _prev = MyCapytain.common.reference.URN(_prev_xpath[0]) return _prev, _next + @staticmethod + def firstUrn(resource): + """ Parse a resource to get the first URN + + :param resource: XML Resource + :type resource: etree._Element + :return: Tuple representing previous and next urn + :rtype: (URN, URN) + """ + _child = False + resource = MyCapytain.common.utils.xmlparser(resource) + urn = resource.xpath("//ti:reply/ti:urn/text()", namespaces=MyCapytain.common.utils.NS, magic_string=True) + + if len(urn) > 0: + urn = str(urn[0]) + + return MyCapytain.common.reference.URN(urn) + diff --git a/MyCapytain/resources/texts/local.py b/MyCapytain/resources/texts/local.py index 0cf0986e..d14f0dc7 100644 --- a/MyCapytain/resources/texts/local.py +++ b/MyCapytain/resources/texts/local.py @@ -15,12 +15,12 @@ import warnings from MyCapytain.errors import DuplicateReference, RefsDeclError -from MyCapytain.common.utils import xmlparser, NS +from MyCapytain.common.utils import xmlparser, NS, copyNode, passageLoop, normalizeXpath, normalize from MyCapytain.common.reference import URN, Citation, Reference from MyCapytain.resources.proto import text +from MyCapytain.errors import InvalidSiblingRequest import MyCapytain.resources.texts.tei - - +from lxml import etree class Text(text.Text): @@ -37,17 +37,17 @@ class Text(text.Text): :ivar resource: lxml """ - def __init__(self, urn=None, citation=None, resource=None, autoreffs=True): - self._passages = OrderedDict() # Represents real full passages / reffs informations. Only way to set it up is getValidReff without passage ? + def __init__(self, urn=None, citation=None, resource=None, autoreffs=False): + super(Text, self).__init__(urn=urn, citation=citation) + self._passages = Passage() self._orphan = defaultdict(Reference) # Represents passage we got without asking for all. Storing convenience ? self._cRefPattern = MyCapytain.resources.texts.tei.Citation() - self.resource = None self.xml = None - self._URN = None if citation is not None: self.citation = citation + if resource is not None: self.resource = resource self.xml = xmlparser(resource) @@ -69,8 +69,6 @@ def __findCRefPattern(self, xml): def parse(self): """ Parse the object and generate the children - - :return: """ try: xml = self.xml.xpath(self.citation.scope, namespaces=NS)[0] @@ -80,13 +78,13 @@ def parse(self): except Exception as E: raise E - self._passages = Passage(resource=xml, citation=self.citation, urn=self.urn, id=None) + self._passages = Passage(resource=xml, citation=self.citation, urn=self.urn, reference=None) @property def citation(self): """ Get the lowest cRefPattern in the hierarchy - :rtype: MyCapytain.resources.texts.tei.Citation + :rtype: Citation """ return self._cRefPattern @@ -95,7 +93,7 @@ def citation(self, value): """ Set the cRefPattern :param value: Citation to be saved - :type value: MyCapytain.resources.texts.tei.Citation or Citation + :type value: Citation :raises: TypeError when value is not a TEI Citation or a Citation """ if isinstance(value, MyCapytain.resources.texts.tei.Citation): @@ -108,18 +106,30 @@ def citation(self, value): child=value.child ) - def getPassage(self, reference): + def getPassage(self, reference, hypercontext=True): """ Finds a passage in the current text :param reference: Identifier of the subreference / passages - :type reference: List, MyCapytain.common.reference.Reference - :rtype: Passage + :type reference: list, Reference + :param hypercontext: If set to true, retrieves nodes up to the given one, cleaning non required siblings. + :type hypercontext: bool + :rtype: Passage, ContextPassage :returns: Asked passage + + .. note :: As of MyCapytain 0.1.0, Text().getPassage() returns by default a ContextPassage, thus being able + to handle range. This design change also means that the returned tree is way different that a classic + Passage. To retrieve MyCapytain<=0.0.9 behaviour, use `hypercontext=False`. """ - if isinstance(reference, MyCapytain.common.reference.Reference): - reference = reference["start_list"] + if hypercontext is True: + return self._getPassageContext(reference) + + if isinstance(reference, Reference): + reference = reference.list or reference.start.list - reference = [".".join(reference[:i]) for i in range(1, len(reference) + 1 )] + if self._passages.resource is None: + self.parse() + + reference = [".".join(reference[:i]) for i in range(1, len(reference) + 1)] passages = [self._passages] while len(reference) > 0: passages = [passage for sublist in [p.get(reference[0]) for p in passages] for passage in sublist] @@ -127,65 +137,167 @@ def getPassage(self, reference): return passages[0] - def getPassagePlus(self, reference): - """ Finds a passage in the current text with its previous and following node + def _getPassageContext(self, reference): + """ Retrieves nodes up to the given one, cleaning non required siblings. :param reference: Identifier of the subreference / passages - :type reference: List, MyCapytain.common.reference.Reference - :rtype: text.PassagePlus - :returns: Asked passage with metainformations + :type reference: list, reference + :returns: Asked passage + :rtype: ContextPassage """ - P = self.getPassage(reference=reference) - return text.PassagePlus(P, P.prev.id, P.next.id) + if isinstance(reference, list): + start, end = reference, reference + reference = Reference(".".join(reference)) + elif not reference.end: + start, end = reference.start.list, reference.start.list + else: + start, end = reference.start.list, reference.end.list + + if len(start) > len(self.citation): + raise ReferenceError("URN is deeper than citation scheme") + + citation_start = [citation for citation in self.citation][len(start)-1] + citation_end = [citation for citation in self.citation][len(end)-1] + + start, end = citation_start.fill(passage=start), citation_end.fill(passage=end) + + nodes = etree._ElementTree() + + start, end = normalizeXpath(start.split("/")[2:]), normalizeXpath(end.split("/")[2:]) + + root = copyNode(self.xml) + nodes._setroot(root) + root = passageLoop(self.xml, root, start, end) + + if self.urn: + urn, reference = URN("{}:{}".format(self.urn, reference)), reference + else: + urn, reference = None, reference + return ContextPassage( + urn=urn, + resource=root, parent=self, citation=self.citation + ) def getValidReff(self, level=1, reference=None): """ Retrieve valid passages directly :param level: Depth required. If not set, should retrieve first encountered level (1 based) - :type level: Int - :param reference: Subreference (optional) + :type level: int + :param reference: Passage Reference :type reference: Reference - :rtype: List.basestring :returns: List of levels + :rtype: list(basestring, str) + + : + + .. note:: GetValidReff works for now as a loop using Passage, subinstances of Text, to retrieve the valid + informations. Maybe something is more powerfull ? - .. note:: GetValidReff works for now as a loop using Passage, subinstances of Text, to retrieve the valid informations. Maybe something is more powerfull ? """ + depth = 0 + xml = self.xml + _range = False + if reference: + if isinstance(reference, Reference): + if reference.end is None: + passages = [reference.list] + else: + xml = self.getPassage(reference=reference) + a, b = reference.start.list, reference.end.list + passages = [[]] + + else: + raise TypeError() - if reference is not None: - start = len(reference[2]) - nodes = [".".join(reference[2][0:i+1]) for i in range(0, start)] + [None] - if level <= start: - level = start + 1 + depth = len(passages[0]) else: - nodes = [None for i in range(0, level)] + passages = [[]] + + if level <= len(passages[0]) and reference is not None: + level = len(passages[0]) + 1 + if level > len(self.citation): + return [] + + nodes = [None for i in range(depth, level)] + + citations = [citation for citation in self.citation] - passages = [self._passages] # For consistency while len(nodes) >= 1: - passages = [passage for sublist in [p.get(nodes[0]) for p in passages] for passage in sublist] + passages = [ + refs + [node.get("n")] + for xpath_result, refs in [ + ( + xml.xpath( + citations[len(filling)-1].fill(filling), + namespaces=NS + ), + refs + ) + for filling, refs in + [(refs + [None], refs) for refs in passages] + ] + for node in xpath_result + ] nodes.pop(0) - return [".".join(passage.id) for passage in passages] + if len(passages) == 0: + msg = "Unknown reference {}".format(reference) + raise KeyError(msg) + + passages = [".".join(passage) for passage in passages] + duplicates = set([n for n in passages if passages.count(n) > 1]) + if len(duplicates) > 0: + message = ", ".join(duplicates) + warnings.warn(message, DuplicateReference) + del duplicates + + return passages + + def text(self, exclude=None): + """ Returns the text of the XML resource without the excluded nodes + + :param exclude: List of nodes + :type exclude: list(str) + :return: Text of the text without the text inside removed nodes + :rtype: str + + .. example:: + `epigrammata.exclude(["tei:note"])` would remove all note nodes of the XML and print the text + + """ + return self._passages.text(exclude=exclude) class Passage(MyCapytain.resources.texts.tei.Passage): - """ Passage representing object - + """ Passage class for local texts which is fast but contains the minimum DOM. + + For design purposes, some people would prefer passage to be found quickly (Text indexing for example). + Passage keeps only the node found through the xpath + + **Example** : for a text with a citation scheme with following refsDecl : + `/TEI/text/body/div[@type='edition']/div[@n='$1']/div[@n='$2']/l[@n='$3']` and a passage 1.1.1, this + class will build an XML tree looking like the following + + .. code-block:: xml + + Lorem ipsum + :param urn: A URN identifier - :type urn: MyCapytain.common.reference.URN + :type urn: URN :param resource: A resource - :type resource: lxml.etree._Element + :type resource: etree._Element :param parent: Parent of the current passage - :type parent: MyCapytain.resources.texts.tei.Passage + :type parent: Passage :param citation: Citation for children level - :type citation: MyCapytain.resources.texts.tei.Citation - :param id: Identifier of the subreference without URN informations - :type id: List + :type citation: Citation + :param reference: Identifier of the subreference without URN information + :type reference: Reference, List - .. note:: *id* is used in to identify the current passage in case the URN is unknown + .. warning:: This passage system does not accept range """ - def __init__(self, urn=None, resource=None, parent=None, citation=None, id=None): - super(Passage, self).__init__(urn=urn, resource=resource, parent=parent) + def __init__(self, urn=None, resource=None, parent=None, citation=None, reference=None): + super(Passage, self).__init__(resource=resource, parent=parent) self.__next = False self.__prev = False @@ -194,42 +306,54 @@ def __init__(self, urn=None, resource=None, parent=None, citation=None, id=None) if isinstance(citation, Citation): self.citation = citation - self.__id = [] - - if id is not None: - self.id = id + self.__reference = Reference("") + if urn: + self.urn = urn + if reference: + self.reference = reference self.__children = OrderedDict() self.__parsed = False @property - def id(self): + def reference(self): """ Id represents the passage subreference as a list of basestring - :rtype: list :returns: Representation of the passage subreference as a list + :rtype: Reference """ - return self.__id + return self.__reference - @id.setter - def id(self, value): + @reference.setter + def reference(self, value): """ Set up ID property :param value: Representation of the passage subreference as a list - :type value: list + :type value: list, tuple, Reference .. note:: `Passage.id = [..]` will update automatically the URN property as well if correct """ + _value = None if isinstance(value, (list, tuple)): - self.__id = value - self.__updateURN() + _value = Reference(".".join(value)) + elif isinstance(value, basestring): + _value = Reference(value) + elif isinstance(value, Reference): + _value = value + + if _value and self.__reference != _value: + self.__reference = _value + if self._URN and len(self._URN): + if len(value): + self._URN = URN("{}:{}".format(self._URN.upTo(URN.NO_PASSAGE), str(_value))) + else: + self._URN = URN(self._URN["text"]) @property def urn(self): """ URN Identifier of the object - :rtype: MyCapytain.common.reference.URN - + :rtype: URN """ return self._URN @@ -238,32 +362,26 @@ def urn(self, value): """ Set the urn :param value: URN to be saved - :type value: MyCapytain.common.reference.URN + :type value: URN, basestring, str :raises: *TypeError* when the value is not URN compatible .. note:: `Passage.URN = ...` will update automatically the id property if Passage is set """ a = self._URN + if isinstance(value, basestring): - value = MyCapytain.common.reference.URN(value) - elif not isinstance(value, MyCapytain.common.reference.URN): + value = URN(value) + elif not isinstance(value, URN): raise TypeError() + if str(a) != str(value): self._URN = value - self.__updateURN() - - def __updateURN(self): - """ Private method allowing for update of self.id or self.urn - """ - if self.id is not None and len(self.id) > 0 and isinstance(self.urn, URN): - self.urn = URN(self.urn["text"] + ":" + ".".join(self.id)) - elif self._URN is not None \ - and self._URN["reference"] is not None \ - and len(self._URN["reference"][2]) > 0 \ - and self.id != self._URN["reference"][2]: - self.__id = self._URN["reference"][2] + if value.reference and self.__reference != value.reference: + self.__reference = value.reference + elif not value.reference and self.__reference and len(self.__reference): + self._URN = URN("{}:{}".format(str(value), str(self.__reference))) def get(self, key=None): """ Get a child or multiple children @@ -297,7 +415,7 @@ def __parse(self): return [] elements = self.resource.xpath("."+self.citation.fill(passage=None, xpath=True), namespaces=NS) - ids = [self.id+[element.get("n")] for element in elements] + ids = [self.reference.list+[element.get("n")] for element in elements] ns = [".".join(_id) for _id in ids] # Checking for duplicates @@ -310,7 +428,7 @@ def __parse(self): self.__children[n] = Passage( resource=element, citation=self.citation.child, - id=_id, + reference=_id, urn=self.urn, parent=self ) @@ -319,9 +437,9 @@ def __parse(self): @property def first(self): """ First child of current Passage - - :rtype: None or Passage + :returns: None if current Passage has no children, first child passage if available + :rtype: None, Passage """ try: return self.get(0)[0] @@ -331,9 +449,9 @@ def first(self): @property def last(self): """ Last child of current Passage - - :rtype: None or Passage + :returns: None if current Passage has no children, last child passage if available + :rtype: None, Passage """ try: return self.get(-1)[0] @@ -344,8 +462,8 @@ def last(self): def children(self): """ Children of the passage - :rtype: OrderedDict :returns: Dictionary of chidren, where key are subreferences + :rtype: OrderedDict """ if len(self.__children) == 0 and self.__parsed is False: self.__parse() @@ -356,17 +474,17 @@ def children(self): def next(self): """ Next passage - :rtype: Passage :returns: Next passage at same level + :rtype: Passage """ if self.__next is False: - if self.parent is None: # When top of hierarchy is access, should return None + if self.parent is None: # When top of hierarchy is access, should return None self.__next = None return None keys = list(self.parent.children.copy().keys()) - current = keys.index(".".join(self.id)) + current = keys.index(str(self.reference)) if len(keys) - 1 > current: self.__next = self.parent.get(keys[current + 1])[0] else: @@ -382,17 +500,17 @@ def next(self): def prev(self): """ Previous passage - :rtype: Passage :returns: Previous passage at same level + :rtype: Passage """ if self.__prev is False: - if self.parent is None: # When top of hierarchy is access, should return None + if self.parent is None: # When top of hierarchy is access, should return None self.__prev = None return None keys = list(self.parent.children.copy().keys()) - current = keys.index(".".join(self.id)) + current = keys.index(str(self.reference)) if current > 0: self.__prev = self.parent.get(keys[current - 1])[0] else: @@ -402,4 +520,273 @@ def prev(self): else: self.__prev = n.last - return self.__prev \ No newline at end of file + return self.__prev + + +class ContextPassage(Passage): + """ Passage 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 + the tree of the text up to the passage, keeping attributes of original nodes + + **Example** : for a text with a citation scheme with following refsDecl : + `/TEI/text/body/div[@type='edition']/div[@n='$1']/div[@n='$2']/l[@n='$3']` and a passage 1.1.1-1.2.3, this + class will build an XML tree looking like the following + + .. code-block:: xml + + + + +
+
+ +
+ ... + ... +
+
+ ... +
+
+
+ +
+
+ + :param urn: URN of the source text or of the passage + :type urn: URN + :param resource: Element representing the passage + :type resource: etree._Element, Text + :param parent: Text containing the passage + :type parent: Text + :param citation: Citation scheme of the text + :type citation: Citation + :param reference: Passage reference + :type reference: Reference + + .. note:: + .prev, .next, .first and .last won't run on passage with a range made of two different level, such as + 1.1-1.2.3 or 1-a.b. Those will raise `InvalidSiblingRequest` + + """ + def __init__(self, urn=None, resource=None, parent=None, citation=None, reference=None): + super(ContextPassage, self).__init__(urn=urn, reference=reference) + + if isinstance(resource, etree._Element): + if urn: + self.resource = Text(resource=resource, urn=urn.upTo(URN.NO_PASSAGE), citation=parent.citation) + else: + self.resource = Text(resource=resource, citation=parent.citation) + else: + self.resource = resource + self.parent = parent + self.citation = parent.citation + 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) + + self.__prevnext = None # For caching purpose + + def xpath(self, *args, **kwargs): + """ Perform XPath on the passage XML + + :param args: Ordered arguments for etree._Element().xpath() + :param kwargs: Named arguments + :return: Result list + :rtype: list(etree._Element) + """ + if "smart_strings" not in kwargs: + kwargs["smart_strings"] = False + return self.resource.resource.xpath(*args, **kwargs) + + def tostring(self, *args, **kwargs): + """ Transform the Passage in XML string + + :param args: Ordered arguments for etree.tostring() (except the first one) + :param kwargs: Named arguments + :return: + """ + return etree.tostring(self.resource.resource, *args, **kwargs) + + def __str__(self): + """ Text based representation of the passage + + :returns: XML of the passage in string form + :rtype: basestring + """ + return self.tostring(encoding=str) + + @property + def first(self): + """ First child of current Passage + + :returns: None if current Passage has no children, first child passage if available + :rtype: None, Reference + """ + if self.depth >= len(self.citation): + return None + else: + return self.children[0] + + @property + def last(self): + """ Last child of current Pass + + :returns: None if current Passage has no children, last child passage if available + :rtype: None, Reference + """ + if self.depth >= len(self.citation): + return None + else: + return self.children[-1] + + @property + def children(self): + """ Children of the passage + + :rtype: None, Reference + :returns: Dictionary of chidren, where key are subreferences + """ + self.__raiseDepth() + if self.depth >= len(self.citation): + return [] + elif self.__children and len(self.__children): + return self.__children + else: + self.__children = self.resource.getValidReff(level=self.depth+1) + return self.__children + + @property + def next(self): + """ Next passage + + :returns: Next passage at same level + :rtype: None, Reference + """ + return self.__getSiblings(direction=1) + + @property + def prev(self): + """ Get the Previous passage reference + + :returns: Previous passage reference at the same level + :rtype: None, Reference + """ + return self.__getSiblings(direction=0) + + def text(self, exclude=None): + """ Text content of the passage + + :param exclude: Remove some nodes from text + :type exclude: List + :rtype: basestring + :returns: Text of the xml node + :Example: + >>> P = Passage(resource='Ibis hellob ab excusso missus in astra sago. ') + >>> P.text == "Ibis hello b ab excusso missus in astra sago. " + >>> P.text(exclude=["note"]) == "Ibis hello b ab excusso missus in astra sago. " + + + """ + + if exclude is None: + exclude = "" + else: + exclude = "[{0}]".format( + " and ".join( + "not(./ancestor-or-self::{0})".format(excluded) + for excluded in exclude + ) + ) + + return normalize( + " ".join( + [ + element + for element + in self.xpath( + ".//descendant-or-self::text()" + exclude, + namespaces=NS + ) + ] + ) + ) + + def __raiseDepth(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: + raise InvalidSiblingRequest() + + def __getSiblings(self, direction=1): + """ + + :return: Reference + """ + self.__raiseDepth() + + if self.__prevnext: + return self.__prevnext[direction] + + document_references = list(map(lambda x: str(x), self.parent.getValidReff(level=self.depth))) + range_length = len(self.resource.getValidReff(level=self.depth)) + + if self.reference.end: + start, end = str(self.reference.start), str(self.reference.end) + else: + start = end = str(self.reference.start) + + start = document_references.index(start) + end = document_references.index(end) + + _prev, _next = None, None + + if start == 0: + # If the passage is already at the beginning + _prev = None + elif start - range_length < 0: + if start == end: + _prev = Reference(document_references[0]) + else: + _prev = Reference( + "{}-{}".format(document_references[0], document_references[start-1]) + ) + else: + if start == end: + _prev = Reference(document_references[start-1]) + else: + _prev = Reference( + "{}-{}".format(document_references[start-range_length], document_references[start-1]) + ) + + if start + 1 == len(document_references) or end + 1 == len(document_references): + # If the passage is already at the end + _next = None + elif end + range_length > len(document_references): + if start == end: + _next = Reference(document_references[-1]) + else: + _next = Reference( + "{}-{}".format(document_references[end +1], document_references[-1]) + ) + else: + if start == end: + _next = Reference(document_references[end +1]) + else: + _next = Reference( + "{}-{}".format(document_references[end + 1], document_references[end + range_length]) + ) + + self.__prevnext = (_prev, _next) + return self.__prevnext[direction] + diff --git a/MyCapytain/resources/texts/tei.py b/MyCapytain/resources/texts/tei.py index f8de2227..07854223 100644 --- a/MyCapytain/resources/texts/tei.py +++ b/MyCapytain/resources/texts/tei.py @@ -127,7 +127,8 @@ def text(self, exclude=None): for element in self.resource.xpath( ".//descendant-or-self::text()" + exclude, - namespaces=MyCapytain.common.utils.NS + namespaces=MyCapytain.common.utils.NS, + smart_strings=False ) ] ) diff --git a/doc/MyCapytain.api.rst b/doc/MyCapytain.api.rst index 69cc7a48..a47b0d8a 100644 --- a/doc/MyCapytain.api.rst +++ b/doc/MyCapytain.api.rst @@ -1,69 +1,73 @@ MyCapytain API Documentation ============================ -MyCapytain.common -################# +Utilities, metadata and references +################################## -Module common contains tools such as a namespace dictionnary as well as cross-implementation objects, like URN, Citations... +Module common contains tools such as a namespace dictionary as well as cross-implementation objects, like URN, Citations... -MyCapytain.common.reference -*************************** +URN, References and Citations +***************************** -.. automodule:: MyCapytain.common.reference - :members: - :undoc-members: - :show-inheritance: +.. autoclass:: MyCapytain.common.reference.URN +.. autoclass:: MyCapytain.common.reference.Reference + +.. autoclass:: MyCapytain.common.reference.Citation + :members: fill, __iter__, __len__ -MyCapytain.common.metadata -************************** +Metadata containters +******************** .. automodule:: MyCapytain.common.metadata :members: :undoc-members: :show-inheritance: -MyCapytain.common.utils -*********************** +Utilities +********* .. automodule:: MyCapytain.common.utils :members: :undoc-members: :show-inheritance: -MyCapytain.endpoints -#################### +API Endpoints +############# Module endpoints contains prototypes and implementation of endpoints calls in MyCapytain -MyCapytain.endpoints.ahab -************************* +Ahab +**** .. automodule:: MyCapytain.endpoints.ahab :members: :undoc-members: :show-inheritance: -MyCapytain.endpoints.cts5 -************************* +CTS 5 API +********* .. automodule:: MyCapytain.endpoints.cts5 :members: :undoc-members: :show-inheritance: -MyCapytain.endpoints.proto -************************** +Prototypes +********** .. automodule:: MyCapytain.endpoints.proto :members: :undoc-members: :show-inheritance: -MyCapytain.ressources +Texts and inventories ##################### -MyCapytain.ressources.text -************************** +Text +**** + +TEI based texts ++++++++++++++++ .. autoclass:: MyCapytain.resources.texts.tei.Citation :members: @@ -75,11 +79,27 @@ MyCapytain.ressources.text :undoc-members: :show-inheritance: +Locally read text ++++++++++++++++++ + .. autoclass:: MyCapytain.resources.texts.local.Text :members: :undoc-members: :show-inheritance: +.. autoclass:: MyCapytain.resources.texts.local.Passage + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: MyCapytain.resources.texts.local.ContextPassage + :members: + :undoc-members: + :show-inheritance: + +API's Text results +++++++++++++++++++ + .. autoclass:: MyCapytain.resources.texts.api.Text :members: :undoc-members: @@ -90,25 +110,22 @@ MyCapytain.ressources.text :undoc-members: :show-inheritance: -MyCapytain.resources.xml -************************ +Inventories +*********** -.. automodule:: MyCapytain.resources.xml +.. automodule:: MyCapytain.resources.inventory :members: :undoc-members: :show-inheritance: -MyCapytain.resources.proto.text -******************************* +Prototypes +********** .. automodule:: MyCapytain.resources.proto.text :members: :undoc-members: :show-inheritance: -MyCapytain.resources.proto.inventory -************************************ - .. automodule:: MyCapytain.resources.proto.inventory :members: :undoc-members: diff --git a/doc/MyCapytain.endpoint.rst b/doc/MyCapytain.endpoint.rst index 1e5d29aa..e1e29abd 100644 --- a/doc/MyCapytain.endpoint.rst +++ b/doc/MyCapytain.endpoint.rst @@ -7,3 +7,33 @@ Introduction A first important point for MyCapytains endpoint is that the resources are not parsed into object but should only provide the request by default. +Getting Passage from an Endpoint +################################ + +.. code-block:: python + + from MyCapytain.endpoints import cts5 + from MyCapytain.resources.texts.api import Text + + # We set the variable up, as if we were in a function + # This URN won't work (urn:cts:greekLit:tlg0032.tlg005.perseus-grc1) because it has no TEI namespace + urn = 'urn:cts:latinLit:phi1294.phi002.perseus-lat2' + ref = "1.1-1.2" + + # We set the api up. Endpoint takes one required argument + # (the URI) and one inventory as optional argument + cts = cts5.CTS('http://services2.perseids.org/exist/restxq/cts', inventory="nemo") + + # We set up a text object to be able to retrieve passage of it + # Text in API modules takes endpoint as resource and URN as param + + text = Text(urn=urn, resource=cts) + + # We use the method getPassage which takes a reference argument + passage = text.getPassage(reference=ref) + + # Passage then has different methods and properties + # Most of them (except next, prev and prevnext properties) are inherited from MyCapytain.resources.texts.tei.Text + # For example + print(passage.text(exclude=["note", "head"])) # Will get the text without "note" and "head" TEI nodes + print(passage.xml) # The xml property can be used as an argument for XSLT for example \ No newline at end of file diff --git a/doc/MyCapytain.local.rst b/doc/MyCapytain.local.rst index 5f92caab..55e17beb 100644 --- a/doc/MyCapytain.local.rst +++ b/doc/MyCapytain.local.rst @@ -7,4 +7,28 @@ Introduction The module `MyCapytain.resources.local.text` requires the `guidelines of Capitains `_ to be implemented in your files. Basics and examples -################### \ No newline at end of file +################### + +Getting all passages from a text +******************************** + +.. code-block:: python + + # We import the correct classes from the local module + from MyCapytain.resources.texts.local import Text, Passage + + # We open a file + with open("/tests/testing_data/texts/sample.xml") as f: + # We initiate a Text object giving the IO instance to resource argument + text = Text(resource=f) + + # Text objects have a citation property + # len(Citation(...)) gives the depth of the citation scheme + # in the case of this sample, this would be 3 (Book, Poem, Line) + for ref in text.getValidReff(level=len(text.citation)): + # We retrieve a Passage object for each reference that we find + # We can pass the reference many way, including in the form of a list of strings + psg = text.getPassage(ref.split("."), hypercontext=False) + # We print the passage from which we retrieve nodes + print("\t".join([ref, psg.text(exclude=["note"])])) + diff --git a/requirements.txt b/requirements.txt index 0ef793dc..396e511d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ lxml==3.4.4 mock==1.3.0 future==0.15.2 six>=1.10.0 --e git+https://github.com/PonteIneptique/python-xmlunittest.git#egg=xmlunittest-0.3.2 \ No newline at end of file +xmlunittest>=0.3.2 diff --git a/setup.py b/setup.py index f9119e88..7725077d 100644 --- a/setup.py +++ b/setup.py @@ -19,14 +19,11 @@ ], tests_require=[ "mock==1.3.0", - "xmlunittest==0.3.2" + "xmlunittest>=0.3.2" ], extras_require = { "DOC" : ["Sphinx==1.3.1"] }, test_suite="tests", - dependency_links=[ - "https://github.com/Ponteineptique/python-xmlunittest/tarball/master#egg=xmlunittest-0.3.2" - ], zip_safe=False ) diff --git a/tests/common/test_metadata.py b/tests/common/test_metadata.py index 414b3eb9..566cd054 100644 --- a/tests/common/test_metadata.py +++ b/tests/common/test_metadata.py @@ -6,6 +6,7 @@ from collections import defaultdict from MyCapytain.common.metadata import Metadata, Metadatum + class TestMetadatum(unittest.TestCase): def test_init(self): a = Metadatum("title") diff --git a/tests/common/test_reference.py b/tests/common/test_reference.py index 77bef5c4..403f2470 100644 --- a/tests/common/test_reference.py +++ b/tests/common/test_reference.py @@ -1,6 +1,7 @@ # -*- 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 @@ -13,49 +14,44 @@ def test_str_function(self): a = Reference("1-1") self.assertEqual(str(a), "1-1") - def test_str_getitem(self): + def test_len_ref(self): a = Reference("1.1@Achilles[0]-1.10@Atreus[3]") - self.assertEqual(a["any"], "1.1@Achilles[0]-1.10@Atreus[3]") - self.assertEqual(a["start"], "1.1@Achilles[0]") - self.assertEqual(a["start_list"], ["1", "1"]) - self.assertEqual(a["start_sub"][0], "Achilles") - self.assertEqual(a["end"], "1.10@Atreus[3]") - self.assertEqual(a["end_list"], ["1", "10"]) - self.assertEqual(a["end_sub"][1], "3") - self.assertEqual(a["end_sub"], ("Atreus", "3")) - - def test_int_getItem(self): + self.assertEqual(len(a), 2) + a = Reference("1.1.1") + self.assertEqual(len(a), 3) + + def test_properties(self): a = Reference("1.1@Achilles-1.10@Atreus[3]") - self.assertEqual(a[1], "1.1@Achilles") - self.assertEqual(a[2], ["1", "1"]) - self.assertEqual(a[3][0], "Achilles") - self.assertEqual(a[4], "1.10@Atreus[3]") - self.assertEqual(a[5], ["1", "10"]) - self.assertEqual(a[6][1], "3") - self.assertEqual(a[6], ("Atreus", "3")) + self.assertEqual(str(a.start), "1.1@Achilles") + self.assertEqual(a.start.list, ["1", "1"]) + self.assertEqual(a.start.subreference[0], "Achilles") + self.assertEqual(str(a.end), "1.10@Atreus[3]") + self.assertEqual(a.end.list, ["1", "10"]) + self.assertEqual(a.end.subreference[1], 3) + self.assertEqual(a.end.subreference, ("Atreus", 3)) def test_Unicode_Support(self): a = Reference("1.1@καὶ[0]-1.10@Ἀλκιβιάδου[3]") - self.assertEqual(a[1], "1.1@καὶ[0]") - self.assertEqual(a[2], ["1", "1"]) - self.assertEqual(a[3][0], "καὶ") - self.assertEqual(a[4], "1.10@Ἀλκιβιάδου[3]") - self.assertEqual(a[5], ["1", "10"]) - self.assertEqual(a[6][1], "3") - self.assertEqual(a[6], ("Ἀλκιβιάδου", "3")) + self.assertEqual(str(a.start), "1.1@καὶ[0]") + self.assertEqual(a.start.list, ["1", "1"]) + self.assertEqual(a.start.subreference[0], "καὶ") + self.assertEqual(str(a.end), "1.10@Ἀλκιβιάδου[3]") + self.assertEqual(a.end.list, ["1", "10"]) + self.assertEqual(a.end.subreference[1], 3) + self.assertEqual(a.end.subreference, ("Ἀλκιβιάδου", 3)) def test_NoWord_Support(self): a = Reference("1.1@[0]-1.10@Ἀλκιβιάδου[3]") - self.assertEqual(a[1], "1.1@[0]") - self.assertEqual(a[3][0], "") - self.assertEqual(a[3][1], "0") + self.assertEqual(str(a.start), "1.1@[0]") + self.assertEqual(a.start.subreference[0], "") + self.assertEqual(a.start.subreference[1], 0) def test_No_End_Support(self): a = Reference("1.1@[0]") - self.assertEqual(a[4], None) - self.assertEqual(a[1], "1.1@[0]") - self.assertEqual(a[3][0], "") - self.assertEqual(a[3][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) def test_equality(self): a = Reference("1.1@[0]") @@ -90,121 +86,130 @@ def test_str_function(self): a = URN("urn:cts:greekLit:tlg0012.tlg001.mth-01:1.1@Achilles-1.10@the[2]") self.assertEqual(str(a), "urn:cts:greekLit:tlg0012.tlg001.mth-01:1.1@Achilles-1.10@the[2]") - def test_int_access(self): + def test_properties(self): a = URN("urn:cts:greekLit:tlg0012.tlg001.mth-01:1.1@Achilles-1.10@the[2]") - self.assertEqual(a[0], "urn:cts:greekLit:tlg0012.tlg001.mth-01:1.1@Achilles-1.10@the[2]") - self.assertEqual(a[1], "cts") - self.assertEqual(a[2], "greekLit") - self.assertEqual(a[3], "tlg0012") - self.assertEqual(a[4], "tlg001") - self.assertEqual(a[5], "mth-01") - self.assertEqual(a[6], "1.1@Achilles-1.10@the[2]") - self.assertEqual(a[7], Reference("1.1@Achilles-1.10@the[2]")) - - def test_str_access(self): + self.assertEqual(a.urn_namespace, "cts") + self.assertEqual(a.namespace, "greekLit") + 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]")) + + def test_upTo(self): a = URN("urn:cts:greekLit:tlg0012.tlg001.mth-01:1.1@Achilles-1.10@the[2]") - self.assertEqual(a["full"], "urn:cts:greekLit:tlg0012.tlg001.mth-01:1.1@Achilles-1.10@the[2]") - self.assertEqual(a["urn_namespace"], "urn:cts") - self.assertEqual(a["cts_namespace"], "urn:cts:greekLit") - self.assertEqual(a["textgroup"], "urn:cts:greekLit:tlg0012") - self.assertEqual(a["work"], "urn:cts:greekLit:tlg0012.tlg001") - self.assertEqual(a["text"], "urn:cts:greekLit:tlg0012.tlg001.mth-01") - self.assertEqual(a["passage"], "urn:cts:greekLit:tlg0012.tlg001.mth-01:1.1@Achilles-1.10@the[2]") - self.assertEqual(a["reference"], Reference("1.1@Achilles-1.10@the[2]")) - self.assertEqual(a["start"], "urn:cts:greekLit:tlg0012.tlg001.mth-01:1.1@Achilles") - self.assertEqual(a["end"], "urn:cts:greekLit:tlg0012.tlg001.mth-01:1.10@the[2]") + self.assertEqual(a.upTo(URN.COMPLETE), "urn:cts:greekLit:tlg0012.tlg001.mth-01:1.1@Achilles-1.10@the[2]") + self.assertEqual(a.upTo(URN.NAMESPACE), "urn:cts:greekLit") + self.assertEqual(a.upTo(URN.TEXTGROUP), "urn:cts:greekLit:tlg0012") + self.assertEqual(a.upTo(URN.WORK), "urn:cts:greekLit:tlg0012.tlg001") + self.assertEqual(a.upTo(URN.VERSION), "urn:cts:greekLit:tlg0012.tlg001.mth-01") + self.assertEqual(a.upTo(URN.PASSAGE), "urn:cts:greekLit:tlg0012.tlg001.mth-01:1.1@Achilles-1.10@the[2]") + self.assertEqual(a.upTo(URN.PASSAGE_START), "urn:cts:greekLit:tlg0012.tlg001.mth-01:1.1@Achilles") + self.assertEqual(a.upTo(URN.PASSAGE_END), "urn:cts:greekLit:tlg0012.tlg001.mth-01:1.10@the[2]") def test_equality(self): a = URN("urn:cts:greekLit:tlg0012.tlg001.mth-01:1.1@Achilles-1.10@the[2]") b = URN("urn:cts:greekLit:tlg0012.tlg001.mth-01:1.1@Achilles-1.10@the[2]") c = URN("urn:cts:greekLit:tlg0012.tlg001.mth-01:1.1@Achilles-1.10@the[3]") d = "urn:cts:greekLit:tlg0012.tlg001.mth-01:1.1@Achilles-1.10@the[2]" - self.assertEqual(a,b) - self.assertNotEqual(a,c) - self.assertNotEqual(a,d) + self.assertEqual(a, b) + self.assertNotEqual(a, c) + self.assertNotEqual(a, d) def test_full_emptiness(self): a = URN("urn:cts:greekLit") - self.assertEqual(a["full"], "urn:cts:greekLit") - self.assertEqual(a["urn_namespace"], "urn:cts") - self.assertEqual(a["cts_namespace"], "urn:cts:greekLit") - self.assertIsNone(a["textgroup"]) - self.assertIsNone(a["work"]) - self.assertIsNone(a["text"]) - self.assertIsNone(a["passage"]) - self.assertIsNone(a["reference"]) - self.assertIsNone(a["start"]) - self.assertIsNone(a["end"]) + self.assertEqual(str(a), "urn:cts:greekLit") + self.assertEqual(a.upTo(URN.COMPLETE), "urn:cts:greekLit") + self.assertEqual(a.upTo(URN.NAMESPACE), "urn:cts:greekLit") + self.assertIsNone(a.textgroup) + self.assertIsNone(a.work) + self.assertIsNone(a.version) + self.assertIsNone(a.reference) def test_from_textgroup_emptiness(self): a = URN("urn:cts:greekLit:textgroup") - self.assertEqual(a["full"], "urn:cts:greekLit:textgroup") - self.assertEqual(a["urn_namespace"], "urn:cts") - self.assertEqual(a["cts_namespace"], "urn:cts:greekLit") - self.assertEqual(a["textgroup"], "urn:cts:greekLit:textgroup") - self.assertIsNone(a["work"]) - self.assertIsNone(a["text"]) - self.assertIsNone(a["passage"]) - self.assertIsNone(a["reference"]) - self.assertIsNone(a["start"]) - self.assertIsNone(a["end"]) + self.assertEqual(a.upTo(URN.COMPLETE), "urn:cts:greekLit:textgroup") + self.assertEqual(str(a), "urn:cts:greekLit:textgroup") + self.assertEqual(a.upTo(URN.NAMESPACE), "urn:cts:greekLit") + self.assertEqual(a.upTo(URN.TEXTGROUP), "urn:cts:greekLit:textgroup") + self.assertIsNone(a.work) + self.assertIsNone(a.version) + self.assertIsNone(a.reference) + + def test_set(self): + a = URN("urn:cts:greekLit:textgroup") + a.textgroup = "tg" + self.assertEqual(a.textgroup, "tg") + self.assertEqual(str(a), "urn:cts:greekLit:tg") + a.namespace = "ns" + self.assertEqual(a.namespace, "ns") + self.assertEqual(str(a), "urn:cts:ns:tg") + a.work = "wk" + 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(str(a), "urn:cts:ns:tg.wk:1-2") + a.version = "vs" + self.assertEqual(a.version, "vs") + self.assertEqual(str(a), "urn:cts:ns:tg.wk.vs:1-2") + def test_from_work_emptiness(self): a = URN("urn:cts:greekLit:textgroup.work") - self.assertEqual(a["full"], "urn:cts:greekLit:textgroup.work") - self.assertEqual(a["urn_namespace"], "urn:cts") - self.assertEqual(a["cts_namespace"], "urn:cts:greekLit") - self.assertEqual(a["textgroup"], "urn:cts:greekLit:textgroup") - self.assertEqual(a["work"], "urn:cts:greekLit:textgroup.work") - self.assertIsNone(a["text"]) - self.assertIsNone(a["passage"]) - self.assertIsNone(a["reference"]) - self.assertIsNone(a["start"]) - self.assertIsNone(a["end"]) + self.assertEqual(str(a), "urn:cts:greekLit:textgroup.work") + self.assertEqual(a.upTo(URN.COMPLETE), "urn:cts:greekLit:textgroup.work") + self.assertEqual(a.upTo(URN.NAMESPACE), "urn:cts:greekLit") + self.assertEqual(a.upTo(URN.TEXTGROUP), "urn:cts:greekLit:textgroup") + self.assertEqual(a.upTo(URN.WORK), "urn:cts:greekLit:textgroup.work") + self.assertIsNone(a.version) + self.assertIsNone(a.reference) def test_from_text_emptiness(self): a = URN("urn:cts:greekLit:textgroup.work.text") - self.assertEqual(a["full"], "urn:cts:greekLit:textgroup.work.text") - self.assertEqual(a["urn_namespace"], "urn:cts") - self.assertEqual(a["cts_namespace"], "urn:cts:greekLit") - self.assertEqual(a["textgroup"], "urn:cts:greekLit:textgroup") - self.assertEqual(a["work"], "urn:cts:greekLit:textgroup.work") - self.assertEqual(a["text"], "urn:cts:greekLit:textgroup.work.text") - self.assertIsNone(a["passage"]) - self.assertIsNone(a["reference"]) - self.assertIsNone(a["start"]) - self.assertIsNone(a["end"]) + self.assertEqual(str(a), "urn:cts:greekLit:textgroup.work.text") + self.assertEqual(a.upTo(URN.COMPLETE), "urn:cts:greekLit:textgroup.work.text") + self.assertEqual(a.upTo(URN.NAMESPACE), "urn:cts:greekLit") + self.assertEqual(a.upTo(URN.TEXTGROUP), "urn:cts:greekLit:textgroup") + self.assertEqual(a.upTo(URN.WORK), "urn:cts:greekLit:textgroup.work") + self.assertEqual(a.upTo(URN.VERSION), "urn:cts:greekLit:textgroup.work.text") + self.assertIsNone(a.reference) def test_no_end_text_emptiness(self): a = URN("urn:cts:greekLit:textgroup.work.text:1") - self.assertEqual(a["full"], "urn:cts:greekLit:textgroup.work.text:1") - self.assertEqual(a["urn_namespace"], "urn:cts") - self.assertEqual(a["cts_namespace"], "urn:cts:greekLit") - self.assertEqual(a["textgroup"], "urn:cts:greekLit:textgroup") - self.assertEqual(a["work"], "urn:cts:greekLit:textgroup.work") - self.assertEqual(a["text"], "urn:cts:greekLit:textgroup.work.text") - self.assertEqual(a["passage"], "urn:cts:greekLit:textgroup.work.text:1") - self.assertEqual(a["reference"], Reference("1")) - self.assertEqual(a["start"], "urn:cts:greekLit:textgroup.work.text:1") - self.assertIsNone(a["end"]) + self.assertEqual(str(a), "urn:cts:greekLit:textgroup.work.text:1") + self.assertEqual(a.upTo(URN.COMPLETE), "urn:cts:greekLit:textgroup.work.text:1") + self.assertEqual(a.upTo(URN.NAMESPACE), "urn:cts:greekLit") + self.assertEqual(a.upTo(URN.TEXTGROUP), "urn:cts:greekLit:textgroup") + self.assertEqual(a.upTo(URN.WORK), "urn:cts:greekLit:textgroup.work") + 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.assertIsNone(a.reference.end) def test_missing_text_in_passage_emptiness(self): a = URN("urn:cts:greekLit:textgroup.work:1-2") - self.assertEqual(a["full"], "urn:cts:greekLit:textgroup.work:1-2") - self.assertEqual(a["urn_namespace"], "urn:cts") - self.assertEqual(a["cts_namespace"], "urn:cts:greekLit") - self.assertEqual(a["textgroup"], "urn:cts:greekLit:textgroup") - self.assertEqual(a["work"], "urn:cts:greekLit:textgroup.work") - self.assertIsNone(a["text"]) - self.assertEqual(a["passage"], "urn:cts:greekLit:textgroup.work:1-2") - self.assertEqual(a["reference"], Reference("1-2")) - self.assertEqual(a["start"], "urn:cts:greekLit:textgroup.work:1") - self.assertEqual(a["end"], "urn:cts:greekLit:textgroup.work:2") + self.assertEqual(str(a), "urn:cts:greekLit:textgroup.work:1-2") + self.assertEqual(a.upTo(URN.COMPLETE), "urn:cts:greekLit:textgroup.work:1-2") + self.assertEqual(a.upTo(URN.NAMESPACE), "urn:cts:greekLit") + self.assertEqual(a.upTo(URN.TEXTGROUP), "urn:cts:greekLit:textgroup") + self.assertEqual(a.upTo(URN.WORK), "urn:cts:greekLit:textgroup.work") + self.assertEqual(a.upTo(URN.NO_PASSAGE), "urn:cts:greekLit:textgroup.work") + 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.assertIsNone(a.version) def test_warning_on_empty(self): with self.assertRaises(ValueError): a = URN("urn:cts") + with self.assertRaises(KeyError): + a = URN("urn:cts:ns:tg.work:1") + a.upTo(URN.VERSION) def test_len(self): a = URN("urn:cts:greekLit") @@ -323,7 +328,9 @@ def test_fill(self): ) 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(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']") self.assertEqual(c.fill(None, xpath=True), "//l[@n]") - self.assertEqual(c.fill([None, None]), "/TEI/text/body/div/div[@n]//l[@n]") \ No newline at end of file + 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]") diff --git a/tests/common/test_utils.py b/tests/common/test_utils.py new file mode 100644 index 00000000..cbeef050 --- /dev/null +++ b/tests/common/test_utils.py @@ -0,0 +1,61 @@ +# -*- 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 + + +class TestUtils(unittest.TestCase): + + def test_clean_xpath(self): + """ Cleaning XPATH and normalizing them """ + l = ['tei:text', 'tei:body', 'tei:div', "tei:div[@n='1']", "tei:div[@n='pr']", "tei:l[@n='2']"] + self.assertEqual(normalizeXpath(l), l) + + l = ['tei:text', 'tei:body', 'tei:div', "tei:div[@n='1']", "", "tei:div[@n='pr']", "tei:l[@n='2']"] + self.assertEqual( + normalizeXpath(l), + ['tei:text', 'tei:body', 'tei:div', "tei:div[@n='1']", "/tei:div[@n='pr']", "tei:l[@n='2']"], + "Empty list element should be replaced with / in the next element" + ) + + def test_copy_node_without_children(self): + node = xmlparser("Mc") + + no_text = copy(node) + no_text.text = None # Remove text + [no_text.remove(a) for a in no_text] # Remove nodes + copied_node = copyNode(node) + self.assertEqual( + etree.tostring(copied_node), + etree.tostring(no_text), + "Text without children should have no text nor xml nodes" + ) + self.assertNotIn( + "", + etree.tostring(copied_node, encoding=str), + "Text without children should have no text nor xml nodes" + ) + self.assertNotIn( + "M", + etree.tostring(copied_node, encoding=str), + "Text without children should have no text nor xml nodes" + ) + + def test_copy_node_with_children(self): + node = xmlparser("Mc") + comparison = copy(node) + + copied_node = copyNode(node, children=True) + self.assertEqual( + etree.tostring(copied_node), + etree.tostring(comparison), + "Text without children should have no text nor xml nodes" + ) + self.assertIn( + "", + etree.tostring(copied_node, encoding=str), + "Text without children should have no text nor xml nodes" + ) \ No newline at end of file diff --git a/tests/resources/proto/test_inventory.py b/tests/resources/proto/test_inventory.py index 22ea7710..d0ea5aab 100644 --- a/tests/resources/proto/test_inventory.py +++ b/tests/resources/proto/test_inventory.py @@ -58,7 +58,7 @@ def test_urn_access(self): self.assertEqual(a["urn:cts:greekLit:tg"], a) - with six.assertRaisesRegex(self, ValueError, "Unrecognized urn at level"): + with six.assertRaisesRegex(self, ValueError, "Unrecognized urn at URN Textgroup"): b["urn:cts:greekLit:tg2"] def test_edit_trans(self): diff --git a/tests/resources/test_inventory.py b/tests/resources/test_inventory.py index d83be124..57f047b3 100644 --- a/tests/resources/test_inventory.py +++ b/tests/resources/test_inventory.py @@ -2,22 +2,109 @@ from __future__ import unicode_literals import unittest -from io import open -import lxml.etree as etree -import lxml.objectify import xmlunittest +import lxml.etree as etree +from io import open, StringIO from copy import deepcopy from six import text_type as str +from operator import attrgetter from MyCapytain.resources.inventory import * import MyCapytain.resources.proto.text +class XML_Compare(object): + """ + Original https://gist.github.com/dalelane/a0514b2e283a882d9ef3 + """ + @staticmethod + def sortbyid(elem): + id = elem.get('urn') or elem.get("xml:lang") + if id: + try: + return str(id) + except ValueError: + return "" + return "" + + @staticmethod + def sortbytext(elem): + text = elem.text + if text: + return text + else: + return '' + + @staticmethod + def sortAttrs(item, sorteditem): + attrkeys = sorted(item.keys()) + for key in attrkeys: + sorteditem.set(key, item.get(key)) + + @staticmethod + def sortElements(items, newroot): + # The intended sort order is to sort by XML element name + # If more than one element has the same name, we want to + # sort by their text contents. + # If more than one element has the same name and they do + # not contain any text contents, we want to sort by the + # value of their ID attribute. + # If more than one element has the same name, but has + # no text contents or ID attribute, their order is left + # unmodified. + # + # We do this by performing three sorts in the reverse order + items = sorted(items, key=XML_Compare.sortbyid) + items = sorted(items, key=XML_Compare.sortbytext) + items = sorted(items, key=attrgetter('tag')) + + # Once sorted, we sort each of the items + for item in items: + # Create a new item to represent the sorted version + # of the next item, and copy the tag name and contents + newitem = etree.Element(item.tag) + if item.text and item.text.isspace() == False: + newitem.text = item.text + + # Copy the attributes (sorted by key) to the new item + XML_Compare.sortAttrs(item, newitem) + + # Copy the children of item (sorted) to the new item + XML_Compare.sortElements(list(item), newitem) + + # Append this sorted item to the sorted root + newroot.append(newitem) + + @staticmethod + def sortString(str_xml): + # parse the XML file and get a pointer to the top + xmlroot = etree.parse(StringIO(str_xml)).getroot() + + # create a new XML element that will be the top of + # the sorted copy of the XML file + newxmlroot = etree.Element(xmlroot.tag) + + # create the sorted copy of the XML file + XML_Compare.sortAttrs(xmlroot, newxmlroot) + XML_Compare.sortElements(list(xmlroot), newxmlroot) + + # write the sorted XML file to the temp file + newtree = etree.ElementTree(newxmlroot) + + return etree.tostring(newtree, encoding=str, pretty_print=True) + + def compareSTR(one, other): - return (one.replace("\n", ""), other.replace("\n", "")) + return XML_Compare.sortString(one.replace("\n", "")), XML_Compare.sortString(other.replace("\n", "")) + def compareXML(one, other): - return (etree.tostring(one, encoding=str).replace("\n", ""), other.replace("\n", "")) + parser = etree.XMLParser(remove_blank_text=True) + return ( + etree.tostring(etree.fromstring(etree.tostring(one, encoding=str).replace("\n", ""), parser), method="c14n"), + etree.tostring(etree.fromstring(other.replace("\n", ""), parser), method="c14n") + ) + class TestXMLImplementation(unittest.TestCase, xmlunittest.XmlTestMixin): @@ -48,11 +135,16 @@ def setUp(self): Martialis""" + self.wk + """""".replace("\n", "") self.t = """""" + self.tg + """""".replace("\n", "").strip("\n") - + self.maxDiff = None def tearDown(self): self.getCapabilities.close() + def test_xml_TextInventoryLength(self): + """ Tests TextInventory parses without errors """ + TI = TextInventory(resource=self.getCapabilities, id="TestInv") + self.assertEqual(len(TI), 15) + def test_xml_TextInventoryParsing(self): """ Tests TextInventory parses without errors """ TI = TextInventory(resource=self.getCapabilities, id="TestInv") @@ -161,6 +253,21 @@ def test_parse_error(self): resource=5 ) + def test_Inventory_pickle(self): + """ Tests TextInventory parses without errors """ + TI = TextInventory(resource=self.getCapabilities, id="annotsrc") + from pickle import dumps, loads + + dp = dumps(TI) + ti = str(loads(dp)) + + self.assertEqual( + *compareSTR( + ti, + str(TI) + ) + ) + def test_Inventory_metadata(self): """ Tests TextInventory parses without errors """ TI = TextInventory(resource=self.getCapabilities, id="annotsrc") @@ -183,9 +290,9 @@ def test_export(self): - - - + + + @@ -299,6 +406,7 @@ def test_partial_str(self): ) ) + class TestCitation(unittest.TestCase): def test_empty(self): a = Citation(name="none") diff --git a/tests/resources/texts/test_api.py b/tests/resources/texts/test_api.py index 52a91887..07385c04 100644 --- a/tests/resources/texts/test_api.py +++ b/tests/resources/texts/test_api.py @@ -5,14 +5,14 @@ from six import text_type as str from io import open -from MyCapytain.common.utils import xmlparser +from MyCapytain.common.utils import xmlparser, NS from MyCapytain.resources.texts.api import * from MyCapytain.resources.texts.tei import Citation from MyCapytain.endpoints.cts5 import CTS from MyCapytain.common.reference import Reference, URN +from lxml import etree import mock - with open("tests/testing_data/cts/getValidReff.xml") as f: GET_VALID_REFF = xmlparser(f) with open("tests/testing_data/cts/getpassage.xml") as f: @@ -21,6 +21,10 @@ GET_PASSAGE_PLUS = xmlparser(f) with open("tests/testing_data/cts/getprevnexturn.xml") as f: NEXT_PREV = xmlparser(f) +with open("tests/testing_data/cts/getFirstUrn.xml") as f: + Get_FIRST = xmlparser(f) +with open("tests/testing_data/cts/getFirstUrnEmpty.xml") as f: + Get_FIRST_EMPTY = xmlparser(f) with open("tests/testing_data/cts/getlabel.xml") as f: GET_LABEL = xmlparser(f) @@ -187,6 +191,48 @@ def test_getpassageplus(self, requests): } ) + @mock.patch("MyCapytain.endpoints.cts5.requests.get", create=True) + def test_get_prev_next_urn(self, requests): + text = Text("urn:cts:latinLit:phi1294.phi002.perseus-lat2", resource=self.endpoint) + requests.return_value.text = NEXT_PREV + _prev, _next = text.getPrevNextUrn("1.1") + self.assertEqual(str(_prev.reference), "1.pr", "Endpoint should be called and URN should be parsed") + self.assertEqual(str(_next.reference), "1.2", "Endpoint should be called and URN should be parsed") + + @mock.patch("MyCapytain.endpoints.cts5.requests.get", create=True) + def test_first_urn(self, requests): + text = Text("urn:cts:latinLit:phi1294.phi002.perseus-lat2", resource=self.endpoint) + requests.return_value.text = Get_FIRST + first = text.getFirstUrn() + self.assertEqual( + str(first), "urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.pr", + "Endpoint should be called and URN should be parsed" + ) + requests.assert_called_with( + "http://services.perseids.org/api/cts", + params={ + "request": "GetFirstUrn", + "urn": "urn:cts:latinLit:phi1294.phi002.perseus-lat2" + } + ) + + @mock.patch("MyCapytain.endpoints.cts5.requests.get", create=True) + def test_first_urn_when_empty(self, requests): + text = Text("urn:cts:latinLit:phi1294.phi002.perseus-lat2", resource=self.endpoint) + requests.return_value.text = Get_FIRST_EMPTY + first = text.getFirstUrn() + self.assertEqual( + first, None, + "Endpoint should be called and none should be returned if there is none" + ) + requests.assert_called_with( + "http://services.perseids.org/api/cts", + params={ + "request": "GetFirstUrn", + "urn": "urn:cts:latinLit:phi1294.phi002.perseus-lat2" + } + ) + @mock.patch("MyCapytain.endpoints.cts5.requests.get", create=True) def test_init_without_citation(self, requests): text = Text("urn:cts:latinLit:phi1294.phi002.perseus-lat2", resource=self.endpoint) @@ -232,6 +278,7 @@ def test_reffs(self, requests): params={'urn': 'urn:cts:latinLit:phi1294.phi002.perseus-lat3', 'request': 'GetValidReff', 'level': '3'} ) + class TestCTSPassage(unittest.TestCase): """ Test CTS API implementation of Text """ @@ -248,14 +295,19 @@ def setUp(self): name="book", child=b ) - self.endpoint = CTS("http://services.perseids.org/api/cts") + self.url = "http://services.perseids.org/api/cts" + self.endpoint = CTS(self.url) self.endpoint.getPassage = mock.MagicMock(return_value=GET_PASSAGE) self.endpoint.getPrevNextUrn = mock.MagicMock(return_value=NEXT_PREV) - self.text = Text("urn:cts:latinLit:phi1294.phi002.perseus-lat2", self.endpoint, citation=self.citation) + self.endpoint.getFirstUrn = mock.MagicMock(return_value=Get_FIRST) + self.text = Text( + "urn:cts:latinLit:phi1294.phi002.perseus-lat2", self.endpoint, citation=self.citation + ) def test_next_getprevnext(self): """ Test next property, given that next information already exists or not) """ + passage = Passage( urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1", resource=GET_PASSAGE, @@ -263,9 +315,16 @@ def test_next_getprevnext(self): ) # When next does not exist from the original resource - __next = passage.next - self.endpoint.getPrevNextUrn.assert_called_with(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1") - self.endpoint.getPassage.assert_called_with(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.2") + __next = passage.getNext() + + self.endpoint.getPrevNextUrn.assert_called_with( + urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1" + ) + self.endpoint.getPassage.assert_called_with( + urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.2" + ) + self.assertEqual(__next.xml, GET_PASSAGE.xpath("//tei:TEI", namespaces=NS)[0]) + self.assertIsInstance(__next, Passage) def test_next_resource(self): """ Test next property, given that next information already exists @@ -279,10 +338,11 @@ def test_next_resource(self): ) # When next does not exist from the original resource - passage.next + __next = passage.getNext() # print(self.endpoint.getPrevNextUrn.mock_calls) self.endpoint.getPassage.assert_called_with(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.2") - + self.assertEqual(__next.xml, GET_PASSAGE.xpath("//tei:TEI", namespaces=NS)[0]) + self.assertIsInstance(__next, Passage) def test_prev_getprevnext(self): """ Test next property, given that next information already exists or not) @@ -294,9 +354,26 @@ def test_prev_getprevnext(self): ) # When next does not exist from the original resource - __next = passage.prev + __prev = passage.getPrev() self.endpoint.getPrevNextUrn.assert_called_with(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1") self.endpoint.getPassage.assert_called_with(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.pr") + self.assertEqual(__prev.xml, GET_PASSAGE.xpath("//tei:TEI", namespaces=NS)[0]) + self.assertIsInstance(__prev, Passage) + + def test_prev_prev_next_property(self): + """ Test reference property + As of 0.1.0, .next and prev are URNs + """ + passage = Passage( + urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1", + resource=GET_PASSAGE, + parent=self.text + ) + + # When next does not exist from the original resource + self.assertEqual(str(passage.prev.reference), "1.pr") + self.assertEqual(str(passage.next.reference), "1.2") + self.endpoint.getPrevNextUrn.assert_called_with(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1") def test_prev_resource(self): """ Test next property, given that next information already exists @@ -310,6 +387,67 @@ def test_prev_resource(self): ) # When next does not exist from the original resource - __next = passage.prev - # print(self.endpoint.getPrevNextUrn.mock_calls) - self.endpoint.getPassage.assert_called_with(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.pr") \ No newline at end of file + __prev = passage.getPrev() + self.endpoint.getPassage.assert_called_with(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.pr") + self.assertEqual(__prev.xml, GET_PASSAGE.xpath("//tei:TEI", namespaces=NS)[0]) + self.assertIsInstance(__prev, Passage) + + def test_unicode_text(self): + """ Test text properties for pypy + """ + # Now with a resource containing prevnext + + passage = Passage( + urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.1", + resource=GET_PASSAGE, + parent=self.text + ) + + self.assertIn("لا یا ایها الساقی ادر کاسا و ناولها ###", passage.text()) + + def test_first_urn(self): + text = Text("urn:cts:latinLit:phi1294.phi002.perseus-lat2", resource=self.endpoint) + passage = Passage( + urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1", + resource=GET_PASSAGE, + parent=text + ) + self.assertEqual( + str(passage.first), "urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.pr", + "Endpoint should be called and URN should be parsed" + ) + self.endpoint.getFirstUrn.assert_called_with( + "urn:cts:latinLit:phi1294.phi002.perseus-lat2:1" + ) + + def test_get_first(self): + passage = Passage( + urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1", + resource=GET_PASSAGE, + parent=self.text + ) + + # When next does not exist from the original resource + first = passage.getFirst() + self.endpoint.getFirstUrn.assert_called_with("urn:cts:latinLit:phi1294.phi002.perseus-lat2:1") + self.endpoint.getPassage.assert_called_with(urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.pr") + self.assertEqual(first.xml, GET_PASSAGE.xpath("//tei:TEI", namespaces=NS)[0]) + self.assertIsInstance(first, Passage) + + def test_first_urn_when_empty(self): + + endpoint = CTS(self.url) + endpoint.getFirstUrn = mock.MagicMock(return_value=Get_FIRST_EMPTY) + text = Text("urn:cts:latinLit:phi1294.phi002.perseus-lat2", resource=endpoint) + passage = Passage( + urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2:1", + resource=GET_PASSAGE, + parent=text + ) + self.assertEqual( + passage.first, None, + "Endpoint should be called and none should be returned if there is none" + ) + endpoint.getFirstUrn.assert_called_with( + "urn:cts:latinLit:phi1294.phi002.perseus-lat2:1" + ) diff --git a/tests/resources/texts/test_local.py b/tests/resources/texts/test_local.py index fc9990f3..e1fe145d 100644 --- a/tests/resources/texts/test_local.py +++ b/tests/resources/texts/test_local.py @@ -1,34 +1,50 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals - import unittest from six import text_type as str from io import open import xmlunittest import warnings - +from lxml import etree +from copy import copy import MyCapytain.resources.texts.local import MyCapytain.resources.texts.tei import MyCapytain.common.reference import MyCapytain.common.utils import MyCapytain.errors - class TestLocalXMLTextImplementation(unittest.TestCase, xmlunittest.XmlTestMixin): - """ Test XML Implementation of resources found in local file """ def setUp(self): self.text = open("tests/testing_data/texts/sample.xml", "rb") - self.TEI = MyCapytain.resources.texts.local.Text(resource=self.text) + self.TEI = MyCapytain.resources.texts.local.Text( + resource=self.text, + urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2" + ) + self.treeroot = etree._ElementTree() with open("tests/testing_data/texts/text_or_xpath.xml") as f: - self.text_complex = MyCapytain.resources.texts.local.Text(resource=f) + self.text_complex = MyCapytain.resources.texts.local.Text( + resource=f, + urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2" + ) + + with open("tests/testing_data/texts/seneca.xml") as f: + self.seneca = MyCapytain.resources.texts.local.Text( + resource=f + ) def tearDown(self): self.text.close() + def testURN(self): + """ Check that urn is set""" + TEI = MyCapytain.resources.texts.local.Text(resource=self.TEI.xml, + urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2") + self.assertEqual(str(TEI.urn), "urn:cts:latinLit:phi1294.phi002.perseus-lat2") + def testFindCitation(self): self.assertEqual( str(self.TEI.citation), @@ -50,7 +66,9 @@ def testFindComplexCitation(self): def testCitationSetters(self): d = MyCapytain.resources.texts.tei.Citation() - c = MyCapytain.common.reference.Citation(name="ahah", refsDecl="/tei:TEI/tei:text/tei:body/tei:div/tei:div[@n='$1']", child=None) + c = MyCapytain.common.reference.Citation(name="ahah", + refsDecl="/tei:TEI/tei:text/tei:body/tei:div/tei:div[@n='$1']", + child=None) b = MyCapytain.resources.texts.tei.Citation() a = MyCapytain.resources.texts.local.Text(citation=b) """ Test original setting """ @@ -66,29 +84,48 @@ def testCitationSetters(self): self.assertEqual(a.citation.scope, "/tei:TEI/tei:text/tei:body/tei:div") self.assertEqual(a.citation.xpath, "/tei:div[@n='?']") - def testFindCitation(self): + def testValidReffs(self): # Test level 1 - self.assertEqual(self.TEI.getValidReff(), ["1", "2"]) + self.assertEqual(list(map(lambda x: str(x), self.TEI.getValidReff())), ["1", "2"]) # Test level 2 - self.assertEqual(self.TEI.getValidReff(level=2)[0], "1.pr") + self.assertEqual(list(map(lambda x: str(x), self.TEI.getValidReff(level=2)))[0], "1.pr") # Test level 3 - self.assertEqual(self.TEI.getValidReff(level=3)[0], "1.pr.1") - self.assertEqual(self.TEI.getValidReff(level=3)[-1], "2.40.8") + self.assertEqual(list(map(lambda x: str(x), self.TEI.getValidReff(level=3)))[0], "1.pr.1") + self.assertEqual(list(map(lambda x: str(x), self.TEI.getValidReff(level=3)))[-1], "2.40.8") # Test with reference and level - self.assertEqual(self.TEI.getValidReff(reference=MyCapytain.common.reference.Reference("2.1"),level=3)[1], "2.1.2") - self.assertEqual(self.TEI.getValidReff(reference=MyCapytain.common.reference.Reference("2.1"),level=3)[-1], "2.1.12") + self.assertEqual( + str(self.TEI.getValidReff(reference=MyCapytain.common.reference.Reference("2.1"), level=3)[1]), + "2.1.2" + ) + self.assertEqual( + str(self.TEI.getValidReff(reference=MyCapytain.common.reference.Reference("2.1"), level=3)[-1]), + "2.1.12" + ) + self.assertEqual( + self.TEI.getValidReff(reference=MyCapytain.common.reference.Reference("2.38-2.39"), level=3), + ["2.38.1", "2.38.2", "2.39.1", "2.39.2"] + ) # Test with reference and level autocorrected because too small - self.assertEqual(self.TEI.getValidReff(reference=MyCapytain.common.reference.Reference("2.1"),level=0)[-1], "2.1.12") - self.assertEqual(self.TEI.getValidReff(reference=MyCapytain.common.reference.Reference("2.1"),level=2)[-1], "2.1.12") + self.assertEqual( + str(self.TEI.getValidReff(reference=MyCapytain.common.reference.Reference("2.1"), level=0)[-1]), + "2.1.12", + "Level should be autocorrected to len(citation) + 1" + ) + self.assertEqual( + str(self.TEI.getValidReff(reference=MyCapytain.common.reference.Reference("2.1"), level=2)[-1]), + "2.1.12", + "Level should be autocorrected to len(citation) + 1 even if level == len(citation)" + ) # Test when already too deep - self.assertEqual(self.TEI.getValidReff(reference=MyCapytain.common.reference.Reference("2.1.1"),level=3), []) + self.assertEqual(self.TEI.getValidReff(reference=MyCapytain.common.reference.Reference("2.1.1"), level=3), []) # Test wrong citation - with self.assertRaises(KeyError): - self.assertEqual(self.TEI.getValidReff(reference=MyCapytain.common.reference.Reference("2.hellno"),level=3), []) + with self.assertRaises(KeyError): + self.assertEqual( + self.TEI.getValidReff(reference=MyCapytain.common.reference.Reference("2.hellno"), level=3), []) def test_warning(self): with open("tests/testing_data/texts/duplicate_references.xml") as xml: @@ -96,61 +133,284 @@ def test_warning(self): with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - for i in [1,2,3]: - passages = text.getValidReff(level=i) + for i in [1, 2, 3]: + text.getValidReff(level=i) self.assertEqual(len(w), 3, "There should be warning on each level") - self.assertEqual(issubclass(w[-1].category, MyCapytain.errors.DuplicateReference), True, "Warning should be DuplicateReference") + self.assertEqual(issubclass(w[-1].category, MyCapytain.errors.DuplicateReference), True, + "Warning should be DuplicateReference") self.assertEqual(str(w[0].message), "1", "Warning message should be list of duplicate") def test_wrong_main_scope(self): with open("tests/testing_data/texts/sample2.xml", "rb") as file: with self.assertRaises(MyCapytain.resources.texts.local.RefsDeclError): - text = MyCapytain.resources.texts.local.Text(resource=file) + text = MyCapytain.resources.texts.local.Text(resource=file, autoreffs=True) def test_reffs(self): """ Check that every level is returned trough reffs property """ - self.assertEqual(("1" in self.TEI.reffs), True) - self.assertEqual(("1.pr" in self.TEI.reffs), True) - self.assertEqual(("2.40.8" in self.TEI.reffs), True) + self.assertEqual(("1" in list(map(lambda x: str(x), self.TEI.reffs))), True) + self.assertEqual(("1.pr" in list(map(lambda x: str(x), self.TEI.reffs))), True) + self.assertEqual(("2.40.8" in list(map(lambda x: str(x), self.TEI.reffs))), True) def test_complex_reffs(self): """ Test when there is a (something|something) xpath """ - self.assertEqual(("pr.1" in self.text_complex.reffs), True) + self.assertEqual(("pr.1" in list(map(lambda x: str(x), self.text_complex.reffs))), True) def test_urn(self): """ Test setters and getters for urn """ # Should work with string - self.TEI.urn = "urn:cts:latinLit:tg.wk.v" + self.TEI.urn = "urn:cts:latinLit:tg.wk.v" self.assertEqual(isinstance(self.TEI.urn, MyCapytain.common.reference.URN), True) self.assertEqual(str(self.TEI.urn), "urn:cts:latinLit:tg.wk.v") # Test for URN - self.TEI.urn = MyCapytain.common.reference.URN("urn:cts:latinLit:tg.wk.v2") + self.TEI.urn = MyCapytain.common.reference.URN("urn:cts:latinLit:tg.wk.v2") self.assertEqual(isinstance(self.TEI.urn, MyCapytain.common.reference.URN), True) self.assertEqual(str(self.TEI.urn), "urn:cts:latinLit:tg.wk.v2") # Test it fails if not basestring or URN - with self.assertRaises(TypeError): + with self.assertRaises(TypeError): self.TEI.urn = 2 def test_get_passage(self): - a = self.TEI.getPassage(["1", "pr", "2"]) + self.TEI.parse() + a = self.TEI.getPassage(["1", "pr", "2"], hypercontext=False) self.assertEqual(a.text(), "tum, ut de illis queri non possit quisquis de se bene ") # With reference - a = self.TEI.getPassage(MyCapytain.common.reference.Reference("2.5.5")) + a = self.TEI.getPassage(MyCapytain.common.reference.Reference("2.5.5"), hypercontext=False) self.assertEqual(a.text(), "Saepe domi non es, cum sis quoque, saepe negaris: ") - def test_get_passage_plus(self): - """ Test GetPassage Plus """ - # No label in local - a = self.TEI.getPassagePlus(["1", "pr", "2"]) + def test_get_passage_autoparse(self): + self.assertEqual(self.TEI._passages.resource, None) + a = self.TEI.getPassage(MyCapytain.common.reference.Reference("2.5.5"), hypercontext=False) + self.assertNotEqual(self.TEI._passages.resource, None) + self.assertEqual( + a.text(), "Saepe domi non es, cum sis quoque, saepe negaris: ", + "Text are automatically parsed in GetPassage hypercontext = False" + ) - self.assertEqual(a.prev, ["1", "pr", "1"]) - self.assertEqual(a.next, ["1", "pr", "3"]) - self.assertEqual(a.passage.text(), "tum, ut de illis queri non possit quisquis de se bene ") + def test_get_Passage_context_no_double_slash(self): + """ Check that get Passage contexts return right information """ + simple = self.TEI.getPassage(MyCapytain.common.reference.Reference("1.pr.2")) + str_simple = simple.tostring(encoding=str) + text = MyCapytain.resources.texts.local.Text( + resource=str_simple, + citation=self.TEI.citation, + autoreffs=True + ) + self.assertEqual( + text.getPassage(MyCapytain.common.reference.Reference("1.pr.2"), hypercontext=False).text().strip(), + "tum, ut de illis queri non possit quisquis de se bene", + "Ensure passage finding with context is fully TEI / Capitains compliant (One reference Passage)" + ) + + simple = self.TEI.getPassage(MyCapytain.common.reference.Reference("1.pr.2-1.pr.7")) + str_simple = simple.tostring(encoding=str) + text = MyCapytain.resources.texts.local.Text( + resource=str_simple, + citation=self.TEI.citation, + autoreffs=True + ) + self.assertEqual( + text.getPassage(MyCapytain.common.reference.Reference("1.pr.2"), hypercontext=False).text().strip(), + "tum, ut de illis queri non possit quisquis de se bene", + "Ensure passage finding with context is fully TEI / Capitains compliant (Same level same parent range Passage)" + ) + self.assertEqual( + text.getPassage(MyCapytain.common.reference.Reference("1.pr.3"), hypercontext=False).text().strip(), + "senserit, cum salva infimarum quoque personarum re-", + "Ensure passage finding with context is fully TEI / Capitains compliant (Same level same parent range Passage)" + ) + self.assertEqual( + list(map(lambda x: str(x), text.getValidReff(level=3))), + ["1.pr.2", "1.pr.3", "1.pr.4", "1.pr.5", "1.pr.6", "1.pr.7"], + "Ensure passage finding with context is fully TEI / Capitains compliant (Same level same parent range Passage)" + ) + + simple = self.TEI.getPassage(MyCapytain.common.reference.Reference("1.pr.2-1.1.6")) + str_simple = simple.tostring(encoding=str) + text = MyCapytain.resources.texts.local.Text( + resource=str_simple, + citation=self.TEI.citation, + autoreffs=True + ) + self.assertEqual( + text.getPassage(MyCapytain.common.reference.Reference("1.pr.2"), hypercontext=False).text().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 Passage)" + ) + self.assertEqual( + text.getPassage(MyCapytain.common.reference.Reference("1.1.6"), hypercontext=False).text().strip(), + "Rari post cineres habent poetae.", + "Ensure passage finding with context is fully TEI / Capitains compliant (Same level range Passage)" + ) + self.assertEqual( + list(map(lambda x: str(x), text.getValidReff(level=3))), + [ + "1.pr.2", "1.pr.3", "1.pr.4", "1.pr.5", "1.pr.6", "1.pr.7", + "1.pr.8", "1.pr.9", "1.pr.10", "1.pr.11", "1.pr.12", "1.pr.13", + "1.pr.14", "1.pr.15", "1.pr.16", "1.pr.17", "1.pr.18", "1.pr.19", + "1.pr.20", "1.pr.21", "1.pr.22", + "1.1.1", "1.1.2", "1.1.3", "1.1.4", "1.1.5", "1.1.6", + ], + "Ensure passage finding with context is fully TEI / Capitains compliant (Same level range Passage)" + ) + + simple = self.TEI.getPassage(MyCapytain.common.reference.Reference("1.pr.2-1.2")) + str_simple = simple.tostring(encoding=str) + text = MyCapytain.resources.texts.local.Text( + resource=str_simple, + citation=self.TEI.citation, + autoreffs=True + ) + self.assertEqual( + text.getPassage(MyCapytain.common.reference.Reference("1.pr.2"), hypercontext=False).text().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 Passage)" + ) + self.assertEqual( + text.getPassage(MyCapytain.common.reference.Reference("1.1.6"), hypercontext=False).text().strip(), + "Rari post cineres habent poetae.", + "Ensure passage finding with context is fully TEI / Capitains compliant (Different level range Passage)" + ) + self.assertEqual( + list(map(lambda x: str(x), text.getValidReff(level=3))), + [ + "1.pr.2", "1.pr.3", "1.pr.4", "1.pr.5", "1.pr.6", "1.pr.7", + "1.pr.8", "1.pr.9", "1.pr.10", "1.pr.11", "1.pr.12", "1.pr.13", + "1.pr.14", "1.pr.15", "1.pr.16", "1.pr.17", "1.pr.18", "1.pr.19", + "1.pr.20", "1.pr.21", "1.pr.22", + "1.1.1", "1.1.2", "1.1.3", "1.1.4", "1.1.5", "1.1.6", + '1.2.1', '1.2.2', '1.2.3', '1.2.4', '1.2.5', '1.2.6', '1.2.7', '1.2.8' + ], + "Ensure passage finding with context is fully TEI / Capitains compliant (Different level range Passage)" + ) + + def test_get_passage_with_list(self): + """ In range, passage in between could be removed from the original text by error + """ + simple = self.TEI.getPassage(["1", "pr", "2"]) + self.assertEqual( + simple.text().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 Passage)" + ) + + def test_type_accepted_reference_validreff(self): + """ In range, passage in between could be removed from the original text by error + """ + with self.assertRaises(TypeError): + self.TEI.getValidReff(reference=["1", "pr", "2", "5"]) + + def test_citation_length_error(self): + """ In range, passage in between could be removed from the original text by error + """ + with self.assertRaises(ReferenceError): + self.TEI.getPassage(["1", "pr", "2", "5"]) + + def test_ensure_passage_is_not_removed(self): + """ In range, passage in between could be removed from the original text by error + """ + simple = self.TEI.getPassage(MyCapytain.common.reference.Reference("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) + + simple = self.TEI.getPassage(MyCapytain.common.reference.Reference("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) + self.assertIn("1.2.4", orig_refs) + self.assertIn("1.2.5", orig_refs) + + def test_get_passage_hypercontext_complex_xpath(self): + simple = self.text_complex.getPassage(MyCapytain.common.reference.Reference("pr.1-1.2")) + str_simple = simple.tostring(encoding=str) + text = MyCapytain.resources.texts.local.Text( + resource=str_simple, + citation=self.text_complex.citation, + autoreffs=True + ) + self.assertIn( + "Pervincis tandem", + text.getPassage(MyCapytain.common.reference.Reference("pr.1"), hypercontext=False).text( + exclude=["tei:note"]).strip(), + "Ensure passage finding with context is fully TEI / Capitains compliant (Different level range Passage)" + ) + self.assertEqual( + text.getPassage(MyCapytain.common.reference.Reference("1.2"), hypercontext=False).text().strip(), + "lusimus quos in Suebae gratiam virgunculae,", + "Ensure passage finding with context is fully TEI / Capitains compliant (Different level range Passage)" + ) + self.assertEqual( + list(map(lambda x: str(x), text.getValidReff(level=2))), + [ + "pr.1", "1.1", "1.2" + ], + "Ensure passage finding with context is fully TEI / Capitains compliant (Different level range Passage)" + ) + + def test_Text_text_function(self): + simple = self.seneca.getPassage(MyCapytain.common.reference.Reference("1")) + str_simple = simple.tostring(encoding=str) + text = MyCapytain.resources.texts.local.Text( + resource=str_simple, + citation=self.seneca.citation, + autoreffs=True + ) + self.assertEqual( + text.text(exclude=["tei:note"]).strip(), + "Di coniugales tuque genialis tori,", + "Ensure text methods works on Text object" + ) + + def test_get_passage_hypercontext_double_slash_xpath(self): + simple = self.seneca.getPassage(MyCapytain.common.reference.Reference("1-10")) + str_simple = simple.tostring(encoding=str) + text = MyCapytain.resources.texts.local.Text( + resource=str_simple, + citation=self.seneca.citation, + autoreffs=True + ) + self.assertEqual( + text.getPassage(MyCapytain.common.reference.Reference("1"), hypercontext=False).text( + exclude=["tei:note"]).strip(), + "Di coniugales tuque genialis tori,", + "Ensure passage finding with context is fully TEI / Capitains compliant (Different level range Passage)" + ) + self.assertEqual( + text.getPassage(MyCapytain.common.reference.Reference("10"), hypercontext=False).text().strip(), + "aversa superis regna manesque impios", + "Ensure passage finding with context is fully TEI / Capitains compliant (Different level range Passage)" + ) + self.assertEqual( + list(map(lambda x: str(x), text.getValidReff(level=1))), + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"], + "Ensure passage finding with context is fully TEI / Capitains compliant (Different level range Passage)" + ) + + simple = self.seneca.getPassage(MyCapytain.common.reference.Reference("1")) + str_simple = simple.tostring(encoding=str) + text = MyCapytain.resources.texts.local.Text( + resource=str_simple, + citation=self.seneca.citation, + autoreffs=True + ) + self.assertEqual( + text.getPassage(MyCapytain.common.reference.Reference("1"), hypercontext=False).text( + exclude=["tei:note"]).strip(), + "Di coniugales tuque genialis tori,", + "Ensure passage finding with context is fully TEI / Capitains compliant (Different level range Passage)" + ) + self.assertEqual( + list(map(lambda x: str(x), text.getValidReff(level=1))), + ["1"], + "Ensure passage finding with context is fully TEI / Capitains compliant (Different level range Passage)" + ) class TestLocalXMLPassageImplementation(unittest.TestCase, xmlunittest.XmlTestMixin): @@ -169,12 +429,12 @@ def test_urn(self): """ Test URN and ids getters/setters """ a = MyCapytain.resources.texts.local.Passage() - - #~Test simple set up + + # ~Test simple set up a.urn = self.URN self.assertEqual(str(a.urn), "urn:cts:latinLit:phi1294.phi002.perseus-lat2") # Test update on ID update - a.id = ["1", "pr", "1"] + a.reference = "1.pr.1" self.assertEqual(str(a.urn), "urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.pr.1") # Should keep the ID if URN changes a.urn = self.URN_2 @@ -183,7 +443,7 @@ def test_urn(self): a = MyCapytain.resources.texts.local.Passage(urn=self.URN) self.assertEqual(str(a.urn), "urn:cts:latinLit:phi1294.phi002.perseus-lat2") # Test init with id and URN - a = MyCapytain.resources.texts.local.Passage(urn=self.URN, id=["1", "pr", "1"]) + a = MyCapytain.resources.texts.local.Passage(urn=self.URN, reference=["1", "pr", "1"]) self.assertEqual(str(a.urn), "urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.pr.1") # Should raise error if not URN for consistency with self.assertRaises(TypeError): @@ -192,86 +452,235 @@ def test_urn(self): a = MyCapytain.resources.texts.local.Passage() a.urn = "urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.pr.1" self.assertEqual(str(a.urn), "urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.pr.1") - self.assertEqual(a.id, ["1", "pr", "1"]) + self.assertEqual(str(a.reference), "1.pr.1") # This should affect id ! # Test Id value on init - a = MyCapytain.resources.texts.local.Passage(id=["1", "pr", "1"]) - self.assertEqual(a.id, ["1", "pr", "1"]) + a = MyCapytain.resources.texts.local.Passage(reference=["1", "pr", "1"]) + self.assertEqual(str(a.reference), "1.pr.1") def test_next(self): """ Test next property """ # Normal passage checking - p = self.TEI.getPassage(["1", "pr", "1"]) - self.assertEqual(p.next.id, ["1", "pr", "2"]) + self.TEI.parse() + p = self.TEI.getPassage(["1", "pr", "1"], hypercontext=False) + self.assertEqual(str(p.next.reference), "1.pr.2") # End of lowest level passage checking but not end of parent level - p = self.TEI.getPassage(["1", "pr", "22"]) - self.assertEqual(p.next.id, ["1", "1", "1"]) + p = self.TEI.getPassage(["1", "pr", "22"], hypercontext=False) + self.assertEqual(str(p.next.reference), "1.1.1") # End of lowest level passage and end of parent level - p = self.TEI.getPassage(["1", "39", "8"]) - self.assertEqual(p.next.id, ["2", "pr", "sa"]) + p = self.TEI.getPassage(["1", "39", "8"], hypercontext=False) + self.assertEqual(str(p.next.reference), "2.pr.sa") # Last line should always be None - p = self.TEI.getPassage(["2", "40", "8"]) + p = self.TEI.getPassage(["2", "40", "8"], hypercontext=False) self.assertIsNone(p.next) - p = self.TEI.getPassage(["2", "40"]) + p = self.TEI.getPassage(["2", "40"], hypercontext=False) self.assertIsNone(p.next) - p = self.TEI.getPassage(["2"]) + p = self.TEI.getPassage(["2"], hypercontext=False) self.assertIsNone(p.next) def test_children(self): """ Test children property """ # Normal children checking with open("tests/testing_data/texts/sample.xml", "rb") as text: - self.TEI = MyCapytain.resources.texts.local.Text(resource=text) + self.TEI = MyCapytain.resources.texts.local.Text(resource=text, autoreffs=True) - p = self.TEI.getPassage(["1", "pr"]) - self.assertEqual(p.children["1.pr.1"].id, ["1", "pr", "1"]) + p = self.TEI.getPassage(["1", "pr"], hypercontext=False) + self.assertEqual(str(p.children["1.pr.1"].reference), "1.pr.1") - p = self.TEI.getPassage(["1", "pr", "1"]) + p = self.TEI.getPassage(["1", "pr", "1"], hypercontext=False) self.assertEqual(len(p.children), 0) def test_first(self): """ Test first property """ # Test when there is one - p = self.TEI.getPassage(["1", "pr"]) - self.assertEqual(p.first.id, ["1", "pr", "1"]) + self.TEI.parse() + p = self.TEI.getPassage(["1", "pr"], hypercontext=False) + self.assertEqual(str(p.first.reference), "1.pr.1") # #And failing when no first - p = self.TEI.getPassage(["1", "pr", "1"]) + p = self.TEI.getPassage(["1", "pr", "1"], hypercontext=False) self.assertEqual(p.first, None) def test_last(self): """ Test last property """ + self.TEI.parse() # Test when there is one - p = self.TEI.getPassage(["1", "pr"]) - self.assertEqual(p.last.id, ["1", "pr", "22"]) + p = self.TEI.getPassage(["1", "pr"], hypercontext=False) + self.assertEqual(str(p.last.reference), "1.pr.22") # #And failing when no last - p = self.TEI.getPassage(["1", "pr", "1"]) + p = self.TEI.getPassage(["1", "pr", "1"], hypercontext=False) self.assertEqual(p.last, None) def test_prev(self): """ Test prev property """ + self.TEI.parse() # Normal passage checking - p = self.TEI.getPassage(["2", "40", "8"]) - self.assertEqual(p.prev.id, ["2", "40", "7"]) - p = self.TEI.getPassage(["2", "40"]) - self.assertEqual(p.prev.id, ["2", "39"]) - p = self.TEI.getPassage(["2"]) - self.assertEqual(p.prev.id, ["1"]) + p = self.TEI.getPassage(["2", "40", "8"], hypercontext=False) + self.assertEqual(str(p.prev.reference), "2.40.7") + p = self.TEI.getPassage(["2", "40"], hypercontext=False) + self.assertEqual(str(p.prev.reference), "2.39") + p = self.TEI.getPassage(["2"], hypercontext=False) + self.assertEqual(str(p.prev.reference), "1") # test failing passage - p = self.TEI.getPassage(["1", "pr", "1"]) + p = self.TEI.getPassage(["1", "pr", "1"], hypercontext=False) self.assertEqual(p.prev, None) - p = self.TEI.getPassage(["1", "pr"]) + p = self.TEI.getPassage(["1", "pr"], hypercontext=False) self.assertEqual(p.prev, None) - p = self.TEI.getPassage(["1"]) + p = self.TEI.getPassage(["1"], hypercontext=False) self.assertEqual(p.prev, None) # First child should get to parent's prev last child - p = self.TEI.getPassage(["1", "1", "1"]) - self.assertEqual(p.prev.id, ["1", "pr", "22"]) + p = self.TEI.getPassage(["1", "1", "1"], hypercontext=False) + self.assertEqual(str(p.prev.reference), "1.pr.22") # Beginning of lowest level passage and beginning of parent level - p = self.TEI.getPassage(["2", "pr", "sa"]) - self.assertEqual(p.prev.id, ["1", "39", "8"]) \ No newline at end of file + p = self.TEI.getPassage(["2", "pr", "sa"], hypercontext=False) + self.assertEqual(str(p.prev.reference), "1.39.8") + + +class TestPassageRange(unittest.TestCase): + def setUp(self): + with open("tests/testing_data/texts/sample.xml", "rb") as text: + self.text = MyCapytain.resources.texts.local.Text( + resource=text, urn="urn:cts:latinLit:phi1294.phi002.perseus-lat2" + ) + self.passage = self.text.getPassage(MyCapytain.common.reference.Reference("1.pr.2-1.pr.7")) + + def test_errors(self): + """ Ensure that some results throws errors according to some standards """ + DifferentRangePassage = self.text.getPassage( + MyCapytain.common.reference.Reference("1.pr.2-1.2") + ) + with self.assertRaises(MyCapytain.errors.InvalidSiblingRequest, msg="Different range passage have no siblings"): + a = DifferentRangePassage.next + + with self.assertRaises(MyCapytain.errors.InvalidSiblingRequest, msg="Different range passage have no siblings"): + a = DifferentRangePassage.prev + + def test_prevnext_on_first_passage(self): + DifferentRangePassage = self.text.getPassage( + MyCapytain.common.reference.Reference("1.pr.1-1.2.1") + ) + self.assertEqual( + str(DifferentRangePassage.next), "1.2.2-1.5.2", + "Next reff should be the same length as sibling" + ) + self.assertEqual( + DifferentRangePassage.prev, None, + "Prev reff should be none if we are on the first passage of the text" + ) + + def test_prevnext_on_close_to_first_passage(self): + DifferentRangePassage = self.text.getPassage( + MyCapytain.common.reference.Reference("1.pr.10-1.2.1") + ) + self.assertEqual( + str(DifferentRangePassage.next), "1.2.2-1.4.1", + "Next reff should be the same length as sibling" + ) + self.assertEqual( + str(DifferentRangePassage.prev), "1.pr.1-1.pr.9", + "Prev reff should start at the beginning of the text, no matter the length of the reference" + ) + + def test_prevnext_on_last_passage(self): + DifferentRangePassage = self.text.getPassage( + MyCapytain.common.reference.Reference("2.39.2-2.40.8") + ) + self.assertEqual( + DifferentRangePassage.next, None, + "Next reff should be none if we are on the last passage of the text" + ) + self.assertEqual( + str(DifferentRangePassage.prev), "2.37.6-2.39.1", + "Prev reff should be the same length as sibling" + ) + + def test_prevnext_on_close_to_last_passage(self): + DifferentRangePassage = self.text.getPassage( + MyCapytain.common.reference.Reference("2.39.2-2.40.5") + ) + self.assertEqual( + str(DifferentRangePassage.next), "2.40.6-2.40.8", + "Next reff should finish at the end of the text, no matter the length of the reference" + ) + self.assertEqual( + str(DifferentRangePassage.prev), "2.37.9-2.39.1", + "Prev reff should be the same length as sibling" + ) + + def test_prevnext(self): + DifferentRangePassage = self.text.getPassage( + MyCapytain.common.reference.Reference("1.pr.5-1.pr.6") + ) + self.assertEqual( + str(DifferentRangePassage.next), "1.pr.7-1.pr.8", + "Next reff should be the same length as sibling" + ) + self.assertEqual( + str(DifferentRangePassage.prev), "1.pr.3-1.pr.4", + "Prev reff should be the same length as sibling" + ) + DifferentRangePassage = self.text.getPassage( + MyCapytain.common.reference.Reference("1.pr.5") + ) + self.assertEqual( + str(DifferentRangePassage.next), "1.pr.6", + "Next reff should be the same length as sibling" + ) + self.assertEqual( + str(DifferentRangePassage.prev), "1.pr.4", + "Prev reff should be the same length as sibling" + ) + + DifferentRangePassage = self.text.getPassage( + MyCapytain.common.reference.Reference("1.pr") + ) + self.assertEqual( + str(DifferentRangePassage.next), "1.1", + "Next reff should be the same length as sibling" + ) + self.assertEqual( + DifferentRangePassage.prev, None, + "Prev reff should be None when at the start" + ) + + DifferentRangePassage = self.text.getPassage( + MyCapytain.common.reference.Reference("2.40") + ) + self.assertEqual( + str(DifferentRangePassage.prev), "2.39", + "Prev reff should be the same length as sibling" + ) + self.assertEqual( + DifferentRangePassage.next, None, + "Next reff should be None when at the start" + ) + + def test_first_list(self): + DifferentRangePassage = self.text.getPassage( + MyCapytain.common.reference.Reference("2.39") + ) + self.assertEqual( + str(DifferentRangePassage.first), "2.39.1", + "First reff should be the first" + ) + self.assertEqual( + str(DifferentRangePassage.last), "2.39.2", + "Last reff should be the last" + ) + + DifferentRangePassage = self.text.getPassage( + MyCapytain.common.reference.Reference("2.39-2.40") + ) + self.assertEqual( + str(DifferentRangePassage.first), "2.39.1", + "First reff should be the first" + ) + self.assertEqual( + str(DifferentRangePassage.last), "2.40.8", + "Last reff should be the last" + ) \ No newline at end of file diff --git a/tests/testing_data/cts/getFirstUrn.xml b/tests/testing_data/cts/getFirstUrn.xml new file mode 100644 index 00000000..588d7b1c --- /dev/null +++ b/tests/testing_data/cts/getFirstUrn.xml @@ -0,0 +1,8 @@ + + + GetFirstUrn + + + urn:cts:latinLit:phi1294.phi002.perseus-lat2:1.pr + + \ No newline at end of file diff --git a/tests/testing_data/cts/getFirstUrnEmpty.xml b/tests/testing_data/cts/getFirstUrnEmpty.xml new file mode 100644 index 00000000..d57dc17f --- /dev/null +++ b/tests/testing_data/cts/getFirstUrnEmpty.xml @@ -0,0 +1,8 @@ + + + GetFirstUrn + + + + + \ No newline at end of file diff --git a/tests/testing_data/cts/getpassage.xml b/tests/testing_data/cts/getpassage.xml index 2511a3a7..4ab2da7a 100644 --- a/tests/testing_data/cts/getpassage.xml +++ b/tests/testing_data/cts/getpassage.xml @@ -28,7 +28,7 @@ Argutis epigrammaton libellis: Cui, lector studiose, quod dedisti - Viventi decus atque sentienti, + لا یا ایها الساقی ادر کاسا و ناولها ### Rari post cineres habent poetae. diff --git a/tests/testing_data/texts/seneca.xml b/tests/testing_data/texts/seneca.xml new file mode 100644 index 00000000..0e3b2c82 --- /dev/null +++ b/tests/testing_data/texts/seneca.xml @@ -0,0 +1,1727 @@ + + + + + Medea + Seneca + Rudolf Peiper + Gustav Richter + Perseus Project, Tufts University + Gregory Crane + + Prepared under the supervision of + Lisa Cerrato + William Merrill + Elli Mylonas + David Smith + + The National Endowment for the Humanities + + About 100Kb + + Trustees of Tufts University + Medford, MA + Perseus Project + + + + + L. Annaeus Seneca + Tragoediae + Rudolf Peiper + Gustav Richter + + Leipzig + Teubner + 1921 + + + Internet Archive + + + + Medea + + + Iason + + + creo + + + Nutrix + + + Nuntius + + + Chorus Corinthiorum + + + Hyllus + + + + + + + +

This pointer pattern extracts line

+
+
+ + + +
+ + + Latin + + + + tagging + New URN, Epidoc and CTS + +
+ + + +
+ + + + + + + Medea + + Di coniugales tuque genialis tori, + Lucina, custos quaeque domituram freta + Tiphyn novam frenare docuisti ratem, + et tu, profundi saeve dominator maris, + clarumque Titan dividens orbi diem, + tacitisque praebens conscium sacris iubar + Hecate triformis, quosque iuravit mihi + deos Iason, quosque Medeae magis + fas est precari: noctis aeternae chaos, + aversa superis regna manesque impios + dominumque regni tristis et dominam fide + meliore raptam, voce non fausta precor, + nunc, nunc adeste, sceleris ultrices deae, + crinem solutis squalidae serpentibus, + atram cruentis manibus amplexae facem, + adeste, thalamis horridae quondam meis + quales stetistis: coniugi letum novae + letumque socero et regiae stirpi date, + date peius aliud, quod precer sponso malum: + vivat, per urbes erret ignotas egens + exul pavens invisus incerti laris, + iam notus hospes limen alienum expetat, + me coniugem optet quoque non aliud queam + + peius precari, liberos similes patri " + similesque matri, parta iam, parta ultio est: + peperi, querelas verbaque in cassum sero? + non ibo in hostes? manibus excutiam faces + caeloque lucem, spectat hoc nostri sator + Sol generis, et spectatur, et curru insidens + per solita puri spatia decurrit poli? + non redit in ortus et remetitur diem? + da, da per auras curribus patriis vehi, + committe habenas, genitor, et flagrantibus + ignifera loris tribue moderari iuga: + gemino Corinthos litore opponens moras + cremata flammis maria committat duo. + hoc restat unum, pronubam thalamo feram + ut ipsa pinum postque sacrificas preces + caedam dicatis victimas altaribus, + per viscera ipsa quaere supplicio viam, + si vivis, anime, si quid antiqui tibi + remanet vigoris; pelle femineos metus + et inhospitalem Caucasum mente indue, + quodcumque vidit Pontus aut Phasis nefas, + videbit Isthmos. effera ignota horrida, + tremenda caelo pariter ac terris mala + mens intus agitat: vulnera et caedem et vagum + funus per artus— levia memoravi nimis: + haec virgo feci; gravior exurgat dolor: + maiora iam me scelera post partus decent. + accingere ira teque in exitium para + furore toto. paria narrentur tua + repudia thalamis: quo virum linques modo? + hoc quo secuta es. rumpe iam segnes moras: + quae scelere parta est, scelere linquenda est domus. + + + + + Chorus + Ad regum thalamos numine prospero + qui caelum superi quique regnat fretum + assint cum populis rite faventibus, + primum sceptriferis colla Tonantibus + taurus celsa ferat tergore candido; + Latinam nivei femina corporis + intemptata iugo placet et asperi + Martis sanguineas quae cohibet manus, + quae dat belligeris foedera gentibus + et cornu retinet divite copiam, + donetur tenera mitior hostia, + et tu, qui facibus legitimis ades, + noctem discutiens auspice dextera + huc incede gradu marcidus ebrio, + praecingens roseo tempora vinculo. + et tu quae, gemini praevia temporis, + tarde, stella, redis semper amantibus: + te matres, avide te cupiunt nurus + quamprimum radios spargere lucidos. + Vincit virgineus decor + longe Cecropias nurus, + et quas Taygeti iugis + exercet iuvenum modo + muris quod caret oppidum, + et quas Aonius latex + Alpheosque sacer lavat. + Si forma velit aspici, + cedent Aesonio duci + + proles fulminis improbi + aptat qui iuga tigribus. + nec non, qui tripodas movet, + frater virginis asperae, + cedet Castore cum suo + Pollux caestibus aptior. + sic, sic, caelicolae, precor, + vincat femina coniuges, + vir longe superet viros. + Haec cum femineo .constitit in choro, + unius facies praenitet omnibus. + sic cum sole perit sidereus decor, + et densi latitant Pleiadum greges + cum Phoebe solidum lumine non suo + orbem circuitis cornibus alligat. + ostro sic niveus puniceo color + perfusus rubuit, sic nitidum iubar + pastor luce nova roscidus aspicit. + ereptus thalamis Phasidis horridi, + effrenae solitus pectora coniugis + invita trepidus prendere dextera, + felix Aeoliam corripe virginem . + nunc primum soceris, sponse, volentibus. + concesso, iuvenes, ludite iurgio, + hinc illinc, iuvenes, mittite carmina: + rara est in dominos iusta licentia. + Candida thyrsigeri proles generosa Lyaei, + multifidam iam tempus erat succendere pinum: + excute sollemnem digitis marcentibus ignem. + + festa dicax fundat convicia fescenninus. + solvat turba iocos— tacitis eat illa tenebris, + si qua peregrino nubit fugitiva marito. + + + + Medea Nvtrix + Occidimus, aures pepulit hymenaeus meas. + vix ipsa tantum, vix adnue credo malum, + hoc facere Iason potuit, erepto patre + patria atque regno sedibus solam exteris + deserere durus? merita contempsit mea + qui scelere flammas viderat vinci et mare? + adeone credit omne consumptum nefas? + incerta vaecors mente vaesana feror + partes in omnes; unde me ulcisci queam? + utinam esset illi frater! est coniunx: in hanc + ferrum exigatur. hoc meis satis est malis? + si quod Pelasgae, si quod urbes barbarae + novere facinus quod tuae ignorent manus, + nunc est parandum. scelera te hortentur tua + et cuncta redeant f inclitum regni decus + raptum et nefandae virginis parvus comes + divisus ense, funus ingestum. patri + sparsum que ponto corpus et Peliae senis + decocta aeno membra: funestum impie + quam saepe fudi sanguinem, et nullum scelus + irata feci: suasit infelix amor. + Quid tamen Iason potuit, alieni arbitri + iurisque factus? debuit ferro obvium + offerre pectus— melius, a melius, dolor + furiose, loquere, si potest, vivat meus, + + ut fuit, Iason; si minus, vivat tamen + memorque nostri muneris parcat mihi. + culpa est Creontis tota, qui sceptro impotens + coniugia solvit quique genetricem abstrahit + gnatis et arto pignore astrictam fidem + dirimit: petatur solus hic, poenas luat + quas debet, alto cinere cumulabo domum; + videbit atrum verticem flammis agi + Malea longas navibus flectens moras. + + + + Nvtr. + Sile, obsecro, questusque secreto abditos + manda dolori, gravia quisquis vulnera + patiente et aequo mutus animo pertulit, + referre potuit: ira quae tegitur nocet; + professa perdunt odia vindictae locum. + + + + Med. + Levis est dolor qui capere consilium potest + et clepere sese: magna non latitant mala. + libet ire contra, + + + Nvtr. + Siste furialem impetum, + alumna: vix te tacita defendit quies. + + + + Med. + Fortuna fortes metuit, ignavos premit. + + + + Nvtr. + Tunc est probanda, si locum virtus habet. + + + + Med. + Numquam potest non esse virtuti locus. + + + + Nvtr. + Spes nulla rebus monstrat adflictis viam. + + + + Med. + Qui nil potest sperare, desperet nihil. + + + + Nvtr. + Abiere Colchi, coniugis nulla est fides + nihilque superest opibus e tantis tibi. + + + + Med. + Medea superest, hic mare et terras vides + ferrumque et ignes et deos et fulmina. + + + + Nvtr. + Rex est timendus, + + + Med. + Rex meus fuerat pater. + + + + + Nvtr. + Non metuis arma? + + + Med. + Sint licet terra edita. + + + + + Nvtr. + Moriere. + + + Med. + Cupio. + + + Nvtr. + Profuge. + + + Med. + Paenituit fugae. + + + + + + Nvtr. + Medea + + + Med. + Fiam + + + Nvtr. + Mater es. + + + Med. + Cui sim vides. + + + + + Nvtr. + Profugere dubitas? + + + Med. + Fugiam, ut ulciscar prius. + + + + + Nvtr. + Vindex sequetur, + + + Med. + Forsan inveniam moras. + + + + Nvtr. + Compesce verba, parce iam, demens, minis + + animosque minue: tempori aptari decet. + + + + Med. + Fortuna opes auferre, non animum potest, + sed cuius ictu regius cardo strepit? + ipse' est Pelasgo tumidus imperio Creo. + + + + Creo Medea + Medea, Colchi noxium Aeetae genus, + nondum meis exportat e regnis pedem? + molitur aliquid: nota fraus, nota est manus, + cui parcet illa quemve securum sinet? + abolere propere pessimam ferro luem + equidem parabam: precibus evicit gener, + concessa vita est, liberet fines metu + abeatque tuta. fert gradum contra ferox + minaxque nostros propius affatus petit, + arcete, famuli, tactu et accessu procul, + iubete sileat. regium imperium pati + aliquando discat, vade veloci fuga + monstrumque saevum horribile iamdudum avehe. + + + + Med. + Quod crimen aut quae culpa multatur fuga? + + + + Cr. + Quae causa pellat, innocens mulier rogat. + + + + Med. + Si iudicas, cognosce, si regnas, iube. + + + + Cr. + Aequum atque iniquum regis imperium feras. + + + + Med. + Iniqua numquam regna perpetuo manent. + + + + + Cr. + I, querere Colchis, + + + Med. + Redeo: qui advexit, ferat. + + + + Cr. + Vox constitute sera decreto venit. + + + + Med. + Qui statuit aliquid parte inaudita altera, + aequum licet statuerit, haud aequus fuit. + C R. Auditus a te Pelia supplicium tulit? + sed fare, causae detur egregiae locus. + + + + Med. + Difficile quam sit animum ab ira flectere + iam concitatum quamque regale hoc putet + sceptris superbas quisquis admovit manus, + qua coepit ire, regia didici mea. + quamvis enim sim clade miseranda obruta, + expulsa supplex sola deserta, undique + adflicta, quondam nobili falsi patre + avoque clarum Sole deduxi genus. + quodcumque placidis flexibus Phasis rigat + Pontusque quicquid Scythicus a tergo videt, + palustribus qua maria dulcescunt aquis, + armata peltis quicquid exterret cohors + inclusa ripis vidua Thermodontiis, + hoc omne noster genitor imperio regit. + generosa, felix, decore regali potens + fulsi: petebant tunc meos thalamos proci, + qui nunc petuntur. rapida fortuna ac levis + praecepsque regno eripuit, exilio dedit. + confide regnis, cum levis magnas opes + huc ferat et illuc casus— hoc reges habent + magnificum et ingens, nulla quod rapiat dies: + prodesse miseris, supplices fido lare + protegere, solum hoc Colchico regno extuli, + decus illud ingens Graeciae et florem inclitum, + + praesidia Achivae gentis et prolem deum, + servasse memet, munus est Orpheus meum, + qui saxa cantu mulcet et silvas trahit, + geminum que numen Castor et Pollux meum est + satique Borea quique trans Pontum quoque + summota Lynceus lumine inmisso videt, + omnesque Minyae: nam ducem taceo ducum, + pro quo nihil debetur: hunc nulli imputo; + vobis revexi ceteros, unum mihi. + incesse nunc et cuncta flagitia ingere. + fatebor: obici crimen hoc solum potest, + Argo reversa, virgini placeat pudor + paterque placeat: tota cum ducibus ruet + Pelasga tellus, hic tuus primum gener + tauri ferocis ore flammanti occidet. + fortuna causam quae volet nostram premat, + non paenitet servasse tot regum decus. + quodcumque culpa praemium ex omni tuli, + hoc est penes te. si placet, damna ream; + sed redde crimen, sum nocens, fateor, Creo: + talem sciebas esse, cum genua attigi + fidemque supplex praesidis dextrae peti; + terra hac miseriis angulum ac sedem rogo + latebrasque viles: urbe si pelli placet, + detur remotus aliquis in regnis locus. + + + + Cr. + Non esse me qui sceptra violentus geram + nec qui superbo miserias calcem pede, + testatus equidem videor haud clare parum + generum exulem legendo et adflictum et gravi + + terrore pavidum, quippe quem poenae expetit + letoque Acastus regna Thessalica optinens. + senio trementem debili atque aevo gravem + patrem peremptum queritur et caesi senis + discissa membra, cum dolo captae tuo + piae sorores impium auderent nefas. + potest Iason, si tuam causam amoves, + suam tueri: nullus innocuum cruor + contaminavit, afuit ferro manus + proculque vestro purus a coetu stetit. + tu, tu malorum machinatrix facinorum, + cui feminae nequitia ad audenda omnia, + robur virile est, nulla famae memoria, + egredere, purga regna, letales simul + tecum aufer herbas, libera cives metu, + alia sedens tellure sollicita deos. + + + + Med. + Profugere cogis? redde fugienti ratem + vel redde comitem— fugere cur solam iubes? + non sola veni. bella si metuis pati, + utrumque regno pelle, cur sontes duos + distinguis? illi Pelia, non nobis iacet; + fugam, rapinas adice, desertum patrem + lacerum que fratrem, quicquid etiam nunc novas + docet maritus coniuges, non est meum: + totiens nocens sum facta, sed numquam mihi. + + + + Cr. + Iam exisse decuit, quid seris fando moras? + + + + Med. + Supplex recedens illud extremum precor, + ne culpa natos matris insontes trahat. + + + + Cr. + Vade: hos paterno ut genitor excipiam sinu. + + + + Med. + Per ego auspicatos regii thalami toros, + + per spes futuras perque regnorum status, + Fortuna varia dubia quos agitat vice, + precor, brevem largire fugienti moram, + dum extrema natis mater infigo oscula, + fortasse moriens. + + + Cr. + Fraudibus tempus petis. + + + + Med. + Quae fraus timeri tempore exiguo potest? + + + + Cr. + Nullum ad nocendum tempus angustum est malis. + + + + Med. + Parumne miserae temporis lacrimis negas? + + + + Cr. + Etsi repugnat precibus infixus timor, + unus parando dabitur exilio dies. + + + + Med. + Nimis est, recidas aliquid ex isto licet: + et ipsa propero. + + + Cr. + Capite supplicium lues, + clarum priusquam Phoebus attollat diem + nisi cedis Isthmo. sacra me thalami vocant, + vocat precari festus Hymenaeo dies. + + + + Chorus + Audax nimium qui freta primus + rate tam fragili perfida rupit + terrasque suas posterga videns + animam levibus credidit auris, + dubioque secans aequora cursu + potuit tenui fidere ligno + inter vitae mortisque vias + nimium gracili limite ducto. + + Candida nostri saecula patres + videre, procul fraude remota. + sua quisque piger litora tangens + patrioque senex factus in arvo, + parvo dives, nisi quas tulerat + + + natale solum, non norat opes: + + nondum quisquam sidera norat. + stellisque quibus pingitur aether + non erat usus. nondum pluvias + Hyadas poterat vitare ratis. + non Oleniae lumina caprae, + nec quae sequitur flectitque senex + Attica tardus plaustra Bootes, + nondum Boreas, + nondum Zephyrus nomen habebant. + Ausus Tiphys pandere vasto + carbasa ponto + legesque novas scribere ventis: + nunc lina sinu tendere toto, + nunc prolato pede transversos + captare notos, nunc antemnas + medio tutas ponere malo, + nunc in summo religare loco, + cum iam totos avidus nimium + navita flatus optat et alto + rubicunda tremunt sipara velo. + + b«ne dissaepti foedera mundi + traxit in unum Thessala pinus + iussitque pati verbera pontum, + partemque metus fieri nostri + mare sepositum. + dedit illa graves improba poenas + per tam longos ducta timores, + cum duo montes, claustra profundi, + hinc atque illinc subito impulsu + velut aetherio gemerent sonitu, + nubesque ipsas + + mare deprensum spargeret austris. + + palluit audax Tiphys et omnes + labente manu misit habenas, + Orpbeus tacuit torpente lyra + ipsaque vocem perdidit Argo. + quid cum Siculi virgo Pelori, + rabidos utero succincta canes, + omnis pariter solvit hiatus? ' + quis non totos horruit artus + totiens uno latrante malo? + quid cum Ausonium dirae pestes + voce canora mare mulcerent. + cum Pieria resonans cithara + Thracius Orpheus + solitam cantu retinere rates + paene coegit Sirena sequi? + quod fuit huius pretium cursus? + aurea pellis + maiusque mari Medea malum, + merces prima digna carina. + Nunc iam cessit pontus et omnes + + patitur leges: non Palladia + compacta manu regum referens + inclita remos quaeritur Argo— + quaelibet altum cumba pererrat; + terminus omnis motus et urbes + muros terra posuere nova, + nil qua fuerat sede reliquit + pervius orbis: + Indus gelidum potat Araxen, + Albin Persae Rhenumque bibunt. + venient annis saecula seris, + quibus Oceanus vincula rerum + laxet et ingens pateat tellus + + Tethysque novos detegat orbes + nec sit terris ultima Thule. + + + + Nvtrix Medea + Alumna, celerem quo rapis tectis pedem? + resiste et iras comprime ac retine impetum. + Incerta qualis entheos gressus tulit + cum iam recepto maenas insanit deo + Pindi nivalis vertice aut Nysae iugis, + talis recursat huc et huc motu effero, + furoris ore signa lymphati gerens, + flammata facies spiritum ex alto citat, + proclamat, oculos uberi fletu rigat, + renidet: omnis specimen affectus capit, + + quo pondus animi vergat, ubi ponat minas, + + haeret: minatur aestuat queritur gemit. + + ubi se iste fluctus franget? exundat furor. + non facile secum versat aut medium scelus; + se vincet: irae novimus veteris notas, + magnum aliquid instat, efferum immane impium: + vultum furoris cerno, di fallant metum! + + + + Med. + Si quaeris odio, misera, quem statuas modum : + imitare amorem, regias egone ut faces + inulta patiar? segnis hic ibit dies, + tanto petitus ambitu, tanto datus? + dum terra caelum media libratum feret + nitidusque certas mundus evolvet vices + numerosque harenis derit et solem dies, + noctem sequentur astra, dum siccas polus + + versabit Arctos, flumina in pontum cadent. + numquam meus cessabit in poenas furor + crescetque semper, quae ferarum immanitas, + quae Scylla, quae Charybdis Ausonium mare + Siculumque sorbens quaeve anhelantem premens + Titana tantis Aetna fervebit minis? + non rapidus amnis, non procellosum mare + Pontusve coro saevus aut vis ignium + adiuta flatu possit inhibere impetum + irasque nostras: sternam et evertam omnia. + Timuit Creontem ac bella Thessalici ducis? + amor timere neminem verus potest, + sed cesserit coactus et dederit manus: + adire certe et coniugem extremo alioqui + sermone potuit— hoc quoque extimuit ferox; + laxare certe tempus immitis fugae + genero licebat— liberis unus dies + datus est duobus, non queror tempus breve: + multum patebit. faciet hic faciet dies + quod nullus umquam taceat— invadam deos + et cuncta quatiam. + + + Nvtr. + Recipe turbatum malis. + era, pectus, animum mitiga. + + + Med. + Sola est quies, + mecum ruina cuncta si video obruta: + mecum omnia abeant. trahere, cum pereas, libet. + + + + Nvtr. + Quam multa sint timenda, si perstas, vide: + nemo potentes aggredi tutus potest. + + + + Iason Medea + O dura fata semper et sortem asperam, + cum saevit et cum parcit ex aequo malam! + + remedia quotiens invenit nobis deus + periculis peiora: si vellem fidem + praestare meritis coniugis, leto fuit + caput offerendum; si mori nollem, fide + misero carendum, non timor vicit fidem, + sed trepida pietas: quippe sequeretur necem + proles parentum, sancta si caelum incolis + Iustitia, numen invoco ac testor tuum: + nati patrem vicere. quin ipsam quoque, + etsi ferox est corde nec patiens iugi, + consulere natis malle quam thalamis reor. + constituit animus precibus iratam aggredi. + atque ecce, viso memet exiluit, furit, + fert odia prae se: totus in vultu est dolor. + + + + Med. + Fugimus, Iason: fugimus— hoc non est novum, + mutare sedes; causa fugiendi nova est: + pro te solebam fugere, discedo exeo, + penatibus profugere quam cogis tuis: + at quo remittas? Phasin et Colchos petam + patriumque regnum quaeque fraternus cruor + perfudit arva? quas peti terras iubes? + quae maria monstras? Pontici fauces freti + per quas revexi nobilem regum manum 455 + adulterum secuta per Symplegadas? + parvamne lolcon, Thessala an Tempe petam? + quascumque aperui tibi vias, clausi mihi— + quo me remittis? exuli exilium imperas + + nec das. eatur. regius iussit gener: + nihil recuso, dira supplicia ingere: + merui cruentis paelicem poenis premat + regalis ira, vinculis oneret manus + clausamque saxo noctis aeternae obruat: + minora meritis patiar— ingratum caput, + revolvat animus igneos tauri halitus + + interque saevos gentis indomitae metus + + + armifero in arvo flanimeum Aeetae pecus, + + hostisque subiti tela, cum iussu meo + terrigena miles mutua caede occidit; + adice expetita spolia Phrixei arietis + somnoque iussum lumina ignoto dare + insomne monstrum, traditum fratrem neci + et scelere in uno non semel factum scelus, + ausasqne natas fraude deceptas mea + secare membra non revicturi senis: + + per spes tuorum liberum et certum larem, + per victa monstra, per manus, pro te quibus + numquam peperci, perque prseteritos metus, + per caelum et undas, coniugi testes mei, + miserere, redde supplici felix vicem. + + aliena quaerens regna deserni mea: + + ex opibus illis, quas procul raptas Scythae + usque a perustis Indiae populis agunt, + quas quia referta vix domus gaza capit, + ornamus auro nemora, nil exul tuli + nisi fratris artus: hos quoque impendi tibi; + tibi patria cessit, tibi pater, frater, pudor— + hac dote nupsi. redde fugienti sua. + + + + Ias. + Perimere cum te vellet infestus Creo, + lacrimis meis evictus exilium dedit. + + + + Med. + Poenam putabam: munus ut video est fuga. + + + + Ias. + Dum licet abire, profuge teque hinc eripe: + gravis ira regum est semper, + + + Med. + Hoc suades mihi, + praestas Creusae: paelicem invisam amoves. + + + + + Ias. + Medea amores obicit? + + + Med. + Et caedem et dolos. + + + + Ias. + Obicere tandem quod potes crimen mihi? + + + + + Med. + Quodcumque feci. + + + Ias. + Restat hoc unum insuper, + tuis ut etiam sceleribus fiam nocens. + + + + Med. + Tua illa, tua sunt illa: cui prodest scelus + is fecit— omnes coniugem infamem arguant, + solus tuere, solus insontem voca: + tibi innocens sit quisquis est pro te nocens. + + + + Ias. + Ingrata vita est cuius acceptae pudet. + + + + Med. + Retineuda non est cuius acceptae pudet. + + + + Ias. + Quin potius ira concitum pectus doma, + placare natis. + + + Med. + Abdico eiuro abnuo— + meis Creusa liberis fratres dabit? + + + + Ias. + Regina natis exulum, afflictis potens. + + + + Med. + Ne veniat umquam tam malus miseris dies, + qui prole foeda misceat prolem inclitam. + + Phoebi nepotes Sisyphi nepotibus + + + + + Ias. + Quid, misera, meque teque in exitium trahis? + abscede quaeso, + + + Med. + Supplicem audivit Creo. + + + + + Ias. + Quid facere possim, loquere, + + + Med. + Pro me? vel scelus. + + + + + Ias. + Hinc rex et illinc— + + + Med. + Est (et hic maior metus) + Medea, nos † confligere, certemus sine, + sit pretium Iason. + + + Ias. + Cedo defessus malis. + + et ipsa casus saepe iam expertos time. + + + + Med. + Fortuna semper omnis infra me stetit. + + + + + Ias. + Acastus instat, + + + Med. + Propior est hostis Creo: + utrumque profuge. non ut in socerum manus + armes nec ut te caede cognata inquines + Medea cogit: innocens mecum fuge. + + + + Ias. + Et quis resistet, gemina si bella ingruant, + Creo atque Acastus arma si iungant sua? + + + + Med. + His adice Colchos, adice et Aeeten ducem, + Scythas Pelasgis iunge: demersos dabo. + + + + + Ias. + Alta extimesco sceptra, + + + Med. + Ne cupias vide. + + + + Ias. + Suspecta ne sint, longa colloquia amputa. + + + + Med. + Nunc summe toto Iuppiter caelo tona, + intende dextram, vindices flammas para + omnemque ruptis nubibus mundum quate. + nec deligenti tela librentur manu + rei me vel istum: quisquis e nobis cadet + nocens peribit, non potest in nos tuum + errare fulmen, + + + Ias. + Sana meditari incipe + et placida fare, si quod ex soceri domo + potest fugam levare solamen, pete. + + + + Med. + Contemnere animus regias, ut scis, opes + potest soletque; liberos tantum fugae + habere comites liceat in quorum' sinu + lacrimas profundam. te novi nati manent. + + + + Ias. + .Parere precibus cupere me fateor tuis; + pietas vetat: namque istud ut possim pati, + non ipse memet cogat et rex et socer. + haec causa vitae est, hoc perusti pectoris + curis levamen. spiritu citius queam + carere, membris, luce. + + + Med. + Sic natos amat? + + bene est, tenetur, vulneri patuit locus.— + suprema certe liceat abeuntem loqui + mandata, liceat ultimum amplexum dare: + gratum est et illud, voce iam extrema peto, + ne, si qua noster dubius effudit dolor, + maneant in animo verba: melioris tibi + memoria nostri sedeat; haec irae data + oblitterentur. + + + Ias. + Omnia ex animo expuli + precorque et ipse, fervidam ut mentem regas + placideque tractes: miserias lenit quies. + + + + Med. + Discessit, itane est? vadis oblitus mei + et tot meorum facinorum? excidimus tibi? + numquam excidemus. hoc age, omnis advoca + vires et artes, fructus est scelerum tibi + nullum scelus putare, vix fraudi est locus: + timemur. hac aggredere, qua nemo potest + quicquam timere, perge, nunc aude, incipe + quicquid potest Medea, quicquid non potest. + Tu, fida nutrix, socia maeroris mei + variique casus, misera consilia adiuva, + est palla nobis, munus aetherium, domus + decusque regni, pignus Aeetae datum + a Sole generis, est et auro textili + monile fulgens quodque gemmarum nitor + distinguit aurum, quo solent cingi comae, + haec nostra nati dona nubenti ferant, + sed ante diris inlita ac tincta artibus, + vocetur Hecate, sacra letifica appara: + statuantur arae, flamma iam tectis sonet. + + Nulla vis flammae tumidive venti + tanta, nec teli metuenda torti, + quanta cum coniunx viduata taedis + ardet et odit; + non ubi hibernos nebulosus imbres + Auster advexit properatque torrens + Hister et iunctos vetat esse pontes + ac vagus errat; + non ubi impellit Rhodanus profundum, + aut ubi in rivos nivibus solutis + sole iam forti medioque vere + tabuit Haemus. + caecus est ignis stimulatus ira + nec regi curat patiturve frenos + aut timet mortem: cupit ire in ipsos + obvius enses. + parcite, o divi, veniam precamur, + vivat ut tutus mare qui subegit, + sed furit vinci dominus profundi + regna secunda. + ausus aeternos agitare currus + immemor metae iuvenis paternae + quos polo sparsit furiosus ignes + ipse recepit. + + constitit nulli via nota magno: + vade qua tutum populo priori, + rumpe nec sacro vidente sancta + foedera mundi. + Quisquis audacis tetigit carinae + nobiles remos nemorisque sacri + Pelion densa spoliavit umbra, + quisquis intravit scopulos vagantes + et tot emensus pelagi labores + barbara funem religavit ora + raptor externi rediturus auri, + exitu diro temerata ponti + iura piavit. + exigit poenas mare provocatum: + Tiphys in primis, domitor profundi, + liquit indocto regimen magistro; + litore externo, procul a paternis + occidens regnis tumuloque vili + tectus ignotas iacet inter umbras. + Aulis amissi memor inde regis + portibus lentis retinet carinas + stare querentes. + ille vocali genitus Camena, + cuius ad chordas modulante plectro + restitit torrens, siluere venti, + cura suo cantu volucris relicto + adfuit tota comitante silva, + Thracios sparsus iacuit per agros, + + at caput tristi fluitavit Hebro: + contigit notam Styga Tartarumque, + non rediturus. + stravit Alcides Aquilone natos, + patre Neptuno genitum necavit + sumere innumeras solitum figuras: + ipse post terrae pelagique pacem, + post feri Ditis patefacta regna, + vivus ardenti recubans in Oeta + praebuit saevis sua membra flammis, + tabe consumptus gemini cruoris + munere nuptae. + stravit Ancaeum violentus ictu + saetiger; fratrem, Meleagre, matris + impius m actas morerisque dextra + matris iratae, meruere cuncti + morte quod crimen tener expiavit + Herculi magno puer inrepertus, + raptus, heu, tutas puer inter undas, + ite nunc, fortes, perarate pontum + fonte timendo. + Idmonem, quamvis bene fata nosset, + condidit serpens Libycis harenis; + omnibus verax, sibi falsus uni + concidit Mopsus caruitque Thebis. + ille si vere cecinit futura, + exul errabit Thetidis maritus + + + fulmine et ponto moriens Oileus + + *patrioque pendet crimine poenas: + + igne fallaci nociturus Argis + Nauplius praeceps cadet in profundum, + + coniugis fatum redimens Pheraei + uxor impendes animam marito, + ipse qui praedam spoliumque iussit + aureum prima revehi carina, + arsit accenso Pelias aeno. + + ustus angustas vagus inter undas. + + iam satis, divi, mare vindicastis: + parcite iusso. + + + + Nvtrix + Pavet animus, horret, magna pernicies adest. + immane quantum augescit et semet dolor + accendit ipse vimque praeteritam integrat. + vidi furentem saepe et aggressam deos, + caelum trahentem: maius his, maius parat + Medea monstrum, namque ut attonito gradu + evasit et penetrale funestam attigit, + totas opes effundit et quicquid diu + etiam ipsa timuit promit atque omnem explicat + turbam malorum, arcana secreta abdita, + + et triste laeva congregans sacrum manu + pestes vocat quascumque ferventes creat + harena Libyae quasque perpetua nive + Taurus cohercet frigore Arctoo rigens, + et omne monstrum, tracta magicis cantibus + squamifera latebris turba desertis adest. + hic saeva serpens corpus immensum trahit + trifidamque linguam exertat et quaerit quibus + mortifera veniat: carmine audito stupet + tumidumque nodis corpus aggestis plicat + cogitque in orbεs. 'parva sunt' inquit 'mala + et vile telum est. ima quod tellus creat: + caelo petam venena, iam iam tempus est + aliquid movere fraude vulgari altius. + huc ille vasti more torrentis iacens + descendat anguis, cuius immensos duae. + maior minorque, sentiunt nodos ferae + (maior Pelasgis apta. Sidoniis minor) + pressasque tandem solvat Ophiuchus manus + virusque fundat; adsit ad cantus meos + lacessere ausus gemina Python numina. + et Hydra et omnis redeat Herculea manu + succisa serpens, caede se reparans sua. + tu quoque relictis pervigil Colchis ades, + sopite primum cantibus, serpens, meis.' + Postquam evocavit omne serpentum genus, + congerit in unum frugis infaustae mala: + quaecumque generat invius saxis Eryx, + quae fert opertis hieme perpetua iugis + + sparsus cruore Caucasus Promethei, + + et quis sagittas divites Arabes linunt + + pharetraque pugnax Mediis aut Parthi leves, + + aut quos sub axe frigido sucos legunt + lucis Suebae nobiles Hyrcaniis; + quodcumque tellus vere nidifico creat + aut rigida cum iam bruma discnssit decus + nemorum et nivali cuncta constrinxit gelu, + quodcumque gramen flore mortifero viret + cuiusve fortis sucus in radicibus + causas nocendi gignit, attrectat manu. + Haemonius illas contulit pestes Athos, + has Pindus ingens, illa Pangaei iugis + teneram cruenta falce deposuit comam; + has aluit altum gurgitem Tigris premens, + Danuvius illas, has per arentes plagas + tepidis Hydaspes gemmifer currens aquis, + nomenque terris qui dedit Baetis suis + Hesperia pulsans maria languenti vado. + haec passa ferrum est, dum parat Phoebus diem, + illius alta nocte succisus frutex; + at huius ungue secta cantato seges. + Mortifera carpit gramina ac serpentium + saniem exprimit miscetque et obscenas aves + maestique cor bubonis et raucae strigis + exsecta vivae viscera, haec scelerum artifex + discreta ponit; his rapax vis ignium, + his gelida pigri frigoris glacies inest. + + addit venenis verba non illis minus + metuenda, sonuit ecce vaesano gradu + canitque. mundus vocibus primis tremit. + + + + Medea + Compreeor vulgus silentum vosque ferales deos + et Chaos caecum atque opacam Ditis umbrosi domum. + Tartari ripis ligatae squalido Mortis specu + supplicis. animae, remissis currite ad thalamos novos: + rota resistat membra torquens, tangat Ixion humum, + Tantalus securus undas hauriat Pirenidas. + + vos quoque, urnis quas foratis inritus ludit labor, + Danaides, coite: vestras hic. dies quaerit manus. + + gravior uni poena sedeat coniugis socero mei: + lubricus per saxa retro Sisyphum volvat lapis, + + nunc meis vocata sacris, noctium sidus, veni + pessimos induta vultus, fronte non una minax. + Tibi move gentis vinculo solvens comam + secreta nudo nemora lustravi pede + et evocavi nubibus siccis aquas + egique ad imum maria, et Oceanus graves + interius undas aestibus victis dedit; + pariterque mundus lege confusa aetheris + et solem et astra vidit et vetitum mare + tetigistis, ursae. temporum flexi vices: + aestiva tellus floruit cantu meo, + coacta messem vidit hibernam Ceres; + violenta Phasis vertit in fontem vada + + et Hister, in tot ora divisus, truces + compressit undas omnibus ripis piger. + Sonuere fluctus, tumuit insanum mare + tacente vento; nemoris antiqui domus + emisit umbras vocis imperio meae. + die relicto Phoebus in medio stetit + Hyadesque nostris cantibus motae labant: + adesse sacris tempus est, Phoebe, tuis. + tibi haec cruenta serta texuntur manu, + no vena quae serpens ligat, + tibi haec Typhoeus membra quae discors + qui regna concussit Iovis, + vectoris istic perfidi sanguis inest, + quem Nessus expirans dedit. + Oetaeus isto cinere defecit rogus, + qui virus Herculeum bibit, + piae sororis, impiae matris, facem + ultricis Althaeae vides, + reliquit istas invio plumas specu + Harpyia, dum Zeten fugit, + his adice pinnas sauciae Stymphalidos + Lernaea passae spicula, + sonuistis, arae, tripodas agnosco meos + favente commotos dea. + Video Triviae currus agiles, + non quos pleno lucida vultu + pernox agitat, + + sed quos facie Iurida maesta, + cum Thessalicis vexata minis + caelum freno propiore legit. + sic face tristem pallida lucem + funde per auras, + horrore novo terre populos + + inque auxilium, Dictynna, tuum + pretiosa sonent aera Corinthi. + tibi sanguineo caespite sacrum + sollemne damus, + tibi de medio rapta sepulchro + fax nocturnos sustulit ignes, + tibi mota caput + fiexa voces cervice dedi, + tibi funereo de more iacens + passos cingit vitta capillos, + tibi iactatur + + tristis Stygia ramus ab unda, + tibi nudato pectore maenas + sacro feriam bracchia cultro. + manet noster sanguis ad aras: + assuesce, manus, stringere ferrum + carosque pati posse cruores. + sacrum laticem percussa dedi. + quodsi nimium saepe vocari + querens votis, ignosce precor: + causa vocandi, Persei, tuos + saepius arcus una atque eadem est + semper Iason. + tu nunc vestes tinge Creusae, + quas cum primum sumpserit, imas + urat serpens flamma medullas. + ignis fulvo elusus in auro + latet obscurus, quem mihi caeli + + qui furta luit viscere feto + dedit et docuit condere vires + arte, Prometheus, dedit et tenui + sulphure tectos Mulciber ignes, + et vivacis fulgura flammae + de cognato Phaethonte tuli. + habeo en dirae dona Chimaerae, + habeo flammas + usto tauri gutture raptas, + quas permixto felle Medusae + tacitum iussi servare malum, + adde venenis stimulos, Hecate, + donisque meis + semina flammae condita serva. + fallant visus tactusque ferant, + meet in pectus venasque calor, + stillent artus ossaque tument + vincatque suas + flagrante coma nova nupta faces. + + Vota tenentur: ter latratus + audax Hecate dedit et sacros + edidit ignes face lucifera. + Peracta vis est omnis: huc natos voca, + pretiosa per quos dona nubenti feram, + ite, ite, nati, matris infaustae genus, + placate vobis munere et multa prece + dominam ac novercam.. vadite et celeres domum + referte gressus, ultimo amplexu ut fruar, + + + + Chorus + Quonam cruenta maenas + praeceps amore saevo + rapitur? quod impotenti + + facilius parat furore?— + vultus citatus ira + riget et caput feroci + quatiens superba motu + regi minatur ultro. + quis credat exulem? + flagrant genae rubentes. + pallor fugat ruborem, + nullum vagante forma + servat diu colorem.— + huc fert pedes et illuc, + ut tigris orba natis + cursu furente lustrat + Grangeticum nemus. + frenare nescit iras + Medea, non amores; + nunc ira amorque causam + iunxere: quid sequetur? + quando efferet Pelasgis + nefanda Colchis arvis + gressum metuque solvet + regnum simulque reges?— + nunc, Phoebe, mitte currus + nullo morante loro, + nox condat alma lucem, + mergat diem timendum + dux noctis Hesperus. + + + nuntius chorus nutrix medea iason + Periere cuncta, concidit regni status, + nata atque, genitor cinere permixto iacent. + + + + + Chor. + Qua fraude capti? + + + Nvnt. + Qua solent reges capi: + donis. + + + Chor. + In illis esse quis potuit dolus? + + + + Nvnt. + Et ipse miror vixque iam facto malo + potuisse fieri credo. + + + Chor. + Quis cladis modus? + + + + Nvnt. + Avidus per omnem regiae partem furit . + ut iussus ignis: iam domus tota occidit, + urbi timetur. + + + Chor. + Vnda flammas opprimat. + + + + Nvnt. + Et hoc in ista clade mirandum accidit: + alit unda flammas, quoque prohibetur magis, + magis ardet ignis: ipsa praesidia occupat. + + + + Nvtr. + Effer citatum sede Pelopea gradum, + Medea, praeceps quaslibet terras pete. + + + + Med. + Egone ut recedam? si profugissem prius, + ad hoc redirem, nuptias specto novas, + quid, anime, cessas? sequere felicem impetum. + pars ultionis ista, qua gaudes, quota est? + amas adhuc, furiosa, si satis est tibi + caelebs Iason, quaere poenarum genus + haut usitatum iamque sic temet para: + fas omne cedat, abeat expulsus pudor; + vindicta levis est quam ferunt purae manus, + incumbe in iras teque languentem excita + penitusque veteres pectore ex imo impetus + violenter hauri. quicquid admissum est adhuc, + pietas vocetur, hoc agam et faxo sciant + quam levia fuerint quamque vulgaris notae + quae commodavi scelera, prolusit dolor + per ista noster: quid manus poterant rudes + audere magnum? quid puellaris furor? + Medea nunc sum; crevit ingenium malis. + + Iuvat, iuvat rapuisse fraternum caput; + artus iuvat secuisse et arcano patrem + spoliasse sacro, iuvat in exitium senis + armasse natas, quaere materiam, dolor: + ad omne facinus non rudem dextram afferes. + Quo te igitur, ira, mittis, aut quae perfido + intendis hosti tela? nescio quid ferox + decrevit animus intus et nondum sibi + audet fateri, stulta properavi nimis: + ex paelice utinam liberos hostis meus + aliquos haberet— quicquid ex illo tuum est, + Creusa peperit, placuit hoc poenae genus, + meritoque placuit: ultimum, agnosco, scelus + animo parandum est— liberi quondam mei, + vos pro paternis sceleribus poenas date. + Cor pepulit horror, membra torpescunt gelu + pectusque tremuit. ira discessit loco + materque tota coniuge expulsa redit, + egone ut meorum liberum ac prolis meae + fundam cruorem? melius, a, demens furor! + incognitum istud facinus ac dirum nefas + a me quoque absit; quod scelus miseri luent? + scelus est Iason genitor et maius scelus + Medea mater— occidant, non sunt mei; + pereant, mei sunt. crimine et culpa carent, + sunt innocentes: fateor, et frater fuit. + quid, anime, titubas? ora quid lacrimae rigant + variamque nunc huc ira, nunc illuc amor + diducit? anceps aestus incertam rapit; + ut saeva rapidi bella cum venti gerunt + utrimque fluctus maria discordes agunt + dubiumque fervet pelagus, haut aliter meum + + cor fluctuatur. ira pietatem fugat + iramque pietas— cede pietati, dolor. + Huc, cara proles, unicum afflictae domus + solamen, huc vos ferte et infusos mihi + coniungite artus, habeat incolumes pater, + dum et mater habeat, urguet exilium ac fuga. + iam iam meo rapientur avulsi e sinu, + flentes, gementes osculis pereant patri, + periere matri, rursus increscit dolor + et fervet odium, repetit invitam manum + antiqua Erinys. ira, qua ducis, sequor, + utinam superbae turba Tantalidos meo + exisset utero bisque septenos parens + natos tulissem! sterilis in poenas fui— + fratri patri que quod sat est, peperi duos. + Quonam ista tendit turba Furiarum impotens? + quem quaerit aut quo flammeos ictus parat, + aut cui cruentas agmen infernum faces + intentat? ingens anguis excusso sonat + tortus flagello, quem trabe infesta petit + Megaera? cuius umbra dispersis venit + incerta membris? frater est, poenas petit: + dabimus., sed omnes, fige luminibus faces, + lania, perure, pectus en Furiis patet. + Discedere a me, frater, ultrices deas + manesque ad imos ire securas iube: + mihi me relinque et utere hac, frater, manu + quae strinxit ensem— victima manes tuos + placamus ista. quid repens affert sonus? + + parentur arma meque in exitium petunt. + excelsa nostrae tecta conscendam domus + caede incohata, perge tu mecum comes. + tuum quoque ipsa corpus hinc mecum aveham. + nunc hoc age, anime: non in occulto tibi est + perdenda virtus; approba populo manum. + + + + Ias. + Quicumque regum cladibus fidus doles, + concurre, ut ipsam sceleris auctorem horridi + capiamus, huc, huc fortis, armiferi cohors + conferte tela, vertite ex imo domum. + + + + Med. + Iam iam recepi sceptra germanum patrem, + spoliumque Colchi pecudis auratae tenent; + rediere regna, rapta virginitas redit, + o placida tandem numina, o festum diem, + o nuptialem! vade, perfectum est scelus, + vindicta nondum: perage, dum faciunt manus, + quid nunc moreris, anime? quid dubitas potens? + iam cecidit ira. paenitet facti, pudet, + quid, misera, feci? misera? paeniteat licet, + feci— voluptas magna me invitam subit, + et ecce crescit, derat hoc unum mihi, + spectator iste. nil adhuc facti reor: + quicquid sine isto fecimus sceleris perit. + + + + Ias. + En ipsa tecti parte praecipiti imminet. + huc rapiat ignes aliquis, ut flammis cadat + suis perusta. + + + Med. + Congere extremum tuis + ' natis, Iason, funus, ac tumulum strue: + coniunx socerque iusta iam functis habent, + a me sepulti; gnatus hic fatum tulit, + hic te vidente dabitur exitio pari. + + + + Ias. + Per numen omne perque communes fugas + + torosque, quos non nostra violavit fides, + iam parce nato. si quod est crimen, meum est: + me dedo morti; noxium macta caput. + + + + Med. + Hac qua recusas, qua doles, ferrum exigam. + i nunc, superbe, virginum thalamos pete, + relinque matres, + + + Ias. + Vnus est poenae satis. + + + + Med. + Si posset una caede satiari haec manus, + nullam petisset, ut duos perimam, tamen + nimium est dolori numerus angustus meo. + + in matre si quod pignus etiamnunc latet, + + + scrutabor ense viscera et ferro extraham. + + + + + Ias. + Iam perage coeptum facinus, haut ultra precor, + moramque saltem supplicis dona meis. + + + + Med. + Perfruere lento scelere, ne propera, dolor: + meus dies est; tempore accepto utimur. + + + + + Ias. + Infesta, memet perime. + + + Med. + Misereri iubes. + bene est, peractum est. plura non habui, dolor, + quae tibi litarem. lumina huc tumida alleva, + ingrate Iason, coniugem agnoscis tuam? + sic fugere soleo, patuit in caelum via: + squamosa gemini colla serpentes iugo + summissa praebent, recipe iam gnatoa, parens; + ego inter auras aliti curru vehar. + + + + Ias. + Per alta vade spatia sublimi aethere, + testare nullos esse, qua veneris, deos. + +
+ +
+
\ No newline at end of file