masonite-framework-orm 3.0.1__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 (116) hide show
  1. masonite_framework_orm-3.0.1.dist-info/METADATA +87 -0
  2. masonite_framework_orm-3.0.1.dist-info/RECORD +116 -0
  3. masonite_framework_orm-3.0.1.dist-info/WHEEL +5 -0
  4. masonite_framework_orm-3.0.1.dist-info/entry_points.txt +3 -0
  5. masonite_framework_orm-3.0.1.dist-info/licenses/LICENSE +21 -0
  6. masonite_framework_orm-3.0.1.dist-info/top_level.txt +1 -0
  7. masoniteorm/__init__.py +1 -0
  8. masoniteorm/collection/Collection.py +605 -0
  9. masoniteorm/collection/__init__.py +1 -0
  10. masoniteorm/commands/CanOverrideConfig.py +16 -0
  11. masoniteorm/commands/CanOverrideOptionsDefault.py +22 -0
  12. masoniteorm/commands/Command.py +6 -0
  13. masoniteorm/commands/Entry.py +43 -0
  14. masoniteorm/commands/MakeMigrationCommand.py +57 -0
  15. masoniteorm/commands/MakeModelCommand.py +78 -0
  16. masoniteorm/commands/MakeModelDocstringCommand.py +37 -0
  17. masoniteorm/commands/MakeObserverCommand.py +54 -0
  18. masoniteorm/commands/MakeSeedCommand.py +54 -0
  19. masoniteorm/commands/MigrateCommand.py +46 -0
  20. masoniteorm/commands/MigrateFreshCommand.py +41 -0
  21. masoniteorm/commands/MigrateRefreshCommand.py +41 -0
  22. masoniteorm/commands/MigrateResetCommand.py +25 -0
  23. masoniteorm/commands/MigrateRollbackCommand.py +26 -0
  24. masoniteorm/commands/MigrateStatusCommand.py +51 -0
  25. masoniteorm/commands/SeedRunCommand.py +35 -0
  26. masoniteorm/commands/ShellCommand.py +205 -0
  27. masoniteorm/commands/__init__.py +18 -0
  28. masoniteorm/commands/stubs/create_migration.stub +20 -0
  29. masoniteorm/commands/stubs/create_seed.stub +9 -0
  30. masoniteorm/commands/stubs/model.stub +9 -0
  31. masoniteorm/commands/stubs/observer.stub +101 -0
  32. masoniteorm/commands/stubs/table_migration.stub +19 -0
  33. masoniteorm/config.py +123 -0
  34. masoniteorm/connections/BaseConnection.py +101 -0
  35. masoniteorm/connections/ConnectionFactory.py +59 -0
  36. masoniteorm/connections/ConnectionResolver.py +132 -0
  37. masoniteorm/connections/MSSQLConnection.py +176 -0
  38. masoniteorm/connections/MySQLConnection.py +232 -0
  39. masoniteorm/connections/PostgresConnection.py +225 -0
  40. masoniteorm/connections/SQLiteConnection.py +179 -0
  41. masoniteorm/connections/__init__.py +6 -0
  42. masoniteorm/exceptions.py +38 -0
  43. masoniteorm/expressions/__init__.py +1 -0
  44. masoniteorm/expressions/expressions.py +288 -0
  45. masoniteorm/factories/Factory.py +112 -0
  46. masoniteorm/factories/__init__.py +1 -0
  47. masoniteorm/helpers/__init__.py +0 -0
  48. masoniteorm/helpers/misc.py +22 -0
  49. masoniteorm/migrations/Migration.py +330 -0
  50. masoniteorm/migrations/__init__.py +1 -0
  51. masoniteorm/models/MigrationModel.py +9 -0
  52. masoniteorm/models/Model.py +1209 -0
  53. masoniteorm/models/Model.pyi +1366 -0
  54. masoniteorm/models/Pivot.py +5 -0
  55. masoniteorm/models/__init__.py +1 -0
  56. masoniteorm/observers/ObservesEvents.py +27 -0
  57. masoniteorm/observers/__init__.py +1 -0
  58. masoniteorm/pagination/BasePaginator.py +10 -0
  59. masoniteorm/pagination/LengthAwarePaginator.py +34 -0
  60. masoniteorm/pagination/SimplePaginator.py +28 -0
  61. masoniteorm/pagination/__init__.py +2 -0
  62. masoniteorm/providers/ORMProvider.py +39 -0
  63. masoniteorm/providers/__init__.py +1 -0
  64. masoniteorm/query/EagerRelation.py +42 -0
  65. masoniteorm/query/QueryBuilder.py +2486 -0
  66. masoniteorm/query/__init__.py +1 -0
  67. masoniteorm/query/grammars/BaseGrammar.py +1027 -0
  68. masoniteorm/query/grammars/MSSQLGrammar.py +194 -0
  69. masoniteorm/query/grammars/MySQLGrammar.py +238 -0
  70. masoniteorm/query/grammars/PostgresGrammar.py +213 -0
  71. masoniteorm/query/grammars/SQLiteGrammar.py +228 -0
  72. masoniteorm/query/grammars/__init__.py +4 -0
  73. masoniteorm/query/processors/MSSQLPostProcessor.py +58 -0
  74. masoniteorm/query/processors/MySQLPostProcessor.py +48 -0
  75. masoniteorm/query/processors/PostgresPostProcessor.py +49 -0
  76. masoniteorm/query/processors/SQLitePostProcessor.py +49 -0
  77. masoniteorm/query/processors/__init__.py +4 -0
  78. masoniteorm/relationships/BaseRelationship.py +161 -0
  79. masoniteorm/relationships/BelongsTo.py +124 -0
  80. masoniteorm/relationships/BelongsToMany.py +604 -0
  81. masoniteorm/relationships/HasMany.py +66 -0
  82. masoniteorm/relationships/HasManyThrough.py +269 -0
  83. masoniteorm/relationships/HasOne.py +111 -0
  84. masoniteorm/relationships/HasOneThrough.py +275 -0
  85. masoniteorm/relationships/MorphMany.py +152 -0
  86. masoniteorm/relationships/MorphOne.py +156 -0
  87. masoniteorm/relationships/MorphTo.py +111 -0
  88. masoniteorm/relationships/MorphToMany.py +108 -0
  89. masoniteorm/relationships/__init__.py +10 -0
  90. masoniteorm/schema/Blueprint.py +1161 -0
  91. masoniteorm/schema/Column.py +144 -0
  92. masoniteorm/schema/ColumnDiff.py +0 -0
  93. masoniteorm/schema/Constraint.py +5 -0
  94. masoniteorm/schema/ForeignKeyConstraint.py +28 -0
  95. masoniteorm/schema/Index.py +5 -0
  96. masoniteorm/schema/Schema.py +359 -0
  97. masoniteorm/schema/Table.py +94 -0
  98. masoniteorm/schema/TableDiff.py +86 -0
  99. masoniteorm/schema/__init__.py +3 -0
  100. masoniteorm/schema/platforms/MSSQLPlatform.py +367 -0
  101. masoniteorm/schema/platforms/MySQLPlatform.py +513 -0
  102. masoniteorm/schema/platforms/Platform.py +97 -0
  103. masoniteorm/schema/platforms/PostgresPlatform.py +551 -0
  104. masoniteorm/schema/platforms/SQLitePlatform.py +481 -0
  105. masoniteorm/schema/platforms/__init__.py +4 -0
  106. masoniteorm/scopes/BaseScope.py +6 -0
  107. masoniteorm/scopes/SoftDeleteScope.py +56 -0
  108. masoniteorm/scopes/SoftDeletesMixin.py +13 -0
  109. masoniteorm/scopes/TimeStampsMixin.py +12 -0
  110. masoniteorm/scopes/TimeStampsScope.py +47 -0
  111. masoniteorm/scopes/UUIDPrimaryKeyMixin.py +8 -0
  112. masoniteorm/scopes/UUIDPrimaryKeyScope.py +51 -0
  113. masoniteorm/scopes/__init__.py +8 -0
  114. masoniteorm/scopes/scope.py +15 -0
  115. masoniteorm/seeds/Seeder.py +42 -0
  116. masoniteorm/seeds/__init__.py +1 -0
@@ -0,0 +1,1209 @@
1
+ import inspect
2
+ import json
3
+ import logging
4
+ from datetime import date as datetimedate
5
+ from datetime import datetime
6
+ from datetime import time as datetimetime
7
+ from decimal import Decimal
8
+ from typing import Any, Dict
9
+
10
+ import pendulum
11
+ from inflection import tableize, underscore
12
+
13
+ from ..collection import Collection
14
+ from ..config import load_config
15
+ from ..exceptions import ModelNotFound
16
+ from ..observers import ObservesEvents
17
+ from ..query import QueryBuilder
18
+ from ..scopes import TimeStampsMixin
19
+
20
+ """This is a magic class that will help using models like User.first() instead of having to instatiate a class like
21
+ User().first()
22
+ """
23
+
24
+
25
+ class ModelMeta(type):
26
+ def __getattr__(self, attribute, *args, **kwargs):
27
+ """This method is called between a Model and accessing a property. This is a quick and easy
28
+ way to instantiate a class before the first method is called. This is to avoid needing
29
+ to do this:
30
+
31
+ User().where(..)
32
+
33
+ and instead, with this class inherited as a meta class, we can do this:
34
+
35
+ User.where(...)
36
+
37
+ This class (potentially magically) instantiates the class even though we really didn't instantiate it.
38
+
39
+ Args:
40
+ attribute (string): The name of the attribute
41
+
42
+ Returns:
43
+ Model|mixed: An instantiated model's attribute
44
+ """
45
+ instantiated = self()
46
+ return getattr(instantiated, attribute)
47
+
48
+
49
+ class BoolCast:
50
+ """Casts a value to a boolean"""
51
+
52
+ def get(self, value):
53
+ """
54
+ Cast the value to assign to the model attribute
55
+ """
56
+ return bool(value)
57
+
58
+ def set(self, value):
59
+ """
60
+ Cast the value for use in insert/update queries
61
+ """
62
+ return bool(value)
63
+
64
+
65
+ class JsonCast:
66
+ """Casts a value to JSON"""
67
+
68
+ def get(self, value):
69
+ """
70
+ Cast the value to assign to the model attribute
71
+ """
72
+ if isinstance(value, str):
73
+ try:
74
+ return json.loads(value)
75
+ except ValueError:
76
+ return None
77
+
78
+ return value
79
+
80
+ def set(self, value):
81
+ """
82
+ Cast the value for use in insert/update queries
83
+ """
84
+ if isinstance(value, str):
85
+ # make sure the string is valid JSON
86
+ json.loads(value)
87
+ return value
88
+
89
+ return json.dumps(value, default=str)
90
+
91
+
92
+ class IntCast:
93
+ """Casts a value to a int"""
94
+
95
+ def get(self, value):
96
+ """
97
+ Cast the value to assign to the model attribute
98
+ """
99
+ return int(value)
100
+
101
+ def set(self, value):
102
+ """
103
+ Cast the value for use in insert/update queries
104
+ """
105
+ return int(value)
106
+
107
+
108
+ class FloatCast:
109
+ """Casts a value to a float"""
110
+
111
+ def get(self, value):
112
+ """
113
+ Cast the value to assign to the model attribute
114
+ """
115
+ return float(value)
116
+
117
+ def set(self, value):
118
+ """
119
+ Cast the value for use in insert/update queries
120
+ """
121
+ return float(value)
122
+
123
+
124
+ class DateCast:
125
+ """Casts a value to a float"""
126
+
127
+ def get(self, value):
128
+ """
129
+ Cast the value to assign to the model attribute
130
+ """
131
+ return pendulum.parse(value).to_date_string()
132
+
133
+ def set(self, value):
134
+ """
135
+ Cast the value for use in insert/update queries
136
+ """
137
+ return pendulum.parse(value).to_date_string()
138
+
139
+
140
+ class DecimalCast:
141
+ """Casts a value to Decimal for accuracy"""
142
+
143
+ def get(self, value):
144
+ """
145
+ Cast the value to assign to the model attribute
146
+ """
147
+ return Decimal(str(value))
148
+
149
+ def set(self, value):
150
+ """
151
+ Cast the value for use in insert/update queries
152
+ """
153
+ return str(value)
154
+
155
+
156
+ class Model(TimeStampsMixin, ObservesEvents, metaclass=ModelMeta):
157
+ """The ORM Model class
158
+
159
+ Base Classes:
160
+ TimeStampsMixin (TimeStampsMixin): Adds scopes to add timestamps when something is inserted
161
+ metaclass (ModelMeta, optional): Helps instantiate a class when it hasn't been instantiated. Defaults to ModelMeta.
162
+ """
163
+
164
+ __fillable__ = ["*"]
165
+ __guarded__ = []
166
+ __dry__ = False
167
+ __table__ = None
168
+ __connection__ = "default"
169
+ __resolved_connection__ = None
170
+ __selects__ = []
171
+
172
+ __observers__ = {}
173
+ __has_events__ = True
174
+
175
+ _booted = False
176
+ _scopes = {}
177
+ __primary_key__ = "id"
178
+ __primary_key_type__ = "int"
179
+ __casts__ = {}
180
+ __dates__ = []
181
+ __hidden__ = []
182
+ __relationship_hidden__ = {}
183
+ __visible__ = []
184
+ __timestamps__ = True
185
+ __timezone__ = "UTC"
186
+ __with__ = ()
187
+ __force_update__ = False
188
+
189
+ date_created_at = "created_at"
190
+ date_updated_at = "updated_at"
191
+
192
+ builder: QueryBuilder
193
+
194
+ """Pass through will pass any method calls to the model directly through to the query builder.
195
+ Anytime one of these methods are called on the model it will actually be called on the query builder class.
196
+ """
197
+ __passthrough__ = set(
198
+ (
199
+ "add_select",
200
+ "aggregate",
201
+ "all",
202
+ "avg",
203
+ "between",
204
+ "bulk_create",
205
+ "chunk",
206
+ "count",
207
+ "decrement",
208
+ "delete",
209
+ "distinct",
210
+ "doesnt_exist",
211
+ "doesnt_have",
212
+ "exists",
213
+ "find_or",
214
+ "find_or_404",
215
+ "first_or_fail",
216
+ "first",
217
+ "first_where",
218
+ "first_or_create",
219
+ "force_update",
220
+ "from_",
221
+ "from_raw",
222
+ "get",
223
+ "get_table_schema",
224
+ "group_by_raw",
225
+ "group_by",
226
+ "has",
227
+ "having",
228
+ "having_raw",
229
+ "increment",
230
+ "in_random_order",
231
+ "join_on",
232
+ "join",
233
+ "joins",
234
+ "last",
235
+ "left_join",
236
+ "limit",
237
+ "lock_for_update",
238
+ "make_lock",
239
+ "max",
240
+ "min",
241
+ "new_from_builder",
242
+ "new",
243
+ "not_between",
244
+ "offset",
245
+ "on",
246
+ "or_where",
247
+ "or_where_null",
248
+ "order_by_raw",
249
+ "order_by",
250
+ "paginate",
251
+ "right_join",
252
+ "select_raw",
253
+ "select",
254
+ "set_global_scope",
255
+ "set_schema",
256
+ "shared_lock",
257
+ "simple_paginate",
258
+ "skip",
259
+ "statement",
260
+ "sum",
261
+ "table_raw",
262
+ "take",
263
+ "to_qmark",
264
+ "to_sql",
265
+ "truncate",
266
+ "update",
267
+ "when",
268
+ "where_between",
269
+ "where_column",
270
+ "where_date",
271
+ "or_where_doesnt_have",
272
+ "or_has",
273
+ "or_where_has",
274
+ "or_doesnt_have",
275
+ "or_where_not_exists",
276
+ "or_where_date",
277
+ "where_exists",
278
+ "where_from_builder",
279
+ "where_has",
280
+ "where_in",
281
+ "where_like",
282
+ "where_not_between",
283
+ "where_not_in",
284
+ "where_not_like",
285
+ "where_not_null",
286
+ "where_null",
287
+ "where_raw",
288
+ "without_global_scopes",
289
+ "where",
290
+ "where_doesnt_have",
291
+ "with_",
292
+ "with_count",
293
+ "latest",
294
+ "oldest",
295
+ "value",
296
+ )
297
+ )
298
+
299
+ __cast_map__ = {}
300
+
301
+ __internal_cast_map__ = {
302
+ "bool": BoolCast,
303
+ "json": JsonCast,
304
+ "int": IntCast,
305
+ "float": FloatCast,
306
+ "date": DateCast,
307
+ "decimal": DecimalCast,
308
+ }
309
+
310
+ def __init__(self):
311
+ self.__attributes__ = {}
312
+ self.__original_attributes__ = {}
313
+ self.__dirty_attributes__ = {}
314
+ if not hasattr(self, "__appends__"):
315
+ self.__appends__ = []
316
+ self._relationships = {}
317
+ self._global_scopes = {}
318
+
319
+ self.boot()
320
+
321
+ @classmethod
322
+ def get_primary_key(self):
323
+ """Gets the primary key column
324
+
325
+ Returns:
326
+ mixed
327
+ """
328
+ return self.__primary_key__
329
+
330
+ def get_primary_key_type(self):
331
+ """Gets the primary key column type
332
+
333
+ Returns:
334
+ mixed
335
+ """
336
+ return self.__primary_key_type__
337
+
338
+ def get_primary_key_value(self):
339
+ """Gets the primary key value.
340
+
341
+ Raises:
342
+ AttributeError: Raises attribute error if the model does not have an
343
+ attribute with the primary key.
344
+
345
+ Returns:
346
+ str|int
347
+ """
348
+ try:
349
+ return getattr(self, self.get_primary_key())
350
+ except AttributeError:
351
+ name = self.__class__.__name__
352
+ raise AttributeError(
353
+ f"class '{name}' has no attribute {self.get_primary_key()}. Did you set the primary key correctly on the model using the __primary_key__ attribute?"
354
+ )
355
+
356
+ def get_foreign_key(self):
357
+ """Gets the foreign key based on this model name.
358
+
359
+ Args:
360
+ relationship (str): The relationship name.
361
+
362
+ Returns:
363
+ str
364
+ """
365
+ return underscore(
366
+ self.__class__.__name__ + "_" + self.get_primary_key()
367
+ )
368
+
369
+ def query(self):
370
+ return self.get_builder()
371
+
372
+ def get_builder(self):
373
+ if hasattr(self, "builder"):
374
+ return self.builder
375
+
376
+ self.builder = QueryBuilder(
377
+ connection=self.__connection__,
378
+ table=self.get_table_name(),
379
+ connection_details=self.get_connection_details(),
380
+ model=self,
381
+ scopes=self._scopes.get(self.__class__),
382
+ dry=self.__dry__,
383
+ )
384
+
385
+ return self.builder
386
+
387
+ def get_selects(self):
388
+ return self.__selects__
389
+
390
+ @classmethod
391
+ def get_columns(cls):
392
+ return list(cls.first().__attributes__.keys())
393
+
394
+ def get_connection_details(self):
395
+ resolver = load_config().DB
396
+ return resolver.get_connection_details()
397
+
398
+ def boot(self):
399
+ if not self._booted:
400
+ self.observe_events(self, "booting")
401
+ for base_class in inspect.getmro(self.__class__):
402
+ class_name = base_class.__name__
403
+
404
+ if class_name.endswith("Mixin"):
405
+ getattr(self, "boot_" + class_name)(self.get_builder())
406
+ elif (
407
+ base_class != Model
408
+ and issubclass(base_class, Model)
409
+ and "__fillable__" in base_class.__dict__
410
+ and "__guarded__" in base_class.__dict__
411
+ ):
412
+ raise AttributeError(
413
+ f"{type(self).__name__} must specify either __fillable__ or __guarded__ properties, but not both."
414
+ )
415
+
416
+ self._booted = True
417
+ self.observe_events(self, "booted")
418
+
419
+ self.append_passthrough(list(self.get_builder()._macros.keys()))
420
+
421
+ def append_passthrough(self, passthrough):
422
+ self.__passthrough__.update(passthrough)
423
+ return self
424
+
425
+ @classmethod
426
+ def get_table_name(cls):
427
+ """Gets the table name.
428
+
429
+ Returns:
430
+ str
431
+ """
432
+ return cls.__table__ or tableize(cls.__name__)
433
+
434
+ @classmethod
435
+ def table(cls, table):
436
+ """Gets the table name.
437
+
438
+ Returns:
439
+ str
440
+ """
441
+ cls.__table__ = table
442
+ return cls
443
+
444
+ @classmethod
445
+ def find(cls, record_id, query=False):
446
+ """Finds a row by the primary key ID.
447
+
448
+ Arguments:
449
+ record_id {int} -- The ID of the primary key to fetch.
450
+
451
+ Returns:
452
+ Model
453
+ """
454
+ if isinstance(record_id, (list, tuple)):
455
+ builder = cls().where_in(cls.get_primary_key(), record_id)
456
+ else:
457
+ builder = cls().where(cls.get_primary_key(), record_id)
458
+
459
+ if query:
460
+ return builder
461
+ else:
462
+ if isinstance(record_id, (list, tuple)):
463
+ return builder.get()
464
+
465
+ return builder.first()
466
+
467
+ @classmethod
468
+ def find_or_fail(cls, record_id, query=False):
469
+ """Finds a row by the primary key ID or raise a ModelNotFound exception.
470
+
471
+ Arguments:
472
+ record_id {int} -- The ID of the primary key to fetch.
473
+
474
+ Returns:
475
+ Model
476
+ """
477
+ result = cls.find(record_id, query)
478
+
479
+ if not result:
480
+ raise ModelNotFound()
481
+
482
+ return result
483
+
484
+ def is_loaded(self):
485
+ return bool(self.__attributes__)
486
+
487
+ def is_created(self):
488
+ return self.get_primary_key() in self.__attributes__
489
+
490
+ def add_relation(self, relations):
491
+ self._relationships.update(relations)
492
+ return self
493
+
494
+ @classmethod
495
+ def hydrate(cls, result, relations=None):
496
+ """Takes a result and loads it into a model
497
+
498
+ Args:
499
+ result ([type]): [description]
500
+ relations (dict, optional): [description]. Defaults to {}.
501
+
502
+ Returns:
503
+ [type]: [description]
504
+ """
505
+ if result is None:
506
+ return None
507
+
508
+ relations = relations or {}
509
+ if isinstance(result, (list, tuple)):
510
+ response = []
511
+ for element in result:
512
+ response.append(cls.hydrate(element))
513
+ return cls.new_collection(response)
514
+
515
+ elif isinstance(result, dict):
516
+ model = cls()
517
+ dic = {}
518
+ for key, value in result.items():
519
+ if key in model.get_dates() and value:
520
+ value = model.get_new_date(value)
521
+ dic.update({key: value})
522
+
523
+ logger = logging.getLogger("masoniteorm.models.hydrate")
524
+ logger.setLevel(logging.INFO)
525
+ logger.propagate = False
526
+ logger.info(
527
+ f"Hydrating Model {cls.__name__}",
528
+ extra={
529
+ "class_name": cls.__name__,
530
+ "class_module": cls.__module__,
531
+ },
532
+ )
533
+
534
+ model.observe_events(model, "hydrating")
535
+ model.__attributes__.update(dic or {})
536
+ model.__original_attributes__.update(dic or {})
537
+ model.add_relation(relations)
538
+ model.observe_events(model, "hydrated")
539
+ return model
540
+
541
+ elif hasattr(result, "serialize"):
542
+ model = cls()
543
+ model.__attributes__.update(result.serialize())
544
+ model.__original_attributes__.update(result.serialize())
545
+ return model
546
+ else:
547
+ model = cls()
548
+ model.observe_events(model, "hydrating")
549
+ model.__attributes__.update(dict(result))
550
+ model.__original_attributes__.update(dict(result))
551
+ model.observe_events(model, "hydrated")
552
+ return model
553
+
554
+ def fill(self, attributes):
555
+ self.__attributes__.update(attributes)
556
+ return self
557
+
558
+ def fill_original(self, attributes):
559
+ self.__original_attributes__.update(attributes)
560
+ return self
561
+
562
+ @classmethod
563
+ def new_collection(cls, data):
564
+ """Takes a result and puts it into a new collection.
565
+ This is designed to be able to be overidden by the user.
566
+
567
+ Args:
568
+ data (list|dict): Could be any data type but will be loaded directly into a collection.
569
+
570
+ Returns:
571
+ Collection
572
+ """
573
+ return Collection(data)
574
+
575
+ @classmethod
576
+ def create(cls, dictionary=None, query=False, cast=True, **kwargs):
577
+ """Creates new records based off of a dictionary as well as data set on the model
578
+ such as fillable values.
579
+
580
+ Args:
581
+ dictionary (dict, optional): [description]. Defaults to {}.
582
+ query (bool, optional): [description]. Defaults to False.
583
+ cast (bool, optional): [description]. Whether or not to cast passed values.
584
+
585
+ Returns:
586
+ self: A hydrated version of a model
587
+ """
588
+ if query:
589
+ return cls.builder.create(
590
+ dictionary, query=True, cast=cast, **kwargs
591
+ )
592
+
593
+ return cls.builder.create(dictionary, cast=cast, **kwargs)
594
+
595
+ def cast_value(self, attribute: str, value: Any):
596
+ """
597
+ Given an attribute name and a value, casts the value using the model's registered caster.
598
+ If no registered caster exists, returns the unmodified value.
599
+ """
600
+ if value is None:
601
+ return None
602
+
603
+ cast_method = self.__casts__.get(attribute)
604
+
605
+ if isinstance(cast_method, str):
606
+ cast_map = self.get_cast_map()
607
+ return cast_map[cast_method]().set(value)
608
+
609
+ if cast_method:
610
+ return cast_method(value)
611
+
612
+ return value
613
+
614
+ def cast_values(self, attributes: Dict[str, Any]) -> Dict[str, Any]:
615
+ """
616
+ Runs provided dictionary through all model casters and returns the result.
617
+
618
+ Does not mutate the passed dictionary.
619
+ """
620
+ updated_attribs = {}
621
+ for key, value in attributes.items():
622
+ if key in self.get_dates():
623
+ updated_attribs.update(
624
+ {key: self.get_new_datetime_string(value)}
625
+ )
626
+ elif key in self.__casts__:
627
+ updated_attribs.update({key: self.cast_value(key, value)})
628
+ else:
629
+ updated_attribs.update({key: value})
630
+
631
+ return updated_attribs
632
+
633
+ def fresh(self):
634
+ return (
635
+ self.get_builder()
636
+ .where(self.get_primary_key(), self.get_primary_key_value())
637
+ .first()
638
+ )
639
+
640
+ def serialize(self, exclude=None, include=None):
641
+ """Takes the data as a model and converts it into a dictionary.
642
+
643
+ Returns:
644
+ dict
645
+ """
646
+ serialized_dictionary = self.__attributes__.copy()
647
+
648
+ # prevent using both exclude and include at the same time
649
+ if exclude is not None and include is not None:
650
+ raise AttributeError(
651
+ "Can not define both includes and exclude values."
652
+ )
653
+
654
+ if exclude is not None:
655
+ self.__hidden__ = exclude
656
+
657
+ if include is not None:
658
+ self.__visible__ = include
659
+
660
+ # prevent using both hidden and visible at the same time
661
+ if self.__visible__ and self.__hidden__:
662
+ raise AttributeError(
663
+ f"class model '{self.__class__.__name__}' defines both __visible__ and __hidden__."
664
+ )
665
+
666
+ if self.__visible__:
667
+ new_serialized_dictionary = {
668
+ k: serialized_dictionary[k]
669
+ for k in self.__visible__
670
+ if k in serialized_dictionary
671
+ }
672
+ serialized_dictionary = new_serialized_dictionary
673
+ else:
674
+ for key in self.__hidden__:
675
+ if key in serialized_dictionary:
676
+ serialized_dictionary.pop(key)
677
+
678
+ for date_column in self.get_dates():
679
+ if (
680
+ date_column in serialized_dictionary
681
+ and serialized_dictionary[date_column]
682
+ ):
683
+ serialized_dictionary[date_column] = (
684
+ self.get_new_serialized_date(
685
+ serialized_dictionary[date_column]
686
+ )
687
+ )
688
+
689
+ serialized_dictionary.update(self.__dirty_attributes__)
690
+
691
+ # The builder is inside the attributes but should not be serialized
692
+ if "builder" in serialized_dictionary:
693
+ serialized_dictionary.pop("builder")
694
+
695
+ # Serialize relationships as well
696
+ serialized_dictionary.update(self.relations_to_dict())
697
+
698
+ for append in self.__appends__:
699
+ serialized_dictionary.update({append: getattr(self, append)})
700
+
701
+ remove_keys = []
702
+ for key, value in serialized_dictionary.items():
703
+ if key in self.__hidden__:
704
+ remove_keys.append(key)
705
+ if hasattr(value, "serialize"):
706
+ value = value.serialize(
707
+ self.__relationship_hidden__.get(key, [])
708
+ )
709
+ if isinstance(value, datetime):
710
+ value = self.get_new_serialized_date(value)
711
+ if key in self.__casts__:
712
+ value = self._uncast_value(key, value)
713
+
714
+ serialized_dictionary.update({key: value})
715
+
716
+ for key in remove_keys:
717
+ serialized_dictionary.pop(key)
718
+
719
+ return serialized_dictionary
720
+
721
+ def to_json(self):
722
+ """Converts a model to JSON
723
+
724
+ Returns:
725
+ string
726
+ """
727
+ return json.dumps(self.serialize(), default=str)
728
+
729
+ @classmethod
730
+ def first_or_create(cls, wheres, creates: dict = None):
731
+ """Get the first record matching the attributes or create it.
732
+
733
+ Returns:
734
+ Model
735
+ """
736
+ if creates is None:
737
+ creates = {}
738
+ self = cls()
739
+ record = self.where(wheres).first()
740
+ total = {}
741
+ total.update(creates)
742
+ total.update(wheres)
743
+ if not record:
744
+ return self.create(total, id_key=cls.get_primary_key())
745
+ return record
746
+
747
+ @classmethod
748
+ def update_or_create(cls, wheres, updates):
749
+ self = cls()
750
+ record = self.where(wheres).first()
751
+ total = {}
752
+ total.update(updates)
753
+ total.update(wheres)
754
+ if not record:
755
+ return self.create(total, id_key=cls.get_primary_key()).fresh()
756
+
757
+ return self.where(wheres).update(total)
758
+
759
+ def relations_to_dict(self):
760
+ """Converts a models relationships to a dictionary
761
+
762
+ Returns:
763
+ [type]: [description]
764
+ """
765
+ new_dic = {}
766
+ for key, value in self._relationships.items():
767
+ if value == {}:
768
+ new_dic.update({key: {}})
769
+ else:
770
+ if value is None:
771
+ new_dic.update({key: {}})
772
+ continue
773
+ elif isinstance(value, list):
774
+ value = Collection(value).serialize()
775
+ elif isinstance(value, dict):
776
+ pass
777
+ else:
778
+ value = value.serialize()
779
+
780
+ new_dic.update({key: value})
781
+
782
+ return new_dic
783
+
784
+ def touch(self, date=None, query=True):
785
+ """Updates the current timestamps on the model"""
786
+
787
+ if not self.__timestamps__:
788
+ return False
789
+
790
+ self._update_timestamps(date=date)
791
+
792
+ return self.save(query=query)
793
+
794
+ def _update_timestamps(self, date=None):
795
+ """Sets the updated at date to the current time or a specified date
796
+
797
+ Args:
798
+ date (datetime.datetime, optional): a date. If none is specified then it will use the current date Defaults to None.
799
+ """
800
+ self.updated_at = date or self._current_timestamp()
801
+
802
+ def _current_timestamp(self):
803
+ return datetime.now()
804
+
805
+ def __getattr__(self, attribute):
806
+ """Magic method that is called when an attribute does not exist on the model.
807
+
808
+ Args:
809
+ attribute (string): the name of the attribute being accessed or called.
810
+
811
+ Returns:
812
+ mixed: Could be anything that a method can return.
813
+ """
814
+
815
+ new_name_accessor = "get_" + attribute + "_attribute"
816
+
817
+ if (new_name_accessor) in self.__class__.__dict__:
818
+ return self.__class__.__dict__.get(new_name_accessor)(self)
819
+
820
+ if (
821
+ "__dirty_attributes__" in self.__dict__
822
+ and attribute in self.__dict__["__dirty_attributes__"]
823
+ ):
824
+ return self.get_dirty_value(attribute)
825
+
826
+ if (
827
+ "__attributes__" in self.__dict__
828
+ and attribute in self.__dict__["__attributes__"]
829
+ ):
830
+ if attribute in self.get_dates():
831
+ return (
832
+ self.get_new_date(self.get_value(attribute))
833
+ if self.get_value(attribute)
834
+ else None
835
+ )
836
+ return self.get_value(attribute)
837
+
838
+ if attribute in self.__passthrough__:
839
+
840
+ def method(*args, **kwargs):
841
+ return getattr(self.get_builder(), attribute)(*args, **kwargs)
842
+
843
+ return method
844
+
845
+ if attribute in self.__dict__.get("_relationships", {}):
846
+ return self.__dict__["_relationships"][attribute]
847
+
848
+ if attribute not in self.__dict__:
849
+ name = self.__class__.__name__
850
+
851
+ raise AttributeError(
852
+ f"class model '{name}' has no attribute {attribute}"
853
+ )
854
+
855
+ return None
856
+
857
+ def only(self, attributes: list) -> dict:
858
+ if isinstance(attributes, str):
859
+ attributes = [attributes]
860
+ results: dict[str, Any] = {}
861
+ for attribute in attributes:
862
+ if " as " in attribute:
863
+ attribute, alias = attribute.split(" as ")
864
+ alias = alias.strip()
865
+ attribute = attribute.strip()
866
+ else:
867
+ alias = attribute.strip()
868
+ attribute = attribute.strip()
869
+
870
+ results[alias] = self.get_raw_attribute(attribute)
871
+
872
+ return results
873
+
874
+ def __setattr__(self, attribute, value):
875
+ if hasattr(self, "set_" + attribute + "_attribute"):
876
+ method = getattr(self, "set_" + attribute + "_attribute")
877
+ value = method(value)
878
+
879
+ if attribute in self.__casts__:
880
+ value = self.cast_value(attribute, value)
881
+
882
+ if attribute in self.get_dates():
883
+ value = self.get_new_datetime_string(value)
884
+
885
+ try:
886
+ if not attribute.startswith("_"):
887
+ self.__dict__["__dirty_attributes__"].update(
888
+ {attribute: value}
889
+ )
890
+ else:
891
+ self.__dict__[attribute] = value
892
+ except KeyError:
893
+ pass
894
+
895
+ def get_raw_attribute(self, attribute):
896
+ """Gets an attribute without having to call the models magic methods. Gets around infinite recursion loops.
897
+
898
+ Args:
899
+ attribute (string): The attribute to fetch
900
+
901
+ Returns:
902
+ mixed: Any value an attribute can be.
903
+ """
904
+ return self.__attributes__.get(attribute)
905
+
906
+ def is_dirty(self):
907
+ return bool(self.__dirty_attributes__)
908
+
909
+ def get_original(self, key):
910
+ return self.__original_attributes__.get(key)
911
+
912
+ def get_dirty(self, key):
913
+ return self.__dirty_attributes__.get(key)
914
+
915
+ def get_dirty_keys(self):
916
+ return list(self.get_dirty_attributes().keys())
917
+
918
+ def save(self, query=False):
919
+ builder = self.get_builder()
920
+
921
+ if "builder" in self.__dirty_attributes__:
922
+ self.__dirty_attributes__.pop("builder")
923
+
924
+ self.observe_events(self, "saving")
925
+
926
+ if not query:
927
+ if self.is_loaded():
928
+ result = builder.update(
929
+ self.__dirty_attributes__, ignore_mass_assignment=True
930
+ )
931
+ else:
932
+ result = self.create(
933
+ self.__dirty_attributes__,
934
+ query=query,
935
+ id_key=self.get_primary_key(),
936
+ ignore_mass_assignment=True,
937
+ )
938
+ self.observe_events(self, "saved")
939
+ self.__dirty_attributes__ = {}
940
+ if self.is_loaded():
941
+ return self
942
+ return result
943
+
944
+ if self.is_loaded():
945
+ result = builder.update(
946
+ self.__dirty_attributes__,
947
+ dry=query,
948
+ ignore_mass_assignment=True,
949
+ )
950
+ else:
951
+ result = self.create(self.__dirty_attributes__, query=query)
952
+
953
+ return result
954
+
955
+ def get_value(self, attribute):
956
+ value = self.__attributes__[attribute]
957
+ if attribute in self.__casts__:
958
+ return self._uncast_value(attribute, value)
959
+
960
+ return value
961
+
962
+ def get_dirty_value(self, attribute):
963
+ value = self.__dirty_attributes__[attribute]
964
+ if attribute in self.__casts__:
965
+ return self._uncast_value(attribute, value)
966
+
967
+ return value
968
+
969
+ def all_attributes(self):
970
+ attributes = self.__attributes__
971
+ attributes.update(self.get_dirty_attributes())
972
+ for key, value in attributes.items():
973
+ if key in self.__casts__:
974
+ attributes.update({key: self._uncast_value(key, value)})
975
+
976
+ return attributes
977
+
978
+ def delete_attribute(self, key):
979
+ if key in self.__attributes__:
980
+ del self.__attributes__[key]
981
+ return True
982
+
983
+ return False
984
+
985
+ def get_dirty_attributes(self):
986
+ if "builder" in self.__dirty_attributes__:
987
+ self.__dirty_attributes__.pop("builder")
988
+ return self.__dirty_attributes__ or {}
989
+
990
+ def get_cast_map(self):
991
+ cast_map = self.__internal_cast_map__
992
+ cast_map.update(self.__cast_map__)
993
+ return cast_map
994
+
995
+ def _uncast_value(self, attribute, value):
996
+ if value is None:
997
+ return None
998
+
999
+ cast_method = self.__casts__[attribute]
1000
+
1001
+ if isinstance(cast_method, str):
1002
+ cast_map = self.get_cast_map()
1003
+ return cast_map[cast_method]().get(value)
1004
+
1005
+ if cast_method:
1006
+ return cast_method(value)
1007
+
1008
+ return value
1009
+
1010
+ @classmethod
1011
+ def load(cls, *loads):
1012
+ cls.boot()
1013
+ cls._loads += loads
1014
+ return cls.builder
1015
+
1016
+ def __getitem__(self, attribute):
1017
+ return getattr(self, attribute)
1018
+
1019
+ def get_dates(self):
1020
+ """
1021
+ Get the attributes that should be converted to dates.
1022
+
1023
+ :rtype: list
1024
+ """
1025
+ defaults = [self.date_created_at, self.date_updated_at]
1026
+
1027
+ return self.__dates__ + defaults
1028
+
1029
+ def get_new_date(self, _datetime=None):
1030
+ """
1031
+ Get the attributes that should be converted to dates.
1032
+
1033
+ :rtype: list
1034
+ """
1035
+ import pendulum
1036
+
1037
+ if not _datetime:
1038
+ return pendulum.now(tz=self.__timezone__)
1039
+ elif isinstance(_datetime, str):
1040
+ return pendulum.parse(_datetime, tz=self.__timezone__)
1041
+ elif isinstance(_datetime, datetime):
1042
+ return pendulum.instance(_datetime, tz=self.__timezone__)
1043
+ elif isinstance(_datetime, datetimedate):
1044
+ return pendulum.datetime(
1045
+ _datetime.year,
1046
+ _datetime.month,
1047
+ _datetime.day,
1048
+ tz=self.__timezone__,
1049
+ )
1050
+ elif isinstance(_datetime, datetimetime):
1051
+ return pendulum.parse(
1052
+ f"{_datetime.hour}:{_datetime.minute}:{_datetime.second}",
1053
+ tz=self.__timezone__,
1054
+ )
1055
+
1056
+ return pendulum.instance(_datetime, tz=self.__timezone__)
1057
+
1058
+ def get_new_datetime_string(self, _datetime=None):
1059
+ """
1060
+ Given an optional datetime value, constructs and returns a new datetime string.
1061
+ If no datetime is specified, returns the current time.
1062
+
1063
+ :rtype: list
1064
+ """
1065
+ return self.get_new_date(_datetime).to_datetime_string()
1066
+
1067
+ def get_new_serialized_date(self, _datetime):
1068
+ """
1069
+ Get the attributes that should be converted to dates.
1070
+
1071
+ :rtype: list
1072
+ """
1073
+ return self.get_new_date(_datetime).isoformat()
1074
+
1075
+ def set_appends(self, appends):
1076
+ """
1077
+ Get the attributes that should be converted to dates.
1078
+
1079
+ :rtype: list
1080
+ """
1081
+ self.__appends__ += appends
1082
+ return self
1083
+
1084
+ def save_many(self, relation, relating_records):
1085
+ if isinstance(relating_records, Model):
1086
+ raise ValueError(
1087
+ "Saving many records requires an iterable like a collection or a list of models and not a Model object. To attach a model, use the 'attach' method."
1088
+ )
1089
+
1090
+ for related_record in relating_records:
1091
+ self.attach(relation, related_record)
1092
+
1093
+ def detach_many(self, relation, relating_records):
1094
+ if isinstance(relating_records, Model):
1095
+ raise ValueError(
1096
+ "Detaching many records requires an iterable like a collection or a list of models and not a Model object. To detach a model, use the 'detach' method."
1097
+ )
1098
+
1099
+ related = getattr(self.__class__, relation)
1100
+ for related_record in relating_records:
1101
+ if not related_record.is_created():
1102
+ related_record.create(related_record.all_attributes())
1103
+ else:
1104
+ related_record.save()
1105
+
1106
+ related.detach(self, related_record)
1107
+
1108
+ def related(self, relation):
1109
+ related = getattr(self.__class__, relation)
1110
+ return related.relate(self)
1111
+
1112
+ def get_related(self, relation):
1113
+ related = getattr(self.__class__, relation)
1114
+ return related
1115
+
1116
+ def attach(self, relation, related_record):
1117
+ related = getattr(self.__class__, relation)
1118
+ return related.attach(self, related_record)
1119
+
1120
+ def detach(self, relation, related_record):
1121
+ related = getattr(self.__class__, relation)
1122
+
1123
+ if not related_record.is_created():
1124
+ related_record = related_record.create(
1125
+ related_record.all_attributes()
1126
+ )
1127
+ else:
1128
+ related_record.save()
1129
+
1130
+ return related.detach(self, related_record)
1131
+
1132
+ def save_quietly(self):
1133
+ """This method calls the save method on a model without firing the saved & saving observer events. Saved/Saving
1134
+ are toggled back on once save_quietly has been ran.
1135
+
1136
+ Instead of calling:
1137
+
1138
+ User().save(...)
1139
+
1140
+ you can use this:
1141
+
1142
+ User.save_quietly(...)
1143
+ """
1144
+ self.without_events()
1145
+ saved = self.save()
1146
+ self.with_events()
1147
+ return saved
1148
+
1149
+ def delete_quietly(self):
1150
+ """This method calls the delete method on a model without firing the delete & deleting observer events.
1151
+ Instead of calling:
1152
+
1153
+ User().delete(...)
1154
+
1155
+ you can use this:
1156
+
1157
+ User.delete_quietly(...)
1158
+
1159
+ Returns:
1160
+ self
1161
+ """
1162
+ delete = (
1163
+ self.without_events()
1164
+ .where(self.get_primary_key(), self.get_primary_key_value())
1165
+ .delete()
1166
+ )
1167
+ self.with_events()
1168
+ return delete
1169
+
1170
+ def attach_related(self, relation, related_record):
1171
+ return self.attach(relation, related_record)
1172
+
1173
+ @classmethod
1174
+ def filter_fillable(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]:
1175
+ """
1176
+ Filters provided dictionary to only include fields specified in the model's __fillable__ property
1177
+
1178
+ Passed dictionary is not mutated.
1179
+ """
1180
+ if cls.__fillable__ != ["*"]:
1181
+ dictionary = {
1182
+ x: dictionary[x] for x in cls.__fillable__ if x in dictionary
1183
+ }
1184
+ return dictionary
1185
+
1186
+ @classmethod
1187
+ def filter_mass_assignment(
1188
+ cls, dictionary: Dict[str, Any]
1189
+ ) -> Dict[str, Any]:
1190
+ """
1191
+ Filters the provided dictionary in preparation for a mass-assignment operation
1192
+
1193
+ Wrapper around filter_fillable() & filter_guarded(). Passed dictionary is not mutated.
1194
+ """
1195
+ return cls.filter_guarded(cls.filter_fillable(dictionary))
1196
+
1197
+ @classmethod
1198
+ def filter_guarded(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]:
1199
+ """
1200
+ Filters provided dictionary to exclude fields specified in the model's __guarded__ property
1201
+
1202
+ Passed dictionary is not mutated.
1203
+ """
1204
+ if cls.__guarded__ == ["*"]:
1205
+ # If all fields are guarded, all data should be filtered
1206
+ return {}
1207
+ return {
1208
+ f: dictionary[f] for f in dictionary if f not in cls.__guarded__
1209
+ }