sf-veritas 0.9.7__py3-none-any.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.

Potentially problematic release.


This version of sf-veritas might be problematic. Click here for more details.

Files changed (86) hide show
  1. sf_veritas/.gitignore +2 -0
  2. sf_veritas/__init__.py +4 -0
  3. sf_veritas/app_config.py +49 -0
  4. sf_veritas/cli.py +336 -0
  5. sf_veritas/constants.py +3 -0
  6. sf_veritas/custom_excepthook.py +285 -0
  7. sf_veritas/custom_log_handler.py +53 -0
  8. sf_veritas/custom_output_wrapper.py +107 -0
  9. sf_veritas/custom_print.py +34 -0
  10. sf_veritas/django_app.py +5 -0
  11. sf_veritas/env_vars.py +83 -0
  12. sf_veritas/exception_handling_middleware.py +18 -0
  13. sf_veritas/exception_metaclass.py +69 -0
  14. sf_veritas/frame_tools.py +112 -0
  15. sf_veritas/import_hook.py +62 -0
  16. sf_veritas/infra_details/__init__.py +3 -0
  17. sf_veritas/infra_details/get_infra_details.py +24 -0
  18. sf_veritas/infra_details/kubernetes/__init__.py +3 -0
  19. sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
  20. sf_veritas/infra_details/kubernetes/get_details.py +7 -0
  21. sf_veritas/infra_details/running_on/__init__.py +17 -0
  22. sf_veritas/infra_details/running_on/kubernetes.py +11 -0
  23. sf_veritas/interceptors.py +252 -0
  24. sf_veritas/local_env_detect.py +118 -0
  25. sf_veritas/package_metadata.py +6 -0
  26. sf_veritas/patches/__init__.py +0 -0
  27. sf_veritas/patches/concurrent_futures.py +19 -0
  28. sf_veritas/patches/constants.py +1 -0
  29. sf_veritas/patches/exceptions.py +82 -0
  30. sf_veritas/patches/multiprocessing.py +32 -0
  31. sf_veritas/patches/network_libraries/__init__.py +51 -0
  32. sf_veritas/patches/network_libraries/aiohttp.py +100 -0
  33. sf_veritas/patches/network_libraries/curl_cffi.py +93 -0
  34. sf_veritas/patches/network_libraries/http_client.py +64 -0
  35. sf_veritas/patches/network_libraries/httpcore.py +152 -0
  36. sf_veritas/patches/network_libraries/httplib2.py +76 -0
  37. sf_veritas/patches/network_libraries/httpx.py +123 -0
  38. sf_veritas/patches/network_libraries/niquests.py +192 -0
  39. sf_veritas/patches/network_libraries/pycurl.py +71 -0
  40. sf_veritas/patches/network_libraries/requests.py +187 -0
  41. sf_veritas/patches/network_libraries/tornado.py +139 -0
  42. sf_veritas/patches/network_libraries/treq.py +122 -0
  43. sf_veritas/patches/network_libraries/urllib_request.py +129 -0
  44. sf_veritas/patches/network_libraries/utils.py +101 -0
  45. sf_veritas/patches/os.py +17 -0
  46. sf_veritas/patches/threading.py +32 -0
  47. sf_veritas/patches/web_frameworks/__init__.py +45 -0
  48. sf_veritas/patches/web_frameworks/aiohttp.py +133 -0
  49. sf_veritas/patches/web_frameworks/async_websocket_consumer.py +132 -0
  50. sf_veritas/patches/web_frameworks/blacksheep.py +107 -0
  51. sf_veritas/patches/web_frameworks/bottle.py +142 -0
  52. sf_veritas/patches/web_frameworks/cherrypy.py +246 -0
  53. sf_veritas/patches/web_frameworks/django.py +307 -0
  54. sf_veritas/patches/web_frameworks/eve.py +138 -0
  55. sf_veritas/patches/web_frameworks/falcon.py +229 -0
  56. sf_veritas/patches/web_frameworks/fastapi.py +145 -0
  57. sf_veritas/patches/web_frameworks/flask.py +186 -0
  58. sf_veritas/patches/web_frameworks/klein.py +40 -0
  59. sf_veritas/patches/web_frameworks/litestar.py +217 -0
  60. sf_veritas/patches/web_frameworks/pyramid.py +89 -0
  61. sf_veritas/patches/web_frameworks/quart.py +155 -0
  62. sf_veritas/patches/web_frameworks/robyn.py +114 -0
  63. sf_veritas/patches/web_frameworks/sanic.py +120 -0
  64. sf_veritas/patches/web_frameworks/starlette.py +144 -0
  65. sf_veritas/patches/web_frameworks/strawberry.py +269 -0
  66. sf_veritas/patches/web_frameworks/tornado.py +129 -0
  67. sf_veritas/patches/web_frameworks/utils.py +55 -0
  68. sf_veritas/print_override.py +13 -0
  69. sf_veritas/regular_data_transmitter.py +358 -0
  70. sf_veritas/request_interceptor.py +399 -0
  71. sf_veritas/request_utils.py +104 -0
  72. sf_veritas/server_status.py +1 -0
  73. sf_veritas/shutdown_flag.py +11 -0
  74. sf_veritas/subprocess_startup.py +3 -0
  75. sf_veritas/test_cli.py +145 -0
  76. sf_veritas/thread_local.py +436 -0
  77. sf_veritas/timeutil.py +114 -0
  78. sf_veritas/transmit_exception_to_sailfish.py +28 -0
  79. sf_veritas/transmitter.py +58 -0
  80. sf_veritas/types.py +44 -0
  81. sf_veritas/unified_interceptor.py +323 -0
  82. sf_veritas/utils.py +39 -0
  83. sf_veritas-0.9.7.dist-info/METADATA +83 -0
  84. sf_veritas-0.9.7.dist-info/RECORD +86 -0
  85. sf_veritas-0.9.7.dist-info/WHEEL +4 -0
  86. sf_veritas-0.9.7.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,436 @@
1
+ import builtins
2
+ import threading
3
+ import uuid
4
+ from contextlib import contextmanager
5
+ from contextvars import ContextVar
6
+ from typing import Any, Dict, Optional, Set, Tuple, Union
7
+ from uuid import UUID
8
+
9
+ from . import app_config
10
+ from .constants import NONSESSION_APPLOGS
11
+ from .env_vars import SF_DEBUG
12
+
13
+ # Define context variables
14
+ # You CANNOT switch this for another type - this is the ONLY version that works as of October 2024.
15
+ # Thread variables do not work, nor do globals.
16
+ # See https://elshad-karimov.medium.com/pythons-contextvars-the-most-powerful-feature-you-ve-never-heard-of-d636f4d34030
17
+ trace_id_ctx = ContextVar("trace_id", default=None)
18
+ handled_exceptions_ctx = ContextVar("handled_exceptions", default=set())
19
+ reentrancy_guard_logging_active_ctx = ContextVar(
20
+ "reentrancy_guard_logging_active", default=False
21
+ )
22
+ reentrancy_guard_logging_preactive_ctx = ContextVar(
23
+ "reentrancy_guard_logging_preactive", default=False
24
+ )
25
+ reentrancy_guard_print_active_ctx = ContextVar(
26
+ "reentrancy_guard_print_active", default=False
27
+ )
28
+ reentrancy_guard_print_preactive_ctx = ContextVar(
29
+ "reentrancy_guard_print_preactive", default=False
30
+ )
31
+ reentrancy_guard_exception_active_ctx = ContextVar(
32
+ "reentrancy_guard_exception_active", default=False
33
+ )
34
+ reentrancy_guard_exception_preactive_ctx = ContextVar(
35
+ "reentrancy_guard_exception_preactive", default=False
36
+ )
37
+ suppress_network_recording_ctx = ContextVar("suppress_network_recording", default=False)
38
+ reentrancy_guard_sys_stdout_active_ctx = ContextVar(
39
+ "reentrancy_guard_sys_stdout_active", default=False
40
+ )
41
+
42
+ # Thread-local storage as a fallback
43
+ _thread_locals = threading.local()
44
+
45
+ _shared_trace_registry = {}
46
+ _shared_trace_registry_lock = threading.RLock()
47
+
48
+
49
+ def _set_shared_trace_id(trace_id: str) -> None:
50
+ with _shared_trace_registry_lock:
51
+ _shared_trace_registry["trace_id"] = trace_id
52
+
53
+
54
+ def _get_shared_trace_id() -> Optional[str]:
55
+ with _shared_trace_registry_lock:
56
+ return _shared_trace_registry.get("trace_id")
57
+
58
+
59
+ # Generalized get, set, and get_or_set functions for all properties
60
+ def _get_context_or_thread_local(
61
+ ctx_var: ContextVar, attr_name: str, default: Any
62
+ ) -> Any:
63
+ return ctx_var.get() or getattr(_thread_locals, attr_name, default)
64
+
65
+
66
+ def _set_context_and_thread_local(
67
+ ctx_var: ContextVar, attr_name: str, value: Any
68
+ ) -> Any:
69
+ ctx_var.set(value)
70
+ setattr(_thread_locals, attr_name, value)
71
+ return value
72
+
73
+
74
+ def unset_sf_trace_id() -> None:
75
+ """
76
+ Fully unsets the Sailfish trace ID from contextvars, thread-local storage,
77
+ and the shared trace registry.
78
+ """
79
+ _set_shared_trace_id(None)
80
+ _set_context_and_thread_local(trace_id_ctx, "trace_id", None)
81
+ if SF_DEBUG:
82
+ print("[[DEBUG]] unset_sf_trace_id: trace_id cleared", log=False)
83
+
84
+
85
+ def _get_or_set_context_and_thread_local(
86
+ ctx_var: ContextVar, attr_name: str, value_if_not_set
87
+ ) -> Tuple[bool, Any]:
88
+ value = ctx_var.get() or getattr(_thread_locals, attr_name, None)
89
+ if value is None:
90
+ return _set_context_and_thread_local(ctx_var, attr_name, value_if_not_set)
91
+ return value
92
+
93
+
94
+ # Trace ID functions
95
+ def get_sf_trace_id() -> Optional[Union[str, UUID]]:
96
+ shared_trace_id = _get_shared_trace_id()
97
+ if shared_trace_id:
98
+ return shared_trace_id
99
+ return _get_context_or_thread_local(trace_id_ctx, "trace_id", None)
100
+
101
+
102
+ def set_sf_trace_id(trace_id: Union[str, UUID]) -> Union[str, UUID]:
103
+ _set_shared_trace_id(str(trace_id))
104
+ return _set_context_and_thread_local(trace_id_ctx, "trace_id", trace_id)
105
+
106
+
107
+ def get_or_set_sf_trace_id(
108
+ new_trace_id_if_not_set: Optional[str] = None,
109
+ is_associated_with_inbound_request: bool = False,
110
+ ) -> Tuple[bool, Union[str, UUID]]:
111
+ if new_trace_id_if_not_set:
112
+ if SF_DEBUG:
113
+ print(
114
+ f"[trace_id] Setting new trace_id from argument: {new_trace_id_if_not_set}",
115
+ log=False,
116
+ )
117
+ set_sf_trace_id(new_trace_id_if_not_set)
118
+ return True, new_trace_id_if_not_set
119
+
120
+ trace_id = get_sf_trace_id()
121
+ if trace_id:
122
+ if SF_DEBUG:
123
+ print(f"[trace_id] Returning existing trace_id: {trace_id}", log=False)
124
+ return False, trace_id
125
+
126
+ if SF_DEBUG:
127
+ print("[trace_id] No trace_id found. Generating new trace_id.", log=False)
128
+ unique_id = uuid.uuid4()
129
+ trace_id = f"{NONSESSION_APPLOGS}-v3/{app_config._sailfish_api_key}/{unique_id}"
130
+ set_sf_trace_id(trace_id)
131
+ if SF_DEBUG:
132
+ print(f"[trace_id] Generated and set new trace_id: {trace_id}", log=False)
133
+ return True, trace_id
134
+
135
+
136
+ # Handled exceptions functions
137
+ def get_handled_exceptions() -> Set[Any]:
138
+ return _get_context_or_thread_local(
139
+ handled_exceptions_ctx, "handled_exceptions", set()
140
+ )
141
+
142
+
143
+ def set_handled_exceptions(exceptions_set: Set[Any]) -> Set[Any]:
144
+ return _set_context_and_thread_local(
145
+ handled_exceptions_ctx, "handled_exceptions", exceptions_set
146
+ )
147
+
148
+
149
+ def get_or_set_handled_exceptions(default: set = None) -> Tuple[bool, Set[Any]]:
150
+ if default is None:
151
+ default = set()
152
+ return _get_or_set_context_and_thread_local(
153
+ handled_exceptions_ctx, "handled_exceptions", default
154
+ )
155
+
156
+
157
+ def mark_exception_handled(exception) -> None:
158
+ """
159
+ Marks an exception as handled to avoid duplicate processing.
160
+ """
161
+ handled_exceptions = get_handled_exceptions()
162
+ handled_exceptions.add(id(exception))
163
+ set_handled_exceptions(handled_exceptions)
164
+
165
+ # Set the `_handled` attribute on the exception if it exists
166
+ if hasattr(exception, "_handled"):
167
+ setattr(exception, "_handled", True)
168
+
169
+
170
+ def has_handled_exception(exception) -> bool:
171
+ """
172
+ Checks if an exception has been handled.
173
+ """
174
+ # Check both thread-local context and the `_handled` attribute
175
+ return id(exception) in get_handled_exceptions() or getattr(
176
+ exception, "_handled", False
177
+ )
178
+
179
+
180
+ def reset_handled_exceptions() -> Set[Any]:
181
+ return set_handled_exceptions(set())
182
+
183
+
184
+ # Reentrancy guards functions (logging)
185
+ def get_reentrancy_guard_logging_active() -> bool:
186
+ return _get_context_or_thread_local(
187
+ reentrancy_guard_logging_active_ctx, "reentrancy_guard_logging_active", False
188
+ )
189
+
190
+
191
+ def set_reentrancy_guard_logging_active(value: bool) -> bool:
192
+ return _set_context_and_thread_local(
193
+ reentrancy_guard_logging_active_ctx, "reentrancy_guard_logging_active", value
194
+ )
195
+
196
+
197
+ def get_or_set_reentrancy_guard_logging_active(
198
+ value_if_not_set: bool,
199
+ ) -> Tuple[bool, bool]:
200
+ return _get_or_set_context_and_thread_local(
201
+ reentrancy_guard_logging_active_ctx,
202
+ "reentrancy_guard_logging_active",
203
+ value_if_not_set,
204
+ )
205
+
206
+
207
+ def activate_reentrancy_guards_logging() -> bool:
208
+ set_reentrancy_guard_logging_active(True)
209
+ set_reentrancy_guard_logging_preactive(True)
210
+ return True
211
+
212
+
213
+ def get_reentrancy_guard_logging_preactive() -> bool:
214
+ return _get_context_or_thread_local(
215
+ reentrancy_guard_logging_preactive_ctx,
216
+ "reentrancy_guard_logging_preactive",
217
+ False,
218
+ )
219
+
220
+
221
+ def set_reentrancy_guard_logging_preactive(value: bool) -> bool:
222
+ return _set_context_and_thread_local(
223
+ reentrancy_guard_logging_preactive_ctx,
224
+ "reentrancy_guard_logging_preactive",
225
+ value,
226
+ )
227
+
228
+
229
+ def get_or_set_reentrancy_guard_logging_preactive(
230
+ value_if_not_set: bool,
231
+ ) -> Tuple[bool, bool]:
232
+ return _get_or_set_context_and_thread_local(
233
+ reentrancy_guard_logging_preactive_ctx,
234
+ "reentrancy_guard_logging_preactive",
235
+ value_if_not_set,
236
+ )
237
+
238
+
239
+ def activate_reentrancy_guards_logging_preactive() -> bool:
240
+ return set_reentrancy_guard_logging_preactive(True)
241
+
242
+
243
+ # Reentrancy guards functions (stdout)
244
+ def get_reentrancy_guard_sys_stdout_active() -> bool:
245
+ return _get_context_or_thread_local(
246
+ reentrancy_guard_sys_stdout_active_ctx,
247
+ "reentrancy_guard_sys_stdout_active",
248
+ False,
249
+ )
250
+
251
+
252
+ def set_reentrancy_guard_sys_stdout_active(value: bool) -> bool:
253
+ return _set_context_and_thread_local(
254
+ reentrancy_guard_sys_stdout_active_ctx,
255
+ "reentrancy_guard_sys_stdout_active",
256
+ value,
257
+ )
258
+
259
+
260
+ def activate_reentrancy_guards_sys_stdout() -> bool:
261
+ set_reentrancy_guard_sys_stdout_active(True)
262
+ return True
263
+
264
+
265
+ # Reentrancy guards functions (print)
266
+ def get_reentrancy_guard_print_active() -> bool:
267
+ return _get_context_or_thread_local(
268
+ reentrancy_guard_print_active_ctx, "reentrancy_guard_print_active", False
269
+ )
270
+
271
+
272
+ def set_reentrancy_guard_print_active(value: bool) -> bool:
273
+ return _set_context_and_thread_local(
274
+ reentrancy_guard_print_active_ctx, "reentrancy_guard_print_active", value
275
+ )
276
+
277
+
278
+ def get_or_set_reentrancy_guard_print_active(
279
+ value_if_not_set: bool,
280
+ ) -> Tuple[bool, bool]:
281
+ return _get_or_set_context_and_thread_local(
282
+ reentrancy_guard_print_active_ctx,
283
+ "reentrancy_guard_print_active",
284
+ value_if_not_set,
285
+ )
286
+
287
+
288
+ def activate_reentrancy_guards_print() -> bool:
289
+ set_reentrancy_guard_print_active(True)
290
+ set_reentrancy_guard_print_preactive(True)
291
+ return True
292
+
293
+
294
+ def get_reentrancy_guard_print_preactive() -> bool:
295
+ return _get_context_or_thread_local(
296
+ reentrancy_guard_print_preactive_ctx, "reentrancy_guard_print_preactive", False
297
+ )
298
+
299
+
300
+ def set_reentrancy_guard_print_preactive(value: bool) -> bool:
301
+ return _set_context_and_thread_local(
302
+ reentrancy_guard_print_preactive_ctx, "reentrancy_guard_print_preactive", value
303
+ )
304
+
305
+
306
+ def get_or_set_reentrancy_guard_print_preactive(
307
+ value_if_not_set: bool,
308
+ ) -> Tuple[bool, bool]:
309
+ return _get_or_set_context_and_thread_local(
310
+ reentrancy_guard_print_preactive_ctx,
311
+ "reentrancy_guard_print_preactive",
312
+ value_if_not_set,
313
+ )
314
+
315
+
316
+ def activate_reentrancy_guards_print_preactive() -> bool:
317
+ return set_reentrancy_guard_print_preactive(True)
318
+
319
+
320
+ # Reentrancy guards functions (exception)
321
+ def get_reentrancy_guard_exception_active() -> bool:
322
+ return _get_context_or_thread_local(
323
+ reentrancy_guard_exception_active_ctx,
324
+ "reentrancy_guard_exception_active",
325
+ False,
326
+ )
327
+
328
+
329
+ def set_reentrancy_guard_exception_active(value: bool) -> bool:
330
+ return _set_context_and_thread_local(
331
+ reentrancy_guard_exception_active_ctx,
332
+ "reentrancy_guard_exception_active",
333
+ value,
334
+ )
335
+
336
+
337
+ def get_or_set_reentrancy_guard_exception_active(
338
+ value_if_not_set: bool,
339
+ ) -> Tuple[bool, bool]:
340
+ return _get_or_set_context_and_thread_local(
341
+ reentrancy_guard_exception_active_ctx,
342
+ "reentrancy_guard_exception_active",
343
+ value_if_not_set,
344
+ )
345
+
346
+
347
+ def activate_reentrancy_guards_exception() -> bool:
348
+ set_reentrancy_guard_exception_active(True)
349
+ set_reentrancy_guard_exception_preactive(True)
350
+ return True
351
+
352
+
353
+ def get_reentrancy_guard_exception_preactive() -> bool:
354
+ return _get_context_or_thread_local(
355
+ reentrancy_guard_exception_preactive_ctx,
356
+ "reentrancy_guard_exception_preactive",
357
+ False,
358
+ )
359
+
360
+
361
+ def set_reentrancy_guard_exception_preactive(value: bool) -> bool:
362
+ return _set_context_and_thread_local(
363
+ reentrancy_guard_exception_preactive_ctx,
364
+ "reentrancy_guard_exception_preactive",
365
+ value,
366
+ )
367
+
368
+
369
+ def get_or_set_reentrancy_guard_exception_preactive(
370
+ value_if_not_set: bool,
371
+ ) -> Tuple[bool, bool]:
372
+ return _get_or_set_context_and_thread_local(
373
+ reentrancy_guard_exception_preactive_ctx,
374
+ "reentrancy_guard_exception_preactive",
375
+ value_if_not_set,
376
+ )
377
+
378
+
379
+ def activate_reentrancy_guards_exception_preactive() -> bool:
380
+ return set_reentrancy_guard_exception_preactive(True)
381
+
382
+
383
+ # Get and set context
384
+ def get_context() -> Dict[str, Any]:
385
+ """Get the current context values for all properties."""
386
+ return {
387
+ "trace_id": get_sf_trace_id(),
388
+ "handled_exceptions": get_handled_exceptions(),
389
+ "reentrancy_guard_logging_active": get_reentrancy_guard_logging_active(),
390
+ "reentrancy_guard_logging_preactive": get_reentrancy_guard_logging_preactive(),
391
+ "reentrancy_guard_print_active": get_reentrancy_guard_print_active(),
392
+ "reentrancy_guard_print_preactive": get_reentrancy_guard_print_preactive(),
393
+ "reentrancy_guard_exception_active": get_reentrancy_guard_exception_active(),
394
+ "reentrancy_guard_exception_preactive": get_reentrancy_guard_exception_preactive(),
395
+ "reentrancy_guard_sys_stdout_active": get_reentrancy_guard_sys_stdout_active(),
396
+ }
397
+
398
+
399
+ def set_context(context) -> None:
400
+ """Set the current context values for all properties."""
401
+ set_sf_trace_id(context.get("trace_id"))
402
+ set_handled_exceptions(context.get("handled_exceptions", set()))
403
+ set_reentrancy_guard_logging_active(
404
+ context.get("reentrancy_guard_logging_active", False)
405
+ )
406
+ set_reentrancy_guard_logging_preactive(
407
+ context.get("reentrancy_guard_logging_preactive", False)
408
+ )
409
+ set_reentrancy_guard_print_active(
410
+ context.get("reentrancy_guard_print_active", False)
411
+ )
412
+ set_reentrancy_guard_print_preactive(
413
+ context.get("reentrancy_guard_print_preactive", False)
414
+ )
415
+ set_reentrancy_guard_exception_active(
416
+ context.get("reentrancy_guard_exception_active", False)
417
+ )
418
+ set_reentrancy_guard_exception_preactive(
419
+ context.get("reentrancy_guard_exception_preactive", False)
420
+ )
421
+ set_reentrancy_guard_sys_stdout_active(
422
+ context.get("reentrancy_guard_sys_stdout_active", False)
423
+ )
424
+
425
+
426
+ @contextmanager
427
+ def suppress_network_recording():
428
+ token = suppress_network_recording_ctx.set(True)
429
+ try:
430
+ yield
431
+ finally:
432
+ suppress_network_recording_ctx.reset(token)
433
+
434
+
435
+ def is_network_recording_suppressed() -> bool:
436
+ return suppress_network_recording_ctx.get()
sf_veritas/timeutil.py ADDED
@@ -0,0 +1,114 @@
1
+ import inspect
2
+ import threading
3
+ import time
4
+ from datetime import datetime, timedelta, timezone
5
+ from time import sleep
6
+ from typing import Tuple
7
+
8
+ import ntplib
9
+ import requests
10
+ from dateutil import parser
11
+ from .env_vars import PRINT_CONFIGURATION_STATUSES
12
+
13
+
14
+ def is_log_argument_supported():
15
+ try:
16
+ signature = inspect.signature(print)
17
+ return "log" in signature.parameters
18
+ except (TypeError, ValueError) as err:
19
+ return False
20
+
21
+
22
+ # Determine if the `log` argument is supported
23
+ PRINT_ARGS = {"log": False} if is_log_argument_supported() else {}
24
+
25
+
26
+ class TimeSync:
27
+ _instance = None
28
+ _lock = threading.Lock()
29
+
30
+ @classmethod
31
+ def get_instance(cls):
32
+ if not cls._instance:
33
+ with cls._lock:
34
+ if not cls._instance:
35
+ cls._instance = cls() # Initialize singleton
36
+ return cls._instance
37
+
38
+ def __init__(self):
39
+ if getattr(self, "_initialized", False): # Avoid reinitialization
40
+ return
41
+ if PRINT_CONFIGURATION_STATUSES:
42
+ print("Configurating global time lock", **PRINT_ARGS)
43
+ self.sync_utc_datetime, self.local_time_ms = self._fetch_utc_time()
44
+ self._initialized = True
45
+ if PRINT_CONFIGURATION_STATUSES:
46
+ print("Configurating global time lock...DONE", **PRINT_ARGS)
47
+
48
+ def _fetch_utc_time(self) -> Tuple[datetime, int]:
49
+ try:
50
+ return self._fetch_with_retries(self._fetch_ntp_time)
51
+ except Exception as e:
52
+ if PRINT_CONFIGURATION_STATUSES:
53
+ print(f"NTP sync failed: {e}. Falling back to HTTP APIs.", **PRINT_ARGS)
54
+ return self._fetch_with_retries(self._fetch_utc_time_from_apis)
55
+
56
+ def _fetch_with_retries(
57
+ self, fetch_func, retries=3, backoff_factor=1
58
+ ) -> Tuple[datetime, int]:
59
+ for attempt in range(retries):
60
+ try:
61
+ return fetch_func()
62
+ except Exception as e:
63
+ if PRINT_CONFIGURATION_STATUSES:
64
+ print(f"Attempt {attempt + 1} failed: {e}", **PRINT_ARGS)
65
+ sleep(backoff_factor * (2**attempt))
66
+ raise Exception("All retries failed")
67
+
68
+ def _fetch_ntp_time(self) -> Tuple[datetime, int]:
69
+ client = ntplib.NTPClient()
70
+ response = client.request("pool.ntp.org", version=3)
71
+ utc_datetime = datetime.utcfromtimestamp(response.tx_time).replace(
72
+ tzinfo=timezone.utc
73
+ )
74
+ local_time_ms = int(time.time() * 1000)
75
+ if PRINT_CONFIGURATION_STATUSES:
76
+ print("Time successfully synced using NTP.", **PRINT_ARGS)
77
+ return utc_datetime, local_time_ms
78
+
79
+ def _fetch_utc_time_from_apis(self) -> Tuple[datetime, int]:
80
+ apis = [
81
+ "https://worldtimeapi.org/api/timezone/Etc/UTC",
82
+ "http://worldclockapi.com/api/json/utc/now",
83
+ ]
84
+ for api in apis:
85
+ try:
86
+ response = requests.get(api, timeout=5)
87
+ response.raise_for_status()
88
+ data = response.json()
89
+ if "datetime" in data:
90
+ utc_datetime = parser.isoparse(data["datetime"])
91
+ elif "currentDateTime" in data:
92
+ utc_datetime = parser.isoparse(data["currentDateTime"])
93
+ else:
94
+ raise ValueError("Unexpected API response format")
95
+ local_time_ms = int(time.time() * 1000)
96
+ if PRINT_CONFIGURATION_STATUSES:
97
+ print(f"Time successfully synced using {api}.", **PRINT_ARGS)
98
+ return utc_datetime, local_time_ms
99
+ except Exception as e:
100
+ if PRINT_CONFIGURATION_STATUSES:
101
+ print(f"Failed to fetch time from {api}: {e}", **PRINT_ARGS)
102
+ if PRINT_CONFIGURATION_STATUSES:
103
+ print("All time sources failed. Falling back to system time.", **PRINT_ARGS)
104
+ utc_datetime = datetime.now(timezone.utc)
105
+ local_time_ms = int(time.time() * 1000)
106
+ return utc_datetime, local_time_ms
107
+
108
+ def get_utc_time_in_ms(self) -> int:
109
+ current_local_time_ms = int(time.time() * 1000)
110
+ elapsed_ms = current_local_time_ms - self.local_time_ms
111
+ current_utc_datetime = self.sync_utc_datetime + timedelta(
112
+ milliseconds=elapsed_ms
113
+ )
114
+ return int(current_utc_datetime.timestamp() * 1000)
@@ -0,0 +1,28 @@
1
+ from .custom_excepthook import transmit_exception
2
+
3
+
4
+ def transmit_exception_to_sailfish(
5
+ exc: BaseException,
6
+ force_transmit: bool = False,
7
+ ):
8
+ """
9
+ Transmit an exception to Sailfish using the original traceback captured at the
10
+ point the exception was raised.
11
+
12
+ :param exc: The exception instance.
13
+ :param force_transmit: If True, will transmit even if the exception might
14
+ have already been handled or flagged.
15
+ """
16
+ # Get the exception type and traceback from the exception itself
17
+ exc_type = type(exc)
18
+ exc_traceback = exc.__traceback__ # Automatically fetch the original traceback
19
+
20
+ # In some implementations, you might keep a `_handled` attribute to avoid double transmission.
21
+ if not force_transmit and getattr(exc, "_handled", False):
22
+ return # Already transmitted
23
+
24
+ # Actually send it over to Sailfish
25
+ transmit_exception(exc_type, exc, exc_traceback)
26
+
27
+ # Mark as handled to avoid re-transmission
28
+ setattr(exc, "_handled", True)
@@ -0,0 +1,58 @@
1
+ from typing import Any, Dict, Optional
2
+
3
+ from .env_vars import SF_DEBUG
4
+ from .interceptors import CollectMetadataTransmitter
5
+ from .thread_local import get_or_set_sf_trace_id
6
+
7
+ collect_metadata_transmitter = CollectMetadataTransmitter()
8
+
9
+
10
+ class SailfishTransmitter(object):
11
+ @classmethod
12
+ def identify(
13
+ cls,
14
+ user_id: str,
15
+ traits: Optional[Dict[str, Any]] = None,
16
+ traits_json: Optional[str] = None,
17
+ override: bool = False,
18
+ ) -> None:
19
+ if traits is not None or traits_json is not None:
20
+ return cls.add_or_update_metadata(user_id, traits, traits_json, override)
21
+ return cls.add_or_update_metadata(user_id, dict(), override=override)
22
+
23
+ @classmethod
24
+ def add_or_update_metadata(
25
+ cls,
26
+ user_id: str,
27
+ traits: Optional[Dict[str, Any]] = None,
28
+ traits_json: Optional[str] = None,
29
+ override: bool = False,
30
+ ) -> None:
31
+ """
32
+ Sets traits and sends to the Sailfish AI backend
33
+
34
+ Args:
35
+ user_id: unique identifier for the user; common uses are username or email
36
+ traits: dictionary of contents to add or update in the user's traits. Defaults to None.
37
+ traits_json: json string of contents to add or update in the user's traits. Defaults to None.
38
+ """
39
+ if traits is None and traits_json is None:
40
+ raise Exception(
41
+ 'Must pass in either traits or traits_json to "add_or_update_traits"'
42
+ )
43
+ if SF_DEBUG:
44
+ print(
45
+ "[[DEBUG - add_or_update_traits]] starting thread [[/DEBUG]]", log=False
46
+ )
47
+
48
+ _, trace_id = get_or_set_sf_trace_id()
49
+ if SF_DEBUG:
50
+ print(
51
+ "add_or_update_metadata...SENDING DATA...args=",
52
+ (user_id, traits, traits_json, override, trace_id),
53
+ trace_id,
54
+ log=False,
55
+ )
56
+ collect_metadata_transmitter.do_send(
57
+ (user_id, traits, traits_json, override, trace_id), trace_id
58
+ )
sf_veritas/types.py ADDED
@@ -0,0 +1,44 @@
1
+ import json
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, Dict, List, Optional
4
+
5
+
6
+ @dataclass
7
+ class FrameInfo:
8
+ file: str
9
+ line: int
10
+ function: str
11
+ code: str
12
+ locals: Optional[Dict[str, Any]] = field(default_factory=dict)
13
+ offender: Optional[bool] = False
14
+
15
+ def to_dict(self) -> Dict[str, Any]:
16
+ frame_info_dict = {
17
+ "file": self.file,
18
+ "line": self.line,
19
+ "function": self.function,
20
+ "code": self.code,
21
+ }
22
+ if self.locals:
23
+ frame_info_dict["locals"] = {k: str(v) for k, v in self.locals.items()}
24
+ if self.offender:
25
+ frame_info_dict["offender"] = self.offender
26
+ return frame_info_dict
27
+
28
+ def to_json(self) -> str:
29
+ return json.dumps(self.to_dict())
30
+
31
+
32
+ def get_trace_from_json(data_json) -> List[FrameInfo]:
33
+ data = json.loads(data_json)
34
+ return [FrameInfo(**item) for item in data]
35
+
36
+
37
+ class CustomJSONEncoderForFrameInfo(json.JSONEncoder):
38
+ def default(self, obj):
39
+ if isinstance(obj, FrameInfo):
40
+ return obj.to_dict()
41
+ try:
42
+ return super().default(obj)
43
+ except TypeError:
44
+ return str(obj)