diff options
Diffstat (limited to 'ick2/persistent.py')
-rw-r--r-- | ick2/persistent.py | 150 |
1 files changed, 150 insertions, 0 deletions
diff --git a/ick2/persistent.py b/ick2/persistent.py new file mode 100644 index 0000000..1d79e3d --- /dev/null +++ b/ick2/persistent.py @@ -0,0 +1,150 @@ +# Copyright (C) 2018-2019 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_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 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(kind=kind, rid=rid) + with open(filename, 'r') as f: + as_dict = yaml.load(f, Loader=yaml.CSafeLoader) + 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.dump( + resource.as_dict(), stream=f, Dumper=yaml.CSafeDumper) + + def remove_resource(self, kind, rid): + filename = self._filename(kind, rid) + os.remove(filename) + + +class NotFound(Exception): + + def __init__(self, kind, rid): + super().__init__( + 'Resource {}:{} not found'.format( + kind or "unknown", rid or "unknown")) + + +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) |