everysk-lib 1.10.2__cp312-cp312-win_amd64.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 (137) hide show
  1. everysk/__init__.py +30 -0
  2. everysk/_version.py +683 -0
  3. everysk/api/__init__.py +61 -0
  4. everysk/api/api_requestor.py +167 -0
  5. everysk/api/api_resources/__init__.py +23 -0
  6. everysk/api/api_resources/api_resource.py +371 -0
  7. everysk/api/api_resources/calculation.py +779 -0
  8. everysk/api/api_resources/custom_index.py +42 -0
  9. everysk/api/api_resources/datastore.py +81 -0
  10. everysk/api/api_resources/file.py +42 -0
  11. everysk/api/api_resources/market_data.py +223 -0
  12. everysk/api/api_resources/parser.py +66 -0
  13. everysk/api/api_resources/portfolio.py +43 -0
  14. everysk/api/api_resources/private_security.py +42 -0
  15. everysk/api/api_resources/report.py +65 -0
  16. everysk/api/api_resources/report_template.py +39 -0
  17. everysk/api/api_resources/tests.py +115 -0
  18. everysk/api/api_resources/worker_execution.py +64 -0
  19. everysk/api/api_resources/workflow.py +65 -0
  20. everysk/api/api_resources/workflow_execution.py +93 -0
  21. everysk/api/api_resources/workspace.py +42 -0
  22. everysk/api/http_client.py +63 -0
  23. everysk/api/tests.py +32 -0
  24. everysk/api/utils.py +262 -0
  25. everysk/config.py +451 -0
  26. everysk/core/_tests/serialize/test_json.py +336 -0
  27. everysk/core/_tests/serialize/test_orjson.py +295 -0
  28. everysk/core/_tests/serialize/test_pickle.py +48 -0
  29. everysk/core/cloud_function/main.py +78 -0
  30. everysk/core/cloud_function/tests.py +86 -0
  31. everysk/core/compress.py +245 -0
  32. everysk/core/datetime/__init__.py +12 -0
  33. everysk/core/datetime/calendar.py +144 -0
  34. everysk/core/datetime/date.py +424 -0
  35. everysk/core/datetime/date_expression.py +299 -0
  36. everysk/core/datetime/date_mixin.py +1475 -0
  37. everysk/core/datetime/date_settings.py +30 -0
  38. everysk/core/datetime/datetime.py +713 -0
  39. everysk/core/exceptions.py +435 -0
  40. everysk/core/fields.py +1176 -0
  41. everysk/core/firestore.py +555 -0
  42. everysk/core/fixtures/_settings.py +29 -0
  43. everysk/core/fixtures/other/_settings.py +18 -0
  44. everysk/core/fixtures/user_agents.json +88 -0
  45. everysk/core/http.py +691 -0
  46. everysk/core/lists.py +92 -0
  47. everysk/core/log.py +709 -0
  48. everysk/core/number.py +37 -0
  49. everysk/core/object.py +1469 -0
  50. everysk/core/redis.py +1021 -0
  51. everysk/core/retry.py +51 -0
  52. everysk/core/serialize.py +674 -0
  53. everysk/core/sftp.py +414 -0
  54. everysk/core/signing.py +53 -0
  55. everysk/core/slack.py +127 -0
  56. everysk/core/string.py +199 -0
  57. everysk/core/tests.py +240 -0
  58. everysk/core/threads.py +199 -0
  59. everysk/core/undefined.py +70 -0
  60. everysk/core/unittests.py +73 -0
  61. everysk/core/workers.py +241 -0
  62. everysk/sdk/__init__.py +23 -0
  63. everysk/sdk/base.py +98 -0
  64. everysk/sdk/brutils/cnpj.py +391 -0
  65. everysk/sdk/brutils/cnpj_pd.py +129 -0
  66. everysk/sdk/engines/__init__.py +26 -0
  67. everysk/sdk/engines/cache.py +185 -0
  68. everysk/sdk/engines/compliance.py +37 -0
  69. everysk/sdk/engines/cryptography.py +69 -0
  70. everysk/sdk/engines/expression.cp312-win_amd64.pyd +0 -0
  71. everysk/sdk/engines/expression.pyi +55 -0
  72. everysk/sdk/engines/helpers.cp312-win_amd64.pyd +0 -0
  73. everysk/sdk/engines/helpers.pyi +26 -0
  74. everysk/sdk/engines/lock.py +120 -0
  75. everysk/sdk/engines/market_data.py +244 -0
  76. everysk/sdk/engines/settings.py +19 -0
  77. everysk/sdk/entities/__init__.py +23 -0
  78. everysk/sdk/entities/base.py +784 -0
  79. everysk/sdk/entities/base_list.py +131 -0
  80. everysk/sdk/entities/custom_index/base.py +209 -0
  81. everysk/sdk/entities/custom_index/settings.py +29 -0
  82. everysk/sdk/entities/datastore/base.py +160 -0
  83. everysk/sdk/entities/datastore/settings.py +17 -0
  84. everysk/sdk/entities/fields.py +375 -0
  85. everysk/sdk/entities/file/base.py +215 -0
  86. everysk/sdk/entities/file/settings.py +63 -0
  87. everysk/sdk/entities/portfolio/base.py +248 -0
  88. everysk/sdk/entities/portfolio/securities.py +241 -0
  89. everysk/sdk/entities/portfolio/security.py +580 -0
  90. everysk/sdk/entities/portfolio/settings.py +97 -0
  91. everysk/sdk/entities/private_security/base.py +226 -0
  92. everysk/sdk/entities/private_security/settings.py +17 -0
  93. everysk/sdk/entities/query.py +603 -0
  94. everysk/sdk/entities/report/base.py +214 -0
  95. everysk/sdk/entities/report/settings.py +23 -0
  96. everysk/sdk/entities/script.py +310 -0
  97. everysk/sdk/entities/secrets/base.py +128 -0
  98. everysk/sdk/entities/secrets/script.py +119 -0
  99. everysk/sdk/entities/secrets/settings.py +17 -0
  100. everysk/sdk/entities/settings.py +48 -0
  101. everysk/sdk/entities/tags.py +174 -0
  102. everysk/sdk/entities/worker_execution/base.py +307 -0
  103. everysk/sdk/entities/worker_execution/settings.py +63 -0
  104. everysk/sdk/entities/workflow_execution/base.py +113 -0
  105. everysk/sdk/entities/workflow_execution/settings.py +32 -0
  106. everysk/sdk/entities/workspace/base.py +99 -0
  107. everysk/sdk/entities/workspace/settings.py +27 -0
  108. everysk/sdk/settings.py +67 -0
  109. everysk/sdk/tests.py +105 -0
  110. everysk/sdk/worker_base.py +47 -0
  111. everysk/server/__init__.py +9 -0
  112. everysk/server/applications.py +63 -0
  113. everysk/server/endpoints.py +516 -0
  114. everysk/server/example_api.py +69 -0
  115. everysk/server/middlewares.py +80 -0
  116. everysk/server/requests.py +62 -0
  117. everysk/server/responses.py +119 -0
  118. everysk/server/routing.py +64 -0
  119. everysk/server/settings.py +36 -0
  120. everysk/server/tests.py +36 -0
  121. everysk/settings.py +98 -0
  122. everysk/sql/__init__.py +9 -0
  123. everysk/sql/connection.py +232 -0
  124. everysk/sql/model.py +376 -0
  125. everysk/sql/query.py +417 -0
  126. everysk/sql/row_factory.py +63 -0
  127. everysk/sql/settings.py +49 -0
  128. everysk/sql/utils.py +129 -0
  129. everysk/tests.py +23 -0
  130. everysk/utils.py +81 -0
  131. everysk/version.py +15 -0
  132. everysk_lib-1.10.2.dist-info/.gitignore +5 -0
  133. everysk_lib-1.10.2.dist-info/METADATA +326 -0
  134. everysk_lib-1.10.2.dist-info/RECORD +137 -0
  135. everysk_lib-1.10.2.dist-info/WHEEL +5 -0
  136. everysk_lib-1.10.2.dist-info/licenses/LICENSE.txt +9 -0
  137. everysk_lib-1.10.2.dist-info/top_level.txt +2 -0
everysk/core/object.py ADDED
@@ -0,0 +1,1469 @@
1
+ ###############################################################################
2
+ #
3
+ # (C) Copyright 2023 EVERYSK TECHNOLOGIES
4
+ #
5
+ # This is an unpublished work containing confidential and proprietary
6
+ # information of EVERYSK TECHNOLOGIES. Disclosure, use, or reproduction
7
+ # without authorization of EVERYSK TECHNOLOGIES is prohibited.
8
+ #
9
+ ###############################################################################
10
+ from _collections_abc import dict_items, dict_keys, dict_values
11
+ from collections.abc import Iterator
12
+ from copy import copy, deepcopy
13
+ from inspect import isroutine
14
+ from types import GenericAlias, UnionType
15
+ from typing import Any, Self, get_args
16
+
17
+ from everysk.core.datetime import Date, DateTime
18
+ from everysk.core.exceptions import DefaultError, FieldValueError, RequiredError
19
+
20
+ CLASS_KEY: str = '__class_path__'
21
+ CONFIG_ATTRIBUTE_NAME: str = '_config'
22
+
23
+
24
+ ###############################################################################
25
+ # Private functions Implementation
26
+ ###############################################################################
27
+ def __get_field_value__(obj: Any, attr: str, value: Any) -> Any:
28
+ """
29
+ Function that get the cleaned value for a Field and validate this value.
30
+
31
+ Args:
32
+ obj (Any): A class or a instance of BaseObject.
33
+ attr (str): The attribute name.
34
+ value (Any): The value that is assigned to this attribute.
35
+
36
+ Raises:
37
+ FieldValueError: If we find validation errors.
38
+ """
39
+ # Transforms the given value to Undefined if it matches the default_parse_string.
40
+ value = _transform_to_undefined(value)
41
+
42
+ # Get all attributes that the object has
43
+ attributes = getattr(obj, MetaClass._attr_name) # pylint: disable=protected-access
44
+ try:
45
+ field = attributes[attr]
46
+ except KeyError:
47
+ return value
48
+
49
+ # field can be the type itself or an instance of BaseField
50
+ if isinstance(field, BaseField):
51
+ try:
52
+ value = field.get_cleaned_value(value)
53
+ except Exception as error:
54
+ # Add attribute name to error
55
+ error.args = (f'{attr}: {error!s}',)
56
+ raise FieldValueError(error.args) from error
57
+
58
+ field.validate(attr, value)
59
+ else:
60
+ _validate(attr, value, field)
61
+
62
+ return value
63
+
64
+
65
+ def _transform_to_undefined(value: Any) -> Any:
66
+ """
67
+ Transforms the given value to Undefined if it matches the default parse string.
68
+
69
+ Args:
70
+ value (Any): The value to transform.
71
+
72
+ Returns:
73
+ Any: The transformed value.
74
+ """
75
+ if isinstance(value, str) and value == Undefined.default_parse_string:
76
+ value = Undefined
77
+
78
+ return value
79
+
80
+
81
+ def _required(attr_name: str, value: Any) -> None:
82
+ """
83
+ Checks if value is required, required values can't be: None, '', [], {}.
84
+
85
+ Raises:
86
+ RequiredError: When value is required and match with False conditions.
87
+ """
88
+ if value in (Undefined, None, '', (), [], {}):
89
+ raise RequiredError(f'The {attr_name} attribute is required.')
90
+
91
+
92
+ def _validate(attr_name: str, value: Any, attr_type: type | UnionType) -> None: # pylint: disable=too-many-branches, too-many-return-statements
93
+ """
94
+ Validates that the given value is of the expected attribute type. The function supports special type checks for
95
+ Date and DateTime types and handles general type validation for other types. It allows the value to pass through
96
+ if it matches the expected type, is None, or is an instance of Undefined. The function uses custom checks for Date
97
+ and DateTime to accommodate their unique validation requirements.
98
+
99
+ Args:
100
+ attr_name (str):
101
+ The name of the attribute being validated. This is used for generating error messages.
102
+
103
+ value (Any):
104
+ The value to be validated against the expected type.
105
+
106
+ attr_type (type):
107
+ The expected type of the value. This can be a standard Python type, a custom type, or
108
+ specific types like Date and DateTime which have dedicated validation logic.
109
+
110
+ Raises:
111
+ FieldValueError: When value is not of value_type.
112
+ """
113
+ # We always accept these 2 values
114
+ if value is None or value is Undefined:
115
+ return
116
+
117
+ if attr_type == Date:
118
+ # If we are expecting a Date we don't need to check other things
119
+ if Date.is_date(value):
120
+ return
121
+
122
+ if attr_type == DateTime:
123
+ # If we are expecting a DateTime we don't need to check other things
124
+ if DateTime.is_datetime(value):
125
+ return
126
+
127
+ # TypeError: typing.Any cannot be used with isinstance()
128
+ if attr_type is Any:
129
+ return
130
+
131
+ # https://everysk.atlassian.net/browse/COD-4286
132
+ # If we use 'class'/callable as a annotation, the isinstance will fail
133
+ # because attr_type will be a string/function so we need to discard it first
134
+ if attr_type is callable:
135
+ if callable(value):
136
+ return
137
+
138
+ # If it is string, when we use classes as annotations we check if the name of the class is the same
139
+ if isinstance(attr_type, str):
140
+ if type(value).__name__ == attr_type:
141
+ return
142
+
143
+ try:
144
+ # Check if value is an instance of the expected type
145
+ # If attr_type is a UnionType - int | float, the isinstance will work
146
+ if isinstance(value, attr_type):
147
+ return
148
+ except TypeError:
149
+ pass
150
+
151
+ ## Subscriptable types like List, Dict, Tuple, etc
152
+ if isinstance(attr_type, GenericAlias):
153
+ # We need to check if the value is a instance of the origin type
154
+ if isinstance(value, attr_type.__origin__):
155
+ # We not check the content of the list, dict, tuple, etc
156
+ return
157
+
158
+ if isinstance(attr_type, UnionType):
159
+ # Try to validate with all members of the union
160
+ for attr_type_ in get_args(attr_type):
161
+ try:
162
+ _validate(attr_name, value, attr_type_)
163
+ return # test passed with some member
164
+ except FieldValueError:
165
+ pass
166
+
167
+ raise FieldValueError(f'Key {attr_name} must be {attr_type}.')
168
+
169
+
170
+ ###############################################################################
171
+ # BaseField Class Implementation
172
+ ###############################################################################
173
+ class BaseField:
174
+ """Base class of all fields that will guarantee their type."""
175
+
176
+ ## Public attributes
177
+ attr_type: type | UnionType = None
178
+ default: Any = None
179
+ readonly: bool = False
180
+ required: bool = False
181
+ required_lazy: bool = False
182
+ empty_is_none: bool = False
183
+
184
+ def __init__(
185
+ self,
186
+ attr_type: type | UnionType = None,
187
+ default: Any = None,
188
+ readonly: bool = False,
189
+ required: bool = False,
190
+ required_lazy: bool = False,
191
+ empty_is_none: bool = False,
192
+ **kwargs,
193
+ ) -> None:
194
+ """
195
+ Use kwargs to set more attributes on the Field.
196
+
197
+ Raises:
198
+ DefaultError: For default values they can't be empty [] or {}.
199
+ RequiredError: If field is readonly, then default value is required.
200
+ """
201
+ self.attr_type = attr_type
202
+ if required and required_lazy:
203
+ raise FieldValueError("Required and required_lazy can't be booth True.")
204
+
205
+ self.required = required
206
+ self.required_lazy = required_lazy
207
+
208
+ if default is not None and not default and isinstance(default, (list, dict)):
209
+ # For default values they can't be empty [] or {} - because this can cause
210
+ # some issues with class attributes where these type can aggregate values.
211
+ raise DefaultError('Default value cannot be a list or a dict.')
212
+
213
+ if readonly and (default is None or default is Undefined):
214
+ raise RequiredError('If field is readonly, then default value is required.')
215
+
216
+ self.readonly = readonly
217
+
218
+ # We use this flag to convert '' to None
219
+ self.empty_is_none = empty_is_none
220
+
221
+ # Other attributes will be assigned directly
222
+ for key, value in kwargs.items():
223
+ setattr(self, key, value)
224
+
225
+ # For the last We need to store the cleaned value
226
+ self.default = self.get_cleaned_value(default)
227
+
228
+ def __repr__(self) -> str:
229
+ """
230
+ The `__repr__` method from `BaseField` returns the name of the
231
+ instantiated class.
232
+
233
+ Returns:
234
+ str: The name of the class.
235
+ """
236
+ return self.__class__.__name__
237
+
238
+ def __eq__(self, obj: object) -> bool:
239
+ """
240
+ One object will be equal to another one if all `__dict__`
241
+ attributes are the same.
242
+
243
+ Args:
244
+ obj (object): The obj for comparison.
245
+ """
246
+ return self.__dict__ == obj.__dict__
247
+
248
+ def transform_to_none(self, value: Any) -> Any:
249
+ """
250
+ Transforms value to None if needed.
251
+
252
+ Args:
253
+ value (Any): The value to be converted to `None`.
254
+
255
+ Example:
256
+ >>> field = BaseField(attr_type=str, empty_is_none=False)
257
+ >>> field.transform_to_none('')
258
+ ''
259
+
260
+ >>> field = BaseField(attr_type=str, empty_is_none=True)
261
+ >>> field.transform_to_none('')
262
+ None
263
+ """
264
+ if self.empty_is_none and value == '':
265
+ value = None
266
+
267
+ return value
268
+
269
+ def get_cleaned_value(self, value: Any) -> Any:
270
+ """
271
+ This function first converts the value to None if needed, then
272
+ checks if the `value` is a callable. If it is, the function calls
273
+ the `value` and stores its result by reassigning the `value` variable.
274
+ Finally, the function calls the `clean_value` method.
275
+
276
+ Args:
277
+ value (Any): The cleaned value to be retrieved.
278
+
279
+ Returns:
280
+ Any: The cleaned value.
281
+ """
282
+ # We first verify if we need to transform the value to None
283
+ value = self.transform_to_none(value)
284
+
285
+ # Then we run the get_value method when value is a callable
286
+ value = self.get_value(value)
287
+
288
+ # Then we run the clean_value method
289
+ value = self.clean_value(value)
290
+
291
+ return value
292
+
293
+ def get_value(self, value: Any) -> Any:
294
+ """
295
+ This function checks if the `value` is a callable
296
+ By either returning the `value` or returning the
297
+ result of the callable function.
298
+
299
+ Must be implemented in child classes that need do some changes
300
+ on received value before clean_value.
301
+
302
+ Args:
303
+ value (Any): The value to be possibly called.
304
+
305
+ Returns:
306
+ Any: The result of the callable function or the original value.
307
+ """
308
+ # If value is callable we call it
309
+ if callable(value):
310
+ value = value()
311
+
312
+ return value
313
+
314
+ def clean_value(self, value: Any) -> Any:
315
+ """
316
+ This method is always called when we assigned a value to some attribute.
317
+ Must be implemented in child classes to change the behavior of a given field.
318
+ Below we have an example that reimplements the `clean_value()`.
319
+
320
+ Args:
321
+ value (Any): The value to be cleaned.
322
+
323
+ Usage:
324
+ >>> from everysk.core.fields import BoolField
325
+ >>> from everysk.core.object import BaseObject
326
+
327
+ >>> class MyBoolField(BoolField):
328
+ ... def clean_value(self, value):
329
+ ... if value == 'test':
330
+ ... return True
331
+ ... return False
332
+
333
+ >>> class MyClass(BaseObject):
334
+ ... f1 = MyBoolField()
335
+
336
+ >>> a = MyClass()
337
+ >>> a.f1 = True
338
+ >>> a.f1
339
+ False
340
+
341
+ >>> a.f1 = 'test'
342
+ >>> a.f1
343
+ True
344
+ """
345
+ return value
346
+
347
+ def validate(self, attr_name: str, value: Any, attr_type: type | UnionType = None) -> None:
348
+ """
349
+ Checks if value is required and if is of correct type.
350
+ This method can be reimplemented in child classes to modify
351
+ the behavior.
352
+
353
+ Raises:
354
+ RequiredError: If required and None is passed
355
+ FieldValueError: If value type don't match with required type.
356
+
357
+ Usage:
358
+ >>> from everysk.core.fields import StrField
359
+ >>> from everysk.core.object import BaseObject
360
+
361
+ >>> class MyStrField(StrField):
362
+ ... def validate(self, attr_name, value):
363
+ ... if value == 'test':
364
+ ... raise ValueError("value argument cannot be 'test'")
365
+
366
+ >>> class MyClass(BaseObject):
367
+ ... f1 = MyStrField()
368
+
369
+ >>> a = MyClass()
370
+ >>> a.f1 = 'test'
371
+ ValueError: value argument cannot be 'test'
372
+ """
373
+ if attr_type is None:
374
+ attr_type = self.attr_type
375
+
376
+ if self.readonly:
377
+ # This is necessary to be able to at least assign the default value to the field
378
+ if value != self.default:
379
+ raise FieldValueError(f"The field '{attr_name}' value cannot be changed.")
380
+
381
+ if self.required and not self.required_lazy:
382
+ _required(attr_name, value)
383
+
384
+ _validate(attr_name, value, attr_type)
385
+
386
+ def __getattr__(self, name: str) -> Any:
387
+ """
388
+ This method is used to handle pylint errors where the method/attribute does not exist.
389
+ The problem is that we change the field in the MetaClass to the Field's default value,
390
+ so StrField does not have the str methods but the result is a string.
391
+ This method will only be executed if the method/attributes do not exist in the Field class.
392
+
393
+ Args:
394
+ name (str): The name of the method/attribute.
395
+ """
396
+ # https://pythonhint.com/post/2118347356810295/avoid-pylint-warning-e1101-instance-of-has-no-member-for-class-with-dynamic-attributes
397
+ if not isinstance(self.attr_type, UnionType):
398
+ return getattr(self.attr_type, name)
399
+
400
+ # We try to get the attribute from the ones that are in the tuple
401
+ for attr_type in self.attr_type.__args__:
402
+ try:
403
+ return getattr(attr_type, name)
404
+ except AttributeError:
405
+ pass
406
+
407
+ # If no attribute was found we raise the error
408
+ raise AttributeError(f"type object '{self.attr_type}' has no attribute '{name}'.")
409
+
410
+
411
+ ###############################################################################
412
+ # MetaClass Implementation
413
+ ###############################################################################
414
+ def _silent(func: callable) -> callable:
415
+ """
416
+ Function that creates a silent decorator for the given function.
417
+ This decorator will catch any exception raised by the function and store it.
418
+
419
+ Args:
420
+ func (callable): The function to be decorated.
421
+ """
422
+
423
+ def wrapper(self, *args, **kwargs):
424
+ # pylint: disable=broad-exception-caught, protected-access
425
+ silent = kwargs.pop('silent', self._silent)
426
+ try:
427
+ return func(self, *args, **kwargs)
428
+ except Exception as error:
429
+ if not silent:
430
+ raise error
431
+ if self._errors is None:
432
+ self._errors = {}
433
+ self._errors['init'] = deepcopy(error)
434
+
435
+ return None
436
+
437
+ return wrapper
438
+
439
+
440
+ class MetaClass(type):
441
+ _attr_name: str = '__attributes__'
442
+ _anno_name: str = '__annotations__'
443
+
444
+ def __call__(cls, *args: tuple, **kwargs: dict) -> Self:
445
+ """
446
+ Method that creates a sequence of class call like:
447
+ before_init
448
+ init
449
+ after_init
450
+ If the BaseObject class implement one of these methods they will be executed.
451
+ https://discuss.python.org/t/add-a-post-method-equivalent-to-the-new-method-but-called-after-init/5449/11
452
+ """
453
+ errors: dict[str : Exception | None] = {'before_init': None, 'init': None, 'after_init': None}
454
+ # We get the silent from kwargs otherwise we get from the class
455
+ silent = kwargs.get('silent', cls._silent)
456
+
457
+ ## If the before init method is implemented we run it
458
+ try:
459
+ # If the method returns a dict we update kwargs
460
+ dct = cls.__before_init__(**kwargs)
461
+ if isinstance(dct, dict):
462
+ kwargs = dct
463
+ except Exception as error: # pylint: disable=broad-exception-caught
464
+ if not silent:
465
+ raise error
466
+ errors['before_init'] = deepcopy(error)
467
+
468
+ ## Here we create the object and initialize the silent for init must be inside the _silent function
469
+ obj = super().__call__(*args, **kwargs)
470
+ if obj._errors:
471
+ errors['init'] = obj._errors['init']
472
+
473
+ ## If the after init method is implemented we run it
474
+ try:
475
+ obj.__after_init__()
476
+ except Exception as error: # pylint: disable=broad-exception-caught
477
+ if not silent:
478
+ raise error
479
+ errors['after_init'] = deepcopy(error)
480
+
481
+ # Store the errors
482
+ if any(errors.values()):
483
+ obj._errors = errors
484
+ # Execute the handler for errors
485
+ obj._init_error_handler(kwargs, errors)
486
+
487
+ # Set the instance to be Frozen or not
488
+ try:
489
+ config = getattr(obj, CONFIG_ATTRIBUTE_NAME)
490
+ obj._is_frozen = config.frozen # pylint: disable=attribute-defined-outside-init
491
+ except AttributeError:
492
+ pass
493
+
494
+ return obj
495
+
496
+ def __new__(mcs, name: str, bases: tuple, attrs: dict) -> Self:
497
+ """
498
+ This method is executed every time a BaseObject Class is created in the Python runtime.
499
+ We changed this method to create the config and attributes properties and update the annotations.
500
+
501
+ Example:
502
+ >>> from everysk.core.object import BaseObject, CONFIG_ATTRIBUTE_NAME
503
+ >>> class MyClass(BaseObject):
504
+ ... class Config:
505
+ ... value: int = 1
506
+
507
+ >>> obj1 = MyClass()
508
+ >>> obj2 = MyClass()
509
+ >>> configC = getattr(MyClass, CONFIG_ATTRIBUTE_NAME)
510
+ >>> config1 = getattr(obj1, CONFIG_ATTRIBUTE_NAME)
511
+ >>> config2 = getattr(obj2, CONFIG_ATTRIBUTE_NAME)
512
+ >>> configC == config1 == config2
513
+ ... True
514
+
515
+ >>> configC.value, config1.value, config2.value
516
+ (1, 1, 1)
517
+
518
+ >>> config2.value = 3
519
+ >>> configC.value, config1.value, config2.value
520
+ (3, 3, 3)
521
+
522
+ >>> MyClass.Config
523
+ ---------------------------------------------------------------------------
524
+ AttributeError Traceback (most recent call last)
525
+ ----> 1 MyClass.Config
526
+
527
+ AttributeError: type object 'MyClass' has no attribute 'Config'
528
+
529
+ Args:
530
+ mcs (type): Represents this class.
531
+ name (str): The name for the new class.
532
+ bases (tuple): All inheritance classes.
533
+ attrs (dict): All attributes that the new class has.
534
+
535
+ Returns:
536
+ type: Return the new class object.
537
+ """
538
+ # Creating the config property.
539
+ if 'Config' in attrs:
540
+ # If Config is in the class we just create it
541
+ Config = attrs.pop('Config') # pylint: disable=invalid-name
542
+ attrs[CONFIG_ATTRIBUTE_NAME] = Config()
543
+ else:
544
+ # If Config is not in the class we get the first one from the bases
545
+ for base in bases:
546
+ if hasattr(base, CONFIG_ATTRIBUTE_NAME):
547
+ config = getattr(base, CONFIG_ATTRIBUTE_NAME)
548
+ attrs[CONFIG_ATTRIBUTE_NAME] = deepcopy(config)
549
+ break
550
+
551
+ # We need all attributes to validate the types later
552
+ # So we need to get the parents attributes too
553
+ attributes = {}
554
+ for parent in bases:
555
+ attributes.update(getattr(parent, mcs._attr_name, {}))
556
+
557
+ # We could not update the info inside the original attrs dict because:
558
+ # RuntimeError: dictionary changed size during iteration
559
+ # So we remove the attributes that we need to update
560
+ attributes.update(attrs.pop(mcs._attr_name, {}))
561
+ annotations: dict = attrs.pop(mcs._anno_name, {})
562
+ new_attrs = {}
563
+ for attr_name, attr_value in attrs.items():
564
+ if attr_name == '__init__':
565
+ # To run all init in silent mode we need to decorate it
566
+ new_attrs[attr_name] = _silent(attr_value)
567
+
568
+ # We discard all python dunder attributes, all functions, all properties, Undefined and None values
569
+ elif (
570
+ not attr_name.startswith('__')
571
+ and not isroutine(attr_value)
572
+ and not isinstance(attr_value, property)
573
+ and attr_value is not None
574
+ and attr_value is not Undefined
575
+ ):
576
+ # For BaseFields we need to use the properties and set the correct value in the attribute
577
+ if isinstance(attr_value, BaseField):
578
+ # We keep a copy of the value inside the __attributes__
579
+ attributes[attr_name] = attr_value
580
+
581
+ # We set the correct value to this attribute in the class
582
+ new_attrs[attr_name] = attr_value.default
583
+
584
+ # We create the annotation for this attribute
585
+ if attr_name not in annotations:
586
+ annotations[attr_name] = attr_value.attr_type
587
+ else:
588
+ # For normal attributes we only store the class
589
+ attributes[attr_name] = type(attr_value)
590
+
591
+ # Now we update annotations for attributes that are not annotated
592
+ # Ex: var = 1
593
+ if attr_name not in annotations:
594
+ annotations[attr_name] = type(attr_value)
595
+
596
+ # With both completed now we need to get the fields that are only annotations
597
+ # class MyClass:
598
+ # var: str
599
+ for key in annotations.keys() - attributes.keys():
600
+ attributes[key] = annotations[key]
601
+ # We set the default value to None to avoid break the code
602
+ new_attrs[key] = None
603
+
604
+ # We remove config from these to avoid validation/serialization problems
605
+ attributes.pop(CONFIG_ATTRIBUTE_NAME, None)
606
+
607
+ annotations.pop(CONFIG_ATTRIBUTE_NAME, None)
608
+
609
+ # Readonly attributes need to go in the exclude list to generate the to_dict result correctly
610
+ if CONFIG_ATTRIBUTE_NAME in attrs and hasattr(attrs[CONFIG_ATTRIBUTE_NAME], 'exclude_keys'):
611
+ readonly_keys = {key for key, value in attributes.items() if getattr(value, 'readonly', False)}
612
+ attrs[CONFIG_ATTRIBUTE_NAME].exclude_keys = attrs[CONFIG_ATTRIBUTE_NAME].exclude_keys.union(readonly_keys)
613
+
614
+ # Then we update the attributes list for this new class
615
+ attrs[mcs._attr_name] = attributes
616
+ attrs[mcs._anno_name] = annotations
617
+ attrs.update(new_attrs)
618
+
619
+ return super().__new__(mcs, name, bases, attrs)
620
+
621
+ def __setattr__(cls, __name: str, __value: Any) -> None:
622
+ """
623
+ Method that sets the values on fields of the class.
624
+
625
+ Args:
626
+ __name (str): The attribute name.
627
+ __value (Any): The value that is set.
628
+ """
629
+ return super().__setattr__(__name, __get_field_value__(cls, __name, __value))
630
+
631
+
632
+ ###############################################################################
633
+ # BaseObject Class Implementation
634
+ ###############################################################################
635
+ class _BaseObject(metaclass=MetaClass):
636
+ """
637
+ To ensure correct check for data keys
638
+ uses https://docs.python.org/3/library/typing.html standards.
639
+
640
+ >>> from utils.object import BaseObject
641
+ >>> class MyObject(BaseObject):
642
+ ... var_int: int = None
643
+ ...
644
+ >>> obj = MyObject(var_int='a')
645
+ Traceback (most recent call last):
646
+ File "<stdin>", line 1, in <module>
647
+ File "/var/app/utils/object.py", line 151, in __init__
648
+ setattr(self, key, value)
649
+ File "/var/app/utils/object.py", line 209, in __setattr__
650
+ value = self.__get_clean_value__(__name, value)
651
+ File "/var/app/utils/object.py", line 192, in __get_clean_value__
652
+ _validate(attr, value, annotations[attr])
653
+ File "/var/app/utils/object.py", line 56, in _validate
654
+ raise DataTypeError(f'Key {attr_name} must be {attr_type}.')
655
+ utils.exceptions.DataTypeError: Key var_int must be <class 'int'>.
656
+ """
657
+
658
+ ## Private attributes
659
+ __slots__ = ('_need_validation',) # If we need to validate the data on setattr.
660
+ _errors: dict = None # Keep the init errors.
661
+ _is_frozen: bool = False # This will control if we can update data on this class.
662
+ _silent: bool = False # If true an error that happen on init will be stored in self._error.
663
+
664
+ def __new__(cls, *args, **kwargs) -> Self:
665
+ obj = super().__new__(cls)
666
+
667
+ # Initialize the _need_validation attribute always as True to validate all fields
668
+ if not hasattr(obj, '_need_validation') or obj._need_validation is None:
669
+ obj._need_validation = True
670
+
671
+ return obj
672
+
673
+ @classmethod
674
+ def __before_init__(cls, **kwargs: dict) -> dict:
675
+ """
676
+ Method that runs before the __init__ method.
677
+ If needed we must return the kwargs that will be passed to the init
678
+ otherwise return None to not change the current behavior.
679
+ """
680
+ return kwargs
681
+
682
+ def __init__(self, **kwargs: dict) -> None:
683
+ # Validate all required fields
684
+ attributes = self.__get_attributes__()
685
+ for attr_name, field in attributes.items():
686
+ if getattr(field, 'required', False):
687
+ _required(attr_name=attr_name, value=kwargs.get(attr_name))
688
+
689
+ # Set all kwargs on the object
690
+ for key, value in kwargs.items():
691
+ setattr(self, key, value)
692
+
693
+ def __after_init__(self) -> None:
694
+ """
695
+ Method that runs after the __init__ method.
696
+ This method must return None.
697
+ """
698
+
699
+ def __check_frozen__(self) -> None:
700
+ """
701
+ Method that checks if this class is a Frozen object and raises attribute error.
702
+ """
703
+ if self._is_frozen:
704
+ raise AttributeError(f'Class {self.get_full_doted_class_path()} is frozen and cannot be modified.')
705
+
706
+ def __copy__(self) -> Self:
707
+ """
708
+ A shallow copy constructs a new compound object and then (to the extent possible)
709
+ inserts references into it to the objects found in the original.
710
+ If the object is Frozen the copy will not be.
711
+ This method is used when we call copy(obj).
712
+ """
713
+ # We need to copy the __dict__
714
+ obj = copy(self.__dict__)
715
+
716
+ # We must not copy the config attribute because the copy could override the original in the class
717
+ obj.pop(CONFIG_ATTRIBUTE_NAME, None)
718
+
719
+ # We create a new obj
720
+ obj = type(self)(**obj)
721
+ return obj
722
+
723
+ def __deepcopy__(self, memo: dict = None) -> Self:
724
+ """
725
+ A deep copy constructs a new compound object and then, recursively,
726
+ inserts copies into it of the objects found in the original.
727
+ This method is used when we call deepcopy(obj).
728
+
729
+ Args:
730
+ memo (dict, optional): A memory object to avoid copy twice. Defaults to None.
731
+ """
732
+ # We need to copy the __dict__
733
+ obj = deepcopy(self.__dict__, memo)
734
+
735
+ # We must not copy the config attribute because the copy could override the original in the class
736
+ obj.pop(CONFIG_ATTRIBUTE_NAME, None)
737
+
738
+ # We create a new obj
739
+ obj = type(self)(**obj)
740
+ return obj
741
+
742
+ def __delattr__(self, __name: str) -> None:
743
+ """
744
+ Method that removes __name from the object.
745
+
746
+ Args:
747
+ __name (str): The name of the attribute that will be removed.
748
+
749
+ Raises:
750
+ AttributeError: If is frozen.
751
+ """
752
+ self.__check_frozen__()
753
+ super().__delattr__(__name)
754
+
755
+ def __get_attributes__(self) -> dict:
756
+ """
757
+ Get all attributes from this class.
758
+ """
759
+ return getattr(self, MetaClass._attr_name) # pylint: disable=protected-access
760
+
761
+ def __get_clean_value__(self, attr: str, value: Any) -> Any:
762
+ """
763
+ Pass value to a clean function and checks if value match's the correct type.
764
+
765
+ Raises:
766
+ FieldValueError: If value and type don't match the correct type.
767
+ """
768
+ return __get_field_value__(self, attr, value)
769
+
770
+ def _init_error_handler(self, kwargs: dict, errors: dict[str, Exception]) -> None:
771
+ """
772
+ This method is called at the end of the init process if the silent param is True.
773
+
774
+ Args:
775
+ errors (dict): A dict {'before_init': None | Exception, 'init': None | Exception, 'after_init': None | Exception}
776
+ """
777
+
778
+ def __setattr__(self, __name: str, __value: Any) -> None:
779
+ """
780
+ Method changed from BaseClass for check de integrity of data.
781
+ This method is executed on setting attributes in the object.
782
+ Ex: obj.attr = 1
783
+
784
+ Raises:
785
+ AttributeError: If is frozen.
786
+ """
787
+ self.__check_frozen__()
788
+
789
+ if getattr(self, '_need_validation', True):
790
+ __value = self.__get_clean_value__(__name, __value)
791
+
792
+ super().__setattr__(__name, __value)
793
+
794
+ @classmethod
795
+ def __set_attribute__(cls, attr_name: str, attr_type: Any, attr_value: Any) -> None:
796
+ """
797
+ Method that updates the list of attributes/annotations for this class.
798
+ Normally this is used to update a class after it was created.
799
+
800
+ Args:
801
+ attr_name (str): The name for the new attribute.
802
+ attr_type (Any): The type for the new attribute.
803
+ attr_value (Any): The value for the new attribute.
804
+ """
805
+ attributes = getattr(cls, MetaClass._attr_name, {}) # pylint: disable=protected-access
806
+ annotations = getattr(cls, MetaClass._anno_name, {}) # pylint: disable=protected-access
807
+ try:
808
+ # For BaseFields
809
+ annotations[attr_name] = attr_type.attr_type
810
+ except AttributeError:
811
+ # Normal types
812
+ annotations[attr_name] = attr_type
813
+
814
+ attributes[attr_name] = attr_type
815
+ # After we set the attributes/annotations we set the value in the class
816
+ setattr(cls, attr_name, attr_value)
817
+
818
+ ## Public methods
819
+ def get_full_doted_class_path(self) -> str:
820
+ """
821
+ Return full doted class path to be used on import functions.
822
+
823
+ Example:
824
+ 'everysk.core.BaseObject'
825
+ """
826
+ return f'{self.__module__}.{self.__class__.__name__}'
827
+
828
+ def replace(self, **changes) -> Self:
829
+ """
830
+ Creates a new object of the same type as self, replacing fields with values from changes.
831
+
832
+ Args:
833
+ **changes (dict): All named params that are passed to this method.
834
+
835
+ Returns:
836
+ Self: A copy of this object with the new values.
837
+
838
+ Example:
839
+ >>> from everysk.core.object import BaseObject
840
+ >>> obj = BaseObject(attr=1)
841
+ >>> obj.attr
842
+ 1
843
+ >>> copy = obj.replace(attr=2)
844
+ >>> copy.attr
845
+ 2
846
+ """
847
+ obj = deepcopy(self.__dict__)
848
+
849
+ # We must not copy the config attribute because the copy could override the original in the class
850
+ obj.pop(CONFIG_ATTRIBUTE_NAME, None)
851
+
852
+ obj.update(changes)
853
+ return type(self)(**obj)
854
+
855
+ def validate_required_fields(self) -> None:
856
+ """
857
+ Try to validate all fields that are checked with required_lazy, because fields with
858
+ required are always validate on the init.
859
+ """
860
+ attributes = self.__get_attributes__()
861
+ for attr_name, field in attributes.items():
862
+ if getattr(field, 'required_lazy', False):
863
+ _required(attr_name=attr_name, value=getattr(self, attr_name, None))
864
+
865
+
866
+ ###############################################################################
867
+ # BaseObjectConfig Implementation
868
+ ###############################################################################
869
+ class BaseObjectConfig(_BaseObject):
870
+ exclude_keys: frozenset[str] = frozenset([])
871
+ key_mapping: dict = None
872
+
873
+ def __init__(self, **kwargs: dict) -> None:
874
+ """Use kwargs to set more attributes on the Config."""
875
+ super().__init__(**kwargs)
876
+ if self.key_mapping is None:
877
+ self.key_mapping = {}
878
+
879
+
880
+ ###############################################################################
881
+ # BaseObject Class Implementation
882
+ ###############################################################################
883
+ class BaseObject(_BaseObject):
884
+ class Config(BaseObjectConfig):
885
+ pass
886
+
887
+ ## Private attributes
888
+ _config: Config = None
889
+
890
+ def __getstate__(self) -> dict:
891
+ """
892
+ This method is used by Pickle module to get the correct serialized data.
893
+ https://docs.python.org/3.11/library/pickle.html#handling-stateful-objects
894
+ """
895
+ # This generates a dictionary with all attributes
896
+ # We need to get the keys that are set in the instance and in the parents
897
+ keys = self.__dict__.keys() | self.__get_attributes__().keys()
898
+
899
+ # Then config key need to be removed and the exclude_keys list too
900
+ config = getattr(self, CONFIG_ATTRIBUTE_NAME)
901
+ keys = keys - {CONFIG_ATTRIBUTE_NAME} - set(config.exclude_keys)
902
+
903
+ dct: dict = {key: getattr(self, key) for key in keys}
904
+
905
+ # We set the _need_validation attribute to False because we already validated all data
906
+ dct['_need_validation'] = False
907
+
908
+ return dct
909
+
910
+ def __setstate__(self, state: dict = None) -> None:
911
+ """
912
+ This method is used by Pickle module to set back the correct serialized data.
913
+ We need to iterate over every key and set the value to the object.
914
+
915
+ Args:
916
+ state (dict): The result from the __getstate__ method used by Pickle.
917
+ """
918
+ if state:
919
+ old_need_validation = self._need_validation
920
+ # Set if we need validate every attribute
921
+ self._need_validation = state.pop('_need_validation', True)
922
+ for key, value in state.items():
923
+ setattr(self, key, value)
924
+ # Set the original value to validate the attributes
925
+ self._need_validation = old_need_validation
926
+
927
+ def to_native(self, add_class_path: str | None = None, recursion: bool = False) -> Any:
928
+ """
929
+ Converts the object to the specified Python type.
930
+
931
+ Args:
932
+ add_class_path (str | None, optional): The class path to add when converting the object. Defaults to None.
933
+ recursion (bool, optional): Indicates whether to recursively convert nested objects. Defaults to False.
934
+
935
+ Returns:
936
+ object: The converted object.
937
+
938
+ """
939
+ return self.to_dict(add_class_path=add_class_path, recursion=recursion)
940
+
941
+ def to_dict(self, add_class_path: bool = False, recursion: bool = False) -> dict:
942
+ """
943
+ This method is used to convert the object to a dictionary.
944
+ If add_class_path is True, the full doted class path will be added to the dictionary.
945
+ If recursion is True, the method will call the to_dict method of the child objects.
946
+
947
+ Args:
948
+ add_class_path (bool, optional): Flag to add the class path in the result. Defaults to False.
949
+ recursion (bool, optional): Flag to transform the children too. Defaults to False.
950
+ """
951
+ dct: dict = {}
952
+ # We need to get the keys that are set in the instance
953
+ keys = self.__dict__.keys()
954
+ if add_class_path:
955
+ # If add_class_path is True we need to add all other keys that are in the class with the default value
956
+ keys = keys | self.__get_attributes__().keys()
957
+
958
+ def no_op(value: Any) -> Any:
959
+ """Function used to return the value in the getattr."""
960
+ return value
961
+
962
+ # Then config key need to be removed and the exclude_keys too
963
+ config = getattr(self, CONFIG_ATTRIBUTE_NAME)
964
+ keys = keys - {CONFIG_ATTRIBUTE_NAME} - set(config.exclude_keys)
965
+
966
+ for key in keys:
967
+ value = getattr(self, key)
968
+ func = getattr(self, f'_process_{key}', no_op)
969
+ key = config.key_mapping.get(key, key)
970
+ result = func(value)
971
+ if recursion and isinstance(result, BaseObject):
972
+ result = result.to_dict(add_class_path=add_class_path, recursion=recursion)
973
+
974
+ dct[key] = result
975
+
976
+ if add_class_path:
977
+ dct[CLASS_KEY] = self.get_full_doted_class_path()
978
+
979
+ return dct
980
+
981
+
982
+ ###############################################################################
983
+ # BaseDictConfig Implementation
984
+ ###############################################################################
985
+ class BaseDictConfig(BaseObjectConfig):
986
+ keys_blacklist: frozenset[str] = frozenset([])
987
+
988
+
989
+ ###############################################################################
990
+ # BaseDict Class Implementation
991
+ ###############################################################################
992
+ class BaseDict(BaseObject):
993
+ """
994
+ Extends BaseObject and also guarantees that BaseDict['key'] is equal to BaseDict.key
995
+
996
+ >>> from utils.object import BaseDict
997
+ >>> class MyDict(BaseDict):
998
+ ... var_int: int = None
999
+ ...
1000
+ >>> obj = MyDict(var_int='test')
1001
+ Traceback (most recent call last):
1002
+ File "<stdin>", line 1, in <module>
1003
+ File "/var/app/utils/object.py", line 151, in __init__
1004
+ raise DataTypeError(f'Key {attr_name} must be {attr_type}.')
1005
+ File "/var/app/utils/object.py", line 279, in __setattr__
1006
+ if key.startswith('_'):
1007
+ File "/var/app/utils/object.py", line 209, in __setattr__
1008
+ DataTypeError: If value and type don't match the correct type.
1009
+ File "/var/app/utils/object.py", line 192, in __get_clean_value__
1010
+ # Create base annotations
1011
+ File "/var/app/utils/object.py", line 56, in _validate
1012
+ elif isinstance(value, attr_type):
1013
+ utils.exceptions.DataTypeError: Key var_int must be <class 'int'>.
1014
+ >>> obj = MyDict(var_int=10)
1015
+ >>> obj['var_int'] == obj.var_int
1016
+ True
1017
+ """
1018
+
1019
+ class Config(BaseDictConfig):
1020
+ pass
1021
+
1022
+ ## Private attributes
1023
+ __slots__ = ('__data__',)
1024
+ _config: Config = None
1025
+
1026
+ ## Private Methods
1027
+ def __new__(cls, *args, **kwargs) -> Self:
1028
+ obj = super().__new__(cls, *args, **kwargs)
1029
+ # We need to create this here because __init__ could crash if we use self.attr
1030
+ # before call the super or if we use silent=True and work with the object later
1031
+ # Create the __data__ attribute if it does not exist
1032
+ if not hasattr(obj, '__data__'):
1033
+ obj.__data__ = {}
1034
+
1035
+ return obj
1036
+
1037
+ def __init__(self, **kwargs) -> None:
1038
+ # This is only for pylint stop complaining about not having __dict__
1039
+ if not hasattr(self, '__dict__'):
1040
+ self.__dict__ = {}
1041
+
1042
+ # Add all kwargs to the object as key/attributes
1043
+ super().__init__(**kwargs)
1044
+
1045
+ # We need to get all attributes from the parent classes too
1046
+ # to work like a dict and to have correct equality checks and
1047
+ # to represent the object as a dict
1048
+ attributes = self.__get_attributes__()
1049
+ for key in attributes.keys() - kwargs.keys():
1050
+ if self.is_valid_key(key=key):
1051
+ self[key] = self[key]
1052
+
1053
+ def __contains__(self, key: str) -> bool:
1054
+ """
1055
+ Check if key is in self or in a parent.
1056
+
1057
+ Args:
1058
+ key (str): The key name to search.
1059
+ """
1060
+ return key in self.__data__
1061
+
1062
+ def __delattr__(self, name: str, caller: str = None) -> None:
1063
+ """
1064
+ Removes an atribute from self.
1065
+
1066
+ Args:
1067
+ name (str): The attribute that will be removed.
1068
+ caller (str, optional): Used to avoid recursion when internally called. Defaults to None.
1069
+
1070
+ Raises:
1071
+ AttributeError: If the attribute is not found.
1072
+ """
1073
+ # Remove attribute
1074
+ super().__delattr__(name)
1075
+
1076
+ # Caller not None means this method was called from __delitem__
1077
+ # then we do not call the method again avoiding infinite loop
1078
+ if caller is None:
1079
+ try:
1080
+ # Some times the attribute will not exists
1081
+ self.__delitem__(name, caller='__delattr__')
1082
+ except KeyError:
1083
+ pass
1084
+
1085
+ def __delitem__(self, key: Any, caller: str = None) -> None:
1086
+ """
1087
+ Removes an key from self.
1088
+
1089
+ Args:
1090
+ key (Any): The key that will be removed.
1091
+ caller (str, optional): Used to avoid recursion when internally called. Defaults to None.
1092
+
1093
+ Raises:
1094
+ KeyError: If the key is not found.
1095
+ """
1096
+ # Remove key
1097
+ self.__data__.__delitem__(key)
1098
+
1099
+ # Caller not None means this method was called from __delattr__
1100
+ # then we do not call the method again avoiding infinite loop
1101
+ if caller is None:
1102
+ self.__delattr__(key, caller='__delitem__')
1103
+
1104
+ def __eq__(self, other: object) -> bool:
1105
+ """
1106
+ Check if two objects are the same.
1107
+
1108
+ Args:
1109
+ other (Any): The other object to compare.
1110
+ """
1111
+ return isinstance(other, BaseDict) and self.__data__ == other.__data__
1112
+
1113
+ def __getattr__(self, name: str) -> Any:
1114
+ """
1115
+ If the self does not have the attribute this method is called.
1116
+ We check if the __data__ does not have the attribute.
1117
+
1118
+ Args:
1119
+ name (str): The name of the desired attribute.
1120
+
1121
+ Raises:
1122
+ AttributeError: If name is not found.
1123
+ """
1124
+ if name != '__data__':
1125
+ try:
1126
+ return getattr(self.__data__, name)
1127
+ except AttributeError:
1128
+ pass
1129
+
1130
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'.")
1131
+
1132
+ def __getitem__(self, key: str) -> Any:
1133
+ """
1134
+ Get key from self or search on a parent.
1135
+
1136
+ Args:
1137
+ key (str): The key name to search.
1138
+
1139
+ Raises:
1140
+ KeyError: If the key does not exist.
1141
+ """
1142
+ if self.is_valid_key(key=key):
1143
+ try:
1144
+ return self.__data__.__getitem__(key)
1145
+ except KeyError:
1146
+ pass
1147
+
1148
+ try:
1149
+ return getattr(self, key)
1150
+ except AttributeError:
1151
+ pass
1152
+
1153
+ raise KeyError(key) # pylint: disable=raise-missing-from
1154
+
1155
+ def __ior__(self, other: Any) -> Any:
1156
+ """
1157
+ This method performs an in-place merge of two objects using the `|=` operator.
1158
+ If `other` is an instance of the same class, it merges the attributes of `other` into the `self` class.
1159
+ Otherwise, it tries to merge the `other` dictionary into the `self` class.
1160
+
1161
+ Args:
1162
+ other (Any): The other object to merge.
1163
+
1164
+ Returns:
1165
+ Any: The instance of the class with the merged attributes.
1166
+ """
1167
+ if isinstance(other, type(self)):
1168
+ self.__dict__ |= other.__dict__
1169
+
1170
+ else:
1171
+ self.__dict__ |= other
1172
+
1173
+ return self
1174
+
1175
+ def __iter__(self) -> Iterator:
1176
+ """
1177
+ Iterate over the object keys.
1178
+ This method is used when we call a for over the instance:
1179
+
1180
+ Example:
1181
+ >>> for key in BaseDict():
1182
+ """
1183
+ return iter(self.__data__)
1184
+
1185
+ def __len__(self) -> int:
1186
+ """
1187
+ Get the length of the dictionary.
1188
+
1189
+ Returns:
1190
+ int: The length of the dictionary.
1191
+ """
1192
+ return len(self.__data__)
1193
+
1194
+ def __ne__(self, other: object) -> bool:
1195
+ """
1196
+ Check if two objects are different.
1197
+
1198
+ Args:
1199
+ other (Any): The other object to compare.
1200
+ """
1201
+ return not self.__eq__(other)
1202
+
1203
+ def __or__(self, other: Any) -> Any:
1204
+ """
1205
+ This method is used to merge two objects using the `|` operator.
1206
+ It merges both objects using the `self.__dict__` method if `other` is an instance of the same class.
1207
+ Otherwise, if `other` is a dictionary, it merges `self.__dict__` with the `other` dictionary.
1208
+
1209
+ Args:
1210
+ other (Any): The other object to merge.
1211
+
1212
+ Returns:
1213
+ NotImplemented: If `other` is not an instance of the same class or an instance of a dictionary.
1214
+ """
1215
+ if isinstance(other, type(self)):
1216
+ return type(self)(**(self.__dict__ | other.__dict__))
1217
+
1218
+ if isinstance(other, dict):
1219
+ return type(self)(**(self.__dict__ | other))
1220
+
1221
+ # don't attempt to compare against unrelated types
1222
+ return NotImplemented
1223
+
1224
+ def __repr__(self) -> str:
1225
+ """
1226
+ This method returns the representation of the object in a string format.
1227
+
1228
+ Returns:
1229
+ str: The string representation of the object.
1230
+ """
1231
+ return self.__data__.__repr__()
1232
+
1233
+ def __ror__(self, other: Any) -> Any:
1234
+ """
1235
+ This method is used to merge two objects using the `|` operator, with the current object being on the right-hand side.
1236
+ It merges the current object with `other` using the `__dict__` method if `other` is an instance of the same class.
1237
+ If `other` is a dictionary, it merges the `other` dictionary with `self.__dict__`.
1238
+
1239
+ Args:
1240
+ other (Any): The other object to merge.
1241
+
1242
+ Returns:
1243
+ NotImplemented: if `other` is not an instance of the same class or an instance of a dictionary.
1244
+ """
1245
+ if isinstance(other, type(self)):
1246
+ return type(self)(**(other.__dict__ | self.__dict__))
1247
+
1248
+ if isinstance(other, dict):
1249
+ return type(self)(**(other | self.__dict__))
1250
+
1251
+ # don't attempt to compare against unrelated types
1252
+ return NotImplemented
1253
+
1254
+ def __setattr__(self, name: str, value: Any, caller: str = None) -> None:
1255
+ """
1256
+ Method changed from BaseClass for guarantee de integrity of data attributes.
1257
+ This method is executed on setting attributes in the object.
1258
+ Ex: obj.attr = 1
1259
+
1260
+ Raises:
1261
+ AttributeError: If is frozen.
1262
+ """
1263
+ super().__setattr__(name, value)
1264
+ # Because self.__get_clean_value__ can change the value we need to pick it from self
1265
+ new_value = getattr(self, name)
1266
+
1267
+ # When value is associated:
1268
+ # directly on the key Ex dict['key'] = value then caller will be __setitem__
1269
+ # directly on attribute Ex dict.key = value then caller will be None
1270
+ # Don't do that to private attributes
1271
+ if caller is None and self.is_valid_key(key=name):
1272
+ # For integrity guarantee writes the value to the dictionary key as well.
1273
+ self.__setitem__(name, new_value, caller='__setattr__')
1274
+
1275
+ def __setitem__(self, key: str, item: Any, caller: str = None) -> None:
1276
+ """
1277
+ Method changed from BaseClass for guarantee de integrity of data keys.
1278
+ This method is executed on setting items in the dictionary.
1279
+ Ex: d = dict(key=1) or d['key'] = 1
1280
+
1281
+ Raises:
1282
+ AttributeError: If is frozen.
1283
+ """
1284
+ if key.startswith('_'):
1285
+ raise KeyError("Keys can't start with '_'.")
1286
+ config = getattr(self, CONFIG_ATTRIBUTE_NAME)
1287
+ if key in config.keys_blacklist:
1288
+ raise KeyError(f'The key cannot be called "{key}".')
1289
+
1290
+ # When value is associated:
1291
+ # directly on the key Ex dict['key'] = value then caller will be None
1292
+ # directly on attribute Ex dict.key = value then caller will be __setattr__
1293
+ if caller is None:
1294
+ # For integrity guarantee writes the value to the attribute as well.
1295
+ self.__setattr__(key, item, caller='__setitem__')
1296
+
1297
+ # The setattr can "clean" the value them we need to catch it again
1298
+ item = getattr(self, key)
1299
+
1300
+ # If key is a property we do not set on dict
1301
+ if not isinstance(getattr(type(self), key, None), property):
1302
+ self.__data__.__setitem__(key, item)
1303
+
1304
+ def __setstate__(self, state: dict = None) -> None:
1305
+ """
1306
+ This method is used by Pickle module to set back the correct serialized data.
1307
+ We need to iterate over every key and set the value to the object.
1308
+
1309
+ Args:
1310
+ state (dict): The result from the __getstate__ method used by Pickle.
1311
+ """
1312
+ # NOTE: coverage not passing here since `self` will always have __data__ attribute
1313
+ # For some old Pickle objects the data attribute will not exists so we need to create it
1314
+ if not hasattr(self, '__data__'):
1315
+ self.__data__ = {} # pylint: disable=attribute-defined-outside-init
1316
+
1317
+ return super().__setstate__(state)
1318
+
1319
+ ## Public Methods
1320
+ def clear(self) -> None:
1321
+ """
1322
+ This method clears the dictionary.
1323
+
1324
+ Raises:
1325
+ AttributeError: If is frozen.
1326
+ """
1327
+ self.__check_frozen__()
1328
+ # Because we integrate key/attributes we need to remove booth
1329
+ # The original clear only remove keys
1330
+ # We need to convert keys to a list, because the original is a iterator
1331
+ for key in list(self.keys()):
1332
+ del self[key]
1333
+
1334
+ def copy(self) -> dict:
1335
+ """
1336
+ Generate a copy for this object, we need to use deepcopy because
1337
+ this object could have some keys/attributes and they need to be on the copy.
1338
+ If the object is Frozen, the copy will not be.
1339
+ """
1340
+ return deepcopy(self)
1341
+
1342
+ def fromkeys(self, keys: list, default: Any = None) -> dict:
1343
+ """
1344
+ Create a new object with the keys, if key does not exists add one with default value.
1345
+ If the object is Frozen, the copy will not be.
1346
+
1347
+ Args:
1348
+ keys (list): The list with the keys for the new object.
1349
+ default (Any, optional): The default value if the key does not exists. Defaults to None.
1350
+ """
1351
+ # Create a new key list with all keys and use a set to avoid duplicates
1352
+ keys_aux = set(keys)
1353
+ keys_aux.update(self.keys())
1354
+
1355
+ dct = self.copy()
1356
+ for key in keys_aux:
1357
+ if key in keys:
1358
+ dct[key] = dct.get(key, default)
1359
+ else:
1360
+ del dct[key]
1361
+
1362
+ return dct
1363
+
1364
+ def get(self, key: str, default: Any = None) -> Any:
1365
+ """
1366
+ Get the value for the key, if key does not exists return default.
1367
+
1368
+ Args:
1369
+ key (str): The key to get the value.
1370
+ default (Any, optional): The default value if the key does not exists. Defaults to None.
1371
+
1372
+ Returns:
1373
+ Any: The value for the key or the default value.
1374
+ """
1375
+ return self.__data__.get(key, default)
1376
+
1377
+ def is_valid_key(self, key: str) -> bool:
1378
+ """
1379
+ This method checks if the key is valid.
1380
+ Valid keys are the ones that not starts with '_' and
1381
+ are not in the config.keys_blacklist.
1382
+
1383
+ Args:
1384
+ key (str): The key that needs to be checked.
1385
+
1386
+ Returns:
1387
+ bool: True if the key is valid, False otherwise.
1388
+ """
1389
+ config = getattr(self, CONFIG_ATTRIBUTE_NAME)
1390
+ return not key.startswith('_') and key not in config.keys_blacklist
1391
+
1392
+ def items(self) -> dict_items:
1393
+ """
1394
+ Return a new view of the dictionary's items ((key, value) pairs).
1395
+ """
1396
+ return self.__data__.items()
1397
+
1398
+ def keys(self) -> dict_keys:
1399
+ """
1400
+ Return a new view of the dictionary's keys.
1401
+ """
1402
+ return self.__data__.keys()
1403
+
1404
+ def pop(self, *args) -> Any:
1405
+ """
1406
+ Remove specified key and return the corresponding value.
1407
+ If the key is not found return the default otherwise raise a KeyError.
1408
+
1409
+ Args:
1410
+ key (str): The key that will be removed.
1411
+ default (Any, optional): The default value if key is not found.
1412
+
1413
+ Raises:
1414
+ AttributeError: If is frozen.
1415
+ KeyError: If the default is not passed and key is not found.
1416
+ """
1417
+ self.__check_frozen__()
1418
+ ret = self.__data__.pop(*args)
1419
+
1420
+ try:
1421
+ # Some times the attribute will not exists
1422
+ self.__delattr__(args[0]) # pylint: disable=unnecessary-dunder-call
1423
+ except AttributeError:
1424
+ pass
1425
+
1426
+ return ret
1427
+
1428
+ def popitem(self) -> tuple:
1429
+ """
1430
+ Remove and return a (key, value) pair as a 2-tuple.
1431
+ Pairs are returned in LIFO (last-in, first-out) order.
1432
+
1433
+ Raises:
1434
+ AttributeError: If is frozen.
1435
+ KeyError: If the dict is empty.
1436
+ """
1437
+ self.__check_frozen__()
1438
+ return self.__data__.popitem()
1439
+
1440
+ def update(self, *args, **kwargs) -> None:
1441
+ """
1442
+ Update self with the key/value pairs that are passed.
1443
+ We check every value to see if it is valid.
1444
+
1445
+ Example:
1446
+ >>> dct = BaseDict()
1447
+ >>> dct.update({'a': 1})
1448
+ >>> dct
1449
+ {'a': 1}
1450
+ >>> dct.update([('b', 2)])
1451
+ >>> dct
1452
+ {'a': 1, 'b': 2}
1453
+
1454
+ Raises:
1455
+ AttributeError: If is frozen.
1456
+ """
1457
+ self.__check_frozen__()
1458
+ dct = dict(*args, **kwargs)
1459
+ for key, value in dct.items():
1460
+ if self.is_valid_key(key=key):
1461
+ self[key] = value
1462
+ else:
1463
+ raise KeyError(f'The key cannot be called "{key}".')
1464
+
1465
+ def values(self) -> dict_values:
1466
+ """
1467
+ Return a new view of the dictionary's values.
1468
+ """
1469
+ return self.__data__.values()