-
Notifications
You must be signed in to change notification settings - Fork 1
/
model.py
1472 lines (1243 loc) · 55.2 KB
/
model.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Hive Flask Quorum
# Copyright (C) 2008-2014 Hive Solutions Lda.
#
# This file is part of Hive Flask Quorum.
#
# Hive Flask Quorum is free software: you can redistribute it and/or modify
# it under the terms of the Apache License as published by the Apache
# Foundation, either version 2.0 of the License, or (at your option) any
# later version.
#
# Hive Flask Quorum is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Apache License for more details.
#
# You should have received a copy of the Apache License along with
# Hive Flask Quorum. If not, see <http://www.apache.org/licenses/>.
__author__ = "João Magalhães <joamag@hive.pt>"
""" The author(s) of the module """
__version__ = "1.0.0"
""" The version of the module """
__revision__ = "$LastChangedRevision$"
""" The revision number of the module """
__date__ = "$LastChangedDate$"
""" The last change date of the module """
__copyright__ = "Copyright (c) 2008-2014 Hive Solutions Lda."
""" The copyright for the module """
__license__ = "Apache License, Version 2.0"
""" The license for the module """
import math
import copy
import flask
import datetime
from . import util
from . import meta
from . import common
from . import legacy
from . import typesf
from . import mongodb
from . import observer
from . import validation
from . import exceptions
ITERABLES = list(legacy.STRINGS) + [dict]
""" The sequence defining the complete set of valid types
for direct evaluation, instead of indirect (recursive)
evaluation, this is required to avoid miss behavior """
RE = lambda v: [i for i in v if not i == ""]
""" Simple lambda function that removes any
empty element from the provided list value """
BUILDERS = {
legacy.UNICODE : lambda v: v.decode("utf-8") if\
type(v) == legacy.BYTES else legacy.UNICODE(v),
list : lambda v: RE(v) if type(v) == list else RE([v]),
bool : lambda v: v if type(v) == bool else\
not v in ("", "0", "false", "False")
}
""" The map associating the various types with the
custom builder functions to be used when applying
the types function, this is relevant for the built-in
types that are meant to avoid using the default constructor """
METAS = dict(
text = lambda v, d: v,
enum = lambda v, d: d["enum"].get(v, None),
date = lambda v, d: datetime.datetime.utcfromtimestamp(float(v)).strftime("%d %b %Y"),
datetime = lambda v, d: datetime.datetime.utcfromtimestamp(float(v)).strftime("%d %b %Y %H:%M:%S")
)
""" The map that contains the various mapping functions
for the meta types that may be described for a field under
the current model specification, the resulting value for
each of these functions should preferably be a string """
TYPE_DEFAULTS = {
legacy.BYTES : None,
legacy.UNICODE : None,
int : None,
float : None,
bool : False,
list : [],
dict : {}
}
""" The default values to be set when a type
conversion fails for the provided string value
the resulting value may be returned when a validation
fails an so it must be used carefully """
TYPE_META = {
typesf.File : "file",
typesf.Files : "files",
typesf.Reference : "reference",
typesf.References : "references",
legacy.BYTES : "string",
legacy.UNICODE : "string",
int : "number",
float : "float",
bool: "bool",
list : "list",
dict : "map"
}
""" Dictionary that defines the default mapping for each
of the base data types against the associated default meta
values for each for them, these meta type values are going
to be used mostly for presentation purposes """
REVERSE = dict(
descending = "ascending",
ascending = "descending",
)
""" The reverse order dictionary that maps a certain
order direction (as a string) with the opposite one
this may be used to "calculate" the reverse value """
DIRTY_PARAMS = (
"map",
"rules",
"meta",
"build",
"skip",
"limit",
"sort",
"raise_e"
)
""" The set containing the complete set of parameter names for
the parameters that are considered to be dirty and that should
be cleaned from any query operation on the data source, otherwise
serious consequences may occur """
OPERATORS = {
"equals" : None,
"not_equals" : "$ne",
"in" : "$in",
"not_in" : "$nin",
"like" : "$regex",
"llike" : "$regex",
"rlike" : "$regex",
"greater" : "$gt",
"greater_equal" : "$gte",
"lesser" : "$lt",
"lesser_equal" : "$lte",
"is_null" : None,
"is_not_null" : "$ne",
"contains" : "$all"
}
""" The map containing the mapping association between the
normalized version of the operators and the infra-structure
specific value for each of this operations, note that some
of the values don't have a valid mapping for this operations
the operator must be ignored and not used explicitly """
VALUE_METHODS = {
"in" : lambda v, t: [t(v) for v in v.split(";")],
"not_in" : lambda v, t: [t(v) for v in v.split(";")],
"like" : lambda v, t: ".*" + legacy.UNICODE(v) + ".*",
"llike" : lambda v, t: legacy.UNICODE(v) + ".*",
"rlike" : lambda v, t: ".*" + legacy.UNICODE(v),
"is_null" : lambda v, t: None,
"is_not_null" : lambda v, t: None,
"contains" : lambda v, t: [v for v in v.split(";")]
}
""" Map that associates each of the normalized operations with
an inline function that together with the data type maps the
the base string based value into the target normalized value """
class Model(legacy.with_meta(meta.Ordered, observer.Observable)):
"""
Abstract model class from which all the models should
directly or indirectly inherit. Should provide the
basic infra-structure for the persistence of data using
a play key value storage engine.
The data should always be store in a dictionary oriented
structure while it's not persisted in the database.
"""
def __init__(self, model = None, **kwargs):
self.__dict__["_events"] = {}
self.__dict__["_extras"] = []
self.__dict__["model"] = model or {}
for name, value in kwargs.items(): setattr(self, name, value)
observer.Observable.__init__(self)
def __str__(self):
cls = self.__class__
default = cls.default()
if not default: return cls._name()
value = self.model[default]
is_string = legacy.is_str(value)
return value if is_string else str(value)
def __unicode__(self):
cls = self.__class__
default = cls.default()
if not default: return cls._name()
value = self.model[default]
is_unicode = legacy.is_unicode(value)
return value if is_unicode else legacy.UNICODE(value)
def __getattribute__(self, name):
try:
model = object.__getattribute__(self, "model")
if name in model: return model[name]
except AttributeError: pass
cls = object.__getattribute__(self, "__class__")
definition = cls.definition()
if name in definition: raise AttributeError(
"attribute '%s' is not set" % name
)
return object.__getattribute__(self, name)
def __setattr__(self, name, value):
is_base = name in self.__dict__
if is_base: self.__dict__[name] = value
else: self.model[name] = value
def __delattr__(self, name):
try:
model = object.__getattribute__(self, "model")
if name in model: del model[name]
except AttributeError: pass
@classmethod
def new(cls, model = None, safe = True, apply = True, build = False, new = True):
"""
Creates a new instance of the model applying the provided model
map to it after the instantiation of the class.
The optional safe flag makes sure that the model attributes marked
as safe are going to be removed and are not valid for apply.
The optional apply flag controls if the provided model (or form)
should be applied to the new model's instance on creation.
The optional build flag may be used to define if the build operations
that may be defined by the user on override should be called for
this instance of entity (should be used only on retrieval).
In order to make sure the resulting instance is safe for creation only
the new flag may be used as this will make sure that the proper identifier
attributes are not set in the instance after the apply operation is done.
:type model: Dictionary
:param model: The map containing the model that is going to be applied
to the new instance to be created.
:type safe: bool
:param safe: If the attributes marked as safe in the model definition
should be removed from the instance after the apply operation.
:type apply: bool
:param apply: If the apply operation should be performed, setting the
provided model (or the current form's model) in the created instance,
this may be used to avoid unwanted form application.
:type build: bool
:param build: If the "custom" build operation should be performed after
the apply operation is performed so that new custom attributes may be
injected into the resulting instance.
:type new: bool
:param new: In case this value is valid the resulting instance is expected
to be considered as new meaning that no identifier attributes are set.
:rtype: Model
:return: The new model instance resulting from the apply of the provided
model and after the proper validations are performed on it.
"""
instance = cls()
apply and instance.apply(model, safe_a = safe)
build and cls.build(instance.model, map = False)
new and instance.assert_is_new()
return instance
@classmethod
def old(cls, model = None, safe = True, apply = True, build = False):
return cls.new(
model = model,
safe = safe,
apply = apply,
build = build,
new = False
)
@classmethod
def singleton(
cls,
model = None,
safe = True,
apply = True,
build = False,
*args,
**kwargs
):
instance = cls.get(raise_e = False, *args, **kwargs)
if instance:
apply and instance.apply(model, safe_a = safe)
else:
model = model or util.get_object()
model = cls.fill(model)
instance = cls.old(
model = model,
safe = safe,
apply = apply,
build = build
)
return instance
@classmethod
def get(cls, *args, **kwargs):
fields, map, rules, meta, build, skip, limit, sort, raise_e = cls._get_attrs(kwargs, (
("fields", None),
("map", False),
("rules", True),
("meta", False),
("build", True),
("skip", 0),
("limit", 0),
("sort", None),
("raise_e", True)
))
fields = cls._sniff(fields, rules = rules)
collection = cls._collection()
model = collection.find_one(
kwargs,
fields = fields,
skip = skip,
limit = limit,
sort = sort
)
if not model and raise_e:
is_devel = common.is_devel()
if is_devel: message = "%s not found for %s" % (cls.__name__, str(kwargs))
else: message = "%s not found" % cls.__name__
raise exceptions.NotFoundError(message)
if not model and not raise_e: return model
cls.types(model)
cls.fill(model)
build and cls.build(model, map = map, rules = rules, meta = meta)
return model if map else cls.old(model = model, safe = False)
@classmethod
def find(cls, *args, **kwargs):
fields, map, rules, meta, build, skip, limit, sort = cls._get_attrs(kwargs, (
("fields", None),
("map", False),
("rules", True),
("meta", False),
("build", True),
("skip", 0),
("limit", 0),
("sort", None)
))
cls._find_s(kwargs)
cls._find_d(kwargs)
fields = cls._sniff(fields, rules = rules)
collection = cls._collection()
models = [cls.fill(cls.types(model)) for model in collection.find(
kwargs,
fields = fields,
skip = skip,
limit = limit,
sort = sort
)]
build and [cls.build(model, map = map, rules = rules, meta = meta) for model in models]
models = models if map else [cls.old(model = model, safe = False) for model in models]
return models
@classmethod
def count(cls, *args, **kwargs):
cls._clean_attrs(kwargs)
collection = cls._collection()
if kwargs:
result = collection.find(kwargs)
result = result.count()
else:
result = collection.count()
return result
@classmethod
def paginate(cls, skip = 0, limit = 1, *args, **kwargs):
# counts the total number of references according to the
# current filter value and then uses this value together
# with the skip value to calculate both the number of pages
# available for the current filter and the current page index
# (note that the index is one index based)
total = cls.count(*args, **kwargs)
count = total / float(limit)
count = math.ceil(count)
count = int(count)
index = skip / float(limit)
index = math.floor(index)
index = int(index) + 1
# creates the base structure for the page populating with the
# base values that may be used for display of the page
page = dict(
count = count,
index = index,
start = skip + 1,
end = skip + limit,
total = total,
sorter = flask.request.args_s.get("sorter", None),
direction = flask.request.args_s.get("direction", "descending")
)
def generate(**kwargs):
# creates a copy of the current definition of the parameters and for each
# of the exclusion parameters removes it from the current structure
params = dict(flask.request.args_s)
if "async" in params: del params["async"]
# retrieves the "special" sorter keyword based argument and the equivalent
# values for the current page in handling, this values are going to be used
# for the calculus of the direction value (in case it's required)
sorter = kwargs.get("sorter", None)
_sorter = page["sorter"]
direction = page["direction"]
reverse = REVERSE.get(direction, "descending")
# verifies if the sorter value is defined in the arguments and if that's
# the case verifies if ir's the same as the current one if that the case
# the direction must be reversed otherwise the default direction is set
if sorter and sorter == _sorter: params["direction"] = reverse
elif sorter: params["direction"] = "descending"
# "copies" the complete set of values from the provided keyword
# based arguments into the parameters map, properly converting them
# into the proper string value and then converts them into the linear
# quoted manner so they they may be used as query string values
for key, value in kwargs.items(): params[key] = str(value)
query = [util.quote(key) + "=" + util.quote(value) for key, value in params.items()]
query = "&".join(query)
return "?" + query if query else query
# updates the current page structure so that the (query) generation
# method is exposed in order to modify the current query
page["query"] = generate
return page
@classmethod
def delete_c(cls, *args, **kwargs):
collection = cls._collection()
collection.remove(kwargs)
@classmethod
def ordered(cls):
ordered = list(cls._ordered)
for name, value in cls.__dict__.items():
if name.startswith("_"): continue
if not name == name.lower(): continue
if not isinstance(value, dict): continue
if name in ordered: continue
ordered.append(name)
return ordered
@classmethod
def fields(cls):
# in case the fields are already "cached" in the current
# class (fast retrieval) returns immediately
if "_fields" in cls.__dict__: return cls._fields
# starts the list that will hold the various field names
# for the class, note that this value will be ordered
# according to the class level and the definition order
fields = []
# retrieves the complete model hierarchy for the current model
# and it's going to be used to iterated through the class levels
# in a top to bottom approach strategy
hierarchy = cls.hierarchy()
# iterates over the complete model hierarchy and retrieves the
# ordered set of attributes from it extending the retrieved fields
# list with the value for each of the model levels
for _cls in hierarchy:
ordered = _cls.ordered()
fields.extend(ordered)
# saves the retrieved set of fields in the current model definition
# and then returns the value to the caller method as requested
cls._fields = fields
return fields
@classmethod
def definition(cls):
# in case the definition is already "cached" in the current
# class (fast retrieval) returns immediately
if "_definition" in cls.__dict__: return cls._definition
# creates the map that will hold the complete definition of
# the current model
definition = {}
# retrieves the complete model hierarchy for the current model
# this should allow the method to retrieve the complete set
# of fields for the current model
hierarchy = cls.hierarchy()
# iterates over all the classes in the hierarchy to creates the
# map that will contain the various names of the current model
# associated with its definition map
for _cls in hierarchy:
for name, value in _cls.__dict__.items():
if name.startswith("_"): continue
if not name == name.lower(): continue
if not isinstance(value, dict): continue
definition[name] = value
# sets the "default" definition for the based identifier
# (underlying identifier attribute)
definition["_id"] = dict()
# saves the currently generated definition under the current
# class and then returns the contents of it to the caller method
cls._definition = definition
return definition
@classmethod
def definition_n(cls, name):
definition = cls.definition()
return definition.get(name, {})
@classmethod
def setup(cls):
indexes = cls.indexes()
collection = cls._collection()
for index in indexes: collection.ensure_index(index)
@classmethod
def validate(cls):
return []
@classmethod
def validate_new(cls):
return cls.validate()
@classmethod
def base_names(cls):
names = cls.fields()
names = [name for name in names if not name.startswith("_")]
return names
@classmethod
def create_names(cls):
names = cls.base_names()
extra = cls.extra_names()
names.extend(extra)
return names
@classmethod
def update_names(cls):
return cls.create_names()
@classmethod
def show_names(cls):
_names = []
names = cls.base_names()
definition = cls.definition()
for name in names:
value = definition.get(name, None)
if value == None: continue
is_private = value.get("private", False)
if is_private: continue
_names.append(name)
return _names
@classmethod
def list_names(cls):
return cls.show_names()
@classmethod
def extra_names(cls):
return []
@classmethod
def build(cls, model, map = False, rules = True, meta = False):
if rules: cls.rules(model, map)
cls._build(model, map)
if meta: cls._meta(model, map)
@classmethod
def rules(cls, model, map):
for name, _value in legacy.eager(model.items()):
definition = cls.definition_n(name)
is_private = definition.get("private", False)
if not is_private: continue
del model[name]
@classmethod
def types(cls, model):
definition = cls.definition()
for name, value in legacy.eager(model.items()):
if name == "_id": continue
if value == None: continue
if not name in definition: continue
model[name] = cls.cast(name, value)
return model
@classmethod
def fill(cls, model = None):
"""
Fills the current models with the proper values so that
no values are unset as this would violate the model definition
integrity. This is required when retrieving an object(s) from
the data source (as some of them may be incomplete).
:type model: Model
:param model: The model that is going to have its unset
attributes filled with "default" data, in case none is provided
all of the attributes will be filled with "default" data.
"""
model = model or dict()
definition = cls.definition()
for name, _definition in definition.items():
if name in model: continue
if name in ("_id",): continue
increment = _definition.get("increment", False)
if increment: continue
_type = _definition.get("type")
default = TYPE_DEFAULTS.get(_type, None)
default = _type._default() if hasattr(_type, "_default") else default
model[name] = default
return model
@classmethod
def cast(cls, name, value):
definition = cls.definition()
if not name in definition: return value
if value == None: return value
_definition = cls.definition_n(name)
_type = _definition.get("type", legacy.UNICODE)
builder = BUILDERS.get(_type, _type)
try:
return builder(value) if builder else value
except:
default = TYPE_DEFAULTS.get(_type, None)
default = _type._default() if hasattr(_type, "_default") else default
return default
@classmethod
def all_parents(cls):
# in case the all parents are already "cached" in the current
# class (fast retrieval) returns immediately
if "_all_parents" in cls.__dict__: return cls._all_parents
# creates the list to hold the various parent
# entity classes, populated recursively
all_parents = []
# retrieves the parent entity classes from
# the current class
parents = cls._bases()
# iterates over all the parents to extend
# the all parents list with the parent entities
# from the parent
for parent in parents:
# retrieves the (all) parents from the parents
# and extends the all parents list with them,
# this extension method avoids duplicates
_parents = parent.all_parents()
all_parents += _parents
# extends the all parents list with the parents
# from the current entity class (avoids duplicates)
all_parents += parents
# caches the all parents element in the class
# to provide fast access in latter access
cls._all_parents = all_parents
# returns the list that contains all the parents
# entity classes
return all_parents
@classmethod
def hierarchy(cls):
# in case the hierarchy are already "cached" in the current
# class (fast retrieval) returns immediately
if "_hierarchy" in cls.__dict__: return cls._hierarchy
# retrieves the complete set of parents for the current class
# and then adds the current class to it
all_parents = cls.all_parents()
hierarchy = all_parents + [cls]
# saves the current hierarchy list under the class and then
# returns the sequence to the caller method
cls._hierarchy = hierarchy
return hierarchy
@classmethod
def increments(cls):
# in case the increments are already "cached" in the current
# class (fast retrieval) returns immediately
if "_increments" in cls.__dict__: return cls._increments
# creates the list that will hold the various names that are
# meant to be automatically incremented
increments = []
# retrieves the map containing the definition of the class with
# the name of the fields associated with their definition
definition = cls.definition()
# iterate over all the names in the definition to retrieve their
# definition and check if their are of type increment
for name in definition:
_definition = cls.definition_n(name)
is_increment = _definition.get("increment", False)
if not is_increment: continue
increments.append(name)
# saves the increment list under the class and then
# returns the sequence to the caller method
cls._increments = increments
return increments
@classmethod
def indexes(cls):
# in case the indexes are already "cached" in the current
# class (fast retrieval) returns immediately
if "_indexes" in cls.__dict__: return cls._indexes
# creates the list that will hold the various names that are
# meant to be indexed in the data source
indexes = []
# retrieves the map containing the definition of the class with
# the name of the fields associated with their definition
definition = cls.definition()
# iterate over all the names in the definition to retrieve their
# definition and check if their are of type index
for name in definition:
_definition = cls.definition_n(name)
is_index = _definition.get("index", False)
if not is_index: continue
indexes.append(name)
# saves the index list under the class and then
# returns the sequence to the caller method
cls._indexes = indexes
return indexes
@classmethod
def safes(cls):
# in case the safes are already "cached" in the current
# class (fast retrieval) returns immediately
if "_safes" in cls.__dict__: return cls._safes
# creates the list that will hold the various names that are
# meant to be safe values in the data source
safes = []
# retrieves the map containing the definition of the class with
# the name of the fields associated with their definition
definition = cls.definition()
# iterate over all the names in the definition to retrieve their
# definition and check if their are of type safe
for name in definition:
_definition = cls.definition_n(name)
is_safe = _definition.get("safe", False)
if not is_safe: continue
safes.append(name)
# saves the safes list under the class and then
# returns the sequence to the caller method
cls._safes = safes
return safes
@classmethod
def immutables(cls):
# in case the immutables are already "cached" in the current
# class (fast retrieval) returns immediately
if "_immutables" in cls.__dict__: return cls._immutables
# creates the list that will hold the various names that are
# meant to be immutable values in the data source
immutables = []
# retrieves the map containing the definition of the class with
# the name of the fields associated with their definition
definition = cls.definition()
# iterate over all the names in the definition to retrieve their
# definition and check if their are of type immutable
for name in definition:
_definition = cls.definition_n(name)
is_immutable = _definition.get("immutable", False)
if not is_immutable: continue
immutables.append(name)
# saves the immutables list under the class and then
# returns the sequence to the caller method
cls._immutables = immutables
return immutables
@classmethod
def default(cls):
# in case the default are already "cached" in the current
# class (fast retrieval) returns immediately
if "_default" in cls.__dict__: return cls._default
# retrieves the complete hierarchy of the model to be used
# for the retrieval of the lowest possible default value for
# the current class hierarchy
hierarchy = cls.hierarchy()
# creates the value that is going to store the default value for
# the current class (in case there's one)
default = None
# iterates over all the classes in the current hierarchy, note
# that the iteration is done from the leaf nodes to the root nodes
# so that the lowest possible default value is found
for _cls in reversed(hierarchy):
for name in legacy.eager(_cls.__dict__.keys()):
# retrieves the definition map for the current name an in
# case the default value is set considers it default otherwise
# continues the loop, nothing to be done
_definition = cls.definition_n(name)
is_default = _definition.get("default", False)
if not is_default: continue
# in case the default value is found sets its name in the
# current default value and then breaks the loop
default = name
break
# in case the default value has been found must break the external
# loop as nothing else remains to be found
if default: break
# saves the default value (name) under the class and then
# returns the sequence to the caller method
cls._default = default
return default
@classmethod
def filter_merge(cls, name, filter, kwargs):
# retrieves a possible previous filter defined for the
# provided name in case it does exists must concatenate
# that previous value in an and statement
filter_p = kwargs.get(name, None)
if filter_p:
# retrieves the and references for the current arguments
# and appends the two filter values (current and previous)
# then deletes the current name reference in the arguments
# and updates the name value to the and value
filter_a = kwargs.get("$and", [])
filter = filter_a + [{name : filter}, {name : filter_p}]
del kwargs[name]
name = "$and"
# sets the currently defined filter structures in the keyword
# based arguments map for the currently defined name
kwargs[name] = filter
@classmethod
def _build(cls, model, map):
pass
@classmethod
def _meta(cls, model, map):
for key, value in legacy.eager(model.items()):
definition = cls.definition_n(key)
meta = cls._solve(key)
mapper = METAS.get(meta, None)
if mapper and not value == None: value = mapper(value, definition)
else: value = value if value == None else legacy.UNICODE(value)
model[key + "_meta"] = value
@classmethod
def _sniff(cls, fields, rules = False):
fields = fields or cls.fields()
fields = list(fields)
if not rules: return fields
for field in list(fields):
definition = cls.definition_n(field)
is_private = definition.get("private", False)
if is_private: fields.remove(field)
return fields
@classmethod
def _solve(cls, name):
definition = cls.definition_n(name)
type = definition.get("type", legacy.UNICODE)
for cls in type.mro():
base = TYPE_META.get(cls, None)
if base: break
return definition.get("meta", base)
@classmethod
def _collection(cls):
name = cls._name()
db = mongodb.get_db()
collection = db[name]
return collection
@classmethod
def _name(cls):
# retrieves the class object for the current instance and then
# converts it into lower case value in order to serve as the
# name of the collection to be used
name = cls.__name__.lower()
return name
@classmethod
def _get_attrs(cls, kwargs, attrs):
_attrs = []
for attr, value in attrs:
if not attr in kwargs:
_attrs.append(value)
continue
value = kwargs[attr]
del kwargs[attr]
_attrs.append(value)
return _attrs
@classmethod
def _clean_attrs(cls, kwargs, dirty = DIRTY_PARAMS):
for key in dirty:
if not key in kwargs: continue
del kwargs[key]
@classmethod
def _find_s(cls, kwargs):
# in case the find string is currently not defined in the
# named arguments map returns immediately as nothing is
# meant to be done on this method
if not "find_s" in kwargs: return
# retrieves the find string into a local variable, then
# removes the find string from the named arguments map
# so that it's not going to be erroneously used by the
# underlying find infra-structure
find_s = kwargs["find_s"]
del kwargs["find_s"]
# retrieves the "name" of the attribute that is considered
# to be the default (representation) for the model in case
# there's none returns immediately, as it's not possible
# to proceed with the filter creation
default = cls.default()
if not default: return
# retrieves the definition for the default attribute and uses
# it to retrieve it's target data type, defaulting to the
# string type in case none is defined in the schema
definition = cls.definition_n(default)
default_t = definition.get("type", legacy.UNICODE)
try:
# in case the target date type for the default field is
# string the right labeled wildcard regex is used for the
# search otherwise the search value to be used is the exact
# match of the value (required type conversion)
if default_t in legacy.STRINGS: find_v = {"$regex" : find_s + ".*"}
else: find_v = default_t(find_s)
except:
# in case there's an error in the conversion for
# the target type value sets the search value as
# invalid (not going to be used in filter)
find_v = None
# in case there's a valid find value to be used sets
# the value in the named arguments map to be used by
# the underlying find infra-structure, note that the
# set is done using a "merge" with the previous values
if not find_v == None:
cls.filter_merge(default, find_v, kwargs)
@classmethod
def _find_d(cls, kwargs):
# in case the find definition is currently not defined in the
# named arguments map returns immediately as nothing is