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.
Files changed (44) hide show
  1. autonomous/__init__.py +1 -1
  2. autonomous/ai/audioagent.py +1 -1
  3. autonomous/ai/imageagent.py +1 -1
  4. autonomous/ai/jsonagent.py +1 -1
  5. autonomous/ai/models/openai.py +81 -53
  6. autonomous/ai/oaiagent.py +1 -14
  7. autonomous/ai/textagent.py +1 -1
  8. autonomous/auth/autoauth.py +10 -10
  9. autonomous/auth/user.py +17 -2
  10. autonomous/db/__init__.py +42 -0
  11. autonomous/db/base/__init__.py +33 -0
  12. autonomous/db/base/common.py +62 -0
  13. autonomous/db/base/datastructures.py +476 -0
  14. autonomous/db/base/document.py +1230 -0
  15. autonomous/db/base/fields.py +767 -0
  16. autonomous/db/base/metaclasses.py +468 -0
  17. autonomous/db/base/utils.py +22 -0
  18. autonomous/db/common.py +79 -0
  19. autonomous/db/connection.py +472 -0
  20. autonomous/db/context_managers.py +313 -0
  21. autonomous/db/dereference.py +291 -0
  22. autonomous/db/document.py +1141 -0
  23. autonomous/db/errors.py +165 -0
  24. autonomous/db/fields.py +2732 -0
  25. autonomous/db/mongodb_support.py +24 -0
  26. autonomous/db/pymongo_support.py +80 -0
  27. autonomous/db/queryset/__init__.py +28 -0
  28. autonomous/db/queryset/base.py +2033 -0
  29. autonomous/db/queryset/field_list.py +88 -0
  30. autonomous/db/queryset/manager.py +58 -0
  31. autonomous/db/queryset/queryset.py +189 -0
  32. autonomous/db/queryset/transform.py +527 -0
  33. autonomous/db/queryset/visitor.py +189 -0
  34. autonomous/db/signals.py +59 -0
  35. autonomous/logger.py +3 -0
  36. autonomous/model/autoattr.py +56 -41
  37. autonomous/model/automodel.py +95 -34
  38. autonomous/storage/imagestorage.py +49 -8
  39. {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/METADATA +2 -2
  40. autonomous_app-0.3.2.dist-info/RECORD +60 -0
  41. {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/WHEEL +1 -1
  42. autonomous_app-0.3.0.dist-info/RECORD +0 -35
  43. {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/LICENSE +0 -0
  44. {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)])