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/log.py ADDED
@@ -0,0 +1,709 @@
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
+ __all__ = ['Logger', 'LoggerManager']
11
+
12
+ import json
13
+ import logging
14
+ import sys
15
+ import traceback as python_traceback
16
+ from contextlib import AbstractContextManager
17
+ from contextvars import ContextVar
18
+ from datetime import datetime as DateTime # noqa: N812
19
+ from threading import Thread
20
+ from types import TracebackType
21
+ from typing import Any
22
+ from zoneinfo import ZoneInfo
23
+
24
+ from everysk.config import settings
25
+
26
+ DEFAULT_APP_SERVER = settings.LOGGING_APP_SERVER
27
+ DEFAULT_TRACE_PARTS = {'version': '', 'trace_id': '', 'span_id': '', 'trace_sample': False}
28
+ HEADER_TRACEPARENT = 'traceparent'
29
+ HEADER_X_CLOUD_TRACE_CONTEXT = 'x-cloud-trace-context'
30
+ LOGGER_MANAGER_CONTEXT_VAR_NAME = 'everysk-lib-log-extra-context-var'
31
+
32
+
33
+ ###############################################################################
34
+ # Private functions Implementation
35
+ ###############################################################################
36
+ def _default(obj: Any) -> str | None:
37
+ """
38
+ Function is used to convert the object to a string inside the json.dumps.
39
+
40
+ Args:
41
+ obj (Any): The object to be converted to a string.
42
+ """
43
+ if isinstance(obj, bytes):
44
+ return obj.decode(json.detect_encoding(obj))
45
+
46
+ return None
47
+
48
+
49
+ def _get_gcp_headers(headers: dict | None = None) -> dict:
50
+ """
51
+ Get the headers to be added to the log.
52
+ The order is if the headers are sent in the log function, set in context or get from the default function.
53
+
54
+ Args:
55
+ headers (dict, optional): The headers generated outside the log. Defaults to None.
56
+
57
+ Returns:
58
+ dict: Only the headers that are in the list HEADER_TRACEPARENT and HEADER_X_CLOUD_TRACE_CONTEXT.
59
+ """
60
+ if not headers:
61
+ headers = LoggerManager._extra.get().get('http_headers', {}) # noqa: SLF001
62
+
63
+ return {key: value for key, value in headers.items() if key in (HEADER_TRACEPARENT, HEADER_X_CLOUD_TRACE_CONTEXT)}
64
+
65
+
66
+ def _get_trace_data(headers: dict) -> dict:
67
+ """
68
+ Get the trace_id, span_id and trace_sample from the headers.
69
+ https://cloud.google.com/trace/docs/trace-context
70
+
71
+ Args:
72
+ headers (dict): The headers dictionary.
73
+ """
74
+ trace_parts = DEFAULT_TRACE_PARTS.copy()
75
+ trace = headers.get(HEADER_TRACEPARENT, '')
76
+ if trace:
77
+ _parse_traceparent(trace, trace_parts)
78
+ else:
79
+ trace = headers.get(HEADER_X_CLOUD_TRACE_CONTEXT, '')
80
+ if trace:
81
+ _parse_x_cloud_trace_context(trace, trace_parts)
82
+
83
+ if trace_parts['trace_id']:
84
+ trace_parts['trace_id'] = f'{settings.LOGGING_GOOGLE_CLOUD_TRACE_ID}/{trace_parts["trace_id"]}'
85
+
86
+ return trace_parts
87
+
88
+
89
+ def _get_traceback() -> str:
90
+ """Get the traceback of the current exception."""
91
+ result = python_traceback.format_exc()
92
+ # When there is no traceback the result is 'NoneType: None\n'
93
+ # Like this is the most common case we check for equality and return an empty string
94
+ if result == 'NoneType: None\n':
95
+ return ''
96
+
97
+ return result
98
+
99
+
100
+ def _parse_traceparent(traceparent: str, trace_parts: dict) -> None:
101
+ """
102
+ Parse the traceparent header and return a dictionary with the version, trace_id, span_id and trace_flags.
103
+ Header example -> traceparent: "00-4bfa9e049143840bef864a7859f2e5df-1c6c592f9e46e3fb-01"
104
+ https://www.w3.org/TR/trace-context/
105
+ From W3C documentation: traceparent: "<version>-<trace-id>-<parent-id>-<trace-flags>"
106
+
107
+ Args:
108
+ traceparent (str): The traceparent header value.
109
+ trace_parts (dict): The dictionary where the parsed values will be stored.
110
+ """
111
+ try:
112
+ trace_parts['version'], trace_parts['trace_id'], trace_parts['span_id'], trace_parts['trace_sample'] = (
113
+ traceparent.split('-')
114
+ )
115
+ trace_parts['trace_sample'] = bool(int(trace_parts['trace_sample']))
116
+ except ValueError:
117
+ pass
118
+
119
+
120
+ def _parse_x_cloud_trace_context(trace_context: str, trace_parts: dict) -> None:
121
+ """
122
+ Parse the x-cloud-trace-context header and return a dictionary with the trace_id and span_id.
123
+ Header example -> x-cloud-trace-context: "4bfa9e049143840bef864a7859f2e5df/2048109991600514043;o=1"
124
+ From Google documentation: X-Cloud-Trace-Context: TRACE_ID/SPAN_ID;o=OPTIONS
125
+
126
+ Args:
127
+ trace_context (str): The x-cloud-trace-context header value.
128
+ trace_parts (dict): The dictionary where the parsed values will be stored.
129
+ """
130
+ try:
131
+ trace_parts['trace_id'], trace_parts['span_id'] = trace_context.split('/')
132
+ trace_parts['span_id'], trace_parts['trace_sample'] = trace_parts['span_id'].split(';')
133
+ trace_parts['trace_sample'] = trace_parts['trace_sample'].endswith('1') # pylint: disable=no-member
134
+ except ValueError:
135
+ pass
136
+
137
+
138
+ ###############################################################################
139
+ # Formatter Class Implementation
140
+ ###############################################################################
141
+ class Formatter(logging.Formatter):
142
+ def _get_default_dict(self, message: str, severity: str) -> dict:
143
+ """
144
+ Python logging default values.
145
+ Severity levels: CRITICAL , ERROR , WARNING , INFO , DEBUG
146
+
147
+ Args:
148
+ message (str): The message to be logged.
149
+ severity (str): The severity of the message.
150
+ """
151
+ return {'message': message, 'severity': severity}
152
+
153
+ def _get_default_extra_dict(
154
+ self, name: str, headers: dict, payload: dict, response: dict, traceback: str, labels: dict
155
+ ) -> dict:
156
+ """
157
+ Get the default extra data dictionary to be added to the log.
158
+ Until now we only have the logName, traceback, http headers and http payload.
159
+
160
+ Args:
161
+ name (str): The name used to create the log.
162
+ headers (dict): A dictionary with the HTTP headers.
163
+ payload (dict): A dictionary with the HTTP payload.
164
+ response (dict): A dictionary with the HTTP response.
165
+ traceback (str): The traceback of the exception.
166
+ labels (dict): A dictionary with the labels to be added to the log.
167
+ """
168
+ return {
169
+ 'logName': name,
170
+ 'labels': labels,
171
+ 'traceback': traceback,
172
+ 'http': {'headers': headers, 'payload': payload, 'response': response},
173
+ }
174
+
175
+ def _get_default_gcp_dict(self, headers: dict, filename: str, line: int, func_name: str) -> dict:
176
+ """
177
+ Default Google Cloud Platform dictionary to be added to the log.
178
+ This dictionary has the trace_id, span_id, trace_sample used to chain the logs in the Google Cloud Logging.
179
+
180
+ Args:
181
+ headers (dict): A dictionary with the HTTP headers.
182
+ filename (str): Filename where the log was placed.
183
+ line (int): The line where the log was placed.
184
+ func_name (str): The function name where the log was placed.
185
+ """
186
+ trace = _get_trace_data(headers)
187
+ return {
188
+ 'logging.googleapis.com/trace': trace['trace_id'],
189
+ 'logging.googleapis.com/spanId': trace['span_id'],
190
+ 'logging.googleapis.com/trace_sampled': trace['trace_sample'],
191
+ 'logging.googleapis.com/sourceLocation': {'file': filename, 'line': line, 'function': func_name},
192
+ }
193
+
194
+ def _get_result_dict(self, record: logging.LogRecord) -> dict:
195
+ """
196
+ Convert the log record to a dictionary.
197
+
198
+ Args:
199
+ record (logging.LogRecord): Record object with all the information about the log.
200
+ """
201
+ result = self._get_default_dict(message=record.getMessage(), severity=record.levelname)
202
+ result.update(
203
+ self._get_default_extra_dict(
204
+ name=record.name,
205
+ headers=getattr(record, 'http_headers', {}),
206
+ payload=getattr(record, 'http_payload', {}),
207
+ response=getattr(record, 'http_response', {}),
208
+ traceback=getattr(record, 'traceback', ''),
209
+ labels=getattr(record, 'labels', {}),
210
+ )
211
+ )
212
+ result.update(
213
+ self._get_default_gcp_dict(
214
+ headers=result['http']['headers'],
215
+ filename=record.pathname,
216
+ line=record.lineno,
217
+ func_name=record.funcName,
218
+ )
219
+ )
220
+ return result
221
+
222
+ def formatMessage(self, record: logging.LogRecord) -> str: # noqa: N802
223
+ """
224
+ Format the message to be displayed in the terminal or Google Log Explorer.
225
+
226
+ Args:
227
+ record (logging.LogRecord): Record object with all the information about the log.
228
+ """
229
+ result = self._get_result_dict(record)
230
+ return json.dumps(result, default=_default)
231
+
232
+
233
+ ###############################################################################
234
+ # LoggerManager Class Implementation
235
+ ###############################################################################
236
+ class LoggerManager(AbstractContextManager):
237
+ ## Private attributes
238
+ _extra: ContextVar = ContextVar(LOGGER_MANAGER_CONTEXT_VAR_NAME, default={}) # noqa: B039
239
+ _old_value: dict = None
240
+
241
+ def __init__(
242
+ self,
243
+ http_headers: dict | None = None,
244
+ http_payload: dict | None = None,
245
+ http_response: dict | None = None,
246
+ labels: dict | None = None,
247
+ stacklevel: int | None = None,
248
+ traceback: str | None = None,
249
+ ) -> None:
250
+ """
251
+ Context class to create a context manager for the Logger object.
252
+ This class is used to add extra information to the log.
253
+ This manager together with everysk.core.threads will keep the context inside the thread.
254
+ If you use python threads you need to pass the context inside the thread.
255
+
256
+ Args:
257
+ http_headers (dict, optional): The HTTP headers to be added to the log. Defaults to None.
258
+ http_payload (dict, optional): The HTTP payload to be added to the log. Defaults to None.
259
+ http_response (dict, optional): The HTTP response to be added to the log. Defaults to None.
260
+ stacklevel (int, optional): The stacklevel to be used on the log. Defaults to None.
261
+ traceback (str, optional): The traceback to be added to the log. Defaults to None.
262
+ labels (dict, optional): A dictionary with the labels to be added to the log. Defaults to None.
263
+ """
264
+ self.http_headers = http_headers
265
+ self.http_payload = http_payload
266
+ self.http_response = http_response
267
+ self.labels = labels
268
+ self.stacklevel = stacklevel
269
+ self.traceback = traceback
270
+
271
+ # Save this value to be used in the __exit__ method to restore the context
272
+ self._old_value = self._extra.get().copy()
273
+
274
+ def __enter__(self) -> 'LoggerManager':
275
+ """
276
+ Method to be executed when the context manager is created and always return self.
277
+ This method is always executed even if "LoggerManager as some_var:" is not used.
278
+ """
279
+ # First we get the original context
280
+ context: dict = self._extra.get()
281
+
282
+ if self.http_headers is not None:
283
+ context['http_headers'] = self.http_headers
284
+
285
+ if self.http_payload is not None:
286
+ context['http_payload'] = self.http_payload
287
+
288
+ if self.http_response is not None:
289
+ context['http_response'] = self.http_response
290
+
291
+ if self.labels is not None:
292
+ context['labels'] = self.labels
293
+
294
+ if self.stacklevel is not None:
295
+ context['stacklevel'] = self.stacklevel
296
+
297
+ if self.traceback is not None:
298
+ context['traceback'] = self.traceback
299
+
300
+ # Finally we store the new context
301
+ self._extra.set(context)
302
+
303
+ return self
304
+
305
+ def __exit__(
306
+ self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
307
+ ) -> bool | None:
308
+ """
309
+ https://docs.python.org/3/library/stdtypes.html#contextmanager.__exit__
310
+
311
+ Returns:
312
+ bool | None: If return is False any exception will be raised.
313
+ """
314
+ # Restore the context to the original value
315
+ self._extra.set(self._old_value)
316
+
317
+ return False
318
+
319
+ @classmethod
320
+ def reset(cls) -> None:
321
+ """
322
+ Reset the context to the default value.
323
+ This method is used to avoid shared values between requests in server.endpoints module.
324
+ """
325
+ cls._extra = ContextVar(LOGGER_MANAGER_CONTEXT_VAR_NAME, default={}) # noqa: B039
326
+
327
+
328
+ ###############################################################################
329
+ # Logger Class Implementation
330
+ ###############################################################################
331
+ class Logger:
332
+ ## Private attributes
333
+ # This needs to be initialized with an empty set because it will be a global list
334
+ _deprecated_hash: set[str] = set() # noqa: RUF012
335
+ _default_stacklevel: int = 3 # 3 -> The place where the log.method was placed; 2 -> This file; 1 -> Python logger
336
+ _log: logging.Logger = None
337
+ _slack_timer: DateTime = None
338
+
339
+ ## Public attributes
340
+ name: str = None
341
+ stacklevel: int = None
342
+
343
+ ## Private methods
344
+ def __init__(self, name: str, stacklevel: int | None = None) -> None:
345
+ """
346
+ Logger class used to send messages to STDOUT or Google CLoud Logging.
347
+ Use stacklevel to show correctly the file and the line of the log, lvl 0 means the python
348
+ logger, lvl 1 means this file, lvl 2 means the place where the log. was placed, if you use
349
+ another file for the log object maybe you need to change to lvl 3.
350
+
351
+ Args:
352
+ name (str, optional): The name of the log. Defaults to "root".
353
+ stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
354
+
355
+ Example:
356
+ >>> from everysk.core.log import Logger
357
+ >>> log = Logger(name='log-test')
358
+ >>> log.debug('Test')
359
+ 2024-02-07 12:49:10,640 - DEBUG - {} - Test
360
+ """
361
+ if name == 'root':
362
+ raise ValueError('The name of the log could not be "root".')
363
+ self.name = name
364
+ if stacklevel is not None and stacklevel > 0:
365
+ self.stacklevel = stacklevel
366
+
367
+ self._log = self._get_python_logger()
368
+
369
+ def _get_extra_data(self, extra: dict, level: int | None = None) -> dict:
370
+ """
371
+ Get the extra data to be added to the log.
372
+ We check if the extra was sent in the function, if not we check if it
373
+ was set in the LoggerManager context, if not we check if we have a
374
+ default function to get the data.
375
+ We only set the payload
376
+
377
+ Args:
378
+ extra (dict): The extra data sent in the function.
379
+ level (int): The level of the log.
380
+ """
381
+ http_headers = self._get_http_headers(extra.pop('http_headers', {}))
382
+ if http_headers:
383
+ extra['http_headers'] = http_headers
384
+
385
+ http_payload = self._get_http_payload(extra.pop('http_payload', {}), level)
386
+ if http_payload:
387
+ extra['http_payload'] = http_payload
388
+
389
+ http_response = self._get_http_response(extra.pop('http_response', {}))
390
+ if http_response:
391
+ extra['http_response'] = http_response
392
+
393
+ # https://everysk.atlassian.net/browse/COD-6151
394
+ # Because we use labels in the format for the normal log, this key always will be present
395
+ labels = extra.pop('labels', {})
396
+ if not labels:
397
+ # If we don't receive labels as param in log functions we get from the context
398
+ labels = LoggerManager._extra.get().get('labels', {}) # noqa: SLF001
399
+
400
+ # Labels are always present in the log even if it is an empty dictionary
401
+ extra['labels'] = labels
402
+
403
+ traceback = extra.pop('traceback', '')
404
+ if not traceback:
405
+ # If we don't receive traceback as param in log functions we get from the context
406
+ # or from the result of traceback module
407
+ traceback = LoggerManager._extra.get().get('traceback', '') or _get_traceback() # noqa: SLF001
408
+ if traceback:
409
+ extra['traceback'] = traceback
410
+
411
+ return extra
412
+
413
+ def _get_http_headers(self, http_headers: dict) -> dict:
414
+ """
415
+ Get the http headers to be added to the log.
416
+ The order is if the headers are sent in the log function, set in context or get from the default function.
417
+ We only search for the payload if the level is ERROR or CRITICAL.
418
+
419
+ Args:
420
+ http_headers (dict): The HTTP headers sent in the log function.
421
+ level (int): The level of the log.
422
+ """
423
+ return _get_gcp_headers(http_headers)
424
+
425
+ def _get_http_payload(self, http_payload: dict, level: int | None = None) -> dict:
426
+ """
427
+ Get the http payload to be added to the log.
428
+ The order is if the payload are sent in the log function, set in context or get from the default function.
429
+ We only search for the payload if the level is ERROR or CRITICAL.
430
+
431
+ Args:
432
+ http_payload (dict): The HTTP payload sent in the log function.
433
+ level (int): The level of the log.
434
+ """
435
+ if not http_payload and level in (logging.ERROR, logging.CRITICAL):
436
+ http_payload = LoggerManager._extra.get().get('http_payload', {}) # noqa: SLF001
437
+
438
+ return http_payload
439
+
440
+ def _get_http_response(self, http_response: dict) -> dict:
441
+ """
442
+ Get the http response to be added to the log.
443
+ The order is if the response are sent in the log function, set in context or get from the default function.
444
+
445
+ Args:
446
+ http_response (dict): The HTTP response sent in the log function.
447
+ """
448
+ if not http_response:
449
+ http_response = LoggerManager._extra.get().get('http_response', {}) # noqa: SLF001
450
+
451
+ return http_response
452
+
453
+ def _get_python_logger(self) -> logging.Logger:
454
+ """
455
+ Method that creates/get the Python Logger object and attach the correct handler.
456
+ The default handler will be the stdout.
457
+ """
458
+ if self._log is not None:
459
+ return self._log
460
+
461
+ # Create the log
462
+ log = logging.getLogger(self.name)
463
+ log.setLevel(logging.DEBUG)
464
+ log.propagate = False # Don't pass message to others loggers
465
+
466
+ # We should only have one handler per log name
467
+ if not hasattr(log, 'handler'):
468
+ log.handler = logging.StreamHandler(stream=sys.stdout)
469
+
470
+ # Set the format that the message is displayed
471
+ if settings.LOGGING_JSON:
472
+ log.handler.setFormatter(Formatter())
473
+ else:
474
+ log.handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(labels)s - %(message)s'))
475
+
476
+ # Add the handler inside the log
477
+ log.addHandler(log.handler)
478
+
479
+ # Set the level that the handler will be using.
480
+ log.handler.setLevel(logging.DEBUG)
481
+
482
+ return log
483
+
484
+ def _get_stacklevel(self, stacklevel: int) -> int:
485
+ """
486
+ Get the stacklevel that was set in the function
487
+ or in the LoggerManager context
488
+ or set in the Logger object
489
+ or use the default stacklevel.
490
+
491
+ Args:
492
+ stacklevel (int): The stacklevel to be used on the log.
493
+ """
494
+ if not stacklevel:
495
+ stacklevel = LoggerManager._extra.get().get('stacklevel', {}) # noqa: SLF001
496
+ if not stacklevel:
497
+ stacklevel = self.stacklevel or self._default_stacklevel
498
+
499
+ return stacklevel
500
+
501
+ def _send_to_log(
502
+ self, level: int, msg: str, *args: tuple, extra: dict | None = None, stacklevel: int | None = None
503
+ ) -> None:
504
+ """
505
+ Send the message to the python logger using the correct level.
506
+ Use stacklevel to show correctly the file and the line of the log, lvl 0 means the python
507
+ logger, lvl 1 means this file, lvl 2 means the place where the log. was placed, if you use
508
+ another file for the log object maybe you need to change to lvl 3.
509
+
510
+ Args:
511
+ level (int): The level of the message.
512
+ msg (str): The message to log.
513
+ *args (tuple): The arguments to be used on the message.
514
+ extra (dict, optional): Extra information to be added to the log. Defaults to None.
515
+ stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
516
+ """
517
+ stacklevel = self._get_stacklevel(stacklevel)
518
+ extra = self._get_extra_data(extra or {}, level)
519
+ self._log.log(level, msg, *args, extra=extra, stacklevel=stacklevel)
520
+
521
+ def _show_deprecated(self, _id: str, *, show_once: bool) -> bool:
522
+ """
523
+ If show_once is False this always return True, otherwise
524
+ checks if this _id is in the self._deprecated_hash set.
525
+
526
+ Args:
527
+ _id (str): String that will be stored to be checked later
528
+ show_once (bool): Flag to store or not the id.
529
+ """
530
+ if show_once:
531
+ if _id in self._deprecated_hash:
532
+ return False
533
+
534
+ self._deprecated_hash.add(_id)
535
+
536
+ return True
537
+
538
+ ## Public methods
539
+ def critical(self, msg: str, *args: tuple, extra: dict | None = None, stacklevel: int | None = None) -> None:
540
+ """
541
+ Log a message with severity CRITICAL on this logger.
542
+ Use stacklevel to show correctly the file and the line of the log, lvl 0 means the python
543
+ logger, lvl 1 means this file, lvl 2 means the place where the log. was placed, if you use
544
+ another file for the log object maybe you need to change to lvl 3.
545
+
546
+ Args:
547
+ msg (str): The message to log.
548
+ *args (tuple): The arguments to be used on the message.
549
+ extra (dict, optional): Extra information to be added to the log. Defaults to None.
550
+ stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
551
+ """
552
+ self._send_to_log(logging.CRITICAL, msg, *args, extra=extra, stacklevel=stacklevel)
553
+
554
+ def debug(self, msg: str, *args: tuple, extra: dict | None = None, stacklevel: int | None = None) -> None:
555
+ """
556
+ Log a message with severity DEBUG on this logger.
557
+ Use stacklevel to show correctly the file and the line of the log, lvl 0 means the python
558
+ logger, lvl 1 means this file, lvl 2 means the place where the log. was placed, if you use
559
+ another file for the log object maybe you need to change to lvl 3.
560
+
561
+ Args:
562
+ msg (str): The message to log.
563
+ *args (tuple): The arguments to be used on the message.
564
+ extra (dict, optional): Extra information to be added to the log. Defaults to None.
565
+ stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
566
+ """
567
+ self._send_to_log(logging.DEBUG, msg, *args, extra=extra, stacklevel=stacklevel)
568
+
569
+ def deprecated( # noqa: D417
570
+ self, msg: str, *args, show_once: bool = True, extra: dict | None = None, stacklevel: int | None = None
571
+ ) -> None:
572
+ """
573
+ Shows a DeprecationWarning message with severity 'WARNING'.
574
+ If show_once is True, then the message will be showed only once.
575
+
576
+ Args:
577
+ msg (str): The message that must be showed.
578
+ show_once (bool, optional): If the message must be showed only once. Defaults to True.
579
+ extra (dict, optional): Extra information to be added to the log. Defaults to None.
580
+ stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
581
+ """
582
+ _id = hash(f'{msg}, {args}')
583
+ if self._show_deprecated(_id=_id, show_once=show_once):
584
+ msg = f'DeprecationWarning: {msg}'
585
+ self.warning(msg, *args, extra=extra, stacklevel=stacklevel)
586
+
587
+ def error(self, msg: str, *args: tuple, extra: dict | None = None, stacklevel: int | None = None) -> None:
588
+ """
589
+ Log a message with severity ERROR on this logger.
590
+ Use stacklevel to show correctly the file and the line of the log, lvl 0 means the python
591
+ logger, lvl 1 means this file, lvl 2 means the place where the log. was placed, if you use
592
+ another file for the log object maybe you need to change to lvl 3.
593
+
594
+ Args:
595
+ msg (str): The message to log.
596
+ *args (tuple): The arguments to be used on the message.
597
+ extra (dict, optional): Extra information to be added to the log. Defaults to None.
598
+ stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
599
+ """
600
+ self._send_to_log(logging.ERROR, msg, *args, extra=extra, stacklevel=stacklevel)
601
+
602
+ def exception(self, msg: str, *args: tuple, extra: dict | None = None, stacklevel: int | None = None) -> None:
603
+ """
604
+ Log a message with severity ERROR on this logger.
605
+ Use stacklevel to show correctly the file and the line of the log, lvl 0 means the python
606
+ logger, lvl 1 means this file, lvl 2 means the place where the log. was placed, if you use
607
+ another file for the log object maybe you need to change to lvl 3.
608
+
609
+ Args:
610
+ msg (str): The message to log.
611
+ *args (tuple): The arguments to be used on the message.
612
+ extra (dict, optional): Extra information to be added to the log. Defaults to None.
613
+ stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
614
+ """
615
+ self.error(msg, *args, extra=extra, stacklevel=stacklevel)
616
+
617
+ def info(self, msg: str, *args: tuple, extra: dict | None = None, stacklevel: int | None = None) -> None:
618
+ """
619
+ Log a message with severity INFO on this logger.
620
+ Use stacklevel to show correctly the file and the line of the log, lvl 0 means the python
621
+ logger, lvl 1 means this file, lvl 2 means the place where the log. was placed, if you use
622
+ another file for the log object maybe you need to change to lvl 3.
623
+
624
+ Args:
625
+ msg (str): The message to log.
626
+ *args (tuple): The arguments to be used on the message.
627
+ extra (dict, optional): Extra information to be added to the log. Defaults to None.
628
+ stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
629
+ """
630
+ self._send_to_log(logging.INFO, msg, *args, extra=extra, stacklevel=stacklevel)
631
+
632
+ def _can_send_slack(self) -> bool:
633
+ """
634
+ Check if the slack message can be sent.
635
+ We keep a track of the last message sent to avoid sending too many messages in less than 3 seconds.
636
+ """
637
+ # We only send the message if we are in PROD and not in unittest
638
+ if settings.PROFILE != 'PROD' or 'unittest' in sys.modules:
639
+ return False
640
+
641
+ # We check if the last message was sent in less than 3 seconds to avoid sending too many messages
642
+ now = DateTime.now(ZoneInfo('UTC')) # pylint: disable=no-member
643
+ if self._slack_timer and (now - self._slack_timer).seconds < 3: # noqa: PLR2004
644
+ return False
645
+
646
+ self._slack_timer = now
647
+ return True
648
+
649
+ def slack(
650
+ self,
651
+ title: str,
652
+ message: str,
653
+ color: str,
654
+ url: str | None = None,
655
+ extra: dict | None = None,
656
+ stacklevel: int | None = None,
657
+ ) -> None:
658
+ """
659
+ Send a message to a Slack channel using Slack WebHooks.
660
+ https://api.slack.com/messaging/webhooks
661
+ The same message will be sent to the default log too:
662
+ danger -> error
663
+ success -> info
664
+ warning -> warning
665
+
666
+ Args:
667
+ title (str): The title of the message.
668
+ message (str): The body of the message.
669
+ color (str): 'danger' | 'success' | 'warning'
670
+ url (str, optional): The slack webhook url. Defaults to settings.SLACK_URL.
671
+ extra (dict, optional): Extra information to be added to the log. Defaults to None.
672
+ stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
673
+ """
674
+ if url is None:
675
+ url = settings.SLACK_URL
676
+
677
+ # We send the message only if url is set and we are in PROD and not in unittest
678
+ if url and self._can_send_slack():
679
+ # The import must be here to avoid circular import inside http module
680
+ from everysk.core.slack import Slack # noqa: PLC0415
681
+
682
+ client = Slack(title=title, message=message, color=color, url=url)
683
+ # This will send the message to Slack without block the request
684
+ Thread(target=client.send).start()
685
+
686
+ log_message = f'Slack message: {title} -> {message}'
687
+ if color == 'danger':
688
+ self.error(log_message, extra=extra, stacklevel=stacklevel)
689
+
690
+ elif color == 'success':
691
+ self.info(log_message, extra=extra, stacklevel=stacklevel)
692
+
693
+ elif color == 'warning':
694
+ self.warning(log_message, extra=extra, stacklevel=stacklevel)
695
+
696
+ def warning(self, msg: str, *args: tuple, extra: dict | None = None, stacklevel: int | None = None) -> None:
697
+ """
698
+ Log a message with severity WARNING on this logger.
699
+ Use stacklevel to show correctly the file and the line of the log, lvl 0 means the python
700
+ logger, lvl 1 means this file, lvl 2 means the place where the log. was placed, if you use
701
+ another file for the log object maybe you need to change to lvl 3.
702
+
703
+ Args:
704
+ msg (str): The message to log.
705
+ *args (tuple): The arguments to be used on the message.
706
+ extra (dict, optional): Extra information to be added to the log. Defaults to None.
707
+ stacklevel (int, optional): The stacklevel to be used on the log. Defaults to 2.
708
+ """
709
+ self._send_to_log(logging.WARNING, msg, *args, extra=extra, stacklevel=stacklevel)