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/sftp.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
#
|
|
3
|
+
# (C) Copyright 2025 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 os
|
|
11
|
+
from io import StringIO
|
|
12
|
+
from subprocess import DEVNULL, PIPE
|
|
13
|
+
from subprocess import run as command
|
|
14
|
+
from types import TracebackType
|
|
15
|
+
|
|
16
|
+
from paramiko import AutoAddPolicy, SFTPAttributes, SFTPClient, SSHClient, rsakey
|
|
17
|
+
|
|
18
|
+
from everysk.config import settings
|
|
19
|
+
from everysk.core.datetime import Date, DateTime
|
|
20
|
+
from everysk.core.object import BaseObject
|
|
21
|
+
|
|
22
|
+
###############################################################################
|
|
23
|
+
# KnownHosts Class Implementation
|
|
24
|
+
###############################################################################
|
|
25
|
+
class KnownHosts(BaseObject):
|
|
26
|
+
content: dict[str, str] = {}
|
|
27
|
+
|
|
28
|
+
def __init__(self, **kwargs):
|
|
29
|
+
super().__init__(**kwargs)
|
|
30
|
+
self.load()
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def filename(self) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Get the known_hosts file full path.
|
|
36
|
+
"""
|
|
37
|
+
return os.path.join(settings.EVERYSK_SFTP_DIR, 'known_hosts')
|
|
38
|
+
|
|
39
|
+
def _verify_file_exist(self) -> bool:
|
|
40
|
+
# If the file already exists we stop here
|
|
41
|
+
if os.path.exists(self.filename):
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
# If the directory does not exist we need to create it
|
|
45
|
+
dir = os.path.dirname(self.filename) # pylint: disable=redefined-builtin
|
|
46
|
+
if not os.path.exists(dir):
|
|
47
|
+
os.makedirs(dir)
|
|
48
|
+
|
|
49
|
+
# If the file does not exist, we create an empty one
|
|
50
|
+
# https://stackoverflow.com/a/12654798
|
|
51
|
+
open(self.filename, 'w', encoding='utf-8').close() # pylint: disable=consider-using-with
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
def add(self, hostname: str) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Add the hostname to the known_hosts in the cache and local file.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
hostname (str): The hostname to add to the known_hosts. Example: 'files.example.com'
|
|
60
|
+
"""
|
|
61
|
+
# Use the ssh-keyscan to get the key of the hostname
|
|
62
|
+
result = command(['ssh-keyscan', hostname], stdout=PIPE, check=False, stderr=DEVNULL)
|
|
63
|
+
# Add the key to the known_hosts locally
|
|
64
|
+
self.content[hostname] = result.stdout.decode('utf-8')
|
|
65
|
+
# Save the known_hosts to the local file
|
|
66
|
+
self.write()
|
|
67
|
+
|
|
68
|
+
def check(self, hostname: str) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Check if the hostname is already in the known_hosts.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
hostname (str): The hostname to check in the known_hosts. Example: 'files.example.com'
|
|
74
|
+
"""
|
|
75
|
+
return hostname in self.content
|
|
76
|
+
|
|
77
|
+
def delete(self, hostname: str) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Delete the hostname from the known_hosts in the cache and local file.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
hostname (str): The hostname to delete from the known_hosts. Example: 'files.example.com'
|
|
83
|
+
"""
|
|
84
|
+
if hostname in self.content:
|
|
85
|
+
del self.content[hostname]
|
|
86
|
+
self.write()
|
|
87
|
+
|
|
88
|
+
def load(self) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Load the known_hosts from the local file $HOME/.ssh/known_hosts.
|
|
91
|
+
"""
|
|
92
|
+
if self._verify_file_exist():
|
|
93
|
+
with open(self.filename, encoding='utf-8') as fd:
|
|
94
|
+
self.content = {line.split(' ')[0]: line for line in fd}
|
|
95
|
+
|
|
96
|
+
def write(self) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Write the known_hosts to the local file $HOME/.ssh/known_hosts for future use.
|
|
99
|
+
"""
|
|
100
|
+
if self._verify_file_exist():
|
|
101
|
+
with open(self.filename, 'w', encoding='utf-8') as fd:
|
|
102
|
+
fd.writelines(self.content.values())
|
|
103
|
+
# Ensure the file is written to disk
|
|
104
|
+
fd.flush()
|
|
105
|
+
os.fsync(fd.fileno())
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
###############################################################################
|
|
109
|
+
# SFTP Class Implementation
|
|
110
|
+
###############################################################################
|
|
111
|
+
class SFTP(BaseObject):
|
|
112
|
+
"""
|
|
113
|
+
SFTP class to connect to the SFTP server.
|
|
114
|
+
We could use the context manager to automatically close the connection when the object is deleted/destroyed.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
compress (bool): If the connection will transfer compressed data. Defaults to True.
|
|
118
|
+
date (Date, DateTime): The date/datetime used to parse the name. Defaults to today.
|
|
119
|
+
hostname (str): The hostname of the SFTP server. Defaults to None.
|
|
120
|
+
password (str): The password of the SFTP server. Defaults to None.
|
|
121
|
+
private_key (str): The private key of the SFTP server. Defaults to None.
|
|
122
|
+
passphrase (str): The passphrase of the private key. Defaults to None.
|
|
123
|
+
port (int): The port of the SFTP server. Defaults to 22.
|
|
124
|
+
timeout (int): The timeout of the SFTP connection. Defaults to 60.
|
|
125
|
+
username (str): The username of the SFTP server. Defaults to None.
|
|
126
|
+
|
|
127
|
+
Example:
|
|
128
|
+
>>> from everysk.core.sftp import SFTP
|
|
129
|
+
>>> with SFTP(username='', password='', hostname='') as sftp:
|
|
130
|
+
... filename = sftp.search_by_last_modification_time(path='/dir', prefix='file_')
|
|
131
|
+
>>> print(filename)
|
|
132
|
+
/dir/2024/11/13/file_11.12.2024.csv
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
## Private attributes
|
|
136
|
+
_client: SFTPClient = None
|
|
137
|
+
|
|
138
|
+
## Public attributes
|
|
139
|
+
compress: bool = True
|
|
140
|
+
date: Date | DateTime = None
|
|
141
|
+
hostname: str = None
|
|
142
|
+
password: str = None
|
|
143
|
+
private_key: str = None
|
|
144
|
+
passphrase: str = None
|
|
145
|
+
port: int = 22
|
|
146
|
+
timeout: int = 240
|
|
147
|
+
username: str = None
|
|
148
|
+
client_extra_args: dict = None
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def client(self) -> SFTPClient:
|
|
152
|
+
"""
|
|
153
|
+
Get the SFTP client to connect to the SFTP server.
|
|
154
|
+
"""
|
|
155
|
+
if self._client is None:
|
|
156
|
+
self._client = self.get_sftp_client(
|
|
157
|
+
hostname=self.hostname,
|
|
158
|
+
port=self.port,
|
|
159
|
+
username=self.username,
|
|
160
|
+
password=self.password,
|
|
161
|
+
private_key=self.private_key,
|
|
162
|
+
passphrase=self.passphrase,
|
|
163
|
+
compress=self.compress,
|
|
164
|
+
timeout=self.timeout,
|
|
165
|
+
**self.client_extra_args,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return self._client
|
|
169
|
+
|
|
170
|
+
## Private methods
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
hostname: str,
|
|
174
|
+
username: str,
|
|
175
|
+
password: str = None,
|
|
176
|
+
port: int = 22,
|
|
177
|
+
private_key: str = None,
|
|
178
|
+
passphrase: str = None,
|
|
179
|
+
date: Date | DateTime = None,
|
|
180
|
+
compress: bool = True,
|
|
181
|
+
timeout: int = 60,
|
|
182
|
+
client_extra_args: dict = None,
|
|
183
|
+
**kwargs,
|
|
184
|
+
):
|
|
185
|
+
"""
|
|
186
|
+
Constructor to initialize the SFTP connection.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
hostname (str, optional): The hostname of the SFTP server. Defaults to None.
|
|
190
|
+
username (str, optional): The username of the SFTP server. Defaults to None.
|
|
191
|
+
password (str, optional): The password of the SFTP server. Defaults to None.
|
|
192
|
+
port (int, optional): The port of the SFTP server. Defaults to 22.
|
|
193
|
+
private_key (str, optional): The private key of the SFTP server. Defaults to None.
|
|
194
|
+
passphrase (str, optional): The passphrase of the private key. Defaults to None.
|
|
195
|
+
date (Date, DateTime, optional): The date/datetime used to parse the name. Defaults to today.
|
|
196
|
+
compress (bool, optional): If the connection will transfer compressed data. Defaults to True.
|
|
197
|
+
timeout (int, optional): The timeout of the SFTP connection. Defaults to 60.
|
|
198
|
+
"""
|
|
199
|
+
if client_extra_args is None:
|
|
200
|
+
client_extra_args = {}
|
|
201
|
+
|
|
202
|
+
if date is None:
|
|
203
|
+
date = Date.today()
|
|
204
|
+
|
|
205
|
+
super().__init__(
|
|
206
|
+
compress=compress,
|
|
207
|
+
date=date,
|
|
208
|
+
hostname=hostname,
|
|
209
|
+
password=password,
|
|
210
|
+
port=port,
|
|
211
|
+
private_key=private_key,
|
|
212
|
+
passphrase=passphrase,
|
|
213
|
+
timeout=timeout,
|
|
214
|
+
username=username,
|
|
215
|
+
client_extra_args=client_extra_args,
|
|
216
|
+
**kwargs,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def __del__(self):
|
|
220
|
+
"""
|
|
221
|
+
Destructor to close the SFTP connection when the object is deleted/destroyed.
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
if self._client is not None:
|
|
225
|
+
# Close the SFTP connection when the object is deleted/destroyed
|
|
226
|
+
self._client.close()
|
|
227
|
+
except Exception: # pylint: disable=broad-except
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
def __enter__(self):
|
|
231
|
+
"""
|
|
232
|
+
Enter the context manager to return the object itself.
|
|
233
|
+
"""
|
|
234
|
+
return self
|
|
235
|
+
|
|
236
|
+
def __exit__(
|
|
237
|
+
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
|
|
238
|
+
):
|
|
239
|
+
"""
|
|
240
|
+
https://docs.python.org/3/library/stdtypes.html#contextmanager.__exit__
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
bool | None: If return is False any exception will be raised.
|
|
244
|
+
"""
|
|
245
|
+
try:
|
|
246
|
+
if self._client is not None:
|
|
247
|
+
# Close the SFTP connection
|
|
248
|
+
self._client.close()
|
|
249
|
+
# Reset the SFTP client
|
|
250
|
+
self._client = None
|
|
251
|
+
except Exception: # pylint: disable=broad-except
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
def sort(self, lst: list, attr: str, reverse: bool = False) -> list:
|
|
255
|
+
"""
|
|
256
|
+
Sort the list of objects by the attribute with order by asc or desc.
|
|
257
|
+
If the attribute is not found, the list will be returned as is.
|
|
258
|
+
If reverse is True, the list will be sorted in descending order.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
lst (list): The list of objects to sort.
|
|
262
|
+
attr (str): The name of the attribute to sort.
|
|
263
|
+
reverse (bool, optional): The final order of the list. Defaults to False.
|
|
264
|
+
"""
|
|
265
|
+
return sorted(lst, key=lambda obj: getattr(obj, attr), reverse=reverse)
|
|
266
|
+
|
|
267
|
+
## Public methods
|
|
268
|
+
def get_sftp_client(
|
|
269
|
+
self,
|
|
270
|
+
hostname: str,
|
|
271
|
+
port: int,
|
|
272
|
+
username: str,
|
|
273
|
+
password: str = None,
|
|
274
|
+
compress: bool = None,
|
|
275
|
+
timeout: int = None,
|
|
276
|
+
private_key: str = None,
|
|
277
|
+
passphrase: str = None,
|
|
278
|
+
) -> SFTPClient:
|
|
279
|
+
"""
|
|
280
|
+
Connect to the SFTP server and return the SFTP client.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
hostname (str): The hostname of the SFTP server.
|
|
284
|
+
port (int): The port of the SFTP server.
|
|
285
|
+
username (str): The username of the SFTP server.
|
|
286
|
+
password (str): The password of the SFTP server.
|
|
287
|
+
compress (bool): If the connection will transfer compressed data.
|
|
288
|
+
timeout (int): The timeout of the SFTP connection.
|
|
289
|
+
private_key (str): The private key of the SFTP server.
|
|
290
|
+
passphrase (str): The passphrase of the private key.
|
|
291
|
+
"""
|
|
292
|
+
ssh = SSHClient()
|
|
293
|
+
# On this point we create the file if it does not exists
|
|
294
|
+
# known_hosts = KnownHosts()
|
|
295
|
+
# Check if the hostname is already known
|
|
296
|
+
# if not known_hosts.check(hostname):
|
|
297
|
+
# # Add the hostname to the known_hosts and write to the local file
|
|
298
|
+
# known_hosts.add(hostname)
|
|
299
|
+
|
|
300
|
+
params = {'hostname': hostname, 'port': port, 'username': username, 'compress': compress, 'timeout': timeout}
|
|
301
|
+
|
|
302
|
+
# check if we have the password
|
|
303
|
+
if password is not None:
|
|
304
|
+
params['password'] = password
|
|
305
|
+
elif private_key is not None:
|
|
306
|
+
key_file = StringIO(private_key)
|
|
307
|
+
params['pkey'] = rsakey.RSAKey.from_private_key(key_file)
|
|
308
|
+
params['passphrase'] = passphrase
|
|
309
|
+
|
|
310
|
+
# Load the known_hosts file
|
|
311
|
+
# This was placed before the connection to have time for Google sync the file creation
|
|
312
|
+
# https://everysk.atlassian.net/browse/COD-10872
|
|
313
|
+
# ssh.load_system_host_keys(filename=known_hosts.filename)
|
|
314
|
+
ssh.set_missing_host_key_policy(AutoAddPolicy) # nosec B507
|
|
315
|
+
|
|
316
|
+
ssh.connect(**params)
|
|
317
|
+
return ssh.open_sftp()
|
|
318
|
+
|
|
319
|
+
def get_file(self, filename: str) -> bytes | None:
|
|
320
|
+
"""
|
|
321
|
+
Get the file content from the SFTP server.
|
|
322
|
+
If the file is not found, return None.
|
|
323
|
+
If the filename has a date format, it will be parsed with the date attribute.
|
|
324
|
+
Example: '/dir/%Y/file_%Y.csv' -> '/dir/2024/file_2024.csv'
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
filename (str): The filename with the path to get the content. Example: '/dir/2024/file_2024.csv'
|
|
328
|
+
"""
|
|
329
|
+
if '%' in filename:
|
|
330
|
+
filename = self.parse_date(filename, self.date)
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
with self.client.open(filename, 'rb') as fd:
|
|
334
|
+
return fd.read()
|
|
335
|
+
except OSError:
|
|
336
|
+
# If the file is not found, return None
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
def save_file(self, filename: str, content: bytes) -> None:
|
|
342
|
+
"""
|
|
343
|
+
Save the file content to the SFTP server.
|
|
344
|
+
If the filename has a date format, it will be parsed with the date attribute.
|
|
345
|
+
Example: '/dir/%Y/file_%Y.csv' -> '/dir/2024/file_2024.csv'
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
filename (str): The filename with the path to save the content. Example: '/dir/2024/file_2024.csv'
|
|
349
|
+
content (bytes): The content of the file to save.
|
|
350
|
+
"""
|
|
351
|
+
if '%' in filename:
|
|
352
|
+
filename = self.parse_date(filename, self.date)
|
|
353
|
+
|
|
354
|
+
# Split the path to create the directories if not exists
|
|
355
|
+
# the first element is empty, so we need to remove it
|
|
356
|
+
dirs = os.path.dirname(filename).split('/')[1:]
|
|
357
|
+
path = ''
|
|
358
|
+
for dir in dirs: # pylint: disable=redefined-builtin
|
|
359
|
+
path = f'{path}/{dir}'
|
|
360
|
+
try:
|
|
361
|
+
self.client.mkdir(path)
|
|
362
|
+
except OSError:
|
|
363
|
+
pass
|
|
364
|
+
|
|
365
|
+
with self.client.open(filename, 'wb') as fd:
|
|
366
|
+
fd.write(content)
|
|
367
|
+
|
|
368
|
+
def search_by_last_modification_time(self, path: str, prefix: str) -> str | None:
|
|
369
|
+
"""
|
|
370
|
+
Search the file by the last modification time with the prefix in the path recursively.
|
|
371
|
+
If the file is not found, return None.
|
|
372
|
+
If the path or prefix has a date format, it will be parsed with the date attribute.
|
|
373
|
+
Example: '/dir/%Y' -> '/dir/2024' or 'file_%m.%d.%Y' -> 'file_11.30.2024'
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
path (str): The path to start search the file.
|
|
377
|
+
prefix (str): The prefix of the file to search. Example: 'file_' or 'file_%m.%d.%Y'
|
|
378
|
+
"""
|
|
379
|
+
if '%' in prefix:
|
|
380
|
+
prefix = self.parse_date(prefix, self.date)
|
|
381
|
+
if '%' in path:
|
|
382
|
+
path = self.parse_date(path, self.date)
|
|
383
|
+
|
|
384
|
+
objs: list[SFTPAttributes] = self.client.listdir_attr(path)
|
|
385
|
+
objs = self.sort(objs, 'st_mtime', reverse=True)
|
|
386
|
+
for file in objs:
|
|
387
|
+
if file.filename.startswith(prefix):
|
|
388
|
+
return f'{path}/{file.filename}'
|
|
389
|
+
|
|
390
|
+
if file.longname.startswith('d'):
|
|
391
|
+
result = self.search_by_last_modification_time(f'{path}/{file.filename}', prefix)
|
|
392
|
+
if result:
|
|
393
|
+
return result
|
|
394
|
+
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
def parse_date(self, name: str, date: Date) -> str:
|
|
398
|
+
"""
|
|
399
|
+
Parse the date format in the name with the date attribute.
|
|
400
|
+
Example: '/dir/%Y/file_%Y.csv' -> '/dir/2024/file_2024.csv'
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
name (str): The name with the date format to parse.
|
|
404
|
+
date (Date): The date to parse the date format.
|
|
405
|
+
"""
|
|
406
|
+
if '%' in name:
|
|
407
|
+
index = name.find('%')
|
|
408
|
+
date_format = name[index : index + 2]
|
|
409
|
+
result = date.strftime(date_format)
|
|
410
|
+
name = name.replace(date_format, result)
|
|
411
|
+
if '%' in name:
|
|
412
|
+
return self.parse_date(name, date)
|
|
413
|
+
|
|
414
|
+
return name
|
everysk/core/signing.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
#
|
|
3
|
+
# (C) Copyright 2025 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 hmac
|
|
11
|
+
|
|
12
|
+
from everysk.config import settings
|
|
13
|
+
from everysk.core.exceptions import SigningError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
SEPARATOR: bytes = b':'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
###############################################################################
|
|
20
|
+
# Public Functions Implementation
|
|
21
|
+
###############################################################################
|
|
22
|
+
def sign(data: str | bytes | bytearray, hash_name: str = 'sha1') -> bytes:
|
|
23
|
+
"""
|
|
24
|
+
Sign the data with the SECRET_KEY.
|
|
25
|
+
Possible hash algorithms are 'md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
data (str | bytes | bytearray): The data to be signed.
|
|
29
|
+
hash_name (str): The name of the hash algorithm to be used.
|
|
30
|
+
"""
|
|
31
|
+
if isinstance(data, str):
|
|
32
|
+
data = data.encode()
|
|
33
|
+
|
|
34
|
+
digest = hmac.new(settings.EVERYSK_SIGNING_KEY.encode(), data, hash_name).hexdigest().encode()
|
|
35
|
+
return SEPARATOR.join([digest, data])
|
|
36
|
+
|
|
37
|
+
def unsign(signed_data: bytes, hash_name: str = 'sha1') -> bytes | bytearray:
|
|
38
|
+
"""
|
|
39
|
+
Unsign the data with the SECRET_KEY.
|
|
40
|
+
Possible hash algorithms are 'md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
signed_data (bytes): The signed data created by the sign method.
|
|
44
|
+
hash_name (str): The name of the hash algorithm to be used.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
SigningError: If the signing key is invalid.
|
|
48
|
+
"""
|
|
49
|
+
digest, data = signed_data.split(SEPARATOR, 1)
|
|
50
|
+
if hmac.compare_digest(digest, hmac.new(settings.EVERYSK_SIGNING_KEY.encode(), data, hash_name).hexdigest().encode()):
|
|
51
|
+
return data
|
|
52
|
+
|
|
53
|
+
raise SigningError
|
everysk/core/slack.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
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 hashlib import sha1
|
|
11
|
+
|
|
12
|
+
from everysk.core.fields import BoolField, ChoiceField, DictField, StrField
|
|
13
|
+
from everysk.core.http import HttpPOSTConnection, httpx
|
|
14
|
+
from everysk.core.redis import RedisCacheCompressed
|
|
15
|
+
|
|
16
|
+
###############################################################################
|
|
17
|
+
# Slack Class Implementation
|
|
18
|
+
###############################################################################
|
|
19
|
+
class Slack(HttpPOSTConnection):
|
|
20
|
+
## Private attributes
|
|
21
|
+
_key_prefix = 'everysk-core-slack'
|
|
22
|
+
_cache = RedisCacheCompressed()
|
|
23
|
+
_cache_timeout = 60 * 1 # 1 minute
|
|
24
|
+
_color_map = DictField(default={'danger': '#a90a05', 'success': '#138138', 'warning': '#e9932d'}, readonly=True)
|
|
25
|
+
is_json = BoolField(default=True, readonly=True)
|
|
26
|
+
|
|
27
|
+
## Public attributes
|
|
28
|
+
color = ChoiceField(default=None, choices=(None, 'danger', 'success', 'warning'))
|
|
29
|
+
message = StrField(default=None, required=True)
|
|
30
|
+
title = StrField(default=None, required=True)
|
|
31
|
+
|
|
32
|
+
def get_payload(self) -> dict:
|
|
33
|
+
"""
|
|
34
|
+
Convert all key/value on self to a dict object and apply some conversions.
|
|
35
|
+
"""
|
|
36
|
+
return {
|
|
37
|
+
'attachments': [{
|
|
38
|
+
'color': self._color_map.get(self.color, '#000000'),
|
|
39
|
+
'blocks': [
|
|
40
|
+
{
|
|
41
|
+
'type': 'header',
|
|
42
|
+
'text': {
|
|
43
|
+
'type': 'plain_text',
|
|
44
|
+
'text': self.title,
|
|
45
|
+
'emoji': True
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
'type': 'divider'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
'type': 'section',
|
|
53
|
+
'text': {
|
|
54
|
+
'type': 'mrkdwn',
|
|
55
|
+
'text': self.message
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
'type': 'divider'
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
def is_title_cached(self) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
Check if the title is already in the cache and set it if not.
|
|
68
|
+
"""
|
|
69
|
+
title_key = self.make_title_key()
|
|
70
|
+
cached_title = self._cache.get(title_key)
|
|
71
|
+
if cached_title:
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
# Keep the title in the cache for 1 minute
|
|
75
|
+
self._cache.set(title_key, True, self._cache_timeout)
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
def is_message_cached(self) -> bool:
|
|
79
|
+
"""
|
|
80
|
+
Check if the message is already in the cache and set it if not.
|
|
81
|
+
"""
|
|
82
|
+
message_key = self.make_message_key()
|
|
83
|
+
cached_message = self._cache.get(message_key)
|
|
84
|
+
if cached_message:
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
# Keep the message in the cache for 1 minute
|
|
88
|
+
self._cache.set(message_key, True, self._cache_timeout)
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def make_title_key(self) -> str:
|
|
92
|
+
"""
|
|
93
|
+
Create a key for the title in the cache.
|
|
94
|
+
"""
|
|
95
|
+
if isinstance(self.title, str):
|
|
96
|
+
value = self.title.encode('utf-8')
|
|
97
|
+
else:
|
|
98
|
+
value = self.title
|
|
99
|
+
|
|
100
|
+
return f'{self._key_prefix}-{sha1(value, usedforsecurity=False).hexdigest()}'
|
|
101
|
+
|
|
102
|
+
def make_message_key(self) -> str:
|
|
103
|
+
"""
|
|
104
|
+
Create a key for the message in the cache.
|
|
105
|
+
"""
|
|
106
|
+
if isinstance(self.message, str):
|
|
107
|
+
value = self.message.encode('utf-8')
|
|
108
|
+
else:
|
|
109
|
+
value = self.message
|
|
110
|
+
|
|
111
|
+
return f'{self._key_prefix}-{sha1(value, usedforsecurity=False).hexdigest()}'
|
|
112
|
+
|
|
113
|
+
def can_send(self) -> bool:
|
|
114
|
+
"""
|
|
115
|
+
Check if the title and message are already in the cache.
|
|
116
|
+
If they are, the message will not be sent because it is a duplicate.
|
|
117
|
+
"""
|
|
118
|
+
return not self.is_title_cached() and not self.is_message_cached()
|
|
119
|
+
|
|
120
|
+
def send(self) -> httpx.Response | None:
|
|
121
|
+
"""
|
|
122
|
+
Sends the message to the Slack Channel.
|
|
123
|
+
"""
|
|
124
|
+
if self.can_send():
|
|
125
|
+
return self.get_response()
|
|
126
|
+
|
|
127
|
+
return None
|