Added autostore convenience type.
[didex.git] / didex / store.py
1 import threading, pickle
2 from . import db, index, cache
3 from .db import txnfun
4
5 class environment(object):
6     def __init__(self, *, path=None, getpath=None, recover=False):
7         if path is not None:
8             self.path = path
9             self.getpath = None
10         else:
11             self.path = None
12             self.getpath = getpath
13         self.recover = recover
14         self.lk = threading.Lock()
15         self.bk = None
16
17     def __call__(self):
18         with self.lk:
19             if self.bk is None:
20                 if self.path is None:
21                     self.path = self.getpath()
22                 self.bk = db.environment(self.path, recover=self.recover)
23             return self.bk
24
25     def close(self):
26         with self.lk:
27             if self.bk is not None:
28                 self.bk.close()
29                 self.bk = None
30
31 class storedesc(object):
32     pass
33
34 def storedescs(obj):
35     t = type(obj)
36     ret = getattr(t, "__didex_attr", None)
37     if ret is None:
38         ret = []
39         for nm, val in t.__dict__.items():
40             if isinstance(val, storedesc):
41                 ret.append((nm, val))
42         t.__didex_attr = ret
43     return ret
44
45 class store(object):
46     def __init__(self, name, *, env=None, path=".", ncache=None):
47         self.name = name
48         self.lk = threading.Lock()
49         if env:
50             self.env = env
51         else:
52             self.env = environment(path=path)
53         self._db = None
54         if ncache is None:
55             ncache = cache.cache()
56         self.cache = ncache
57         self.cache.load = self._load
58
59     def db(self):
60         with self.lk:
61             if self._db is None:
62                 self._db = self.env().db(self.name)
63             return self._db
64
65     def _load(self, id):
66         try:
67             return pickle.loads(self.db().get(id))
68         except:
69             raise KeyError(id, "could not unpickle data")
70
71     def _encode(self, obj):
72         return pickle.dumps(obj)
73
74     def get(self, id, *, load=True):
75         return self.cache.get(id, load=load)
76
77     @txnfun(lambda self: self.db().env.env)
78     def register(self, obj, *, tx):
79         id = self.db().add(self._encode(obj), tx=tx)
80         for nm, attr in storedescs(obj):
81             attr.register(id, obj, tx)
82         self.cache.put(id, obj)
83         return id
84
85     @txnfun(lambda self: self.db().env.env)
86     def unregister(self, id, *, vfy=None, tx):
87         obj = self.get(id)
88         if vfy is not None and obj is not vfy:
89             raise RuntimeError("object identity crisis: " + str(vfy) + " is not cached object " + obj)
90         for nm, attr in storedescs(obj):
91             attr.unregister(id, obj, tx)
92         self.db().remove(id, tx=tx)
93         self.cache.remove(id)
94
95     @txnfun(lambda self: self.db().env.env)
96     def update(self, id, *, vfy=None, tx):
97         obj = self.get(id, load=False)
98         if vfy is not None and obj is not vfy:
99             raise RuntimeError("object identity crisis: " + str(vfy) + " is not cached object " + obj)
100         for nm, attr, in storedescs(obj):
101             attr.update(id, obj, tx)
102         self.db().replace(id, self._encode(obj), tx=tx)
103
104 class autotype(type):
105     def __call__(self, *args, **kwargs):
106         new = super().__call__(*args, **kwargs)
107         new.id = self.store.register(new)
108         return new
109
110 class autostore(object, metaclass=autotype):
111     def __init__(self):
112         self.id = None
113
114     def save(self):
115         self.store.update(self.id, vfy=self)
116
117     def remove(self):
118         self.store.unregister(self.id, vfy=self)
119         self.id = None