summaryrefslogtreecommitdiff
path: root/ick2/persistent.py
diff options
context:
space:
mode:
authorLars Wirzenius <liw@exolobe1>2018-05-13 15:30:23 +0300
committerLars Wirzenius <liw@liw.fi>2018-05-17 21:44:59 +0300
commitb11d31ef23c5dfee6bfa54afbec47fc8b8bab7b1 (patch)
tree2e6b085f8fb023d53c8ac20a97aef2c7d1c11d4b /ick2/persistent.py
parent531dd2c50bfdfcf50bb37f57cf9fc2b69787adcf (diff)
downloadick2-b11d31ef23c5dfee6bfa54afbec47fc8b8bab7b1.tar.gz
Change: how controller stores persistent data
Replace old State class with new FilePersistentState and TransactionalState classes. Use new Resource class instead of raw dicts. Use context managers for creating, updating resources, to avoid mistakes from accidentally not saving changes. Overall persistence should now be rather simpler. This should open up a possibility for changing the controller to insert more actions into the build graph, to trigger notifcations via the workers.
Diffstat (limited to 'ick2/persistent.py')
-rw-r--r--ick2/persistent.py153
1 files changed, 153 insertions, 0 deletions
diff --git a/ick2/persistent.py b/ick2/persistent.py
new file mode 100644
index 0000000..4b216b3
--- /dev/null
+++ b/ick2/persistent.py
@@ -0,0 +1,153 @@
+# Copyright (C) 2018 Lars Wirzenius
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import copy
+import os
+import urllib.parse
+
+
+import yaml
+
+
+import ick2
+
+
+class PersistentStateInterface: # pragma: no cover
+
+ def get_resource_kinds(self):
+ raise NotImplementedError()
+
+ def get_resource_ids(self, kind):
+ raise NotImplementedError()
+
+ def has_resource(self, kind, rid):
+ raise NotImplementedError()
+
+ def get_resource(self, kind, rid):
+ raise NotImplementedError()
+
+ def get_resources(self, kind):
+ return [
+ self.get_resource(kind, rid)
+ for rid in self.get_resource_ids(kind)
+ ]
+
+ def write_resource(self, kind, rid, resource):
+ raise NotImplementedError()
+
+ def remove_resource(self, kind, rid):
+ raise NotImplementedError()
+
+
+class FilePersistentState(PersistentStateInterface):
+
+ def __init__(self):
+ self._dir = None
+
+ def get_directory(self):
+ return self._dir
+
+ def set_directory(self, dirname):
+ self._dir = dirname
+
+ def _safe(self, name):
+ return urllib.parse.quote(name, safe='')
+
+ def _unsafe(self, safe):
+ return urllib.parse.unquote(safe)
+
+ def _unsafe_list(self, safe_names):
+ return [self._unsafe(safe) for safe in safe_names]
+
+ def _dirname(self, kind):
+ return os.path.join(self._dir, self._safe(kind))
+
+ def _filename(self, kind, rid):
+ dirname = self._dirname(kind)
+ return os.path.join(dirname, self._safe(rid))
+
+ def get_resource_kinds(self):
+ return self._unsafe_list(os.listdir(self._dir))
+
+ def has_resource(self, kind, rid):
+ filename = self._filename(kind, rid)
+ return os.path.exists(filename)
+
+ def get_resource_ids(self, kind):
+ dirname = self._dirname(kind)
+ if os.path.exists(dirname):
+ return self._unsafe_list(os.listdir(dirname))
+ return []
+
+ def get_resource(self, kind, rid):
+ filename = self._filename(kind, rid)
+ if not os.path.exists(filename):
+ raise ick2.NotFound()
+ with open(filename, 'r') as f:
+ as_dict = yaml.safe_load(f)
+ return resource_from_dict(as_dict)
+
+ def write_resource(self, kind, rid, resource):
+ dirname = self._dirname(kind)
+ if not os.path.exists(dirname):
+ os.makedirs(dirname)
+
+ filename = self._filename(kind, rid)
+ with open(filename, 'w') as f:
+ yaml.safe_dump(resource.as_dict(), stream=f)
+
+ def remove_resource(self, kind, rid):
+ filename = self._filename(kind, rid)
+ os.remove(filename)
+
+
+class NotFound(Exception):
+
+ def __init__(self):
+ super().__init__('Resource not found')
+
+
+class Resource: # pragma: no cover
+
+ def __init__(self, as_dict=None):
+ self._dict = copy.deepcopy(as_dict or {})
+
+ def as_dict(self):
+ return copy.deepcopy(self._dict)
+
+ def __getitem__(self, key):
+ return self._dict[key]
+
+ def __setitem__(self, key, value):
+ self._dict[key] = value
+
+ def __contains__(self, key):
+ return key in self._dict
+
+ def __len__(self):
+ return len(self._dict)
+
+ def get(self, key, default=None):
+ return self._dict.get(key, default)
+
+ def from_dict(self, as_dict):
+ self._dict.clear()
+ for key in as_dict:
+ self[key] = as_dict[key]
+
+
+def resource_from_dict(as_dict): # pragma: no cover
+ return Resource(as_dict)