everysk-lib 1.10.2__cp312-cp312-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. everysk/__init__.py +30 -0
  2. everysk/_version.py +683 -0
  3. everysk/api/__init__.py +61 -0
  4. everysk/api/api_requestor.py +167 -0
  5. everysk/api/api_resources/__init__.py +23 -0
  6. everysk/api/api_resources/api_resource.py +371 -0
  7. everysk/api/api_resources/calculation.py +779 -0
  8. everysk/api/api_resources/custom_index.py +42 -0
  9. everysk/api/api_resources/datastore.py +81 -0
  10. everysk/api/api_resources/file.py +42 -0
  11. everysk/api/api_resources/market_data.py +223 -0
  12. everysk/api/api_resources/parser.py +66 -0
  13. everysk/api/api_resources/portfolio.py +43 -0
  14. everysk/api/api_resources/private_security.py +42 -0
  15. everysk/api/api_resources/report.py +65 -0
  16. everysk/api/api_resources/report_template.py +39 -0
  17. everysk/api/api_resources/tests.py +115 -0
  18. everysk/api/api_resources/worker_execution.py +64 -0
  19. everysk/api/api_resources/workflow.py +65 -0
  20. everysk/api/api_resources/workflow_execution.py +93 -0
  21. everysk/api/api_resources/workspace.py +42 -0
  22. everysk/api/http_client.py +63 -0
  23. everysk/api/tests.py +32 -0
  24. everysk/api/utils.py +262 -0
  25. everysk/config.py +451 -0
  26. everysk/core/_tests/serialize/test_json.py +336 -0
  27. everysk/core/_tests/serialize/test_orjson.py +295 -0
  28. everysk/core/_tests/serialize/test_pickle.py +48 -0
  29. everysk/core/cloud_function/main.py +78 -0
  30. everysk/core/cloud_function/tests.py +86 -0
  31. everysk/core/compress.py +245 -0
  32. everysk/core/datetime/__init__.py +12 -0
  33. everysk/core/datetime/calendar.py +144 -0
  34. everysk/core/datetime/date.py +424 -0
  35. everysk/core/datetime/date_expression.py +299 -0
  36. everysk/core/datetime/date_mixin.py +1475 -0
  37. everysk/core/datetime/date_settings.py +30 -0
  38. everysk/core/datetime/datetime.py +713 -0
  39. everysk/core/exceptions.py +435 -0
  40. everysk/core/fields.py +1176 -0
  41. everysk/core/firestore.py +555 -0
  42. everysk/core/fixtures/_settings.py +29 -0
  43. everysk/core/fixtures/other/_settings.py +18 -0
  44. everysk/core/fixtures/user_agents.json +88 -0
  45. everysk/core/http.py +691 -0
  46. everysk/core/lists.py +92 -0
  47. everysk/core/log.py +709 -0
  48. everysk/core/number.py +37 -0
  49. everysk/core/object.py +1469 -0
  50. everysk/core/redis.py +1021 -0
  51. everysk/core/retry.py +51 -0
  52. everysk/core/serialize.py +674 -0
  53. everysk/core/sftp.py +414 -0
  54. everysk/core/signing.py +53 -0
  55. everysk/core/slack.py +127 -0
  56. everysk/core/string.py +199 -0
  57. everysk/core/tests.py +240 -0
  58. everysk/core/threads.py +199 -0
  59. everysk/core/undefined.py +70 -0
  60. everysk/core/unittests.py +73 -0
  61. everysk/core/workers.py +241 -0
  62. everysk/sdk/__init__.py +23 -0
  63. everysk/sdk/base.py +98 -0
  64. everysk/sdk/brutils/cnpj.py +391 -0
  65. everysk/sdk/brutils/cnpj_pd.py +129 -0
  66. everysk/sdk/engines/__init__.py +26 -0
  67. everysk/sdk/engines/cache.py +185 -0
  68. everysk/sdk/engines/compliance.py +37 -0
  69. everysk/sdk/engines/cryptography.py +69 -0
  70. everysk/sdk/engines/expression.cp312-win_amd64.pyd +0 -0
  71. everysk/sdk/engines/expression.pyi +55 -0
  72. everysk/sdk/engines/helpers.cp312-win_amd64.pyd +0 -0
  73. everysk/sdk/engines/helpers.pyi +26 -0
  74. everysk/sdk/engines/lock.py +120 -0
  75. everysk/sdk/engines/market_data.py +244 -0
  76. everysk/sdk/engines/settings.py +19 -0
  77. everysk/sdk/entities/__init__.py +23 -0
  78. everysk/sdk/entities/base.py +784 -0
  79. everysk/sdk/entities/base_list.py +131 -0
  80. everysk/sdk/entities/custom_index/base.py +209 -0
  81. everysk/sdk/entities/custom_index/settings.py +29 -0
  82. everysk/sdk/entities/datastore/base.py +160 -0
  83. everysk/sdk/entities/datastore/settings.py +17 -0
  84. everysk/sdk/entities/fields.py +375 -0
  85. everysk/sdk/entities/file/base.py +215 -0
  86. everysk/sdk/entities/file/settings.py +63 -0
  87. everysk/sdk/entities/portfolio/base.py +248 -0
  88. everysk/sdk/entities/portfolio/securities.py +241 -0
  89. everysk/sdk/entities/portfolio/security.py +580 -0
  90. everysk/sdk/entities/portfolio/settings.py +97 -0
  91. everysk/sdk/entities/private_security/base.py +226 -0
  92. everysk/sdk/entities/private_security/settings.py +17 -0
  93. everysk/sdk/entities/query.py +603 -0
  94. everysk/sdk/entities/report/base.py +214 -0
  95. everysk/sdk/entities/report/settings.py +23 -0
  96. everysk/sdk/entities/script.py +310 -0
  97. everysk/sdk/entities/secrets/base.py +128 -0
  98. everysk/sdk/entities/secrets/script.py +119 -0
  99. everysk/sdk/entities/secrets/settings.py +17 -0
  100. everysk/sdk/entities/settings.py +48 -0
  101. everysk/sdk/entities/tags.py +174 -0
  102. everysk/sdk/entities/worker_execution/base.py +307 -0
  103. everysk/sdk/entities/worker_execution/settings.py +63 -0
  104. everysk/sdk/entities/workflow_execution/base.py +113 -0
  105. everysk/sdk/entities/workflow_execution/settings.py +32 -0
  106. everysk/sdk/entities/workspace/base.py +99 -0
  107. everysk/sdk/entities/workspace/settings.py +27 -0
  108. everysk/sdk/settings.py +67 -0
  109. everysk/sdk/tests.py +105 -0
  110. everysk/sdk/worker_base.py +47 -0
  111. everysk/server/__init__.py +9 -0
  112. everysk/server/applications.py +63 -0
  113. everysk/server/endpoints.py +516 -0
  114. everysk/server/example_api.py +69 -0
  115. everysk/server/middlewares.py +80 -0
  116. everysk/server/requests.py +62 -0
  117. everysk/server/responses.py +119 -0
  118. everysk/server/routing.py +64 -0
  119. everysk/server/settings.py +36 -0
  120. everysk/server/tests.py +36 -0
  121. everysk/settings.py +98 -0
  122. everysk/sql/__init__.py +9 -0
  123. everysk/sql/connection.py +232 -0
  124. everysk/sql/model.py +376 -0
  125. everysk/sql/query.py +417 -0
  126. everysk/sql/row_factory.py +63 -0
  127. everysk/sql/settings.py +49 -0
  128. everysk/sql/utils.py +129 -0
  129. everysk/tests.py +23 -0
  130. everysk/utils.py +81 -0
  131. everysk/version.py +15 -0
  132. everysk_lib-1.10.2.dist-info/.gitignore +5 -0
  133. everysk_lib-1.10.2.dist-info/METADATA +326 -0
  134. everysk_lib-1.10.2.dist-info/RECORD +137 -0
  135. everysk_lib-1.10.2.dist-info/WHEEL +5 -0
  136. everysk_lib-1.10.2.dist-info/licenses/LICENSE.txt +9 -0
  137. everysk_lib-1.10.2.dist-info/top_level.txt +2 -0
everysk/core/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
@@ -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