Source code for timelink.kleio.groups.kelement

from typing import Any

from box import Box

from timelink.kleio.utilities import quote_long_text


[docs] class KElement: """Represents an Element in Kleio language. While *Groups* represent historical entities (people, objects, events) *Elements* encapsulate basic items of information (name, gender, date). The value of an Element can have three possible "aspects": 1) "core": the actual information for the element 2) "original" (optional), the original wording when relevant 3) "comment" (optional), a comment of the value. Example in Kleio notation:: person$Joaquim Carvalho%Joachim CarvÂș#Family name added in the margin Can be generated by :: n = KElement('name','Joaquim Carvalho',original='Joachim CarvÂș', comment='Family name added in the margin') id = KElement('id','p-jrc') person = KPerson(id=id,name=n) TODO: does not deal with multiple values in elements. Check KElement in MHK where core, comment and original are Vectors better to create a KAspect class. """ name: str = None core: Any # must have a str representation. comment: str = None original: str = None _element_class = None @property def element_class(self): """This return _element_class if existing or name if not.""" if self._element_class is not None: return self._element_class else: return self.__class__.name @element_class.setter def element_class(self, value): self._element_class = value pass def __init__( self, name: str = None, core: Any = None, comment=None, original=None, element_class=None, ): """ Args: name: name of the Element. If None then self._name is used. core: the core aspect of the Element. Must have __str__ or a tuple (core,comment,original). If a tuple optional arguments are disregarded. comment: Optional; The comment aspect of the Element. original: Optional; The original aspect of the Element. element_class: Optional; in groups from kleio translations this is set by the translator from the the source=parameter. of the str file. If absent it is set here equal to the name of the element. """ if name is not None: self.name = name self.core = None self.comment = None self.original = None if type(core) is tuple and len(core) == 3: self.core = core[0] self.comment = core[1] self.original = core[2] else: self.core = core if comment is not None: self.comment = comment if original is not None: self.original = original if element_class is not None: # if element_class is not None then check if exists and store kclass = self.__class__.get_class_for(element_class) if kclass is not None: self._element_class = kclass else: # if element_class does not exist create a new class self._element_class = self.__class__.extend(name) def __str__(self): return self.core def __int__(self): return int(str(self))
[docs] @classmethod def extend(cls, name: str): """ Creates a new KElement class that extends this one. This allows creating new KElement names for different languages and keep the behaviour of a specific element type (e.g. mapping "tipo" elements behave like "type" elements). When an element of a group is set with an atomic value or a tuple, a new KElement of the class with the same name of the element in the group is used to store the group. :param name: :return: """ new_kelement = type(name, (cls,), {}) new_kelement.name = name return new_kelement
[docs] @classmethod def get_subclasses(cls): """Generator for subclasses of this KElement""" for subclass in cls.__subclasses__(): yield from subclass.get_subclasses() yield subclass
[docs] @classmethod def all_subclasses(cls): """List of all the subclasses of this KElement""" return list(cls.get_subclasses())
[docs] @classmethod def get_class_for(cls, name: str): """ Search in KElement subclasses and return the one with the same element name as the argument. if more than one apply return the more specialized (longer __mro__) If not found return None :param name: name of an element :return: KElement or a subclass """ search_list = sorted( [(a, len(a.__mro__)) for a in cls.all_subclasses()], key=lambda mro: -mro[1] ) for eclass, _mro in search_list: if eclass.name == name: return eclass return None
@classmethod def inherits_from(cls): return [ke for ke in cls.__mro__ if ke is not object and ke is not None]
[docs] @classmethod def get_classes_for(cls, name: str): """Search in Element subclass and return all that have the same element name as the argument see KElement.get_class_for(name) for getting the more specialized""" return [sc for sc in KElement.all_subclasses() if sc.name == name]
[docs] def inherited_names(self): """Return the list of names in the KElement subclasses this element inherits from including its own""" return [ ke.name for ke in self.__class__.__mro__ if ke is not object and ke.name is not None ]
[docs] def extends(self, name: str): """True if name is in the the classes this element inherits from""" return name in self.inherited_names()
[docs] def is_empty(self): """True if all aspects of the element are None or empty string""" e = [ x for x in [self.core, self.comment, self.original] if x is None or x == "" ] if len(e) == 3: return True else: return False
[docs] def to_tuple(self): """Return Element as a tuple (core,comment,original)""" return self.core, self.comment, self.original
[docs] def to_kleio(self, name=True, force_show=False): """ Return element as a kleio string: element=core#comment%original To avoid rendering the element name set name=False :param name: if True(default) prefix value with self.name= :return: a valid Kleio representation of this element value """ if self.extends("invisible_") and force_show is False: return "" if self.is_empty(): return "" c = self.core cc = self.comment o = self.original if c is None or c == "": c = "" else: c = quote_long_text(str(c)) if cc is None or cc == "": cc = "" else: cc = "#" + quote_long_text(str(cc)) if o is None or o == "": o = "" else: o = "%" + quote_long_text(str(o)) kleio = c + cc + o if name: kleio = f"{self.name}={kleio}" return kleio
[docs] def to_dict(self, name=False): """Return Element as a dict {core:_, comment:_, original:_} add name=True to add name to dictionary: {name:_, core:_, comment:_, original:_}""" if name: return { "name": self.name, "core": self.core, "comment": self.comment, "original": self.original, } else: return { "core": self.core, "comment": self.comment, "original": self.original, }
def to_dots(self): return Box(self.to_dict())
# use this for mixins to mark this should show in Kleio # still can override with KElement.to_kleio(force_show=True KleioNoShow = KElement.extend("invisible_") # Default KElement classes gacto2.str # element name=id; identification=yes # element name=type # element name=loc # element name=obs # element name=ref # element name=value # element name=origin # element name=destination # element name=entity # element name=same_as # element name=xsame_as # element name=name # element name=sex # element name=destname # element name=destination # element name=summary; # element name=description; # element name=replace;
[docs] class KDate(KElement): name = "date" def __init__( self, date: Any = None, core=None, comment=None, original=None, element_class=None, ): if core is not None: date = core # to allow core setting in generic code super().__init__(self.name, date, comment, original, element_class)
[docs] class KDay(KElement): name = "day" def __init__( self, day: Any = None, core=None, comment=None, original=None, element_class=None, ): if core is not None: day = core # to allow core setting in generic code if day is None: day = 0 super().__init__(self.name, day, comment, original, element_class) if type(self.core) is str: self.core = int(self.core) if self.core != 0 and (self.core < 1 or self.core > 31): raise ValueError("Day value must be between 1 and 31")
[docs] class KMonth(KElement): name = "month" def __init__( self, month: Any = None, core=None, comment=None, original=None, element_class=None, ): if core is not None: month = core # to allow core if month is None: month = 0 super().__init__(self.name, month, comment, original, element_class) self.core = int(self.core) if type(self.core) is str: self.core = int(self.core) if self.core != 0 and (self.core < 1 or self.core > 12): raise ValueError("Month value must be between 1 and 12")
[docs] class KYear(KElement): """ Represents a year. To have value checking do KYear.set_limits((lower,upper)) !!!! """ name = "year" def __init__(self, year: Any = None, core=None, comment=None, original=None): if core is not None: year = core if year is None: year = 0 super().__init__(self.name, year, comment, original) try: self.core = int(self.core) except ValueError: # warn of value error # this is a hack because many sources do not use the second position as year, and # all sort of data gets there self.core = core
[docs] class KType(KElement): """ Represents a type of object or abstraction """ name = "type"
[docs] class KValue(KElement): """ Represents a general purpose value """ name = "value"
[docs] class KId(KElement): """ Represents an unique id for a group. """ name = "id"
[docs] class KEntityInAttribute(KId, KleioNoShow): name = "entity"
[docs] class KOriginInRel(KId, KleioNoShow): name = "origin"
[docs] class KReplace(KElement): """ Represents the id of a group to be replaced. Example: source$new-id/replace=old-id """ name = "replace"
[docs] class KSameAs(KElement): r""" Represents the id of a group that describes the same real world entity has the one with this element. Used in the same file. Translators should check if the id corresponds to a group in the same file and file an error otherwise Examples: person$Bob Dylan/id=bob-dylan person$Robert Allan Zimmerman/sameas=bob-dylan """ name = "same_as"
[docs] class KXSameAs(KElement): """ Same meaning as KSameAs used when id is not in the file. The difference between KSameAs and KXSameAs is just for error checking during translation. Translators will raise error if a KSameAs id is not found in the same file, but only a warning for KXSameAs. """ name = "xsame_as"
[docs] class KName(KElement): """ Name of person """ name = "name"
[docs] class KSex(KElement): """ male / female ... """ name = "sex"
[docs] class KDescription(KElement): """ Similar to name, for objects """ name = "description"
[docs] class KObs(KElement): """ Element for "obs" normally observations or notes """ name = "obs"
[docs] class KStructure(KElement): """ Element for structure name in sources """ name = "structure"
[docs] class KLoc(KElement): """ Element for location (in a document, e.g. page) in some groups """ name = "loc"
[docs] class KRef(KElement): """ Element for reference number (e.g. a call number in an archive) """ name = "ref"
[docs] class KDestName(KElement): """ Element for destination names in relations """ name = "destname"
[docs] class KDestId(KElement): """ Element for destination id in relations """ name = "destination"
[docs] class KSummary(KElement): """ Element for summaries (long texts) """ name = "summary"
[docs] class KReplaceSourceId(KElement): """ Element for id of source being replaced in source groups """ name = "replace"
# Automatic elements, generated during the translation process
[docs] class KKleiofile(KElement): """ Element for the name of the file from which the source was imported """ name = "kleiofile"
[docs] class KGroupName(KElement): """ Element for groupname of a group """ name = "groupname"
[docs] class KClass(KElement): """ Element for the class of a group """ name = "class"
[docs] class KLevel(KElement): """ Element for nesting level in imported groups """ name = "level"
[docs] class KLine(KElement): """ Element for line number in imported groups """ name = "line"
[docs] class KOrder(KElement): """ Element for group order in imported groups """ name = "order"
[docs] class KInside(KElement): """ Element for id of the enclosing group """ name = "inside"
[docs] class KUndef(KElement): """ Element of undefined class (generated by the kleio translator) This is generated when the translator finds an element for which the is no mapping POM_SOM in any class. An example if the elements for day,month,year that are used for input but are stored as a single value YYYYMMDD and so are not present in the mapping information for database storage. """ name = "undef"