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