blob: fccee090a29a1fe4ec8664d42e9822bd80ccb94b [file] [log] [blame]
Matteo Scandolod2044a42017-08-07 16:08:28 -07001# Copyright 2017-present Open Networking Foundation
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Zack Williams5c2ea232019-01-30 15:23:01 -070015from __future__ import absolute_import, print_function
16
17import imp
Zack Williams045b63d2019-01-22 16:30:57 -070018import os
19import sys
20import threading
21import time
Andy Bavier04ee1912019-01-30 14:17:16 -070022import traceback
Zack Williams5c2ea232019-01-30 15:23:01 -070023
Zack Williams045b63d2019-01-22 16:30:57 -070024from multistructlog import create_logger
Zack Williams5c2ea232019-01-30 15:23:01 -070025from xosconfig import Config
Matteo Scandolod2044a42017-08-07 16:08:28 -070026
Scott Baker96b995a2017-02-15 16:21:12 -080027"""
28Django-like ORM layer for gRPC
29
30Usage:
31 api = ORMStub(stub)
32
33 api.Slices.all() ... list all slices
34
35 someSlice = api.Slices.get(id=1) ... get slice #1
36
37 someSlice.site ... automatically resolves site_id into a site object
38 someSlice.instances ... automatically resolves instances_ids into instance objects
39 someSlice.save() ... saves the slice object
40"""
41
42"""
43import grpc_client, orm
44c=grpc_client.SecureClient("xos-core.cord.lab", username="padmin@vicci.org", password="letmein")
Scott Bakera1eae7a2017-06-06 09:20:15 -070045u=c.xos_orm.User.objects.get(id=1)
46"""
Scott Baker96b995a2017-02-15 16:21:12 -080047
Zack Williams045b63d2019-01-22 16:30:57 -070048log = create_logger(Config().get("logging"))
Scott Baker96b995a2017-02-15 16:21:12 -080049
Scott Baker22796cc2017-02-23 16:53:34 -080050convenience_wrappers = {}
51
Zack Williams5c2ea232019-01-30 15:23:01 -070052
Andy Bavier04ee1912019-01-30 14:17:16 -070053def get_synchronizer_function():
Zack Williams5c2ea232019-01-30 15:23:01 -070054 """
55 Find the topmost synchronizer-specific function in the call stack
56 """
Andy Bavier04ee1912019-01-30 14:17:16 -070057 result = None
Zack Williams5c2ea232019-01-30 15:23:01 -070058 for file, line, func, stmt in traceback.extract_stack():
Andy Bavier04ee1912019-01-30 14:17:16 -070059 if file.startswith("/opt/xos/synchronizers"):
60 if not result:
Zack Williams5c2ea232019-01-30 15:23:01 -070061 result = "%s:%s()" % (file, func)
Andy Bavier04ee1912019-01-30 14:17:16 -070062 if not file.startswith("/opt/xos/synchronizers/new_base"):
Zack Williams5c2ea232019-01-30 15:23:01 -070063 result = "%s:%s()" % (file, func)
Andy Bavier04ee1912019-01-30 14:17:16 -070064 break
65 return result
Zack Williams045b63d2019-01-22 16:30:57 -070066
Zack Williams5c2ea232019-01-30 15:23:01 -070067
Scott Bakerd0f1dc12018-04-23 12:05:32 -070068class ORMGenericContentNotFoundException(Exception):
69 pass
70
Zack Williams045b63d2019-01-22 16:30:57 -070071
Scott Bakerd0f1dc12018-04-23 12:05:32 -070072class ORMGenericObjectNotFoundException(Exception):
73 pass
74
Zack Williams045b63d2019-01-22 16:30:57 -070075
Scott Baker96b995a2017-02-15 16:21:12 -080076class ORMWrapper(object):
77 """ Wraps a protobuf object to provide ORM features """
78
79 def __init__(self, wrapped_class, stub, is_new=False):
80 super(ORMWrapper, self).__setattr__("_wrapped_class", wrapped_class)
81 super(ORMWrapper, self).__setattr__("stub", stub)
82 super(ORMWrapper, self).__setattr__("cache", {})
83 super(ORMWrapper, self).__setattr__("reverse_cache", {})
Sapan Bhatia71f57682017-08-23 20:09:08 -040084 super(ORMWrapper, self).__setattr__("synchronizer_step", None)
Sapan Bhatia2b307f72017-11-02 11:39:17 -040085 super(ORMWrapper, self).__setattr__("dependent", None)
Scott Baker96b995a2017-02-15 16:21:12 -080086 super(ORMWrapper, self).__setattr__("is_new", is_new)
Scott Bakerc4156c32017-12-08 10:58:21 -080087 super(ORMWrapper, self).__setattr__("post_save_fixups", [])
Zack Williams045b63d2019-01-22 16:30:57 -070088 fkmap = self.gen_fkmap()
Scott Baker96b995a2017-02-15 16:21:12 -080089 super(ORMWrapper, self).__setattr__("_fkmap", fkmap)
Zack Williams045b63d2019-01-22 16:30:57 -070090 reverse_fkmap = self.gen_reverse_fkmap()
Scott Baker96b995a2017-02-15 16:21:12 -080091 super(ORMWrapper, self).__setattr__("_reverse_fkmap", reverse_fkmap)
Scott Baker5b7fba02018-10-17 08:46:46 -070092 super(ORMWrapper, self).__setattr__("_initial", self._dict)
93
Zack Williams045b63d2019-01-22 16:30:57 -070094 def fields_differ(self, f1, f2):
95 return f1 != f2
Scott Baker5b7fba02018-10-17 08:46:46 -070096
97 @property
98 def _dict(self):
99 """ Return a dictionary of {fieldname: fieldvalue} for the object.
100
101 This differs for the xos-core implementation of XOSBase. For new object, XOSBase will include field names
102 that are set to default values. ORM ignores fields that are set to default values.
103 """
Zack Williams045b63d2019-01-22 16:30:57 -0700104 d = {}
Scott Baker5b7fba02018-10-17 08:46:46 -0700105 for (fieldDesc, val) in self._wrapped_class.ListFields():
106 name = fieldDesc.name
107 d[name] = val
108 return d
109
110 @property
111 def diff(self):
112 d1 = self._initial
113 d2 = self._dict
Zack Williams5c2ea232019-01-30 15:23:01 -0700114 all_field_names = list(self._wrapped_class.DESCRIPTOR.fields_by_name.keys())
Zack Williams045b63d2019-01-22 16:30:57 -0700115 diffs = []
Scott Baker5b7fba02018-10-17 08:46:46 -0700116 for k in all_field_names:
Zack Williams045b63d2019-01-22 16:30:57 -0700117 if d1.get(k, None) != d2.get(k, None):
118 diffs.append((k, (d1.get(k, None), d2.get(k, None))))
Scott Baker5b7fba02018-10-17 08:46:46 -0700119
Zack Williams045b63d2019-01-22 16:30:57 -0700120 # diffs = [(k, (v, d2[k])) for k, v in d1.items() if self.fields_differ(v,d2[k])]
Scott Baker5b7fba02018-10-17 08:46:46 -0700121 return dict(diffs)
122
123 @property
124 def has_changed(self):
125 return bool(self.diff)
126
127 @property
128 def changed_fields(self):
129 """ Return the list of changed fields.
130
131 This differs for the xos-core implementation of XOSBase. For new object, XOSBase will include field names
132 that are set to default values.
133 """
134 if self.is_new:
Zack Williams5c2ea232019-01-30 15:23:01 -0700135 return list(self._dict.keys())
136 return list(self.diff.keys())
Scott Baker5b7fba02018-10-17 08:46:46 -0700137
138 def has_field_changed(self, field_name):
Zack Williams5c2ea232019-01-30 15:23:01 -0700139 return field_name in list(self.diff.keys())
Scott Baker5b7fba02018-10-17 08:46:46 -0700140
141 def get_field_diff(self, field_name):
142 return self.diff.get(field_name, None)
143
144 def recompute_initial(self):
145 self._initial = self._dict
146
147 def save_changed_fields(self, always_update_timestamp=False):
148 if self.has_changed:
149 update_fields = self.changed_fields
150 if always_update_timestamp and "updated" not in update_fields:
151 update_fields.append("updated")
Zack Williams045b63d2019-01-22 16:30:57 -0700152 self.save(
153 update_fields=sorted(update_fields),
154 always_update_timestamp=always_update_timestamp,
155 )
Scott Baker96b995a2017-02-15 16:21:12 -0800156
Scott Bakerd78f6472017-03-14 17:30:14 -0700157 def create_attr(self, name, value=None):
158 """ setattr(self, ...) will fail for attributes that don't exist in the
159 wrapped grpc class. This is by design. However, if someone really
160 does want to attach a new attribute to this class, then they can
161 call create_attr()
162 """
163 super(ORMWrapper, self).__setattr__(name, value)
164
Scott Baker03a163f2017-05-17 09:21:47 -0700165 def get_generic_foreignkeys(self):
166 """ this is a placeholder until generic foreign key support is added
167 to xproto.
168 """
169 return []
170
Scott Baker96b995a2017-02-15 16:21:12 -0800171 def gen_fkmap(self):
172 fkmap = {}
173
Zack Williams5c2ea232019-01-30 15:23:01 -0700174 all_field_names = list(self._wrapped_class.DESCRIPTOR.fields_by_name.keys())
Scott Bakeraa556b02017-03-07 16:07:34 -0800175
Scott Baker96b995a2017-02-15 16:21:12 -0800176 for (name, field) in self._wrapped_class.DESCRIPTOR.fields_by_name.items():
Zack Williams045b63d2019-01-22 16:30:57 -0700177 if name.endswith("_id"):
178 foreignKey = field.GetOptions().Extensions._FindExtensionByName(
179 "xos.foreignKey"
180 )
181 fk = field.GetOptions().Extensions[foreignKey]
182 if fk and fk.modelName:
183 fkdict = {
184 "src_fieldName": name,
185 "modelName": fk.modelName,
186 "kind": "fk",
187 }
188 if fk.reverseFieldName:
189 fkdict["reverse_fieldName"] = fk.reverseFieldName
190 fkmap[name[:-3]] = fkdict
191 else:
192 # If there's a corresponding _type_id field, then see if this
193 # is a generic foreign key.
194 type_name = name[:-3] + "_type_id"
195 if type_name in all_field_names:
196 fkmap[name[:-3]] = {
197 "src_fieldName": name,
198 "ct_fieldName": type_name,
199 "kind": "generic_fk",
200 }
Scott Baker96b995a2017-02-15 16:21:12 -0800201
Scott Baker03a163f2017-05-17 09:21:47 -0700202 for gfk in self.get_generic_foreignkeys():
Zack Williams045b63d2019-01-22 16:30:57 -0700203 fkmap[gfk["name"]] = {
204 "src_fieldName": gfk["id"],
205 "ct_fieldName": gfk["content_type"],
206 "kind": "generic_fk",
207 }
Scott Baker03a163f2017-05-17 09:21:47 -0700208
Scott Baker96b995a2017-02-15 16:21:12 -0800209 return fkmap
210
211 def gen_reverse_fkmap(self):
212 reverse_fkmap = {}
213
214 for (name, field) in self._wrapped_class.DESCRIPTOR.fields_by_name.items():
Zack Williams045b63d2019-01-22 16:30:57 -0700215 if name.endswith("_ids"):
216 reverseForeignKey = field.GetOptions().Extensions._FindExtensionByName(
217 "xos.reverseForeignKey"
218 )
219 fk = field.GetOptions().Extensions[reverseForeignKey]
220 if fk and fk.modelName:
221 reverse_fkmap[name[:-4]] = {
222 "src_fieldName": name,
223 "modelName": fk.modelName,
224 "writeable": False,
225 }
226 else:
227 manyToManyForeignKey = field.GetOptions().Extensions._FindExtensionByName(
228 "xos.manyToManyForeignKey"
229 )
230 fk = field.GetOptions().Extensions[manyToManyForeignKey]
231 if fk and fk.modelName:
232 reverse_fkmap[name[:-4]] = {
233 "src_fieldName": name,
234 "modelName": fk.modelName,
235 "writeable": True,
236 }
Scott Baker96b995a2017-02-15 16:21:12 -0800237
238 return reverse_fkmap
239
240 def fk_resolve(self, name):
241 if name in self.cache:
Scott Bakerc4156c32017-12-08 10:58:21 -0800242 return self.cache[name]
Scott Baker96b995a2017-02-15 16:21:12 -0800243
244 fk_entry = self._fkmap[name]
Scott Bakeraa556b02017-03-07 16:07:34 -0800245 fk_kind = fk_entry["kind"]
246 fk_id = getattr(self, fk_entry["src_fieldName"])
247
248 if not fk_id:
249 return None
250
Zack Williams045b63d2019-01-22 16:30:57 -0700251 if fk_kind == "fk":
252 id = self.stub.make_ID(id=fk_id)
Scott Bakeraa556b02017-03-07 16:07:34 -0800253 dest_model = self.stub.invoke("Get%s" % fk_entry["modelName"], id)
254
Zack Williams045b63d2019-01-22 16:30:57 -0700255 elif fk_kind == "generic_fk":
256 dest_model = self.stub.genericForeignKeyResolve(
257 getattr(self, fk_entry["ct_fieldName"]), fk_id
258 )._wrapped_class
Scott Bakeraa556b02017-03-07 16:07:34 -0800259
260 else:
261 raise Exception("unknown fk_kind")
Scott Baker96b995a2017-02-15 16:21:12 -0800262
Scott Bakerc4156c32017-12-08 10:58:21 -0800263 dest_model = make_ORMWrapper(dest_model, self.stub)
Scott Baker96b995a2017-02-15 16:21:12 -0800264 self.cache[name] = dest_model
265
Scott Bakerc4156c32017-12-08 10:58:21 -0800266 return dest_model
Scott Baker96b995a2017-02-15 16:21:12 -0800267
268 def reverse_fk_resolve(self, name):
269 if name not in self.reverse_cache:
270 fk_entry = self._reverse_fkmap[name]
Zack Williams045b63d2019-01-22 16:30:57 -0700271 self.reverse_cache[name] = ORMLocalObjectManager(
272 self.stub,
273 fk_entry["modelName"],
274 getattr(self, fk_entry["src_fieldName"]),
275 fk_entry["writeable"],
276 )
Scott Baker96b995a2017-02-15 16:21:12 -0800277
Scott Baker7ab456b2019-01-08 14:58:13 -0800278 return self.reverse_cache[name]
Scott Baker96b995a2017-02-15 16:21:12 -0800279
Scott Bakere72e7612017-02-20 10:07:09 -0800280 def fk_set(self, name, model):
281 fk_entry = self._fkmap[name]
Scott Bakeraa556b02017-03-07 16:07:34 -0800282 fk_kind = fk_entry["kind"]
Scott Bakera1eae7a2017-06-06 09:20:15 -0700283 if model:
284 id = model.id
285 else:
286 id = 0
Scott Bakere72e7612017-02-20 10:07:09 -0800287 setattr(self._wrapped_class, fk_entry["src_fieldName"], id)
288
Zack Williams045b63d2019-01-22 16:30:57 -0700289 if fk_kind == "generic_fk":
290 setattr(
291 self._wrapped_class,
292 fk_entry["ct_fieldName"],
293 model.self_content_type_id,
294 )
Scott Bakeraa556b02017-03-07 16:07:34 -0800295
Scott Bakerc4156c32017-12-08 10:58:21 -0800296 if name in self.cache:
297 old_model = self.cache[name]
298 if fk_entry.get("reverse_fieldName"):
299 # Note this fk change so that we can update the destination model after we save.
Zack Williams045b63d2019-01-22 16:30:57 -0700300 self.post_save_fixups.append(
301 {
302 "src_fieldName": fk_entry["src_fieldName"],
303 "dest_id": id,
304 "dest_model": old_model,
305 "remove": True,
306 "reverse_fieldName": fk_entry.get("reverse_fieldName"),
307 }
308 )
Scott Bakerc4156c32017-12-08 10:58:21 -0800309 del self.cache[name]
Scott Bakere72e7612017-02-20 10:07:09 -0800310
Scott Bakerc4156c32017-12-08 10:58:21 -0800311 if model:
312 self.cache[name] = model
313 if fk_entry.get("reverse_fieldName"):
314 # Note this fk change so that we can update the destination model after we save.
Zack Williams045b63d2019-01-22 16:30:57 -0700315 self.post_save_fixups.append(
316 {
317 "src_fieldName": fk_entry["src_fieldName"],
318 "dest_id": id,
319 "dest_model": model,
320 "remove": False,
321 "reverse_fieldName": fk_entry.get("reverse_fieldName"),
322 }
323 )
Scott Bakerc4156c32017-12-08 10:58:21 -0800324 elif name in self.cache:
325 del self.cache[name]
Scott Bakere72e7612017-02-20 10:07:09 -0800326
Scott Bakerc4156c32017-12-08 10:58:21 -0800327 def do_post_save_fixups(self):
328 # Perform post-save foreign key fixups.
329 # Fixup the models that we've set a foreign key to so that their in-memory representation has the correct
330 # reverse foreign key back to us. We can only do this after a save, because self.id isn't known until
331 # after save.
332 # See unit test test_foreign_key_set_without_invalidate
333 for fixup in self.post_save_fixups:
334 model = fixup["dest_model"]
335 reverse_fieldName_ids = fixup["reverse_fieldName"] + "_ids"
336 if not hasattr(model, reverse_fieldName_ids):
337 continue
338 if fixup["remove"]:
339 reverse_ids = getattr(model, reverse_fieldName_ids)
340 if self.id in reverse_ids:
341 reverse_ids.remove(self.id)
342 else:
343 reverse_ids = getattr(model, reverse_fieldName_ids)
344 if self.id not in reverse_ids:
345 reverse_ids.append(self.id)
346 model.invalidate_cache(fixup["reverse_fieldName"])
347 self.post_save_fixups = []
Scott Bakere72e7612017-02-20 10:07:09 -0800348
Scott Baker96b995a2017-02-15 16:21:12 -0800349 def __getattr__(self, name, *args, **kwargs):
350 # note: getattr is only called for attributes that do not exist in
351 # self.__dict__
352
Scott Baker186372f2017-02-23 13:49:36 -0800353 # pk is a synonym for id
Zack Williams045b63d2019-01-22 16:30:57 -0700354 if name == "pk":
Scott Baker186372f2017-02-23 13:49:36 -0800355 name = "id"
356
Zack Williams5c2ea232019-01-30 15:23:01 -0700357 if name in list(self._fkmap.keys()):
Scott Baker96b995a2017-02-15 16:21:12 -0800358 return self.fk_resolve(name)
359
Zack Williams5c2ea232019-01-30 15:23:01 -0700360 if name in list(self._reverse_fkmap.keys()):
Scott Baker96b995a2017-02-15 16:21:12 -0800361 return self.reverse_fk_resolve(name)
362
Scott Baker37cf9e22018-08-20 14:39:33 -0700363 try:
364 # When sending a reply, XOS will leave the field unset if it is None in the data model. If
365 # HasField(<fieldname>)==False for an existing object, then the caller can infer that field was set to
366 # None.
367 if (not self.is_new) and (not self._wrapped_class.HasField(name)):
368 return None
369 except ValueError:
370 # ValueError is thrown if the field does not exist. We will handle that case in the getattr() below.
371 pass
372
Scott Baker96b995a2017-02-15 16:21:12 -0800373 return getattr(self._wrapped_class, name, *args, **kwargs)
374
375 def __setattr__(self, name, value):
Zack Williams5c2ea232019-01-30 15:23:01 -0700376 if name in list(self._fkmap.keys()):
Scott Bakere72e7612017-02-20 10:07:09 -0800377 self.fk_set(name, value)
378 elif name in self.__dict__:
Zack Williams045b63d2019-01-22 16:30:57 -0700379 super(ORMWrapper, self).__setattr__(name, value)
Scott Baker37cf9e22018-08-20 14:39:33 -0700380 elif value is None:
381 # When handling requests, XOS interprets gRPC HasField(<fieldname>)==False to indicate that the caller
382 # has not set the field and wants it to continue to use its existing (or default) value. That leaves us
383 # with no easy way to support setting a field to None.
384 raise ValueError("Setting a non-foreignkey field to None is not supported")
Scott Baker96b995a2017-02-15 16:21:12 -0800385 else:
386 setattr(self._wrapped_class, name, value)
387
388 def __repr__(self):
Scott Bakerd1940972017-05-01 15:45:32 -0700389 class_name = self._wrapped_class.__class__.__name__
390 id = getattr(self._wrapped_class, "id", "noid")
391 name = getattr(self._wrapped_class, "name", None)
392 if name:
393 return "<%s: %s>" % (class_name, name)
394 else:
395 return "<%s: id-%s>" % (class_name, id)
396
397 def __str__(self):
398 class_name = self._wrapped_class.__class__.__name__
399 id = getattr(self._wrapped_class, "id", "noid")
400 name = getattr(self._wrapped_class, "name", None)
401 if name:
402 return name
403 else:
404 return "%s-%s" % (class_name, id)
405
406 def dumpstr(self):
Scott Baker96b995a2017-02-15 16:21:12 -0800407 return self._wrapped_class.__repr__()
408
Scott Bakerd1940972017-05-01 15:45:32 -0700409 def dump(self):
Zack Williams045b63d2019-01-22 16:30:57 -0700410 print(self.dumpstr())
Scott Bakerd1940972017-05-01 15:45:32 -0700411
Scott Bakere72e7612017-02-20 10:07:09 -0800412 def invalidate_cache(self, name=None):
413 if name:
414 if name in self.cache:
415 del self.cache[name]
416 if name in self.reverse_cache:
417 del self.reverse_cache[name]
Scott Bakere72e7612017-02-20 10:07:09 -0800418 else:
419 self.cache.clear()
420 self.reverse_cache.clear()
Scott Bakere72e7612017-02-20 10:07:09 -0800421
Zack Williams045b63d2019-01-22 16:30:57 -0700422 def save(
423 self,
424 update_fields=None,
425 always_update_timestamp=False,
426 is_sync_save=False,
427 is_policy_save=False,
428 ):
Andy Bavier04ee1912019-01-30 14:17:16 -0700429 classname = self._wrapped_class.__class__.__name__
Scott Baker96b995a2017-02-15 16:21:12 -0800430 if self.is_new:
Zack Williams5c2ea232019-01-30 15:23:01 -0700431 log.debug(
432 "save(): is new",
433 classname=classname,
434 syncstep=get_synchronizer_function(),
Zack Williams045b63d2019-01-22 16:30:57 -0700435 )
Zack Williams5c2ea232019-01-30 15:23:01 -0700436 new_class = self.stub.invoke("Create%s" % classname, self._wrapped_class)
Zack Williams045b63d2019-01-22 16:30:57 -0700437 self._wrapped_class = new_class
438 self.is_new = False
Scott Baker96b995a2017-02-15 16:21:12 -0800439 else:
Andy Bavier04ee1912019-01-30 14:17:16 -0700440 if self.has_changed:
Zack Williams5c2ea232019-01-30 15:23:01 -0700441 log.debug(
442 "save(): updated",
443 classname=classname,
444 changed_fields=self.changed_fields,
445 syncstep=get_synchronizer_function(),
446 )
Andy Bavier04ee1912019-01-30 14:17:16 -0700447 else:
Zack Williams5c2ea232019-01-30 15:23:01 -0700448 log.debug(
449 "save(): no changes",
450 classname=classname,
451 syncstep=get_synchronizer_function(),
452 )
Zack Williams045b63d2019-01-22 16:30:57 -0700453 metadata = []
454 if update_fields:
455 metadata.append(("update_fields", ",".join(update_fields)))
456 if always_update_timestamp:
457 metadata.append(("always_update_timestamp", "1"))
458 if is_policy_save:
459 metadata.append(("is_policy_save", "1"))
460 if is_sync_save:
461 metadata.append(("is_sync_save", "1"))
462 self.stub.invoke(
Zack Williams5c2ea232019-01-30 15:23:01 -0700463 "Update%s" % classname, self._wrapped_class, metadata=metadata
Zack Williams045b63d2019-01-22 16:30:57 -0700464 )
Scott Bakerc4156c32017-12-08 10:58:21 -0800465 self.do_post_save_fixups()
Scott Baker96b995a2017-02-15 16:21:12 -0800466
Scott Baker5b7fba02018-10-17 08:46:46 -0700467 # Now that object has saved, reset our initial state for diff calculation
468 self.recompute_initial()
469
Scott Baker96b995a2017-02-15 16:21:12 -0800470 def delete(self):
471 id = self.stub.make_ID(id=self._wrapped_class.id)
472 self.stub.invoke("Delete%s" % self._wrapped_class.__class__.__name__, id)
473
Scott Baker22796cc2017-02-23 16:53:34 -0800474 def tologdict(self):
475 try:
Zack Williams045b63d2019-01-22 16:30:57 -0700476 d = {"model_name": self._wrapped_class.__class__.__name__, "pk": self.pk}
477 except BaseException:
Scott Baker22796cc2017-02-23 16:53:34 -0800478 d = {}
479
480 return d
481
Scott Bakerbb81e152017-03-02 15:28:36 -0800482 @property
Scott Bakerff104cc2017-08-14 15:24:41 -0700483 def leaf_model(self):
484 # Easy case - this model is already the leaf
485 if self.leaf_model_name == self._wrapped_class.__class__.__name__:
486 return self
487
488 # This model is not the leaf, so use the stub to fetch the leaf model
489 return getattr(self.stub, self.leaf_model_name).objects.get(id=self.id)
490
491 @property
Scott Bakerd2543ed2017-03-07 21:46:48 -0800492 def model_name(self):
493 return self._wrapped_class.__class__.__name__
494
495 @property
Scott Bakerbb81e152017-03-02 15:28:36 -0800496 def ansible_tag(self):
497 return "%s_%s" % (self._wrapped_class.__class__.__name__, self.id)
498
Zack Williams045b63d2019-01-22 16:30:57 -0700499
Scott Bakerb05393b2017-03-01 14:59:55 -0800500class ORMQuerySet(list):
501 """ Makes lists look like django querysets """
Zack Williams045b63d2019-01-22 16:30:57 -0700502
Scott Bakerb05393b2017-03-01 14:59:55 -0800503 def first(self):
Zack Williams045b63d2019-01-22 16:30:57 -0700504 if len(self) > 0:
Scott Bakerb05393b2017-03-01 14:59:55 -0800505 return self[0]
506 else:
507 return None
508
Scott Baker8c7267d2017-03-14 19:34:13 -0700509 def exists(self):
Zack Williams045b63d2019-01-22 16:30:57 -0700510 return len(self) > 0
511
Scott Baker8c7267d2017-03-14 19:34:13 -0700512
Scott Baker96b995a2017-02-15 16:21:12 -0800513class ORMLocalObjectManager(object):
514 """ Manages a local list of objects """
515
Scott Bakerc59f1bc2017-12-04 16:55:05 -0800516 def __init__(self, stub, modelName, idList, writeable):
Scott Baker96b995a2017-02-15 16:21:12 -0800517 self._stub = stub
518 self._modelName = modelName
519 self._idList = idList
Scott Bakerc59f1bc2017-12-04 16:55:05 -0800520 self._writeable = writeable
Scott Baker96b995a2017-02-15 16:21:12 -0800521 self._cache = None
522
523 def resolve_queryset(self):
524 if self._cache is not None:
525 return self._cache
526
527 models = []
528 for id in self._idList:
Zack Williams045b63d2019-01-22 16:30:57 -0700529 models.append(
530 self._stub.invoke("Get%s" % self._modelName, self._stub.make_ID(id=id))
531 )
Scott Baker96b995a2017-02-15 16:21:12 -0800532
533 self._cache = models
534
535 return models
536
537 def all(self):
538 models = self.resolve_queryset()
Zack Williams045b63d2019-01-22 16:30:57 -0700539 return [make_ORMWrapper(x, self._stub) for x in models]
Scott Baker96b995a2017-02-15 16:21:12 -0800540
Scott Baker8c7267d2017-03-14 19:34:13 -0700541 def exists(self):
Zack Williams045b63d2019-01-22 16:30:57 -0700542 return len(self._idList) > 0
Scott Baker8c7267d2017-03-14 19:34:13 -0700543
Scott Bakera1eae7a2017-06-06 09:20:15 -0700544 def count(self):
545 return len(self._idList)
546
Scott Baker8c7267d2017-03-14 19:34:13 -0700547 def first(self):
548 if self._idList:
Zack Williams045b63d2019-01-22 16:30:57 -0700549 model = make_ORMWrapper(
550 self._stub.invoke(
551 "Get%s" % self._modelName, self._stub.make_ID(id=self._idList[0])
552 ),
553 self._stub,
554 )
Scott Baker8c7267d2017-03-14 19:34:13 -0700555 return model
556 else:
557 return None
558
Scott Bakerc59f1bc2017-12-04 16:55:05 -0800559 def add(self, model):
560 if not self._writeable:
561 raise Exception("Only ManyToMany lists are writeable")
562
563 if isinstance(model, int):
564 id = model
565 else:
566 if not model.id:
567 raise Exception("Model %s has no id" % model)
568 id = model.id
569
570 self._idList.append(id)
571
572 def remove(self, model):
573 if not self._writeable:
574 raise Exception("Only ManyToMany lists are writeable")
575
576 if isinstance(model, int):
577 id = model
578 else:
579 if not model.id:
580 raise Exception("Model %s has no id" % model)
581 id = model.id
582
583 self._idList.remove(id)
584
Zack Williams045b63d2019-01-22 16:30:57 -0700585
Scott Baker96b995a2017-02-15 16:21:12 -0800586class ORMObjectManager(object):
587 """ Manages a remote list of objects """
588
Scott Bakerac2f2b52017-02-21 14:53:23 -0800589 # constants better agree with common.proto
Scott Bakerea1f4d02018-12-17 10:21:50 -0800590 DEFAULT = 0
591 ALL = 1
Scott Bakerbae9d842017-03-21 10:44:10 -0700592 SYNCHRONIZER_DIRTY_OBJECTS = 2
593 SYNCHRONIZER_DELETED_OBJECTS = 3
594 SYNCHRONIZER_DIRTY_POLICIES = 4
595 SYNCHRONIZER_DELETED_POLICIES = 5
Scott Bakerac2f2b52017-02-21 14:53:23 -0800596
Scott Bakerea1f4d02018-12-17 10:21:50 -0800597 def __init__(self, stub, modelName, packageName, kind=0):
Scott Baker96b995a2017-02-15 16:21:12 -0800598 self._stub = stub
599 self._modelName = modelName
600 self._packageName = packageName
Scott Bakerea1f4d02018-12-17 10:21:50 -0800601 self._kind = kind
Scott Baker96b995a2017-02-15 16:21:12 -0800602
603 def wrap_single(self, obj):
Scott Baker22796cc2017-02-23 16:53:34 -0800604 return make_ORMWrapper(obj, self._stub)
Scott Baker96b995a2017-02-15 16:21:12 -0800605
606 def wrap_list(self, obj):
Zack Williams045b63d2019-01-22 16:30:57 -0700607 result = []
Scott Baker96b995a2017-02-15 16:21:12 -0800608 for item in obj.items:
Scott Baker22796cc2017-02-23 16:53:34 -0800609 result.append(make_ORMWrapper(item, self._stub))
Scott Bakerb05393b2017-03-01 14:59:55 -0800610 return ORMQuerySet(result)
Scott Baker96b995a2017-02-15 16:21:12 -0800611
612 def all(self):
Zack Williams045b63d2019-01-22 16:30:57 -0700613 if self._kind == self.DEFAULT:
614 return self.wrap_list(
615 self._stub.invoke("List%s" % self._modelName, self._stub.make_empty())
616 )
Scott Bakerea1f4d02018-12-17 10:21:50 -0800617 else:
618 return self.filter()
Scott Baker96b995a2017-02-15 16:21:12 -0800619
Scott Baker22796cc2017-02-23 16:53:34 -0800620 def first(self):
Scott Bakerea1f4d02018-12-17 10:21:50 -0800621 objs = self.all()
Scott Baker22796cc2017-02-23 16:53:34 -0800622 if not objs:
623 return None
624 return objs[0]
625
Scott Bakerac2f2b52017-02-21 14:53:23 -0800626 def filter(self, **kwargs):
627 q = self._stub.make_Query()
Scott Bakerea1f4d02018-12-17 10:21:50 -0800628 q.kind = self._kind
Scott Bakerac2f2b52017-02-21 14:53:23 -0800629
630 for (name, val) in kwargs.items():
631 el = q.elements.add()
632
633 if name.endswith("__gt"):
634 name = name[:-4]
635 el.operator = el.GREATER_THAN
636 elif name.endswith("__gte"):
637 name = name[:-5]
638 el.operator = el.GREATER_THAN_OR_EQUAL
639 elif name.endswith("__lt"):
640 name = name[:-4]
641 el.operator = el.LESS_THAN
642 elif name.endswith("__lte"):
643 name = name[:-5]
644 el.operator = el.LESS_THAN_OR_EQUAL
Scott Bakere1607b82018-09-20 14:10:59 -0700645 elif name.endswith("__iexact"):
646 name = name[:-8]
647 el.operator = el.IEXACT
Scott Bakerac2f2b52017-02-21 14:53:23 -0800648 else:
649 el.operator = el.EQUAL
650
651 el.name = name
652 if isinstance(val, int):
653 el.iValue = val
654 else:
655 el.sValue = val
656
657 return self.wrap_list(self._stub.invoke("Filter%s" % self._modelName, q))
658
659 def filter_special(self, kind):
660 q = self._stub.make_Query()
661 q.kind = kind
662 return self.wrap_list(self._stub.invoke("Filter%s" % self._modelName, q))
663
Scott Baker22796cc2017-02-23 16:53:34 -0800664 def get(self, **kwargs):
Zack Williams5c2ea232019-01-30 15:23:01 -0700665 if list(kwargs.keys()) == ["id"]:
Scott Baker22796cc2017-02-23 16:53:34 -0800666 # the fast and easy case, look it up by id
Zack Williams045b63d2019-01-22 16:30:57 -0700667 return self.wrap_single(
668 self._stub.invoke(
669 "Get%s" % self._modelName, self._stub.make_ID(id=kwargs["id"])
670 )
671 )
Scott Baker22796cc2017-02-23 16:53:34 -0800672 else:
673 # the slightly more difficult case, filter and return the first item
674 objs = self.filter(**kwargs)
675 return objs[0]
Scott Baker96b995a2017-02-15 16:21:12 -0800676
677 def new(self, **kwargs):
Zack Williams045b63d2019-01-22 16:30:57 -0700678 if self._kind != ORMObjectManager.DEFAULT:
679 raise Exception(
680 "Creating objects is only supported by the DEFAULT object manager"
681 )
Scott Bakerea1f4d02018-12-17 10:21:50 -0800682
Scott Bakeraa556b02017-03-07 16:07:34 -0800683 cls = self._stub.all_grpc_classes[self._modelName]
Scott Bakerfe42a6f2017-03-18 09:11:31 -0700684 o = make_ORMWrapper(cls(), self._stub, is_new=True)
Zack Williams045b63d2019-01-22 16:30:57 -0700685 for (k, v) in kwargs.items():
Scott Bakerfe42a6f2017-03-18 09:11:31 -0700686 setattr(o, k, v)
Scott Baker5b7fba02018-10-17 08:46:46 -0700687 o.recompute_initial()
Scott Bakerfe42a6f2017-03-18 09:11:31 -0700688 return o
Scott Baker96b995a2017-02-15 16:21:12 -0800689
Zack Williams045b63d2019-01-22 16:30:57 -0700690
Scott Baker96b995a2017-02-15 16:21:12 -0800691class ORMModelClass(object):
692 def __init__(self, stub, model_name, package_name):
Scott Baker22796cc2017-02-23 16:53:34 -0800693 self.model_name = model_name
Scott Bakeraa556b02017-03-07 16:07:34 -0800694 self._stub = stub
Scott Baker96b995a2017-02-15 16:21:12 -0800695 self.objects = ORMObjectManager(stub, model_name, package_name)
Zack Williams045b63d2019-01-22 16:30:57 -0700696 self.deleted_objects = ORMObjectManager(
697 stub,
698 model_name,
699 package_name,
700 ORMObjectManager.SYNCHRONIZER_DELETED_OBJECTS,
701 )
Scott Baker96b995a2017-02-15 16:21:12 -0800702
Scott Bakerbb81e152017-03-02 15:28:36 -0800703 @property
Scott Baker22796cc2017-02-23 16:53:34 -0800704 def __name__(self):
705 return self.model_name
706
Scott Bakeraa556b02017-03-07 16:07:34 -0800707 @property
708 def content_type_id(self):
709 return self._stub.reverse_content_type_map[self.model_name]
710
Scott Baker8a6d91f2017-03-22 11:23:11 -0700711 def __call__(self, *args, **kwargs):
712 return self.objects.new(*args, **kwargs)
713
Zack Williams045b63d2019-01-22 16:30:57 -0700714
Scott Baker96b995a2017-02-15 16:21:12 -0800715class ORMStub(object):
Zack Williams045b63d2019-01-22 16:30:57 -0700716 def __init__(
717 self,
718 stub,
719 protos,
720 package_name,
721 invoker=None,
722 caller_kind="grpcapi",
723 empty=None,
724 enable_backoff=True,
725 restart_on_disconnect=False,
726 ):
Scott Baker96b995a2017-02-15 16:21:12 -0800727 self.grpc_stub = stub
Scott Bakerb96ba432018-02-26 09:53:48 -0800728 self.protos = protos
729 self.common_protos = protos.common__pb2
Scott Baker96b995a2017-02-15 16:21:12 -0800730 self.all_model_names = []
Scott Bakeraa556b02017-03-07 16:07:34 -0800731 self.all_grpc_classes = {}
732 self.content_type_map = {}
733 self.reverse_content_type_map = {}
Scott Bakeref8d85d2017-02-21 16:44:28 -0800734 self.invoker = invoker
Scott Baker22796cc2017-02-23 16:53:34 -0800735 self.caller_kind = caller_kind
Scott Baker500f8c72017-05-19 09:41:50 -0700736 self.enable_backoff = enable_backoff
Scott Bakerb06e3e02017-12-12 11:05:53 -0800737 self.restart_on_disconnect = restart_on_disconnect
Scott Baker96b995a2017-02-15 16:21:12 -0800738
Scott Bakerf0ee0dc2017-05-15 10:10:05 -0700739 if not empty:
Scott Bakerb96ba432018-02-26 09:53:48 -0800740 empty = self.protos.google_dot_protobuf_dot_empty__pb2.Empty
Scott Bakerf0ee0dc2017-05-15 10:10:05 -0700741 self._empty = empty
742
Scott Baker96b995a2017-02-15 16:21:12 -0800743 for name in dir(stub):
Zack Williams045b63d2019-01-22 16:30:57 -0700744 if name.startswith("Get"):
745 model_name = name[3:]
746 setattr(self, model_name, ORMModelClass(self, model_name, package_name))
Scott Baker96b995a2017-02-15 16:21:12 -0800747
Zack Williams045b63d2019-01-22 16:30:57 -0700748 self.all_model_names.append(model_name)
Scott Baker96b995a2017-02-15 16:21:12 -0800749
Zack Williams045b63d2019-01-22 16:30:57 -0700750 grpc_class = getattr(self.protos, model_name)
751 self.all_grpc_classes[model_name] = grpc_class
Scott Bakeraa556b02017-03-07 16:07:34 -0800752
Zack Williams045b63d2019-01-22 16:30:57 -0700753 ct = grpc_class.DESCRIPTOR.GetOptions().Extensions._FindExtensionByName(
754 "xos.contentTypeId"
755 )
756 if ct:
757 ct = grpc_class.DESCRIPTOR.GetOptions().Extensions[ct]
758 if ct:
759 self.content_type_map[ct] = model_name
760 self.reverse_content_type_map[model_name] = ct
Scott Bakeraa556b02017-03-07 16:07:34 -0800761
762 def genericForeignKeyResolve(self, content_type_id, id):
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700763 if content_type_id.endswith("_decl"):
764 content_type_id = content_type_id[:-5]
765
766 if content_type_id not in self.content_type_map:
Zack Williams045b63d2019-01-22 16:30:57 -0700767 raise ORMGenericContentNotFoundException(
768 "Content_type %s not found in self.content_type_map" % content_type_id
769 )
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700770
Scott Bakeraa556b02017-03-07 16:07:34 -0800771 model_name = self.content_type_map[content_type_id]
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700772
Scott Bakeraa556b02017-03-07 16:07:34 -0800773 model = getattr(self, model_name)
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700774 objs = model.objects.filter(id=id)
775 if not objs:
Zack Williams045b63d2019-01-22 16:30:57 -0700776 raise ORMGenericObjectNotFoundException(
777 "Object %s of model %s was not found" % (id, model_name)
778 )
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700779
Scott Bakeraa556b02017-03-07 16:07:34 -0800780 return model.objects.get(id=id)
781
Scott Baker22796cc2017-02-23 16:53:34 -0800782 def add_default_metadata(self, metadata):
Zack Williams045b63d2019-01-22 16:30:57 -0700783 default_metadata = [("caller_kind", self.caller_kind)]
Scott Baker22796cc2017-02-23 16:53:34 -0800784
Scott Bakerd8246712018-07-12 18:08:31 -0700785 # introspect to see if we're running from a synchronizer thread
786 if getattr(threading.current_thread(), "is_sync_thread", False):
Zack Williams045b63d2019-01-22 16:30:57 -0700787 default_metadata.append(("is_sync_save", "1"))
Scott Bakerd8246712018-07-12 18:08:31 -0700788
789 # introspect to see if we're running from a model_policy thread
790 if getattr(threading.current_thread(), "is_policy_thread", False):
Zack Williams045b63d2019-01-22 16:30:57 -0700791 default_metadata.append(("is_policy_save", "1"))
Scott Bakerd8246712018-07-12 18:08:31 -0700792
Scott Baker22796cc2017-02-23 16:53:34 -0800793 # build up a list of metadata keys we already have
Zack Williams045b63d2019-01-22 16:30:57 -0700794 md_keys = [x[0] for x in metadata]
Scott Baker22796cc2017-02-23 16:53:34 -0800795
796 # add any defaults that we don't already have
797 for md in default_metadata:
798 if md[0] not in md_keys:
Zack Williams045b63d2019-01-22 16:30:57 -0700799 metadata.append((md[0], md[1]))
Scott Baker22796cc2017-02-23 16:53:34 -0800800
Scott Baker57c74822017-02-23 11:13:04 -0800801 def invoke(self, name, request, metadata=[]):
Scott Baker22796cc2017-02-23 16:53:34 -0800802 self.add_default_metadata(metadata)
803
Scott Bakeref8d85d2017-02-21 16:44:28 -0800804 if self.invoker:
805 # Hook in place to call Chameleon's invoke method, as soon as we
806 # have rewritten the synchronizer to use reactor.
Zack Williams045b63d2019-01-22 16:30:57 -0700807 return self.invoker.invoke(
808 self.grpc_stub.__class__, name, request, metadata={}
809 ).result[0]
Scott Baker500f8c72017-05-19 09:41:50 -0700810 elif self.enable_backoff:
Scott Bakeref8d85d2017-02-21 16:44:28 -0800811 # Our own retry mechanism. This works fine if there is a temporary
812 # failure in connectivity, but does not re-download gRPC schema.
Scott Baker500f8c72017-05-19 09:41:50 -0700813 import grpc
Zack Williams045b63d2019-01-22 16:30:57 -0700814
Scott Bakerb06e3e02017-12-12 11:05:53 -0800815 backoff = [0.5, 1, 2, 4, 8]
Scott Bakeref8d85d2017-02-21 16:44:28 -0800816 while True:
Scott Bakeref8d85d2017-02-21 16:44:28 -0800817 try:
818 method = getattr(self.grpc_stub, name)
Scott Baker57c74822017-02-23 11:13:04 -0800819 return method(request, metadata=metadata)
Zack Williams045b63d2019-01-22 16:30:57 -0700820 except grpc._channel._Rendezvous as e:
Scott Bakeref8d85d2017-02-21 16:44:28 -0800821 code = e.code()
822 if code == grpc.StatusCode.UNAVAILABLE:
Scott Bakerb06e3e02017-12-12 11:05:53 -0800823 if self.restart_on_disconnect:
824 # This is a blunt technique... We lost connectivity to the core, and we don't know that
825 # the core is still serving up the same models it was when we established connectivity,
826 # so restart the synchronizer.
827 # TODO: Hash check on the core models to tell if something changed would be better.
Zack Williams045b63d2019-01-22 16:30:57 -0700828 os.execv(sys.executable, ["python"] + sys.argv)
Scott Bakeref8d85d2017-02-21 16:44:28 -0800829 if not backoff:
830 raise Exception("No more retries on %s" % name)
831 time.sleep(backoff.pop(0))
832 else:
833 raise
Zack Williams045b63d2019-01-22 16:30:57 -0700834 except BaseException:
Scott Bakeref8d85d2017-02-21 16:44:28 -0800835 raise
Scott Baker500f8c72017-05-19 09:41:50 -0700836 else:
837 method = getattr(self.grpc_stub, name)
838 return method(request, metadata=metadata)
839
Scott Baker96b995a2017-02-15 16:21:12 -0800840 def make_ID(self, id):
Scott Bakerb96ba432018-02-26 09:53:48 -0800841 return getattr(self.common_protos, "ID")(id=id)
Scott Bakerf0ee0dc2017-05-15 10:10:05 -0700842
843 def make_empty(self):
844 return self._empty()
Scott Baker96b995a2017-02-15 16:21:12 -0800845
Scott Bakerac2f2b52017-02-21 14:53:23 -0800846 def make_Query(self):
Scott Bakerb96ba432018-02-26 09:53:48 -0800847 return getattr(self.common_protos, "Query")()
Scott Bakerac2f2b52017-02-21 14:53:23 -0800848
Scott Bakerf6145a22017-03-29 14:50:25 -0700849 def listObjects(self):
850 return self.all_model_names
851
Zack Williams045b63d2019-01-22 16:30:57 -0700852
Scott Baker22796cc2017-02-23 16:53:34 -0800853def register_convenience_wrapper(class_name, wrapper):
854 global convenience_wrappers
Scott Baker96b995a2017-02-15 16:21:12 -0800855
Scott Baker22796cc2017-02-23 16:53:34 -0800856 convenience_wrappers[class_name] = wrapper
857
Zack Williams045b63d2019-01-22 16:30:57 -0700858
Scott Baker22796cc2017-02-23 16:53:34 -0800859def make_ORMWrapper(wrapped_class, *args, **kwargs):
Scott Baker2f314d52018-08-24 08:31:19 -0700860 cls = None
861
862 if (not cls) and wrapped_class.__class__.__name__ in convenience_wrappers:
Scott Baker22796cc2017-02-23 16:53:34 -0800863 cls = convenience_wrappers[wrapped_class.__class__.__name__]
Scott Baker2f314d52018-08-24 08:31:19 -0700864
Zack Williams045b63d2019-01-22 16:30:57 -0700865 if not cls:
Scott Baker2f314d52018-08-24 08:31:19 -0700866 # Search the list of class names for this model to see if we have any applicable wrappers. The list is always
867 # sorted from most specific to least specific, so the first one we find will automatically be the most relevant
868 # one. If we don't find any, then default to ORMWrapper
869
870 # Note: Only works on objects that have been fetched from the server, not objects that are created on the
871 # client. This is because wrapped_class.class_names is filled in by the server.
872
873 # TODO(smbaker): Ought to be able to make this work with newly created objects after they are saved.
874
875 for name in wrapped_class.class_names.split(","):
876 if name in convenience_wrappers:
877 cls = convenience_wrappers[name]
878
Zack Williams045b63d2019-01-22 16:30:57 -0700879 if not cls:
Scott Baker22796cc2017-02-23 16:53:34 -0800880 cls = ORMWrapper
881
882 return cls(wrapped_class, *args, **kwargs)
883
Zack Williams045b63d2019-01-22 16:30:57 -0700884
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +0200885def import_convenience_methods():
Scott Baker8203dc62019-03-14 16:16:19 -0700886 # The ORM has several built-in convenience methods that are contained here
887 lib_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
888 base_convenience_dir = os.path.join(lib_dir, "convenience")
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +0200889
Scott Baker8203dc62019-03-14 16:16:19 -0700890 # Service convenience methods are placed here during dynamicload
891 service_convenience_dir = "/var/run/xosapi/convenience"
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +0200892
Scott Baker8203dc62019-03-14 16:16:19 -0700893 for api_convenience_dir in [base_convenience_dir, service_convenience_dir]:
894 log.info("Loading convenience methods", api_convenience_dir=api_convenience_dir)
895
896 if not os.path.exists(api_convenience_dir):
897 log.info("No convenience methods found", api_convenience_dir=api_convenience_dir)
898 continue
899
900 for file in os.listdir(api_convenience_dir):
901 if file.endswith(".py") and "test" not in file:
902 pathname = os.path.join(api_convenience_dir, file)
903 try:
904 log.debug("Loading: %s" % file)
905 imp.load_source(file[:-3], pathname)
906 except Exception:
907 log.exception(
908 "Cannot import api convenience method for: %s, %s"
909 % (file[:-3], pathname)
910 )