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
|
@@ -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}'
|