autonomous-app 0.3.0__py3-none-any.whl → 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autonomous/__init__.py +1 -1
- autonomous/ai/audioagent.py +1 -1
- autonomous/ai/imageagent.py +1 -1
- autonomous/ai/jsonagent.py +1 -1
- autonomous/ai/models/openai.py +81 -53
- autonomous/ai/oaiagent.py +1 -14
- autonomous/ai/textagent.py +1 -1
- autonomous/auth/autoauth.py +10 -10
- autonomous/auth/user.py +17 -2
- autonomous/db/__init__.py +42 -0
- autonomous/db/base/__init__.py +33 -0
- autonomous/db/base/common.py +62 -0
- autonomous/db/base/datastructures.py +476 -0
- autonomous/db/base/document.py +1230 -0
- autonomous/db/base/fields.py +767 -0
- autonomous/db/base/metaclasses.py +468 -0
- autonomous/db/base/utils.py +22 -0
- autonomous/db/common.py +79 -0
- autonomous/db/connection.py +472 -0
- autonomous/db/context_managers.py +313 -0
- autonomous/db/dereference.py +291 -0
- autonomous/db/document.py +1141 -0
- autonomous/db/errors.py +165 -0
- autonomous/db/fields.py +2732 -0
- autonomous/db/mongodb_support.py +24 -0
- autonomous/db/pymongo_support.py +80 -0
- autonomous/db/queryset/__init__.py +28 -0
- autonomous/db/queryset/base.py +2033 -0
- autonomous/db/queryset/field_list.py +88 -0
- autonomous/db/queryset/manager.py +58 -0
- autonomous/db/queryset/queryset.py +189 -0
- autonomous/db/queryset/transform.py +527 -0
- autonomous/db/queryset/visitor.py +189 -0
- autonomous/db/signals.py +59 -0
- autonomous/logger.py +3 -0
- autonomous/model/autoattr.py +56 -41
- autonomous/model/automodel.py +95 -34
- autonomous/storage/imagestorage.py +49 -8
- {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/METADATA +2 -2
- autonomous_app-0.3.2.dist-info/RECORD +60 -0
- {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/WHEEL +1 -1
- autonomous_app-0.3.0.dist-info/RECORD +0 -35
- {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/LICENSE +0 -0
- {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import operator
|
|
3
|
+
import threading
|
|
4
|
+
import weakref
|
|
5
|
+
|
|
6
|
+
import pymongo
|
|
7
|
+
from bson import SON, DBRef, ObjectId
|
|
8
|
+
|
|
9
|
+
from autonomous import log
|
|
10
|
+
from autonomous.db.base.common import UPDATE_OPERATORS
|
|
11
|
+
from autonomous.db.base.datastructures import (
|
|
12
|
+
BaseDict,
|
|
13
|
+
BaseList,
|
|
14
|
+
# EmbeddedDocumentList,
|
|
15
|
+
)
|
|
16
|
+
from autonomous.db.common import _import_class
|
|
17
|
+
from autonomous.db.errors import DeprecatedError, ValidationError
|
|
18
|
+
|
|
19
|
+
__all__ = ("BaseField", "ComplexBaseField", "ObjectIdField", "GeoJsonBaseField")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@contextlib.contextmanager
|
|
23
|
+
def _no_dereference_for_fields(*fields):
|
|
24
|
+
"""Context manager for temporarily disabling a Field's auto-dereferencing
|
|
25
|
+
(meant to be used from no_dereference context manager)"""
|
|
26
|
+
try:
|
|
27
|
+
for field in fields:
|
|
28
|
+
field._incr_no_dereference_context()
|
|
29
|
+
yield None
|
|
30
|
+
finally:
|
|
31
|
+
for field in fields:
|
|
32
|
+
field._decr_no_dereference_context()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BaseField:
|
|
36
|
+
"""A base class for fields in a MongoDB document. Instances of this class
|
|
37
|
+
may be added to subclasses of `Document` to define a document's schema.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
name = None # set in TopLevelDocumentMetaclass
|
|
41
|
+
_geo_index = False
|
|
42
|
+
_auto_gen = False # Call `generate` to generate a value
|
|
43
|
+
_thread_local_storage = threading.local()
|
|
44
|
+
|
|
45
|
+
# These track each time a Field instance is created. Used to retain order.
|
|
46
|
+
# The auto_creation_counter is used for fields that MongoEngine implicitly
|
|
47
|
+
# creates, creation_counter is used for all user-specified fields.
|
|
48
|
+
creation_counter = 0
|
|
49
|
+
auto_creation_counter = -1
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
db_field=None,
|
|
54
|
+
required=False,
|
|
55
|
+
default=None,
|
|
56
|
+
unique=False,
|
|
57
|
+
unique_with=None,
|
|
58
|
+
primary_key=False,
|
|
59
|
+
validation=None,
|
|
60
|
+
choices=None,
|
|
61
|
+
null=True,
|
|
62
|
+
sparse=False,
|
|
63
|
+
**kwargs,
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
:param db_field: The database field to store this field in
|
|
67
|
+
(defaults to the name of the field)
|
|
68
|
+
:param required: If the field is required. Whether it has to have a
|
|
69
|
+
value or not. Defaults to False.
|
|
70
|
+
:param default: (optional) The default value for this field if no value
|
|
71
|
+
has been set, if the value is set to None or has been unset. It can be a
|
|
72
|
+
callable.
|
|
73
|
+
:param unique: Is the field value unique or not (Creates an index). Defaults to False.
|
|
74
|
+
:param unique_with: (optional) The other field this field should be
|
|
75
|
+
unique with (Creates an index).
|
|
76
|
+
:param primary_key: Mark this field as the primary key ((Creates an index)). Defaults to False.
|
|
77
|
+
:param validation: (optional) A callable to validate the value of the
|
|
78
|
+
field. The callable takes the value as parameter and should raise
|
|
79
|
+
a ValidationError if validation fails
|
|
80
|
+
:param choices: (optional) The valid choices
|
|
81
|
+
:param null: (optional) If the field value can be null when a default exists. If not set, the default value
|
|
82
|
+
will be used in case a field with a default value is set to None. Defaults to False.
|
|
83
|
+
:param sparse: (optional) `sparse=True` combined with `unique=True` and `required=False`
|
|
84
|
+
means that uniqueness won't be enforced for `None` values (Creates an index). Defaults to False.
|
|
85
|
+
:param **kwargs: (optional) Arbitrary indirection-free metadata for
|
|
86
|
+
this field can be supplied as additional keyword arguments and
|
|
87
|
+
accessed as attributes of the field. Must not conflict with any
|
|
88
|
+
existing attributes. Common metadata includes `verbose_name` and
|
|
89
|
+
`help_text`.
|
|
90
|
+
"""
|
|
91
|
+
self.db_field = db_field if not primary_key else "_id"
|
|
92
|
+
|
|
93
|
+
self.required = required or primary_key
|
|
94
|
+
self.default = default
|
|
95
|
+
self.unique = bool(unique or unique_with)
|
|
96
|
+
self.unique_with = unique_with
|
|
97
|
+
self.primary_key = primary_key
|
|
98
|
+
self.validation = validation
|
|
99
|
+
self.choices = choices
|
|
100
|
+
self.null = null
|
|
101
|
+
self.sparse = sparse
|
|
102
|
+
self._owner_document = None
|
|
103
|
+
|
|
104
|
+
self.__auto_dereference = True
|
|
105
|
+
|
|
106
|
+
# Make sure db_field is a string (if it's explicitly defined).
|
|
107
|
+
if self.db_field is not None and not isinstance(self.db_field, str):
|
|
108
|
+
raise TypeError("db_field should be a string.")
|
|
109
|
+
|
|
110
|
+
# Make sure db_field doesn't contain any forbidden characters.
|
|
111
|
+
if isinstance(self.db_field, str) and (
|
|
112
|
+
"." in self.db_field
|
|
113
|
+
or "\0" in self.db_field
|
|
114
|
+
or self.db_field.startswith("$")
|
|
115
|
+
):
|
|
116
|
+
raise ValueError(
|
|
117
|
+
'field names cannot contain dots (".") or null characters '
|
|
118
|
+
'("\\0"), and they must not start with a dollar sign ("$").'
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Detect and report conflicts between metadata and base properties.
|
|
122
|
+
conflicts = set(dir(self)) & set(kwargs)
|
|
123
|
+
if conflicts:
|
|
124
|
+
raise TypeError(
|
|
125
|
+
"%s already has attribute(s): %s"
|
|
126
|
+
% (self.__class__.__name__, ", ".join(conflicts))
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Assign metadata to the instance
|
|
130
|
+
# This efficient method is available because no __slots__ are defined.
|
|
131
|
+
self.__dict__.update(kwargs)
|
|
132
|
+
|
|
133
|
+
# Adjust the appropriate creation counter, and save our local copy.
|
|
134
|
+
if self.db_field == "_id":
|
|
135
|
+
self.creation_counter = BaseField.auto_creation_counter
|
|
136
|
+
BaseField.auto_creation_counter -= 1
|
|
137
|
+
else:
|
|
138
|
+
self.creation_counter = BaseField.creation_counter
|
|
139
|
+
BaseField.creation_counter += 1
|
|
140
|
+
|
|
141
|
+
def set_auto_dereferencing(self, value):
|
|
142
|
+
self.__auto_dereference = value
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def _no_dereference_context_local(self):
|
|
146
|
+
if not hasattr(self._thread_local_storage, "no_dereference_context"):
|
|
147
|
+
self._thread_local_storage.no_dereference_context = 0
|
|
148
|
+
return self._thread_local_storage.no_dereference_context
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def _no_dereference_context_is_set(self):
|
|
152
|
+
return self._no_dereference_context_local > 0
|
|
153
|
+
|
|
154
|
+
def _incr_no_dereference_context(self):
|
|
155
|
+
self._thread_local_storage.no_dereference_context = (
|
|
156
|
+
self._no_dereference_context_local + 1
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _decr_no_dereference_context(self):
|
|
160
|
+
self._thread_local_storage.no_dereference_context = (
|
|
161
|
+
self._no_dereference_context_local - 1
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def _auto_dereference(self):
|
|
166
|
+
return self.__auto_dereference and not self._no_dereference_context_is_set
|
|
167
|
+
|
|
168
|
+
def __get__(self, instance, owner):
|
|
169
|
+
"""Descriptor for retrieving a value from a field in a document."""
|
|
170
|
+
if instance is None:
|
|
171
|
+
# Document class being used rather than a document object
|
|
172
|
+
return self
|
|
173
|
+
|
|
174
|
+
# Get value from document instance if available
|
|
175
|
+
result = instance._data.get(self.name)
|
|
176
|
+
if not result:
|
|
177
|
+
if self.default is not None:
|
|
178
|
+
result = self.default
|
|
179
|
+
if callable(result):
|
|
180
|
+
result = result()
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
def __set__(self, instance, value):
|
|
184
|
+
"""Descriptor for assigning a value to a field in a document."""
|
|
185
|
+
# If setting to None and there is a default value provided for this
|
|
186
|
+
# field, then set the value to the default value.
|
|
187
|
+
if value is None:
|
|
188
|
+
if self.default is not None:
|
|
189
|
+
value = self.default
|
|
190
|
+
if callable(value):
|
|
191
|
+
value = value()
|
|
192
|
+
if not self.null:
|
|
193
|
+
raise ValueError(
|
|
194
|
+
"Field value cannot be set to None for required fields."
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if instance._initialised:
|
|
198
|
+
try:
|
|
199
|
+
value_has_changed = (
|
|
200
|
+
self.name not in instance._data
|
|
201
|
+
or instance._data[self.name] != value
|
|
202
|
+
)
|
|
203
|
+
if value_has_changed:
|
|
204
|
+
instance._mark_as_changed(self.name)
|
|
205
|
+
except Exception:
|
|
206
|
+
# Some values can't be compared and throw an error when we
|
|
207
|
+
# attempt to do so (e.g. tz-naive and tz-aware datetimes).
|
|
208
|
+
# Mark the field as changed in such cases.
|
|
209
|
+
instance._mark_as_changed(self.name)
|
|
210
|
+
|
|
211
|
+
EmbeddedDocument = _import_class("EmbeddedDocument")
|
|
212
|
+
if isinstance(value, EmbeddedDocument):
|
|
213
|
+
value._instance = weakref.proxy(instance)
|
|
214
|
+
elif isinstance(value, (list, tuple)):
|
|
215
|
+
for v in value:
|
|
216
|
+
if isinstance(v, EmbeddedDocument):
|
|
217
|
+
v._instance = weakref.proxy(instance)
|
|
218
|
+
|
|
219
|
+
instance._data[self.name] = value
|
|
220
|
+
|
|
221
|
+
def error(self, message="", errors=None, field_name=None):
|
|
222
|
+
"""Raise a ValidationError."""
|
|
223
|
+
field_name = field_name if field_name else self.name
|
|
224
|
+
raise ValidationError(message, errors=errors, field_name=field_name)
|
|
225
|
+
|
|
226
|
+
def to_python(self, value):
|
|
227
|
+
"""Convert a MongoDB-compatible type to a Python type."""
|
|
228
|
+
return value
|
|
229
|
+
|
|
230
|
+
def to_mongo(self, value):
|
|
231
|
+
"""Convert a Python type to a MongoDB-compatible type."""
|
|
232
|
+
return self.to_python(value)
|
|
233
|
+
|
|
234
|
+
def _to_mongo_safe_call(self, value, use_db_field=True, fields=None):
|
|
235
|
+
"""Helper method to call to_mongo with proper inputs."""
|
|
236
|
+
f_inputs = self.to_mongo.__code__.co_varnames
|
|
237
|
+
ex_vars = {}
|
|
238
|
+
if "fields" in f_inputs:
|
|
239
|
+
ex_vars["fields"] = fields
|
|
240
|
+
|
|
241
|
+
if "use_db_field" in f_inputs:
|
|
242
|
+
ex_vars["use_db_field"] = use_db_field
|
|
243
|
+
|
|
244
|
+
return self.to_mongo(value, **ex_vars)
|
|
245
|
+
|
|
246
|
+
def prepare_query_value(self, op, value):
|
|
247
|
+
"""Prepare a value that is being used in a query for PyMongo."""
|
|
248
|
+
if op in UPDATE_OPERATORS:
|
|
249
|
+
self.validate(value)
|
|
250
|
+
return value
|
|
251
|
+
|
|
252
|
+
def validate(self, value, clean=True):
|
|
253
|
+
"""Perform validation on a value."""
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
def _validate_choices(self, value):
|
|
257
|
+
Document = _import_class("Document")
|
|
258
|
+
EmbeddedDocument = _import_class("EmbeddedDocument")
|
|
259
|
+
|
|
260
|
+
choice_list = self.choices
|
|
261
|
+
if isinstance(next(iter(choice_list)), (list, tuple)):
|
|
262
|
+
# next(iter) is useful for sets
|
|
263
|
+
choice_list = [k for k, _ in choice_list]
|
|
264
|
+
|
|
265
|
+
# log(
|
|
266
|
+
# value,
|
|
267
|
+
# type(value),
|
|
268
|
+
# choice_list,
|
|
269
|
+
# value in choice_list,
|
|
270
|
+
# )
|
|
271
|
+
# Choices which are other types of Documents
|
|
272
|
+
if isinstance(value, (Document, EmbeddedDocument)):
|
|
273
|
+
if not any(isinstance(value, c) for c in choice_list):
|
|
274
|
+
self.error("Value must be an instance of %s" % (choice_list))
|
|
275
|
+
# Choices which are types other than Documents
|
|
276
|
+
else:
|
|
277
|
+
values = value if isinstance(value, (list, tuple)) else [value]
|
|
278
|
+
if len(set(values) - set(choice_list)):
|
|
279
|
+
self.error("Value must be one of %s" % str(choice_list))
|
|
280
|
+
|
|
281
|
+
def _validate(self, value, **kwargs):
|
|
282
|
+
# Check the Choices Constraint
|
|
283
|
+
if self.choices:
|
|
284
|
+
self._validate_choices(value)
|
|
285
|
+
|
|
286
|
+
# check validation argument
|
|
287
|
+
# log(f"Validating {self.name} with value {value}: {self.validation}")
|
|
288
|
+
if self.validation is not None:
|
|
289
|
+
if callable(self.validation):
|
|
290
|
+
try:
|
|
291
|
+
# breaking change of 0.18
|
|
292
|
+
# Get rid of True/False-type return for the validation method
|
|
293
|
+
# in favor of having validation raising a ValidationError
|
|
294
|
+
ret = self.validation(value)
|
|
295
|
+
if ret is not None:
|
|
296
|
+
raise DeprecatedError(
|
|
297
|
+
"validation argument for `%s` must not return anything, "
|
|
298
|
+
"it should raise a ValidationError if validation fails"
|
|
299
|
+
% self.name
|
|
300
|
+
)
|
|
301
|
+
except ValidationError as ex:
|
|
302
|
+
self.error(str(ex))
|
|
303
|
+
else:
|
|
304
|
+
raise ValueError(
|
|
305
|
+
'validation argument for `"%s"` must be a ' "callable." % self.name
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
self.validate(value, **kwargs)
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def owner_document(self):
|
|
312
|
+
return self._owner_document
|
|
313
|
+
|
|
314
|
+
def _set_owner_document(self, owner_document):
|
|
315
|
+
self._owner_document = owner_document
|
|
316
|
+
|
|
317
|
+
@owner_document.setter
|
|
318
|
+
def owner_document(self, owner_document):
|
|
319
|
+
self._set_owner_document(owner_document)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class ComplexBaseField(BaseField):
|
|
323
|
+
"""Handles complex fields, such as lists / dictionaries.
|
|
324
|
+
|
|
325
|
+
Allows for nesting of embedded documents inside complex types.
|
|
326
|
+
Handles the lazy dereferencing of a queryset by lazily dereferencing all
|
|
327
|
+
items in a list / dict rather than one at a time.
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
def __init__(self, field=None, **kwargs):
|
|
331
|
+
if field is not None and not isinstance(field, BaseField):
|
|
332
|
+
raise TypeError(
|
|
333
|
+
f"field argument must be a Field instance (e.g {self.__class__.__name__}(StringField()))"
|
|
334
|
+
)
|
|
335
|
+
self.field = field
|
|
336
|
+
super().__init__(**kwargs)
|
|
337
|
+
|
|
338
|
+
@staticmethod
|
|
339
|
+
def _lazy_load_refs(instance, name, ref_values, *, max_depth):
|
|
340
|
+
_dereference = _import_class("DeReference")()
|
|
341
|
+
documents = _dereference(
|
|
342
|
+
ref_values,
|
|
343
|
+
max_depth=max_depth,
|
|
344
|
+
instance=instance,
|
|
345
|
+
name=name,
|
|
346
|
+
)
|
|
347
|
+
return documents
|
|
348
|
+
|
|
349
|
+
def __set__(self, instance, value):
|
|
350
|
+
# Some fields e.g EnumField are converted upon __set__
|
|
351
|
+
# So it is fair to mimic the same behavior when using e.g ListField(EnumField)
|
|
352
|
+
# log(f"Setting {self.name}[{instance}] with value {value}")
|
|
353
|
+
EnumField = _import_class("EnumField")
|
|
354
|
+
if self.field and isinstance(self.field, EnumField):
|
|
355
|
+
if isinstance(value, (list, tuple)):
|
|
356
|
+
value = [self.field.to_python(sub_val) for sub_val in value]
|
|
357
|
+
elif isinstance(value, dict):
|
|
358
|
+
value = {key: self.field.to_python(sub) for key, sub in value.items()}
|
|
359
|
+
|
|
360
|
+
return super().__set__(instance, value)
|
|
361
|
+
|
|
362
|
+
def __get__(self, instance, owner):
|
|
363
|
+
"""Descriptor to automatically dereference references."""
|
|
364
|
+
if instance is None:
|
|
365
|
+
# Document class being used rather than a document object
|
|
366
|
+
return self
|
|
367
|
+
|
|
368
|
+
ReferenceField = _import_class("ReferenceField")
|
|
369
|
+
GenericReferenceField = _import_class("GenericReferenceField")
|
|
370
|
+
# EmbeddedDocumentListField = _import_class("EmbeddedDocumentListField")
|
|
371
|
+
|
|
372
|
+
auto_dereference = instance._fields[self.name]._auto_dereference
|
|
373
|
+
dereference = auto_dereference and (
|
|
374
|
+
self.field is None
|
|
375
|
+
or isinstance(self.field, (GenericReferenceField, ReferenceField))
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
if (
|
|
379
|
+
instance._initialised
|
|
380
|
+
and dereference
|
|
381
|
+
and instance._data.get(self.name)
|
|
382
|
+
and not getattr(instance._data[self.name], "_dereferenced", False)
|
|
383
|
+
):
|
|
384
|
+
ref_values = instance._data.get(self.name)
|
|
385
|
+
# log(f"Lazy loading refs for {self.name}: {ref_values}")
|
|
386
|
+
instance._data[self.name] = self._lazy_load_refs(
|
|
387
|
+
ref_values=ref_values, instance=instance, name=self.name, max_depth=1
|
|
388
|
+
)
|
|
389
|
+
# log(f"Lazy loading results: {instance._data[self.name]}")
|
|
390
|
+
if hasattr(instance._data[self.name], "_dereferenced"):
|
|
391
|
+
instance._data[self.name]._dereferenced = True
|
|
392
|
+
value = super().__get__(instance, owner)
|
|
393
|
+
|
|
394
|
+
# Convert lists / values so we can watch for any changes on them
|
|
395
|
+
if isinstance(value, (list, tuple)):
|
|
396
|
+
# if issubclass(type(self), EmbeddedDocumentListField) and not isinstance(
|
|
397
|
+
# value, EmbeddedDocumentList
|
|
398
|
+
# ):
|
|
399
|
+
# value = EmbeddedDocumentList(value, instance, self.name)
|
|
400
|
+
# el
|
|
401
|
+
if not isinstance(value, BaseList):
|
|
402
|
+
value = BaseList(value, instance, self.name)
|
|
403
|
+
instance._data[self.name] = value
|
|
404
|
+
elif isinstance(value, dict) and not isinstance(value, BaseDict):
|
|
405
|
+
value = BaseDict(value, instance, self.name)
|
|
406
|
+
instance._data[self.name] = value
|
|
407
|
+
|
|
408
|
+
if (
|
|
409
|
+
auto_dereference
|
|
410
|
+
and instance._initialised
|
|
411
|
+
and isinstance(value, (BaseList, BaseDict))
|
|
412
|
+
and not value._dereferenced
|
|
413
|
+
):
|
|
414
|
+
value = self._lazy_load_refs(
|
|
415
|
+
ref_values=value, instance=instance, name=self.name, max_depth=1
|
|
416
|
+
)
|
|
417
|
+
value._dereferenced = True
|
|
418
|
+
instance._data[self.name] = value
|
|
419
|
+
# log(f"Value: {value}")
|
|
420
|
+
return value
|
|
421
|
+
|
|
422
|
+
def to_python(self, value):
|
|
423
|
+
"""Convert a MongoDB-compatible type to a Python type."""
|
|
424
|
+
if isinstance(value, str):
|
|
425
|
+
return value
|
|
426
|
+
|
|
427
|
+
if hasattr(value, "to_python"):
|
|
428
|
+
return value.to_python()
|
|
429
|
+
|
|
430
|
+
BaseDocument = _import_class("BaseDocument")
|
|
431
|
+
if isinstance(value, BaseDocument):
|
|
432
|
+
# Something is wrong, return the value as it is
|
|
433
|
+
return value
|
|
434
|
+
|
|
435
|
+
is_list = False
|
|
436
|
+
if not hasattr(value, "items"):
|
|
437
|
+
try:
|
|
438
|
+
is_list = True
|
|
439
|
+
value = {idx: v for idx, v in enumerate(value)}
|
|
440
|
+
except TypeError: # Not iterable return the value
|
|
441
|
+
return value
|
|
442
|
+
|
|
443
|
+
if self.field:
|
|
444
|
+
self.field.set_auto_dereferencing(self._auto_dereference)
|
|
445
|
+
value_dict = {
|
|
446
|
+
key: self.field.to_python(item) for key, item in value.items()
|
|
447
|
+
}
|
|
448
|
+
else:
|
|
449
|
+
Document = _import_class("Document")
|
|
450
|
+
value_dict = {}
|
|
451
|
+
for k, v in value.items():
|
|
452
|
+
if isinstance(v, Document):
|
|
453
|
+
# We need the id from the saved object to create the DBRef
|
|
454
|
+
if v.pk is None:
|
|
455
|
+
self.error(
|
|
456
|
+
"You can only reference documents once they"
|
|
457
|
+
" have been saved to the database"
|
|
458
|
+
)
|
|
459
|
+
collection = v._get_collection_name()
|
|
460
|
+
value_dict[k] = DBRef(collection, v.pk)
|
|
461
|
+
elif hasattr(v, "to_python"):
|
|
462
|
+
value_dict[k] = v.to_python()
|
|
463
|
+
else:
|
|
464
|
+
value_dict[k] = self.to_python(v)
|
|
465
|
+
|
|
466
|
+
if is_list: # Convert back to a list
|
|
467
|
+
return [
|
|
468
|
+
v for _, v in sorted(value_dict.items(), key=operator.itemgetter(0))
|
|
469
|
+
]
|
|
470
|
+
return value_dict
|
|
471
|
+
|
|
472
|
+
def to_mongo(self, value, use_db_field=True, fields=None):
|
|
473
|
+
"""Convert a Python type to a MongoDB-compatible type."""
|
|
474
|
+
Document = _import_class("Document")
|
|
475
|
+
EmbeddedDocument = _import_class("EmbeddedDocument")
|
|
476
|
+
GenericReferenceField = _import_class("GenericReferenceField")
|
|
477
|
+
|
|
478
|
+
if isinstance(value, str):
|
|
479
|
+
return value
|
|
480
|
+
|
|
481
|
+
if hasattr(value, "to_mongo"):
|
|
482
|
+
if isinstance(value, Document):
|
|
483
|
+
return GenericReferenceField().to_mongo(value)
|
|
484
|
+
cls = value.__class__
|
|
485
|
+
val = value.to_mongo(use_db_field, fields)
|
|
486
|
+
# If it's a document that is not inherited add _cls
|
|
487
|
+
if isinstance(value, EmbeddedDocument):
|
|
488
|
+
val["_cls"] = cls.__name__
|
|
489
|
+
return val
|
|
490
|
+
|
|
491
|
+
is_list = False
|
|
492
|
+
if not hasattr(value, "items"):
|
|
493
|
+
try:
|
|
494
|
+
is_list = True
|
|
495
|
+
value = {k: v for k, v in enumerate(value)}
|
|
496
|
+
except TypeError: # Not iterable return the value
|
|
497
|
+
return value
|
|
498
|
+
|
|
499
|
+
if self.field:
|
|
500
|
+
value_dict = {
|
|
501
|
+
key: self.field._to_mongo_safe_call(item, use_db_field, fields)
|
|
502
|
+
for key, item in value.items()
|
|
503
|
+
}
|
|
504
|
+
else:
|
|
505
|
+
value_dict = {}
|
|
506
|
+
for k, v in value.items():
|
|
507
|
+
if isinstance(v, Document):
|
|
508
|
+
# We need the id from the saved object to create the DBRef
|
|
509
|
+
if v.pk is None:
|
|
510
|
+
self.error(
|
|
511
|
+
"You can only reference documents once they"
|
|
512
|
+
" have been saved to the database"
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# If it's a document that is not inheritable it won't have
|
|
516
|
+
# any _cls data so make it a generic reference allows
|
|
517
|
+
# us to dereference
|
|
518
|
+
meta = getattr(v, "_meta", {})
|
|
519
|
+
allow_inheritance = meta.get("allow_inheritance")
|
|
520
|
+
if not allow_inheritance:
|
|
521
|
+
value_dict[k] = GenericReferenceField().to_mongo(v)
|
|
522
|
+
else:
|
|
523
|
+
collection = v._get_collection_name()
|
|
524
|
+
value_dict[k] = DBRef(collection, v.pk)
|
|
525
|
+
elif hasattr(v, "to_mongo"):
|
|
526
|
+
cls = v.__class__
|
|
527
|
+
val = v.to_mongo(use_db_field, fields)
|
|
528
|
+
# If it's a document that is not inherited add _cls
|
|
529
|
+
if isinstance(v, (Document, EmbeddedDocument)):
|
|
530
|
+
val["_cls"] = cls.__name__
|
|
531
|
+
value_dict[k] = val
|
|
532
|
+
else:
|
|
533
|
+
value_dict[k] = self.to_mongo(v, use_db_field, fields)
|
|
534
|
+
|
|
535
|
+
if is_list: # Convert back to a list
|
|
536
|
+
return [
|
|
537
|
+
v for _, v in sorted(value_dict.items(), key=operator.itemgetter(0))
|
|
538
|
+
]
|
|
539
|
+
return value_dict
|
|
540
|
+
|
|
541
|
+
def validate(self, value):
|
|
542
|
+
"""If field is provided ensure the value is valid."""
|
|
543
|
+
errors = {}
|
|
544
|
+
if self.field:
|
|
545
|
+
if hasattr(value, "items"):
|
|
546
|
+
sequence = value.items()
|
|
547
|
+
else:
|
|
548
|
+
sequence = enumerate(value)
|
|
549
|
+
for k, v in sequence:
|
|
550
|
+
try:
|
|
551
|
+
self.field._validate(v)
|
|
552
|
+
except ValidationError as error:
|
|
553
|
+
errors[k] = error.errors or error
|
|
554
|
+
except (ValueError, AssertionError) as error:
|
|
555
|
+
errors[k] = error
|
|
556
|
+
|
|
557
|
+
if errors:
|
|
558
|
+
field_class = self.field.__class__.__name__
|
|
559
|
+
self.error(f"Invalid {field_class} item ({value})", errors=errors)
|
|
560
|
+
# Don't allow empty values if required
|
|
561
|
+
if self.required and not value:
|
|
562
|
+
self.error("Field is required and cannot be empty")
|
|
563
|
+
|
|
564
|
+
def prepare_query_value(self, op, value):
|
|
565
|
+
return self.to_mongo(value)
|
|
566
|
+
|
|
567
|
+
def lookup_member(self, member_name):
|
|
568
|
+
if self.field:
|
|
569
|
+
return self.field.lookup_member(member_name)
|
|
570
|
+
return None
|
|
571
|
+
|
|
572
|
+
def _set_owner_document(self, owner_document):
|
|
573
|
+
if self.field:
|
|
574
|
+
self.field.owner_document = owner_document
|
|
575
|
+
self._owner_document = owner_document
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class ObjectIdField(BaseField):
|
|
579
|
+
"""A field wrapper around MongoDB's ObjectIds."""
|
|
580
|
+
|
|
581
|
+
def to_python(self, value):
|
|
582
|
+
try:
|
|
583
|
+
if not isinstance(value, ObjectId):
|
|
584
|
+
value = ObjectId(value)
|
|
585
|
+
except Exception:
|
|
586
|
+
pass
|
|
587
|
+
return value
|
|
588
|
+
|
|
589
|
+
def to_mongo(self, value):
|
|
590
|
+
if isinstance(value, ObjectId):
|
|
591
|
+
return value
|
|
592
|
+
|
|
593
|
+
try:
|
|
594
|
+
return ObjectId(str(value))
|
|
595
|
+
except Exception as e:
|
|
596
|
+
self.error(str(e))
|
|
597
|
+
|
|
598
|
+
def prepare_query_value(self, op, value):
|
|
599
|
+
if value is None:
|
|
600
|
+
return value
|
|
601
|
+
return self.to_mongo(value)
|
|
602
|
+
|
|
603
|
+
def validate(self, value):
|
|
604
|
+
try:
|
|
605
|
+
ObjectId(str(value))
|
|
606
|
+
except Exception:
|
|
607
|
+
self.error("Invalid ObjectID")
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
class GeoJsonBaseField(BaseField):
|
|
611
|
+
"""A geo json field storing a geojson style object."""
|
|
612
|
+
|
|
613
|
+
_geo_index = pymongo.GEOSPHERE
|
|
614
|
+
_type = "GeoBase"
|
|
615
|
+
|
|
616
|
+
def __init__(self, auto_index=True, *args, **kwargs):
|
|
617
|
+
"""
|
|
618
|
+
:param bool auto_index: Automatically create a '2dsphere' index.\
|
|
619
|
+
Defaults to `True`.
|
|
620
|
+
"""
|
|
621
|
+
self._name = "%sField" % self._type
|
|
622
|
+
if not auto_index:
|
|
623
|
+
self._geo_index = False
|
|
624
|
+
super().__init__(*args, **kwargs)
|
|
625
|
+
|
|
626
|
+
def validate(self, value):
|
|
627
|
+
"""Validate the GeoJson object based on its type."""
|
|
628
|
+
if isinstance(value, dict):
|
|
629
|
+
if set(value.keys()) == {"type", "coordinates"}:
|
|
630
|
+
if value["type"] != self._type:
|
|
631
|
+
self.error(f'{self._name} type must be "{self._type}"')
|
|
632
|
+
return self.validate(value["coordinates"])
|
|
633
|
+
else:
|
|
634
|
+
self.error(
|
|
635
|
+
"%s can only accept a valid GeoJson dictionary"
|
|
636
|
+
" or lists of (x, y)" % self._name
|
|
637
|
+
)
|
|
638
|
+
return
|
|
639
|
+
elif not isinstance(value, (list, tuple)):
|
|
640
|
+
self.error("%s can only accept lists of [x, y]" % self._name)
|
|
641
|
+
return
|
|
642
|
+
|
|
643
|
+
validate = getattr(self, "_validate_%s" % self._type.lower())
|
|
644
|
+
error = validate(value)
|
|
645
|
+
if error:
|
|
646
|
+
self.error(error)
|
|
647
|
+
|
|
648
|
+
def _validate_polygon(self, value, top_level=True):
|
|
649
|
+
if not isinstance(value, (list, tuple)):
|
|
650
|
+
return "Polygons must contain list of linestrings"
|
|
651
|
+
|
|
652
|
+
# Quick and dirty validator
|
|
653
|
+
try:
|
|
654
|
+
value[0][0][0]
|
|
655
|
+
except (TypeError, IndexError):
|
|
656
|
+
return "Invalid Polygon must contain at least one valid linestring"
|
|
657
|
+
|
|
658
|
+
errors = []
|
|
659
|
+
for val in value:
|
|
660
|
+
error = self._validate_linestring(val, False)
|
|
661
|
+
if not error and val[0] != val[-1]:
|
|
662
|
+
error = "LineStrings must start and end at the same point"
|
|
663
|
+
if error and error not in errors:
|
|
664
|
+
errors.append(error)
|
|
665
|
+
if errors:
|
|
666
|
+
if top_level:
|
|
667
|
+
return "Invalid Polygon:\n%s" % ", ".join(errors)
|
|
668
|
+
else:
|
|
669
|
+
return "%s" % ", ".join(errors)
|
|
670
|
+
|
|
671
|
+
def _validate_linestring(self, value, top_level=True):
|
|
672
|
+
"""Validate a linestring."""
|
|
673
|
+
if not isinstance(value, (list, tuple)):
|
|
674
|
+
return "LineStrings must contain list of coordinate pairs"
|
|
675
|
+
|
|
676
|
+
# Quick and dirty validator
|
|
677
|
+
try:
|
|
678
|
+
value[0][0]
|
|
679
|
+
except (TypeError, IndexError):
|
|
680
|
+
return "Invalid LineString must contain at least one valid point"
|
|
681
|
+
|
|
682
|
+
errors = []
|
|
683
|
+
for val in value:
|
|
684
|
+
error = self._validate_point(val)
|
|
685
|
+
if error and error not in errors:
|
|
686
|
+
errors.append(error)
|
|
687
|
+
if errors:
|
|
688
|
+
if top_level:
|
|
689
|
+
return "Invalid LineString:\n%s" % ", ".join(errors)
|
|
690
|
+
else:
|
|
691
|
+
return "%s" % ", ".join(errors)
|
|
692
|
+
|
|
693
|
+
def _validate_point(self, value):
|
|
694
|
+
"""Validate each set of coords"""
|
|
695
|
+
if not isinstance(value, (list, tuple)):
|
|
696
|
+
return "Points must be a list of coordinate pairs"
|
|
697
|
+
elif not len(value) == 2:
|
|
698
|
+
return "Value (%s) must be a two-dimensional point" % repr(value)
|
|
699
|
+
elif not isinstance(value[0], (float, int)) or not isinstance(
|
|
700
|
+
value[1], (float, int)
|
|
701
|
+
):
|
|
702
|
+
return "Both values (%s) in point must be float or int" % repr(value)
|
|
703
|
+
|
|
704
|
+
def _validate_multipoint(self, value):
|
|
705
|
+
if not isinstance(value, (list, tuple)):
|
|
706
|
+
return "MultiPoint must be a list of Point"
|
|
707
|
+
|
|
708
|
+
# Quick and dirty validator
|
|
709
|
+
try:
|
|
710
|
+
value[0][0]
|
|
711
|
+
except (TypeError, IndexError):
|
|
712
|
+
return "Invalid MultiPoint must contain at least one valid point"
|
|
713
|
+
|
|
714
|
+
errors = []
|
|
715
|
+
for point in value:
|
|
716
|
+
error = self._validate_point(point)
|
|
717
|
+
if error and error not in errors:
|
|
718
|
+
errors.append(error)
|
|
719
|
+
|
|
720
|
+
if errors:
|
|
721
|
+
return "%s" % ", ".join(errors)
|
|
722
|
+
|
|
723
|
+
def _validate_multilinestring(self, value, top_level=True):
|
|
724
|
+
if not isinstance(value, (list, tuple)):
|
|
725
|
+
return "MultiLineString must be a list of LineString"
|
|
726
|
+
|
|
727
|
+
# Quick and dirty validator
|
|
728
|
+
try:
|
|
729
|
+
value[0][0][0]
|
|
730
|
+
except (TypeError, IndexError):
|
|
731
|
+
return "Invalid MultiLineString must contain at least one valid linestring"
|
|
732
|
+
|
|
733
|
+
errors = []
|
|
734
|
+
for linestring in value:
|
|
735
|
+
error = self._validate_linestring(linestring, False)
|
|
736
|
+
if error and error not in errors:
|
|
737
|
+
errors.append(error)
|
|
738
|
+
|
|
739
|
+
if errors:
|
|
740
|
+
if top_level:
|
|
741
|
+
return "Invalid MultiLineString:\n%s" % ", ".join(errors)
|
|
742
|
+
else:
|
|
743
|
+
return "%s" % ", ".join(errors)
|
|
744
|
+
|
|
745
|
+
def _validate_multipolygon(self, value):
|
|
746
|
+
if not isinstance(value, (list, tuple)):
|
|
747
|
+
return "MultiPolygon must be a list of Polygon"
|
|
748
|
+
|
|
749
|
+
# Quick and dirty validator
|
|
750
|
+
try:
|
|
751
|
+
value[0][0][0][0]
|
|
752
|
+
except (TypeError, IndexError):
|
|
753
|
+
return "Invalid MultiPolygon must contain at least one valid Polygon"
|
|
754
|
+
|
|
755
|
+
errors = []
|
|
756
|
+
for polygon in value:
|
|
757
|
+
error = self._validate_polygon(polygon, False)
|
|
758
|
+
if error and error not in errors:
|
|
759
|
+
errors.append(error)
|
|
760
|
+
|
|
761
|
+
if errors:
|
|
762
|
+
return "Invalid MultiPolygon:\n%s" % ", ".join(errors)
|
|
763
|
+
|
|
764
|
+
def to_mongo(self, value):
|
|
765
|
+
if isinstance(value, dict):
|
|
766
|
+
return value
|
|
767
|
+
return SON([("type", self._type), ("coordinates", value)])
|