getfaultline 0.1.0__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.
faultline/__init__.py ADDED
@@ -0,0 +1,405 @@
1
+ """Faultline Python SDK: public API and composition root.
2
+
3
+ This is the only module that wires the application, infrastructure and integration layers
4
+ together. Every public function is exception-safe: the SDK never raises into the host app
5
+ (except for exceptions raised by your own code inside ``with_scope()``).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import atexit
11
+ import sys
12
+ import threading
13
+ from collections.abc import Iterator, Mapping, Sequence
14
+ from contextlib import contextmanager
15
+ from typing import Any, Callable, Dict, List, Optional, TypeVar
16
+
17
+ from ._version import __version__
18
+ from .application import CaptureHint, ClientDeps, FaultlineClient, Hub, Integration, get_hub
19
+ from .domain import (
20
+ BeforeSend,
21
+ Breadcrumb,
22
+ Event,
23
+ ExceptionPayload,
24
+ IgnorePattern,
25
+ Level,
26
+ RequestData,
27
+ Scope,
28
+ StackFrame,
29
+ User,
30
+ is_level,
31
+ resolve_options,
32
+ safe_str,
33
+ )
34
+ from .infrastructure import (
35
+ HttpTransport,
36
+ LinecacheSourceReader,
37
+ PythonRuntimeInfo,
38
+ StderrLogger,
39
+ StdRandom,
40
+ SystemClock,
41
+ TracebackStackExtractor,
42
+ UuidGenerator,
43
+ )
44
+ from .integrations import (
45
+ ExcepthookIntegration,
46
+ LoggingIntegration,
47
+ StdlibHttpIntegration,
48
+ ThreadingIntegration,
49
+ )
50
+
51
+ __all__ = [
52
+ "BeforeSend",
53
+ "Breadcrumb",
54
+ "Event",
55
+ "ExceptionPayload",
56
+ "Level",
57
+ "LoggingIntegration",
58
+ "RequestData",
59
+ "Scope",
60
+ "StackFrame",
61
+ "User",
62
+ "__version__",
63
+ "add_breadcrumb",
64
+ "capture_exception",
65
+ "capture_message",
66
+ "close",
67
+ "flush",
68
+ "get_current_scope",
69
+ "init",
70
+ "is_enabled",
71
+ "set_context",
72
+ "set_extra",
73
+ "set_tag",
74
+ "set_tags",
75
+ "set_user",
76
+ "with_scope",
77
+ ]
78
+
79
+ SDK_VERSION = __version__
80
+ ATEXIT_TIMEOUT = 2.0
81
+
82
+ _T = TypeVar("_T")
83
+
84
+ _INTEGRATION_FACTORIES: Dict[str, Callable[[], Integration]] = {
85
+ "excepthook": ExcepthookIntegration,
86
+ "threading": ThreadingIntegration,
87
+ "logging": LoggingIntegration,
88
+ "http": StdlibHttpIntegration,
89
+ }
90
+
91
+ _init_lock = threading.Lock()
92
+ _atexit_registered = False
93
+
94
+
95
+ def _hub() -> Hub:
96
+ return get_hub()
97
+
98
+
99
+ def _guard(fallback: _T, fn: Callable[[], _T]) -> _T:
100
+ try:
101
+ return fn()
102
+ except Exception as err:
103
+ try:
104
+ _hub().logger.error("internal SDK error", err)
105
+ except Exception:
106
+ pass
107
+ return fallback
108
+
109
+
110
+ def _resolve_integrations(settings: Mapping[str, Any], logger: StderrLogger) -> List[Integration]:
111
+ out: List[Integration] = []
112
+ for name, factory in _INTEGRATION_FACTORIES.items():
113
+ setting = settings.get(name, False)
114
+ if setting is True:
115
+ out.append(factory())
116
+ elif setting is False:
117
+ continue
118
+ elif hasattr(setting, "setup") and hasattr(setting, "teardown"):
119
+ out.append(setting)
120
+ else:
121
+ logger.warn(f'integrations["{name}"] must be a bool or an integration instance')
122
+ return out
123
+
124
+
125
+ def _on_exit() -> None:
126
+ try:
127
+ _hub().close(ATEXIT_TIMEOUT)
128
+ except Exception:
129
+ pass
130
+
131
+
132
+ def init(
133
+ api_key: Optional[str] = None,
134
+ *,
135
+ endpoint: Optional[str] = None,
136
+ environment: Optional[str] = None,
137
+ release: Optional[str] = None,
138
+ server_name: Optional[str] = None,
139
+ sample_rate: Optional[float] = None,
140
+ max_breadcrumbs: Optional[int] = None,
141
+ send_default_pii: Optional[bool] = None,
142
+ before_send: Optional[BeforeSend] = None,
143
+ ignore_errors: Optional[Sequence[IgnorePattern]] = None,
144
+ integrations: Optional[Mapping[str, Any]] = None,
145
+ enabled: Optional[bool] = None,
146
+ debug: Optional[bool] = None,
147
+ **unknown: Any,
148
+ ) -> None:
149
+ """Initialize the SDK. Calling it again replaces the previous client. Never raises.
150
+
151
+ ``integrations`` maps ``"excepthook"``, ``"threading"``, ``"logging"`` and ``"http"`` to
152
+ ``True`` / ``False`` or to a configured instance, e.g.
153
+ ``{"logging": LoggingIntegration(capture_errors=True)}``.
154
+ """
155
+
156
+ def run() -> None:
157
+ global _atexit_registered
158
+ with _init_lock:
159
+ hub = _hub()
160
+ previous = hub.client
161
+ hub.teardown_integrations()
162
+ hub.bind_client(None)
163
+ if previous is not None:
164
+ # Let the old client deliver what it already queued, without blocking init.
165
+ threading.Thread(
166
+ target=previous.close,
167
+ args=(ATEXIT_TIMEOUT,),
168
+ name="faultline-close",
169
+ daemon=True,
170
+ ).start()
171
+
172
+ raw: Dict[str, Any] = {
173
+ "api_key": api_key,
174
+ "endpoint": endpoint,
175
+ "environment": environment,
176
+ "release": release,
177
+ "server_name": server_name,
178
+ "sample_rate": sample_rate,
179
+ "max_breadcrumbs": max_breadcrumbs,
180
+ "send_default_pii": send_default_pii,
181
+ "before_send": before_send,
182
+ "ignore_errors": ignore_errors,
183
+ "integrations": integrations,
184
+ "enabled": enabled,
185
+ "debug": debug,
186
+ }
187
+ result = resolve_options(raw)
188
+ logger = StderrLogger(result.debug)
189
+ for key in unknown:
190
+ logger.warn(f'unknown option "{key}"; ignored')
191
+ for w in result.warnings:
192
+ logger.warn(f"invalid option: {w}")
193
+ opts = result.options
194
+ if opts is None:
195
+ logger.error("SDK disabled: " + "; ".join(result.errors))
196
+ return
197
+ if not opts.enabled:
198
+ logger.debug("SDK disabled (enabled=False)")
199
+ return
200
+
201
+ transport = HttpTransport(opts.endpoint, opts.api_key, logger)
202
+ client = FaultlineClient(
203
+ opts,
204
+ ClientDeps(
205
+ transport=transport,
206
+ stack=TracebackStackExtractor(),
207
+ source=LinecacheSourceReader(),
208
+ clock=SystemClock(),
209
+ ids=UuidGenerator(),
210
+ random=StdRandom(),
211
+ logger=logger,
212
+ runtime=PythonRuntimeInfo(),
213
+ sdk_version=SDK_VERSION,
214
+ ),
215
+ )
216
+ hub.bind_client(client)
217
+ hub.install_integrations(_resolve_integrations(opts.integrations, logger))
218
+ if not _atexit_registered:
219
+ atexit.register(_on_exit)
220
+ _atexit_registered = True
221
+ logger.debug(f"initialized (endpoint {opts.endpoint}, sdk {SDK_VERSION})")
222
+
223
+ _guard(None, run)
224
+
225
+
226
+ def _hint(
227
+ tags: Optional[Mapping[str, Any]],
228
+ extra: Optional[Mapping[str, Any]],
229
+ user: Optional[User],
230
+ level: Optional[str],
231
+ fingerprint: Optional[Sequence[str]],
232
+ contexts: Optional[Mapping[str, Mapping[str, Any]]],
233
+ ) -> CaptureHint:
234
+ return CaptureHint(
235
+ tags={str(k): safe_str(v) for k, v in tags.items()} if isinstance(tags, Mapping) else None,
236
+ extra=extra if isinstance(extra, Mapping) else None,
237
+ user=user if isinstance(user, Mapping) else None,
238
+ level=level if level is not None and is_level(level) else None,
239
+ fingerprint=[safe_str(x) for x in fingerprint]
240
+ if isinstance(fingerprint, (list, tuple))
241
+ else None,
242
+ contexts=contexts if isinstance(contexts, Mapping) else None,
243
+ )
244
+
245
+
246
+ def capture_exception(
247
+ exc: Any = None,
248
+ *,
249
+ tags: Optional[Mapping[str, Any]] = None,
250
+ extra: Optional[Mapping[str, Any]] = None,
251
+ user: Optional[User] = None,
252
+ level: Optional[Level] = None,
253
+ fingerprint: Optional[Sequence[str]] = None,
254
+ contexts: Optional[Mapping[str, Mapping[str, Any]]] = None,
255
+ **unknown: Any,
256
+ ) -> Optional[str]:
257
+ """Capture an exception and return its event id (``None`` if dropped or disabled).
258
+
259
+ ``exc`` defaults to the exception currently being handled (``sys.exc_info()``). It may
260
+ also be an ``exc_info`` tuple or any non-exception value (captured as a synthetic error).
261
+ """
262
+
263
+ def run() -> Optional[str]:
264
+ value: Any = exc
265
+ if value is None:
266
+ value = sys.exc_info()[1]
267
+ if value is None:
268
+ _hub().logger.debug("capture_exception called without an active exception")
269
+ return None
270
+ elif isinstance(value, tuple) and len(value) == 3:
271
+ value = value[1]
272
+ hint = _hint(tags, extra, user, level, fingerprint, contexts)
273
+ return _hub().capture_exception(value, hint)
274
+
275
+ return _guard(None, run)
276
+
277
+
278
+ def capture_message(
279
+ message: str,
280
+ level: Level = "info",
281
+ *,
282
+ tags: Optional[Mapping[str, Any]] = None,
283
+ extra: Optional[Mapping[str, Any]] = None,
284
+ user: Optional[User] = None,
285
+ fingerprint: Optional[Sequence[str]] = None,
286
+ contexts: Optional[Mapping[str, Mapping[str, Any]]] = None,
287
+ **unknown: Any,
288
+ ) -> Optional[str]:
289
+ """Capture a plain message (default level ``"info"``) and return its event id."""
290
+
291
+ def run() -> Optional[str]:
292
+ lvl = level if is_level(level) else "info"
293
+ hint = _hint(tags, extra, user, lvl, fingerprint, contexts)
294
+ return _hub().capture_message(message, lvl, hint)
295
+
296
+ return _guard(None, run)
297
+
298
+
299
+ def set_user(user: Optional[User]) -> None:
300
+ """Set (or, with ``None``, clear) the user on the current scope."""
301
+ _guard(None, lambda: _hub().set_user(user if isinstance(user, Mapping) and user else None))
302
+
303
+
304
+ def set_tag(key: str, value: Any) -> None:
305
+ def run() -> None:
306
+ if isinstance(key, str):
307
+ _hub().set_tag(key, safe_str(value))
308
+
309
+ _guard(None, run)
310
+
311
+
312
+ def set_tags(tags: Mapping[str, Any]) -> None:
313
+ def run() -> None:
314
+ if isinstance(tags, Mapping):
315
+ _hub().set_tags({str(k): safe_str(v) for k, v in tags.items()})
316
+
317
+ _guard(None, run)
318
+
319
+
320
+ def set_context(name: str, context: Optional[Mapping[str, Any]]) -> None:
321
+ """Set a named structured context; ``None`` removes it."""
322
+
323
+ def run() -> None:
324
+ if isinstance(name, str):
325
+ _hub().set_context(name, context if isinstance(context, Mapping) else None)
326
+
327
+ _guard(None, run)
328
+
329
+
330
+ def set_extra(key: str, value: Any) -> None:
331
+ def run() -> None:
332
+ if isinstance(key, str):
333
+ _hub().set_extra(key, value)
334
+
335
+ _guard(None, run)
336
+
337
+
338
+ def add_breadcrumb(crumb: Optional[Mapping[str, Any]] = None, **fields: Any) -> None:
339
+ """Add a breadcrumb to the current scope.
340
+
341
+ Pass a mapping or keyword arguments: ``type`` (``default``, ``http``, ``log``, ``query``,
342
+ ``navigation``), ``category``, ``message``, ``level`` and ``data``.
343
+ """
344
+
345
+ def run() -> None:
346
+ merged: Dict[str, Any] = dict(crumb) if isinstance(crumb, Mapping) else {}
347
+ merged.update(fields)
348
+ _hub().add_breadcrumb(merged)
349
+
350
+ _guard(None, run)
351
+
352
+
353
+ @contextmanager
354
+ def with_scope() -> Iterator[Scope]:
355
+ """Fork the current scope for the duration of the ``with`` block.
356
+
357
+ Changes made to the yielded scope (or through ``set_tag`` and friends) apply only inside
358
+ the block, including threads started with ``contextvars.copy_context()`` and asyncio tasks
359
+ created inside it. Exceptions raised by your code propagate unchanged.
360
+ """
361
+ try:
362
+ cm = _hub().with_scope()
363
+ scope = cm.__enter__()
364
+ except Exception:
365
+ yield Scope() # detached scope: the SDK must never break the caller
366
+ return
367
+ try:
368
+ yield scope
369
+ except BaseException:
370
+ if not cm.__exit__(*sys.exc_info()):
371
+ raise
372
+ else:
373
+ try:
374
+ cm.__exit__(None, None, None)
375
+ except Exception:
376
+ pass
377
+
378
+
379
+ def get_current_scope() -> Scope:
380
+ """The scope that ``set_tag`` & co. write to right now."""
381
+ return _guard(Scope(), lambda: _hub().get_scope())
382
+
383
+
384
+ def _timeout(value: Any, default: float = 2.0) -> float:
385
+ """Invalid timeouts (``None``, strings, NaN, negative) fall back to the default."""
386
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
387
+ return default
388
+ if value != value or value < 0:
389
+ return default
390
+ return float(min(value, 3600.0))
391
+
392
+
393
+ def flush(timeout: float = 2.0) -> bool:
394
+ """Block until queued events are sent. Returns ``False`` on timeout. Never raises."""
395
+ return _guard(False, lambda: _hub().flush(_timeout(timeout)))
396
+
397
+
398
+ def close(timeout: float = 2.0) -> bool:
399
+ """Flush, remove the integrations and disable the SDK. Never raises."""
400
+ return _guard(False, lambda: _hub().close(_timeout(timeout)))
401
+
402
+
403
+ def is_enabled() -> bool:
404
+ """``True`` once ``init`` succeeded and the SDK is sending events."""
405
+ return _guard(False, lambda: bool(_hub().client and _hub().client.is_enabled)) # type: ignore[union-attr]
faultline/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,33 @@
1
+ """Application layer: the client, the hub and the ports they depend on."""
2
+
3
+ from .client import CaptureHint, ClientDeps, FaultlineClient
4
+ from .hub import Hub, Integration, get_hub
5
+ from .ports import (
6
+ Clock,
7
+ IdGenerator,
8
+ Logger,
9
+ RandomSource,
10
+ RuntimeInfo,
11
+ SilentLogger,
12
+ SourceContextReader,
13
+ StackExtractor,
14
+ Transport,
15
+ )
16
+
17
+ __all__ = [
18
+ "CaptureHint",
19
+ "ClientDeps",
20
+ "Clock",
21
+ "FaultlineClient",
22
+ "Hub",
23
+ "IdGenerator",
24
+ "Integration",
25
+ "Logger",
26
+ "RandomSource",
27
+ "RuntimeInfo",
28
+ "SilentLogger",
29
+ "SourceContextReader",
30
+ "StackExtractor",
31
+ "Transport",
32
+ "get_hub",
33
+ ]