MetaData Sharing
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

198 lines
4.6 KiB

  1. import base64
  2. import copy
  3. import datetime
  4. import itertools
  5. import json
  6. import unittest
  7. import uuid
  8. from .utils import _makeuuid, _makedatetime, _makebytes, _asn1coder
  9. class _JSONEncoder(json.JSONEncoder):
  10. def default(self, o):
  11. if isinstance(o, uuid.UUID):
  12. return str(o)
  13. elif isinstance(o, datetime.datetime):
  14. o = o.astimezone(datetime.timezone.utc)
  15. return o.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
  16. elif isinstance(o, bytes):
  17. return base64.urlsafe_b64encode(o).decode('US-ASCII')
  18. return json.JSONEncoder.default(self, o)
  19. _jsonencoder = _JSONEncoder()
  20. class _TestJSONEncoder(unittest.TestCase):
  21. def test_defaultfailure(self):
  22. class Foo:
  23. pass
  24. self.assertRaises(TypeError, _jsonencoder.encode, Foo())
  25. # XXX - add validation
  26. # XXX - how to add singletons
  27. class MDBase(object):
  28. '''This is a simple wrapper that turns a JSON object into a pythonesc
  29. object where attribute accesses work.'''
  30. _type = 'invalid'
  31. _generated_properties = {
  32. 'uuid': uuid.uuid4,
  33. 'modified': lambda: datetime.datetime.now(
  34. tz=datetime.timezone.utc),
  35. }
  36. # When decoding, the decoded value should be passed to this function
  37. # to get the correct type
  38. _instance_properties = {
  39. 'uuid': _makeuuid,
  40. 'modified': _makedatetime,
  41. 'created_by_ref': _makeuuid,
  42. 'parent_refs': lambda x: [ _makeuuid(y) for y in x ],
  43. 'sig': _makebytes,
  44. }
  45. # Override on a per subclass basis
  46. _class_instance_properties = {
  47. }
  48. _common_properties = [ 'type', 'created_by_ref' ] # XXX - add lang?
  49. _common_optional = set(('parent_refs', 'sig'))
  50. _common_names = set(_common_properties + list(
  51. _generated_properties.keys()))
  52. _common_names_list = _common_properties + list(
  53. _generated_properties.keys())
  54. def __init__(self, obj={}, **kwargs):
  55. obj = copy.deepcopy(obj)
  56. obj.update(kwargs)
  57. if self._type == MDBase._type:
  58. raise ValueError('call MDBase.create_obj instead so correct class is used.')
  59. if 'type' in obj and obj['type'] != self._type:
  60. raise ValueError(
  61. 'trying to create the wrong type of object, got: %s, expected: %s' %
  62. (repr(obj['type']), repr(self._type)))
  63. if 'type' not in obj:
  64. obj['type'] = self._type
  65. for x in self._common_properties:
  66. if x not in obj:
  67. raise ValueError('common property %s not present' % repr(x))
  68. for x, fun in itertools.chain(
  69. self._instance_properties.items(),
  70. self._class_instance_properties.items()):
  71. if x in obj:
  72. obj[x] = fun(obj[x])
  73. for x, fun in self._generated_properties.items():
  74. if x not in obj:
  75. obj[x] = fun()
  76. self._obj = obj
  77. @classmethod
  78. def create_obj(cls, obj):
  79. '''Using obj as a base, create an instance of MDBase of the
  80. correct type.
  81. If the correct type is not found, a ValueError is raised.'''
  82. if isinstance(obj, cls):
  83. obj = obj._obj
  84. ty = obj['type']
  85. for i in MDBase.__subclasses__():
  86. if i._type == ty:
  87. return i(obj)
  88. else:
  89. raise ValueError('Unable to find class for type %s' %
  90. repr(ty))
  91. def new_version(self, *args, dels=(), replaces=()):
  92. '''For each k, v pair, add the property k as an additional one
  93. (or new one if first), with the value v.
  94. Any key in dels is removed.
  95. Any k, v pair in replaces, replaces the entire key.'''
  96. obj = copy.deepcopy(self._obj)
  97. common = self._common_names | self._common_optional
  98. uniquify = set()
  99. for k, v in args:
  100. if k in common:
  101. obj[k] = v
  102. else:
  103. uniquify.add(k)
  104. obj.setdefault(k, []).append(v)
  105. for k in uniquify:
  106. obj[k] = list(set(obj[k]))
  107. for i in dels:
  108. del obj[i]
  109. for k, v in replaces:
  110. obj[k] = v
  111. del obj['modified']
  112. return self.create_obj(obj)
  113. def __repr__(self): # pragma: no cover
  114. return '%s(%s)' % (self.__class__.__name__, repr(self._obj))
  115. def __getattr__(self, k):
  116. try:
  117. return self._obj[k]
  118. except KeyError:
  119. raise AttributeError(k)
  120. def __setattr__(self, k, v):
  121. if k[0] == '_': # direct attribute
  122. self.__dict__[k] = v
  123. else:
  124. self._obj[k] = v
  125. def __getitem__(self, k):
  126. return self._obj[k]
  127. def __to_dict__(self):
  128. '''Returns an internal object. If modification is necessary,
  129. make sure to .copy() it first.'''
  130. return self._obj
  131. def __eq__(self, o):
  132. return self._obj == o
  133. def __contains__(self, k):
  134. return k in self._obj
  135. def items(self, skipcommon=True):
  136. return [ (k, v) for k, v in self._obj.items() if
  137. not skipcommon or k not in self._common_names ]
  138. def encode(self, meth='asn1'):
  139. if meth == 'asn1':
  140. return _asn1coder.dumps(self)
  141. return _jsonencoder.encode(self._obj)
  142. @classmethod
  143. def decode(cls, s, meth='asn1'):
  144. if meth == 'asn1':
  145. obj = _asn1coder.loads(s)
  146. else:
  147. obj = json.loads(s)
  148. return cls.create_obj(obj)