blob: 1de069381e6201c45c567686e86dffdec469798b [file] [log] [blame]
Scott Baker96b995a2017-02-15 16:21:12 -08001"""
2Django-like ORM layer for gRPC
3
4Usage:
5 api = ORMStub(stub)
6
7 api.Slices.all() ... list all slices
8
9 someSlice = api.Slices.get(id=1) ... get slice #1
10
11 someSlice.site ... automatically resolves site_id into a site object
12 someSlice.instances ... automatically resolves instances_ids into instance objects
13 someSlice.save() ... saves the slice object
14"""
15
16"""
17import grpc_client, orm
18c=grpc_client.SecureClient("xos-core.cord.lab", username="padmin@vicci.org", password="letmein")
19u=c.xos_orm.User.objects.get(id=1)
20"""
21
22import functools
Scott Bakeref8d85d2017-02-21 16:44:28 -080023import grpc
Scott Baker96b995a2017-02-15 16:21:12 -080024from google.protobuf.empty_pb2 import Empty
Scott Bakeref8d85d2017-02-21 16:44:28 -080025import time
Scott Baker96b995a2017-02-15 16:21:12 -080026
27from google.protobuf import symbol_database as _symbol_database
28_sym_db = _symbol_database.Default()
29
Scott Baker22796cc2017-02-23 16:53:34 -080030convenience_wrappers = {}
31
Scott Baker96b995a2017-02-15 16:21:12 -080032class ORMWrapper(object):
33 """ Wraps a protobuf object to provide ORM features """
34
35 def __init__(self, wrapped_class, stub, is_new=False):
36 super(ORMWrapper, self).__setattr__("_wrapped_class", wrapped_class)
37 super(ORMWrapper, self).__setattr__("stub", stub)
38 super(ORMWrapper, self).__setattr__("cache", {})
39 super(ORMWrapper, self).__setattr__("reverse_cache", {})
Scott Bakere72e7612017-02-20 10:07:09 -080040 super(ORMWrapper, self).__setattr__("poisoned", {})
Scott Baker96b995a2017-02-15 16:21:12 -080041 super(ORMWrapper, self).__setattr__("is_new", is_new)
42 fkmap=self.gen_fkmap()
43 super(ORMWrapper, self).__setattr__("_fkmap", fkmap)
44 reverse_fkmap=self.gen_reverse_fkmap()
45 super(ORMWrapper, self).__setattr__("_reverse_fkmap", reverse_fkmap)
46
47 def gen_fkmap(self):
48 fkmap = {}
49
50 for (name, field) in self._wrapped_class.DESCRIPTOR.fields_by_name.items():
51 if name.endswith("_id"):
52 foreignKey = field.GetOptions().Extensions._FindExtensionByName("xos.foreignKey")
53 fk = field.GetOptions().Extensions[foreignKey]
54 if fk:
55 fkmap[name[:-3]] = {"src_fieldName": name, "modelName": fk.modelName}
56
57 return fkmap
58
59 def gen_reverse_fkmap(self):
60 reverse_fkmap = {}
61
62 for (name, field) in self._wrapped_class.DESCRIPTOR.fields_by_name.items():
63 if name.endswith("_ids"):
64 reverseForeignKey = field.GetOptions().Extensions._FindExtensionByName("xos.reverseForeignKey")
65 fk = field.GetOptions().Extensions[reverseForeignKey]
66 if fk:
67 reverse_fkmap[name[:-4]] = {"src_fieldName": name, "modelName": fk.modelName}
68
69 return reverse_fkmap
70
71 def fk_resolve(self, name):
72 if name in self.cache:
Scott Baker22796cc2017-02-23 16:53:34 -080073 return make_ORMWrapper(self.cache[name], self.stub)
Scott Baker96b995a2017-02-15 16:21:12 -080074
75 fk_entry = self._fkmap[name]
76 id=self.stub.make_ID(id=getattr(self, fk_entry["src_fieldName"]))
77 dest_model = self.stub.invoke("Get%s" % fk_entry["modelName"], id)
78
79 self.cache[name] = dest_model
80
Scott Baker22796cc2017-02-23 16:53:34 -080081 return make_ORMWrapper(dest_model, self.stub)
Scott Baker96b995a2017-02-15 16:21:12 -080082
83 def reverse_fk_resolve(self, name):
84 if name not in self.reverse_cache:
85 fk_entry = self._reverse_fkmap[name]
86 self.cache[name] = ORMLocalObjectManager(self.stub, fk_entry["modelName"], getattr(self, fk_entry["src_fieldName"]))
87
88 return self.cache[name]
89
Scott Bakere72e7612017-02-20 10:07:09 -080090 def fk_set(self, name, model):
91 fk_entry = self._fkmap[name]
92 id = model.id
93 setattr(self._wrapped_class, fk_entry["src_fieldName"], id)
94
95 # XXX setting the cache here is a problematic, since the cached object's
96 # reverse foreign key pointers will not include the reference back
97 # to this object. Instead of setting the cache, let's poison the name
98 # and throw an exception if someone tries to get it.
99
100 # To work around this, explicitly call reset_cache(fieldName) and
101 # the ORM will reload the object.
102
103 self.poisoned[name] = True
104
Scott Baker96b995a2017-02-15 16:21:12 -0800105 def __getattr__(self, name, *args, **kwargs):
106 # note: getattr is only called for attributes that do not exist in
107 # self.__dict__
108
Scott Baker186372f2017-02-23 13:49:36 -0800109 # pk is a synonym for id
110 if (name == "pk"):
111 name = "id"
112
Scott Bakere72e7612017-02-20 10:07:09 -0800113 if name in self.poisoned.keys():
114 # see explanation in fk_set()
115 raise Exception("foreign key was poisoned")
116
Scott Baker96b995a2017-02-15 16:21:12 -0800117 if name in self._fkmap.keys():
118 return self.fk_resolve(name)
119
120 if name in self._reverse_fkmap.keys():
121 return self.reverse_fk_resolve(name)
122
123 return getattr(self._wrapped_class, name, *args, **kwargs)
124
125 def __setattr__(self, name, value):
Scott Bakere72e7612017-02-20 10:07:09 -0800126 if name in self._fkmap.keys():
127 self.fk_set(name, value)
128 elif name in self.__dict__:
Scott Baker96b995a2017-02-15 16:21:12 -0800129 super(ORMWrapper,self).__setattr__(name, value)
130 else:
131 setattr(self._wrapped_class, name, value)
132
133 def __repr__(self):
134 return self._wrapped_class.__repr__()
135
Scott Bakere72e7612017-02-20 10:07:09 -0800136 def invalidate_cache(self, name=None):
137 if name:
138 if name in self.cache:
139 del self.cache[name]
140 if name in self.reverse_cache:
141 del self.reverse_cache[name]
142 if name in self.poisoned:
143 del self.poisoned[name]
144 else:
145 self.cache.clear()
146 self.reverse_cache.clear()
147 self.poisoned.clear()
148
Scott Baker57c74822017-02-23 11:13:04 -0800149 def save(self, update_fields=None):
Scott Baker96b995a2017-02-15 16:21:12 -0800150 if self.is_new:
151 new_class = self.stub.invoke("Create%s" % self._wrapped_class.__class__.__name__, self._wrapped_class)
152 self._wrapped_class = new_class
153 self.is_new = False
154 else:
Scott Baker57c74822017-02-23 11:13:04 -0800155 metadata = []
156 if update_fields:
157 metadata.append( ("update_fields", ",".join(update_fields)) )
158 self.stub.invoke("Update%s" % self._wrapped_class.__class__.__name__, self._wrapped_class, metadata=metadata)
Scott Baker96b995a2017-02-15 16:21:12 -0800159
160 def delete(self):
161 id = self.stub.make_ID(id=self._wrapped_class.id)
162 self.stub.invoke("Delete%s" % self._wrapped_class.__class__.__name__, id)
163
Scott Baker22796cc2017-02-23 16:53:34 -0800164 def tologdict(self):
165 try:
166 d = {'model_name':self.__class__.__name__, 'pk': self.pk}
167 except:
168 d = {}
169
170 return d
171
Scott Bakerbb81e152017-03-02 15:28:36 -0800172 @property
173 def ansible_tag(self):
174 return "%s_%s" % (self._wrapped_class.__class__.__name__, self.id)
175
Scott Bakerb05393b2017-03-01 14:59:55 -0800176class ORMQuerySet(list):
177 """ Makes lists look like django querysets """
178 def first(self):
179 if len(self)>0:
180 return self[0]
181 else:
182 return None
183
Scott Baker96b995a2017-02-15 16:21:12 -0800184class ORMLocalObjectManager(object):
185 """ Manages a local list of objects """
186
187 def __init__(self, stub, modelName, idList):
188 self._stub = stub
189 self._modelName = modelName
190 self._idList = idList
191 self._cache = None
192
193 def resolve_queryset(self):
194 if self._cache is not None:
195 return self._cache
196
197 models = []
198 for id in self._idList:
199 models.append(self._stub.invoke("Get%s" % self._modelName, self._stub.make_ID(id=id)))
200
201 self._cache = models
202
203 return models
204
205 def all(self):
206 models = self.resolve_queryset()
Scott Baker22796cc2017-02-23 16:53:34 -0800207 return [make_ORMWrapper(x,self._stub) for x in models]
Scott Baker96b995a2017-02-15 16:21:12 -0800208
209class ORMObjectManager(object):
210 """ Manages a remote list of objects """
211
Scott Bakerac2f2b52017-02-21 14:53:23 -0800212 # constants better agree with common.proto
213 SYNCHRONIZER_DIRTY_OBJECTS = 2;
Scott Baker186372f2017-02-23 13:49:36 -0800214 SYNCHRONIZER_DELETED_OBJECTS = 3;
Scott Bakerac2f2b52017-02-21 14:53:23 -0800215
Scott Baker96b995a2017-02-15 16:21:12 -0800216 def __init__(self, stub, modelName, packageName):
217 self._stub = stub
218 self._modelName = modelName
219 self._packageName = packageName
220
221 def wrap_single(self, obj):
Scott Baker22796cc2017-02-23 16:53:34 -0800222 return make_ORMWrapper(obj, self._stub)
Scott Baker96b995a2017-02-15 16:21:12 -0800223
224 def wrap_list(self, obj):
225 result=[]
226 for item in obj.items:
Scott Baker22796cc2017-02-23 16:53:34 -0800227 result.append(make_ORMWrapper(item, self._stub))
Scott Bakerb05393b2017-03-01 14:59:55 -0800228 return ORMQuerySet(result)
Scott Baker96b995a2017-02-15 16:21:12 -0800229
230 def all(self):
231 return self.wrap_list(self._stub.invoke("List%s" % self._modelName, Empty()))
232
Scott Baker22796cc2017-02-23 16:53:34 -0800233 def first(self):
234 objs=self.wrap_list(self._stub.invoke("List%s" % self._modelName, Empty()))
235 if not objs:
236 return None
237 return objs[0]
238
Scott Bakerac2f2b52017-02-21 14:53:23 -0800239 def filter(self, **kwargs):
240 q = self._stub.make_Query()
241 q.kind = q.DEFAULT
242
243 for (name, val) in kwargs.items():
244 el = q.elements.add()
245
246 if name.endswith("__gt"):
247 name = name[:-4]
248 el.operator = el.GREATER_THAN
249 elif name.endswith("__gte"):
250 name = name[:-5]
251 el.operator = el.GREATER_THAN_OR_EQUAL
252 elif name.endswith("__lt"):
253 name = name[:-4]
254 el.operator = el.LESS_THAN
255 elif name.endswith("__lte"):
256 name = name[:-5]
257 el.operator = el.LESS_THAN_OR_EQUAL
258 else:
259 el.operator = el.EQUAL
260
261 el.name = name
262 if isinstance(val, int):
263 el.iValue = val
264 else:
265 el.sValue = val
266
267 return self.wrap_list(self._stub.invoke("Filter%s" % self._modelName, q))
268
269 def filter_special(self, kind):
270 q = self._stub.make_Query()
271 q.kind = kind
272 return self.wrap_list(self._stub.invoke("Filter%s" % self._modelName, q))
273
Scott Baker22796cc2017-02-23 16:53:34 -0800274 def get(self, **kwargs):
275 if kwargs.keys() == ["id"]:
276 # the fast and easy case, look it up by id
277 return self.wrap_single(self._stub.invoke("Get%s" % self._modelName, self._stub.make_ID(id=kwargs["id"])))
278 else:
279 # the slightly more difficult case, filter and return the first item
280 objs = self.filter(**kwargs)
281 return objs[0]
Scott Baker96b995a2017-02-15 16:21:12 -0800282
283 def new(self, **kwargs):
284 full_model_name = "%s.%s" % (self._packageName, self._modelName)
285 cls = _sym_db._classes[full_model_name]
Scott Baker22796cc2017-02-23 16:53:34 -0800286 return make_ORMWrapper(cls(), self._stub, is_new=True)
Scott Baker96b995a2017-02-15 16:21:12 -0800287
288class ORMModelClass(object):
289 def __init__(self, stub, model_name, package_name):
Scott Baker22796cc2017-02-23 16:53:34 -0800290 self.model_name = model_name
Scott Baker96b995a2017-02-15 16:21:12 -0800291 self.objects = ORMObjectManager(stub, model_name, package_name)
292
Scott Bakerbb81e152017-03-02 15:28:36 -0800293 @property
Scott Baker22796cc2017-02-23 16:53:34 -0800294 def __name__(self):
295 return self.model_name
296
Scott Baker96b995a2017-02-15 16:21:12 -0800297class ORMStub(object):
Scott Baker22796cc2017-02-23 16:53:34 -0800298 def __init__(self, stub, package_name, invoker=None, caller_kind="grpcapi"):
Scott Baker96b995a2017-02-15 16:21:12 -0800299 self.grpc_stub = stub
300 self.all_model_names = []
Scott Bakeref8d85d2017-02-21 16:44:28 -0800301 self.invoker = invoker
Scott Baker22796cc2017-02-23 16:53:34 -0800302 self.caller_kind = caller_kind
Scott Baker96b995a2017-02-15 16:21:12 -0800303
304 for name in dir(stub):
305 if name.startswith("Get"):
306 model_name = name[3:]
307 setattr(self,model_name, ORMModelClass(self, model_name, package_name))
308
309 self.all_model_names.append(model_name)
310
311 def listObjects(self):
312 return self.all_model_names
313
Scott Baker22796cc2017-02-23 16:53:34 -0800314 def add_default_metadata(self, metadata):
315 default_metadata = [ ("caller_kind", self.caller_kind) ]
316
317 # build up a list of metadata keys we already have
318 md_keys=[x[0] for x in metadata]
319
320 # add any defaults that we don't already have
321 for md in default_metadata:
322 if md[0] not in md_keys:
323 metadata.append( (md[0], md[1]) )
324
Scott Baker57c74822017-02-23 11:13:04 -0800325 def invoke(self, name, request, metadata=[]):
Scott Baker22796cc2017-02-23 16:53:34 -0800326 self.add_default_metadata(metadata)
327
Scott Bakeref8d85d2017-02-21 16:44:28 -0800328 if self.invoker:
329 # Hook in place to call Chameleon's invoke method, as soon as we
330 # have rewritten the synchronizer to use reactor.
331 return self.invoker.invoke(self.grpc_stub.__class__, name, request, metadata={}).result[0]
332 else:
333 # Our own retry mechanism. This works fine if there is a temporary
334 # failure in connectivity, but does not re-download gRPC schema.
335 while True:
336 backoff = [0.5, 1, 2, 4, 8]
337 try:
338 method = getattr(self.grpc_stub, name)
Scott Baker57c74822017-02-23 11:13:04 -0800339 return method(request, metadata=metadata)
Scott Bakeref8d85d2017-02-21 16:44:28 -0800340 except grpc._channel._Rendezvous, e:
341 code = e.code()
342 if code == grpc.StatusCode.UNAVAILABLE:
343 if not backoff:
344 raise Exception("No more retries on %s" % name)
345 time.sleep(backoff.pop(0))
346 else:
347 raise
348 except:
349 raise
Scott Baker96b995a2017-02-15 16:21:12 -0800350
351 def make_ID(self, id):
352 return _sym_db._classes["xos.ID"](id=id)
353
Scott Bakerac2f2b52017-02-21 14:53:23 -0800354 def make_Query(self):
355 return _sym_db._classes["xos.Query"]()
356
Scott Baker22796cc2017-02-23 16:53:34 -0800357def register_convenience_wrapper(class_name, wrapper):
358 global convenience_wrappers
Scott Baker96b995a2017-02-15 16:21:12 -0800359
Scott Baker22796cc2017-02-23 16:53:34 -0800360 convenience_wrappers[class_name] = wrapper
361
362def make_ORMWrapper(wrapped_class, *args, **kwargs):
363 if wrapped_class.__class__.__name__ in convenience_wrappers:
364 cls = convenience_wrappers[wrapped_class.__class__.__name__]
365 else:
366 cls = ORMWrapper
367
368 return cls(wrapped_class, *args, **kwargs)
369
370import convenience.instance
Scott Baker96b995a2017-02-15 16:21:12 -0800371