Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

211 rindas
6.6KB

  1. #!/usr/bin/python3
  2. import os, sys, re, time, datetime
  3. import logging
  4. log = logging.getLogger()
  5. class DatabaseError(Exception):
  6. pass
  7. class ItemNotFoundError(DatabaseError, LookupError):
  8. pass
  9. class ItemNameCheckError(DatabaseError, ValueError):
  10. pass
  11. class ItemExistsError(DatabaseError):
  12. pass
  13. class List(object):
  14. """Base class for database lists.
  15. This class provides a real-time view to a file-based database. It can be
  16. used as an iterable with a little bit of magic (like indexing by a
  17. dictionary of matching attributes).
  18. """
  19. def __init__(self, year, data_path, db=None):
  20. self._year = year
  21. self._path = os.path.expanduser(data_path.format(
  22. year=year, directory=self._directory))
  23. if not os.path.exists(self._path):
  24. os.makedirs(self._path, exist_ok=True)
  25. self._db = db
  26. log.debug("{0}: {1}".format(self.__class__.__name__, self._path))
  27. def _item_class(self):
  28. """Returns class object used to instantiate items.
  29. Override in subclasses.
  30. """
  31. return Item
  32. def _item_name(self):
  33. return self._item_class().__name__.lower()
  34. def __iter__(self):
  35. for name in os.listdir(self._path):
  36. match = self._regex.match(name)
  37. if match:
  38. yield self._item_class()(self, year=self._year, **match.groupdict())
  39. def last(self):
  40. return max(iter(self), key=lambda item: item._name)
  41. def __contains__(self, selector):
  42. return bool(self._select(selector))
  43. def __getitem__(self, selector):
  44. items = self.select(selector)
  45. assert(len(items) < 2)
  46. if not items:
  47. raise ItemNotFoundError("{0} '{1}' not found.".format(self._item_class().__name__, selector))
  48. item = items[0]
  49. log.debug("Found matching item: {0}".format(item))
  50. return item
  51. def select(self, selector=None):
  52. """Select items by multiple attributes specified in a selector dict.
  53. Non-dict selectors can be specialcased. See _select docs to
  54. find out built-in special cases.
  55. """
  56. if selector is not None:
  57. return sorted(self._select(selector))
  58. else:
  59. return [self.last()]
  60. def _select(self, selector):
  61. """Return a list of items matching 'name', 'number' or other attributes.
  62. This function specialcases string and int. Other specializations
  63. can be done in subclasses that should call super()._select() with
  64. a dict, str or int argument.
  65. """
  66. if isinstance(selector, str):
  67. selector = {"name": selector}
  68. elif isinstance(selector, int):
  69. selector = {"number": selector}
  70. log.debug("Selecting: {0}".format(selector))
  71. assert isinstance(selector, dict)
  72. return [item for item in self
  73. if all(getattr(item, key) == selector[key] for key in selector)]
  74. def new(self, name):
  75. """Create a new item in this list.
  76. Keyword arguments:
  77. name -- filesystem name of the new item
  78. edit -- edit the item after creation
  79. Returns
  80. """
  81. log.info("Creating {0}: {1}".format(self._item_name(), name))
  82. if not self._regex.match(name):
  83. raise ItemNameCheckError("Name {0} doesn't match {1} regex.".format(name, self._item_name()))
  84. if name in self:
  85. raise ItemExistsError("Item {0} of type {1} already exists.".format(name, self._item_name()))
  86. self._new(os.path.join(self._path, name))
  87. return self[name]
  88. def _new(self, path):
  89. log.debug("Creating {0} file: {1}".format(self._item_name(), path))
  90. stream = os.fdopen(os.open(path, os.O_WRONLY|os.O_EXCL|os.O_CREAT, 0o644), "w")
  91. stream.write(self.data_template)
  92. stream.close()
  93. class Item(object):
  94. """Base class for database list items."""
  95. def __init__(self, list_, **selector):
  96. self._list = list_
  97. self._selector = selector
  98. self._postprocess()
  99. self._name = self._list._template.format(**selector)
  100. self._path = os.path.join(list_._path, self._name)
  101. log.debug("{0!r}".format(self))
  102. def _postprocess(self):
  103. """Postprocess the _selector attribute.
  104. Override in subclasses.
  105. """
  106. def __lt__(self, other):
  107. return self._name < other._name
  108. def __repr__(self):
  109. return "{0}({1!r}, **{2})".format(self.__class__.__name__, self._name, self._selector)
  110. def __str__(self):
  111. return self._name
  112. def __getattr__(self, key):
  113. return self._selector[key]
  114. def delete(self):
  115. log.info("Deleting: {0}".format(self))
  116. path = self._path
  117. newpath = path + "~"
  118. log.debug("Renaming file {0} to {1}.".format(path, newpath))
  119. assert os.path.exists(path)
  120. os.rename(path, newpath)
  121. def data(self):
  122. """Return item's data.
  123. Override in subclasses.
  124. """
  125. return self._data_class()(self)
  126. def _data_class(self):
  127. return Data
  128. class Data(object):
  129. """Base class for database list item data objects."""
  130. _fields = []
  131. _multivalue_fields = []
  132. _line_regex = re.compile(r"^([A-Z][a-zA-Z-]*):\s+(.*?)\s+$")
  133. _comment_regex = re.compile(r"^\s*#")
  134. def __init__(self, item):
  135. self._item = item
  136. self._parse(open(self._item._path))
  137. self._postprocess()
  138. def __getattr__(self, key):
  139. return self._data[key]
  140. def _parse(self, stream):
  141. self._data = self._item._selector.copy()
  142. for f in self._fields:
  143. self._data[f] = None
  144. for f in self._multivalue_fields:
  145. self._data[f] = []
  146. for n, line in enumerate(stream):
  147. if self._comment_regex.match(line):
  148. continue
  149. match = self._line_regex.match(line)
  150. if not match:
  151. log.warning("Ignoring {0}:{1}: {2}".format(n, self._item._name, line))
  152. continue
  153. key, value = match.groups()
  154. key = key.lower().replace("-", "_")
  155. if key in self._fields:
  156. self._data[key] = value
  157. elif key in self._multivalue_fields:
  158. self._data[key].append(value)
  159. else:
  160. log.warning("Key ignored: {0}".format(key))
  161. def _postprocess(self):
  162. """Postprocess item data.
  163. Override in subclasses.
  164. """
  165. def rename_key(self, oldkey, newkey):
  166. """Convenience function mainly intended for subclasses."""
  167. if not self._data.get(newkey) and oldkey in self._data:
  168. self._data[newkey] = self._data[oldkey]
  169. del self._data[oldkey]