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
@@ -0,0 +1,555 @@
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 typing import Any
11
+ from uuid import uuid4
12
+
13
+ from google.cloud import firestore
14
+ from google.cloud.firestore import CollectionReference, Query
15
+ from google.cloud.firestore_v1.base_client import DEFAULT_DATABASE as _DEFAULT_DATABASE
16
+
17
+ from everysk.config import settings
18
+ from everysk.core.compress import compress, decompress
19
+ from everysk.core.datetime import Date, DateTime
20
+ from everysk.core.fields import DateTimeField, ListField, StrField
21
+ from everysk.core.redis import RedisCacheCompressed, RedisLock
22
+ from everysk.core.object import BaseDict, BaseObject, BaseDictConfig
23
+
24
+
25
+ ###############################################################################
26
+ # FirestoreClient Class Implementation
27
+ ###############################################################################
28
+ class FirestoreClient(BaseObject):
29
+ """
30
+ Client that creates a connection with the Firestore database.
31
+
32
+ If the project name and the database name are not passed as params we use
33
+ the ones that are on the settings module.
34
+
35
+ Example:
36
+ >>> from everysk.core.firestore import FirestoreClient
37
+ >>> FirestoreClient(project_name='teste', database_name='default')
38
+
39
+ """
40
+ ## Private attributes
41
+ # Here we really need the global behavior
42
+ _connections: dict = {}
43
+
44
+ ## Public attributes
45
+ database_name = StrField()
46
+ project_name = StrField()
47
+
48
+ def __init__(self, **kwargs) -> None:
49
+ """
50
+ Initializes a FirestoreClient instance with the specified project name and database name
51
+
52
+ Args:
53
+ ** kwargs: Additional keyword arguments.
54
+ """
55
+
56
+ super().__init__(**kwargs)
57
+ if self.database_name is None:
58
+ self.database_name = _DEFAULT_DATABASE
59
+
60
+ if self.project_name is None:
61
+ self.project_name = settings.EVERYSK_GOOGLE_CLOUD_PROJECT
62
+
63
+ @property
64
+ def connection(self):
65
+ """
66
+ This property returns the correct connection to access Firestore.
67
+ If the connection is already created we just return it otherwise we need to create.
68
+ We use lock to avoid concurrency on connection creation.
69
+ """
70
+ key = f'{self.project_name}-{self.database_name}'
71
+ try:
72
+ # First we ty to get the connection
73
+ return FirestoreClient._connections[key]
74
+ except KeyError:
75
+ # If the connection does not exist we create it
76
+ # The lock is to avoid creating multiple connections, because this is expensive
77
+ lock = RedisLock(name=f'everysk-lib-firestore-lock-connection-{key}', timeout=600)
78
+ lock.acquire(blocking=True)
79
+ # At this moment we have 2 facts:
80
+ # 1 - We are the first to get the lock or;
81
+ # 2 - Some other process released the lock and we get it;
82
+ # On the first case we need to get the real connection.
83
+ # On the second case we already have the connection created.
84
+ # so we check just to be fast.
85
+ try:
86
+ if key not in FirestoreClient._connections:
87
+ FirestoreClient._connections[key] = firestore.Client(project=self.project_name, database=self.database_name)
88
+ finally:
89
+ # We need to always release the lock
90
+ lock.release()
91
+
92
+ return FirestoreClient._connections[key]
93
+
94
+ def get_collection(self, collection_name: str) -> CollectionReference:
95
+ """
96
+ Returns the CollectionReference object that refers to the collection_name inside Firestore.
97
+
98
+ Args:
99
+ collection_name (str): The name of the collection.
100
+
101
+ Returns:
102
+ CollectionReference: TheCollectionReference object representing the specified collection
103
+ """
104
+ return self.connection.collection(collection_name)
105
+
106
+
107
+ ###############################################################################
108
+ # BaseDocumentConfig Class Implementation
109
+ ###############################################################################
110
+ class BaseDocumentConfig(BaseDictConfig):
111
+ """
112
+ Base class that has all config values used inside the Document class.
113
+ """
114
+ ## Private attributes
115
+ _client: FirestoreClient = None
116
+
117
+ ## Public attributes
118
+ collection_name = StrField()
119
+ database_name = StrField()
120
+ excluded_keys = ListField()
121
+ project_name = StrField()
122
+
123
+ @property
124
+ def client(self) -> FirestoreClient:
125
+ """
126
+ We use this property to create the connection if it does not exists.
127
+
128
+ If the client instance does not exist, it will be created using the project_name and the database_name attributes.
129
+
130
+ Returns:
131
+ FirestoreClient: The Firestore client instance.
132
+ """
133
+ if self._client is None:
134
+ self._client = FirestoreClient(project_name=self.project_name, database_name=self.database_name)
135
+
136
+ return self._client
137
+
138
+ @property
139
+ def collection(self) -> CollectionReference:
140
+ """
141
+ Alias to the CollectionReference on Firestore
142
+
143
+ This property retrieves the CollectionReference associated with the specified
144
+ collection name from the Firestore client.
145
+
146
+ Raises:
147
+ AttributeError: If the collection_name attribute is empty
148
+
149
+ Returns:
150
+ CollectionReference: The CollectionReference object representing the specified collection
151
+ """
152
+ if not self.collection_name:
153
+ raise AttributeError('The collection_name is empty.')
154
+
155
+ return self.client.get_collection(self.collection_name)
156
+
157
+ def __init__(self, **kwargs) -> None:
158
+ super().__init__(**kwargs)
159
+
160
+ # Fix to keep the excluded_keys attribute always a list
161
+ if self.excluded_keys is None:
162
+ self.excluded_keys = []
163
+
164
+
165
+ ###############################################################################
166
+ # Document Class Implementation
167
+ ###############################################################################
168
+ class Document(BaseDict):
169
+ """
170
+ Class that represents a Document in Firestore database.
171
+
172
+ All documents that use this class will have these fields:
173
+
174
+ - created_at: A DateTime field that is filled when we create the object.
175
+ - firestore_id: A string field that has the Document ID from Firestore.
176
+ - updated_at: A DateTime field that is filled when we save the object.
177
+
178
+ Example:
179
+ >>> from everysk.core.firestore import Document
180
+ >>> doc = Document(firestore_id='example_id', created_at='2024-03-18T12:00:00', updated_at='2024-03-18T12:30:00')
181
+ >>> print(doc)
182
+ {
183
+ 'firestore_id': 'example_id',
184
+ 'created_at': DateTime(2024, 3, 18, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='UTC')),
185
+ 'updated_at': DateTime(2024, 3, 18, 12, 30, tzinfo=zoneinfo.ZoneInfo(key='UTC'))
186
+ }
187
+ """
188
+ # This need to be configured on child classes
189
+ class Config(BaseDocumentConfig):
190
+ pass
191
+
192
+ ## Private attributes
193
+ _config: BaseDocumentConfig = None # We put this here to use the Autocomplete correctly
194
+
195
+ ## Public attributes
196
+ created_at = DateTimeField()
197
+ firestore_id = StrField()
198
+ updated_at = DateTimeField()
199
+
200
+ ## Private methods
201
+ def __init__(self, **kwargs) -> None:
202
+ for key, value in kwargs.items():
203
+ kwargs[key] = self._parser_in(value)
204
+
205
+ super().__init__(**kwargs)
206
+ if self.created_at is None:
207
+ self.created_at = DateTime.now()
208
+
209
+ def _parser_in(self, obj: Any) -> Any:
210
+ """
211
+ Parse all data to convert in python format to be used on the instance.
212
+
213
+ Args:
214
+ obj (Any): The input data to be parsed
215
+
216
+ Notes:
217
+ - If the input is a dictionary, it recursively parses each key-value pair.
218
+ - If the input is a list, it recursively parses each item.
219
+ - If the input is a string, it attempts to parse as a date or a datetime obj
220
+ - If the input is bytes, it attempts to decompress it using pickle
221
+
222
+ Returns:
223
+ Any: The parsed data in Python format.
224
+ """
225
+ ret = obj
226
+ if isinstance(obj, dict):
227
+ ret = {}
228
+ for key, value in obj.items():
229
+ ret[key] = self._parser_in(value)
230
+
231
+ elif isinstance(obj, BaseDict):
232
+ ret = BaseDict()
233
+ for key, value in obj.items():
234
+ ret[key] = self._parser_in(value)
235
+
236
+ elif isinstance(obj, list):
237
+ ret = []
238
+ for item in obj:
239
+ ret.append(self._parser_in(item))
240
+
241
+ elif isinstance(obj, str):
242
+ # To increase the performance to no try to convert all strings
243
+ # and to avoid the problem when the string has 8 digits '20240101'
244
+ # we only try to convert if the string has 2 "-"
245
+ if obj.count('-') == 2:
246
+ try:
247
+ # 2022-01-01
248
+ ret = Date.fromisoformat(obj)
249
+ except (TypeError, ValueError):
250
+ try:
251
+ # 2022-01-01T10:00:00+00:00
252
+ ret = DateTime.fromisoformat(obj)
253
+ except (TypeError, ValueError):
254
+ pass
255
+
256
+ elif isinstance(obj, bytes):
257
+ try:
258
+ ret = decompress(obj, serialize='pickle')
259
+ except Exception: # pylint: disable=broad-except
260
+ pass
261
+
262
+ return ret
263
+
264
+ def _parser_out(self, obj: Any) -> Any:
265
+ """
266
+ Parse all data to convert in Firestore format to be used on the save to Firestore.
267
+
268
+ Args:
269
+ obj (Any): The data to be parsed.
270
+
271
+ Notes:
272
+ - If the input is a dictionary, it recursively parses each key-value pair.
273
+ - If the input is a list, it recursively parses each item.
274
+ - If the input has an 'isoformat' method (e.g., datetime objects), it converts it to ISO format.
275
+ - All other non-primitive types are serialized using pickle and compressed before saving to Firestore.
276
+
277
+ Returns:
278
+ Any: The parsed data in Firestore-compatible format.
279
+ """
280
+ ret = obj
281
+ if obj is not None:
282
+ # We need to convert BaseDict to normal dict when saving to Firestore
283
+ if isinstance(obj, (dict, BaseDict)):
284
+ ret = {}
285
+ for key, value in obj.items():
286
+ ret[key] = self._parser_out(value)
287
+
288
+ elif isinstance(obj, list):
289
+ ret = []
290
+ for item in obj:
291
+ ret.append(self._parser_out(item))
292
+
293
+ elif hasattr(obj, 'isoformat'):
294
+ ret = obj.isoformat()
295
+
296
+ # All other attributes will be treated as obj and sent to Firestore as byte.
297
+ elif not isinstance(obj, (bytes, bool, float, int, str)):
298
+ ret = compress(obj, serialize='pickle')
299
+
300
+ return ret
301
+
302
+ ## Public methods
303
+ @classmethod
304
+ def loads(cls, field: str, condition: str, value: Any) -> list:
305
+ """
306
+ Load a list of instances of this class populated with all firestore data.
307
+ The condition acceptable values are '<', '<=', '==', '>=', '>' and 'in'.
308
+
309
+ Args:
310
+ field (str): The field on firestore document that will be used to filter.
311
+ condition (str): The condition to filter. Example: ==, >=, in.....
312
+ value (Any): The value that must be check.
313
+ """
314
+ return cls.loads_paginated(query=cls._config.collection.where(field, condition, value), order_by=field)
315
+
316
+ @classmethod
317
+ def loads_paginated(cls, query: Query = None, fields: list = None, order_by: str = 'firestore_id', limit: int = 500) -> list:
318
+ """
319
+ This will load all documents from a collection using the "limit" number to
320
+ retrieve batches of data, this avoid timeouts from Google API.
321
+ If fields is None, then all fields will be returned.
322
+ The order_by param must be on the fields list.
323
+ If the order_by field does not exist in the document, then the doc will not return.
324
+
325
+ Args:
326
+ query (Query, optional): A pre filtered query. Defaults to self.collection.
327
+ fields (list, optional): A list of strings. Defaults to None.
328
+ order_by (str, optional): The name of the field used to sort. Defaults to 'firestore_id'.
329
+ limit (int, optional): The limit of documents that will be retrieved at time. Defaults to 500.
330
+
331
+ Raises:
332
+ ValueError: When the order_by is not in the fields.
333
+ """
334
+ if not query:
335
+ query = cls._config.collection
336
+
337
+ if fields:
338
+ if order_by in fields:
339
+ # We need to update fields to get the defaults to avoid mistakes
340
+ fields = set(fields)
341
+ fields.update(['firestore_id', 'created_at', 'updated_at'])
342
+ # We sort this to keep always the same order list
343
+ fields = sorted(list(fields))
344
+ query = query.select(field_paths=fields)
345
+ else:
346
+ raise ValueError(f'The order_by ({order_by}) must be in fields({fields}).')
347
+
348
+ query = query.order_by(order_by).limit(limit)
349
+ # We load the first batch of data
350
+ docs = query.get()
351
+ doc_aux = docs
352
+ # If the size of the first batch is equal to the limit that means we have more data to fetch
353
+ while len(doc_aux) == limit:
354
+ last = doc_aux[-1]
355
+ doc_aux = query.start_after(last).get()
356
+ docs.extend(doc_aux)
357
+
358
+ return [cls(**doc.to_dict()) for doc in docs]
359
+
360
+ # Instance methods
361
+ def get_firestore_id(self) -> str:
362
+ """
363
+ Uses the property firestore_id or generate one from UUID4.
364
+ If the document already has a 'firestore_id' attribute set it simply returns the value.
365
+ Otherwise, it generates a new ID using UUID4.
366
+
367
+ Returns:
368
+ str: The Firestore ID
369
+
370
+ Example:
371
+ >>> from everysk.core.firestore import Document
372
+ >>> doc = Document()
373
+ >>> firestore_id = doc.get_firestore_id()
374
+ >>> print(firestore_id)
375
+ >>> fb440ff261da42b48ff2332952bf240e
376
+ """
377
+ firestore_id = getattr(self, 'firestore_id', None)
378
+ if not firestore_id:
379
+ firestore_id = uuid4().hex
380
+
381
+ return firestore_id
382
+
383
+ def load(self) -> None:
384
+ """
385
+ Load all data from Firestore for self.firestore_id.
386
+ This method retrieves data from Firestore for the Firestore ID associated with the document.
387
+
388
+ Example:
389
+ >>> from everysk.core.firestore import Document
390
+ >>> doc = Document(firestore_id='example_id')
391
+ >>> doc.load()
392
+ """
393
+ doc = self._config.collection.document(self.get_firestore_id()).get()
394
+ if doc.exists:
395
+ # https://everysk.atlassian.net/browse/COD-1818
396
+ # To convert all fields to the correct format we need to pass to _parser_in
397
+ result = self._parser_in(doc.to_dict())
398
+ self.update(result)
399
+
400
+ def save(self, merge: bool = True, timeout: float = 60.0) -> dict:
401
+ """
402
+ Save or Update Firestore document, auto update 'updated_at' attribute before save.
403
+
404
+ Args:
405
+ merge (bool, optional): Determines the behavior of the save operation. If True, the method updates the
406
+ existing document, merging the new data with any existing data. If False, the method
407
+ replaces the existing document entirely with the new data.
408
+
409
+ Returns:
410
+ dict: A dictionary containing information about the save operation
411
+
412
+ Example:
413
+ >>> doc = FirestoreDocument(...)
414
+ >>> doc.save(merge=True) # Updates the document, preserving unspecified fields
415
+ >>> doc.save(merge=False) # Completely replaces the document with new data
416
+ """
417
+ self.firestore_id = self.get_firestore_id()
418
+ self.updated_at = DateTime.now()
419
+ return self._config.collection.document(self.firestore_id).set(
420
+ document_data=self.to_dict(),
421
+ merge=merge,
422
+ timeout=timeout
423
+ )
424
+
425
+ def to_dict(self, add_class_path: bool = False, recursion: bool = False) -> dict:
426
+ """
427
+ Convert the document to a dictionary to save inside Firestore.
428
+ We use the parser_out to convert all data to the correct format.
429
+ The add_class_path is inherited from the BaseDict class and is not used here.
430
+
431
+ Args:
432
+ add_class_path (bool, optional): Add the class path key to the result. Defaults to False.
433
+ recursion (bool, optional): If we need to convert the internal keys. Defaults to False.
434
+ """
435
+ ret = {}
436
+ for key, value in self.items():
437
+ # Discard some keys if needed
438
+ if key not in self._config.excluded_keys:
439
+ # Parse out always do it recursive to not store wrong things in Firestore
440
+ ret[key] = self._parser_out(value)
441
+
442
+ return ret
443
+
444
+
445
+ ###############################################################################
446
+ # BaseDocumentCachedConfig Class Implementation
447
+ ###############################################################################
448
+ class BaseDocumentCachedConfig(BaseDocumentConfig):
449
+ """
450
+ Base class to store the config for DocumentCached instances.
451
+ """
452
+ ## Private attributes
453
+ _cache: RedisCacheCompressed = None
454
+
455
+ ## Public attributes
456
+ key_prefix = StrField(default='firestore-document-redis-cached', readonly=True)
457
+
458
+ @property
459
+ def cache(self) -> RedisCacheCompressed:
460
+ """
461
+ Used to access the cache instance
462
+
463
+ If we don't have a connection we create one.
464
+
465
+ Returns:
466
+ RedisCacheCompressed: The Redis cache instance.
467
+ """
468
+ if self._cache is None:
469
+ self._cache = RedisCacheCompressed()
470
+
471
+ return self._cache
472
+
473
+
474
+ ###############################################################################
475
+ # DocumentCached Class Implementation
476
+ ###############################################################################
477
+ class DocumentCached(Document):
478
+ """
479
+ Document that stores data on Redis and Firestore and read data from Redis,
480
+ remember to activate the cloud function that keep the cache synchronized.
481
+ """
482
+ # This need to be configured on child classes
483
+ class Config(BaseDocumentCachedConfig):
484
+ pass
485
+
486
+ ## Private attributes
487
+ # We put this here to use the Autocomplete correctly
488
+ _config: BaseDocumentCachedConfig = None
489
+
490
+ ## Public attributes
491
+ # This is for only get one document and can't be readonly=True
492
+ # because firestore_id is part of the saved document
493
+ firestore_id = StrField(required=True)
494
+
495
+ # This key will be used to store the Firestore result in Redis and we need to store it
496
+ # along with the document so that the trigger that syncs Firestore and Redis can use it.
497
+ redis_key = StrField()
498
+
499
+ def __init__(self, **kwargs) -> None:
500
+ """
501
+ Initializes a DocumentCached instance.
502
+
503
+ Args:
504
+ **kwargs: Additional keyword arguments
505
+ """
506
+ super().__init__(**kwargs)
507
+ self.load()
508
+ # This keeps the redis_key hash always updated
509
+ self.redis_key = self._config.cache.get_hash_key(self.get_cache_key())
510
+
511
+ def clear_cache_key(self) -> None:
512
+ """
513
+ Delete the content from cache.
514
+ This method removes the cached content from the Redis cache using the cache key generated by the `get_cache_key` method
515
+ """
516
+ self._config.cache.delete(self.get_cache_key())
517
+
518
+ def get_cache_key(self) -> str:
519
+ """
520
+ Returns the key that will be used to get/set the cache
521
+
522
+ Returns:
523
+ str: The cache key constructed using the document's collection name and Firestore ID.
524
+ """
525
+ return f'{self._config.key_prefix}-{self._config.collection_name}-{self.get_firestore_id()}'
526
+
527
+ ## Override methods to work with cache
528
+ def load(self) -> None:
529
+ """
530
+ Load all data from Redis or Firestore
531
+
532
+ This method attempts to retrieve the document data from the Redis cache using
533
+ the cache key generated by the `get_cache_key` method. If the data is found in
534
+ the cache, it updates the document with the cached data. Otherwise, it loads
535
+ the data from Firestore and stores it in the cache.
536
+ """
537
+ key = self.get_cache_key()
538
+ result = self._config.cache.get(key)
539
+ if result is not None:
540
+ self.update(result)
541
+ else:
542
+ super().load()
543
+ self._config.cache.set(key, self.to_dict())
544
+
545
+ def save(self, merge: bool = True, timeout: float = 60) -> dict:
546
+ """
547
+ Save/Update Firestore document, auto update 'updated_at' attribute before save.
548
+ We clear the cache to be able to set the new value.
549
+
550
+ Args:
551
+ merge (bool, optional): If True, apply merging instead of overwriting the state of the document. Defaults to True
552
+ timeout (int, optional): The timeout for this request. Defaults to 60.0
553
+ """
554
+ self.clear_cache_key()
555
+ return super().save(merge, timeout)
@@ -0,0 +1,29 @@
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
+ import re
11
+ from everysk.core.fields import BoolField, StrField
12
+
13
+
14
+ _EVERYSK_PRIVATE = StrField(default='private-attribute')
15
+ EVERYSK_TEST_NAME = StrField(default='test-case', readonly=True)
16
+ EVERYSK_TEST_FAKE = True
17
+ EVERYSK_TEST_INT: int = 1
18
+ EVERYSK_TEST_FIELD_INHERIT = StrField(default='{EVERYSK_TEST_NAME} as {EVERYSK_TEST_FAKE}')
19
+ EVERYSK_TEST_VAR_INHERIT = '{EVERYSK_TEST_FAKE} as {EVERYSK_TEST_NAME}'
20
+ EVERYSK_TEST_BOOL_VAR = BoolField(default=False)
21
+
22
+ # Settings with default values
23
+ EVERYSK_TEST_STR_DEFAULT_NONE: str = None
24
+ EVERYSK_TEST_STR_DEFAULT_UNDEFINED: str = Undefined
25
+
26
+ # https://everysk.atlassian.net/browse/COD-4197
27
+ EVERYSK_TEST_RE_PATTERN: re.Pattern = re.compile(r'[azAZ]')
28
+ EVERYSK_TEST_RE_PATTERN_NONE: re.Pattern = None
29
+ EVERYSK_TEST_RE_PATTERN_UNDEFINED: re.Pattern = Undefined
@@ -0,0 +1,18 @@
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 everysk.core.fields import StrField
11
+
12
+
13
+ _EVERYSK_OTHER_PRIVATE: str = 'private-attribute'
14
+ EVERYSK_TEST_OTHER_NAME = StrField(default='test-other-case', readonly=True)
15
+ EVERYSK_TEST_OTHER_FAKE = True
16
+ EVERYSK_TEST_OTHER_INT: int = 2
17
+ EVERYSK_TEST_OTHER_FIELD_INHERIT = StrField(default='{EVERYSK_TEST_NAME} as {EVERYSK_TEST_FAKE}')
18
+ EVERYSK_TEST_OTHER_VAR_INHERIT = '{EVERYSK_TEST_FAKE} as {EVERYSK_TEST_NAME}'