25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

194 lines
6.0KB

  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. return sorted(self._select(selector))
  51. def _select(self, selector):
  52. """Return a list of items matching 'name', 'number' or other attributes."""
  53. if selector == None:
  54. selector = {}
  55. if isinstance(selector, str):
  56. selector = {"name": selector}
  57. if isinstance(selector, int):
  58. selector = {"number": selector}
  59. log.debug("Selecting: {}".format(selector))
  60. assert isinstance(selector, dict)
  61. return [item for item in self if all(getattr(item, key) == selector[key] for key in selector)]
  62. def new(self, name):
  63. """Create a new item in this list.
  64. Keyword arguments:
  65. name -- filesystem name of the new item
  66. edit -- edit the item after creation
  67. Returns
  68. """
  69. log.info("Creating {}: {}".format(self._item_name(), name))
  70. if not self._regex.match(name):
  71. raise ItemNameCheckError("Name {} doesn't match {} regex.".format(name, self._item_name()))
  72. if name in self:
  73. raise ItemExistsError("Item {} of type {} already exists.".format(name, self._item_name()))
  74. self._new(os.path.join(self._path, name))
  75. return self[name]
  76. def _new(self, path):
  77. log.debug("Creating {} file: {}".format(self._item_name(), path))
  78. stream = os.fdopen(os.open(path, os.O_WRONLY|os.O_EXCL|os.O_CREAT, 0o644), "w")
  79. stream.write(self.data_template)
  80. stream.close()
  81. class Item:
  82. """Base class for database list items."""
  83. def __init__(self, list_, **selector):
  84. self._list = list_
  85. self._selector = selector
  86. self._postprocess()
  87. self._name = self._list._template.format(**selector)
  88. self._path = os.path.join(list_._path, self._name)
  89. log.debug("{!r}".format(self))
  90. def _postprocess(self):
  91. """Postprocess the _selector attribute.
  92. Override in subclasses.
  93. """
  94. def __lt__(self, other):
  95. return self._name < other._name
  96. def __repr__(self):
  97. return "{}({!r}, **{})".format(self.__class__.__name__, self._name, self._selector)
  98. def __str__(self):
  99. return self._name
  100. def __getattr__(self, key):
  101. return self._selector[key]
  102. def delete(self):
  103. log.info("Deleting: {}".format(self))
  104. path = self._path
  105. newpath = path + "~"
  106. log.debug("Renaming file {} to {}.".format(path, newpath))
  107. assert os.path.exists(path)
  108. os.rename(path, newpath)
  109. def data(self):
  110. """Return item's data.
  111. Override in subclasses.
  112. """
  113. return self._data_class()(self)
  114. def _data_class(self):
  115. return Data
  116. class Data:
  117. """Base class for database list item data objects."""
  118. _fields = []
  119. _multivalue_fields = []
  120. _line_regex = re.compile(r"^([A-Z][a-zA-Z-]*):\s+(.*?)\s+$")
  121. def __init__(self, item):
  122. self._item = item
  123. self._parse(open(self._item._path))
  124. self._postprocess()
  125. def __getattr__(self, key):
  126. return self._data[key]
  127. def _parse(self, stream):
  128. self._data = self._item._selector.copy()
  129. for f in self._fields:
  130. self._data[f] = None
  131. for f in self._multivalue_fields:
  132. self._data[f] = []
  133. for n, line in enumerate(stream):
  134. match = self._line_regex.match(line)
  135. if not match:
  136. log.warning("Ignoring {}:{}: {}".format(n, self._item._name, line))
  137. continue
  138. key, value = match.groups()
  139. key = key.lower().replace("-", "_")
  140. if key in self._fields:
  141. self._data[key] = value
  142. elif key in self._multivalue_fields:
  143. self._data[key].append(value)
  144. else:
  145. log.warning("Key ignored: {}".format(key))
  146. def _postprocess(self):
  147. """Postprocess item data.
  148. Override in subclasses.
  149. """
  150. def rename_key(self, oldkey, newkey):
  151. """Convenience function mainly intended for subclasses."""
  152. if not self.__dict__.get(newkey) and oldkey in self._data:
  153. self._data[newkey] = self._data[oldkey]
  154. del self._data[oldkey]