Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

206 рядки
6.4KB

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