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.
- everysk/__init__.py +30 -0
- everysk/_version.py +683 -0
- everysk/api/__init__.py +61 -0
- everysk/api/api_requestor.py +167 -0
- everysk/api/api_resources/__init__.py +23 -0
- everysk/api/api_resources/api_resource.py +371 -0
- everysk/api/api_resources/calculation.py +779 -0
- everysk/api/api_resources/custom_index.py +42 -0
- everysk/api/api_resources/datastore.py +81 -0
- everysk/api/api_resources/file.py +42 -0
- everysk/api/api_resources/market_data.py +223 -0
- everysk/api/api_resources/parser.py +66 -0
- everysk/api/api_resources/portfolio.py +43 -0
- everysk/api/api_resources/private_security.py +42 -0
- everysk/api/api_resources/report.py +65 -0
- everysk/api/api_resources/report_template.py +39 -0
- everysk/api/api_resources/tests.py +115 -0
- everysk/api/api_resources/worker_execution.py +64 -0
- everysk/api/api_resources/workflow.py +65 -0
- everysk/api/api_resources/workflow_execution.py +93 -0
- everysk/api/api_resources/workspace.py +42 -0
- everysk/api/http_client.py +63 -0
- everysk/api/tests.py +32 -0
- everysk/api/utils.py +262 -0
- everysk/config.py +451 -0
- everysk/core/_tests/serialize/test_json.py +336 -0
- everysk/core/_tests/serialize/test_orjson.py +295 -0
- everysk/core/_tests/serialize/test_pickle.py +48 -0
- everysk/core/cloud_function/main.py +78 -0
- everysk/core/cloud_function/tests.py +86 -0
- everysk/core/compress.py +245 -0
- everysk/core/datetime/__init__.py +12 -0
- everysk/core/datetime/calendar.py +144 -0
- everysk/core/datetime/date.py +424 -0
- everysk/core/datetime/date_expression.py +299 -0
- everysk/core/datetime/date_mixin.py +1475 -0
- everysk/core/datetime/date_settings.py +30 -0
- everysk/core/datetime/datetime.py +713 -0
- everysk/core/exceptions.py +435 -0
- everysk/core/fields.py +1176 -0
- everysk/core/firestore.py +555 -0
- everysk/core/fixtures/_settings.py +29 -0
- everysk/core/fixtures/other/_settings.py +18 -0
- everysk/core/fixtures/user_agents.json +88 -0
- everysk/core/http.py +691 -0
- everysk/core/lists.py +92 -0
- everysk/core/log.py +709 -0
- everysk/core/number.py +37 -0
- everysk/core/object.py +1469 -0
- everysk/core/redis.py +1021 -0
- everysk/core/retry.py +51 -0
- everysk/core/serialize.py +674 -0
- everysk/core/sftp.py +414 -0
- everysk/core/signing.py +53 -0
- everysk/core/slack.py +127 -0
- everysk/core/string.py +199 -0
- everysk/core/tests.py +240 -0
- everysk/core/threads.py +199 -0
- everysk/core/undefined.py +70 -0
- everysk/core/unittests.py +73 -0
- everysk/core/workers.py +241 -0
- everysk/sdk/__init__.py +23 -0
- everysk/sdk/base.py +98 -0
- everysk/sdk/brutils/cnpj.py +391 -0
- everysk/sdk/brutils/cnpj_pd.py +129 -0
- everysk/sdk/engines/__init__.py +26 -0
- everysk/sdk/engines/cache.py +185 -0
- everysk/sdk/engines/compliance.py +37 -0
- everysk/sdk/engines/cryptography.py +69 -0
- everysk/sdk/engines/expression.cp312-win_amd64.pyd +0 -0
- everysk/sdk/engines/expression.pyi +55 -0
- everysk/sdk/engines/helpers.cp312-win_amd64.pyd +0 -0
- everysk/sdk/engines/helpers.pyi +26 -0
- everysk/sdk/engines/lock.py +120 -0
- everysk/sdk/engines/market_data.py +244 -0
- everysk/sdk/engines/settings.py +19 -0
- everysk/sdk/entities/__init__.py +23 -0
- everysk/sdk/entities/base.py +784 -0
- everysk/sdk/entities/base_list.py +131 -0
- everysk/sdk/entities/custom_index/base.py +209 -0
- everysk/sdk/entities/custom_index/settings.py +29 -0
- everysk/sdk/entities/datastore/base.py +160 -0
- everysk/sdk/entities/datastore/settings.py +17 -0
- everysk/sdk/entities/fields.py +375 -0
- everysk/sdk/entities/file/base.py +215 -0
- everysk/sdk/entities/file/settings.py +63 -0
- everysk/sdk/entities/portfolio/base.py +248 -0
- everysk/sdk/entities/portfolio/securities.py +241 -0
- everysk/sdk/entities/portfolio/security.py +580 -0
- everysk/sdk/entities/portfolio/settings.py +97 -0
- everysk/sdk/entities/private_security/base.py +226 -0
- everysk/sdk/entities/private_security/settings.py +17 -0
- everysk/sdk/entities/query.py +603 -0
- everysk/sdk/entities/report/base.py +214 -0
- everysk/sdk/entities/report/settings.py +23 -0
- everysk/sdk/entities/script.py +310 -0
- everysk/sdk/entities/secrets/base.py +128 -0
- everysk/sdk/entities/secrets/script.py +119 -0
- everysk/sdk/entities/secrets/settings.py +17 -0
- everysk/sdk/entities/settings.py +48 -0
- everysk/sdk/entities/tags.py +174 -0
- everysk/sdk/entities/worker_execution/base.py +307 -0
- everysk/sdk/entities/worker_execution/settings.py +63 -0
- everysk/sdk/entities/workflow_execution/base.py +113 -0
- everysk/sdk/entities/workflow_execution/settings.py +32 -0
- everysk/sdk/entities/workspace/base.py +99 -0
- everysk/sdk/entities/workspace/settings.py +27 -0
- everysk/sdk/settings.py +67 -0
- everysk/sdk/tests.py +105 -0
- everysk/sdk/worker_base.py +47 -0
- everysk/server/__init__.py +9 -0
- everysk/server/applications.py +63 -0
- everysk/server/endpoints.py +516 -0
- everysk/server/example_api.py +69 -0
- everysk/server/middlewares.py +80 -0
- everysk/server/requests.py +62 -0
- everysk/server/responses.py +119 -0
- everysk/server/routing.py +64 -0
- everysk/server/settings.py +36 -0
- everysk/server/tests.py +36 -0
- everysk/settings.py +98 -0
- everysk/sql/__init__.py +9 -0
- everysk/sql/connection.py +232 -0
- everysk/sql/model.py +376 -0
- everysk/sql/query.py +417 -0
- everysk/sql/row_factory.py +63 -0
- everysk/sql/settings.py +49 -0
- everysk/sql/utils.py +129 -0
- everysk/tests.py +23 -0
- everysk/utils.py +81 -0
- everysk/version.py +15 -0
- everysk_lib-1.10.2.dist-info/.gitignore +5 -0
- everysk_lib-1.10.2.dist-info/METADATA +326 -0
- everysk_lib-1.10.2.dist-info/RECORD +137 -0
- everysk_lib-1.10.2.dist-info/WHEEL +5 -0
- everysk_lib-1.10.2.dist-info/licenses/LICENSE.txt +9 -0
- 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()
|