debugbundle-python 1.4.2__tar.gz → 1.5.0__tar.gz
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.
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/PKG-INFO +7 -5
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/README.md +3 -3
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/pyproject.toml +4 -2
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/core.py +58 -13
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/event_support.py +1 -1
- debugbundle_python-1.5.0/src/debugbundle/redaction.py +226 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/relay.py +8 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle_python.egg-info/PKG-INFO +7 -5
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle_python.egg-info/SOURCES.txt +1 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle_python.egg-info/requires.txt +1 -1
- debugbundle_python-1.5.0/tests/test_privacy_conformance.py +34 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/tests/test_relay.py +11 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/tests/test_repository_metadata.py +1 -1
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/tests/test_sdk.py +23 -0
- debugbundle_python-1.4.2/src/debugbundle/redaction.py +0 -29
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/LICENSE +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/setup.cfg +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/__init__.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/acknowledgement.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/before_send.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/config.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/integrations/__init__.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/integrations/common.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/integrations/django.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/integrations/fastapi.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/integrations/flask.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/integrations/relay_django.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/integrations/relay_fastapi.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/integrations/relay_flask.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/logger_integrations.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/py.typed +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/relay_delivery.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/structlog_integration.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/suppression.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/transport.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/trigger_token.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle_python.egg-info/dependency_links.txt +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle_python.egg-info/top_level.txt +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/tests/test_before_send.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/tests/test_contracts.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/tests/test_framework_integrations.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/tests/test_hooks.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/tests/test_http_integration.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/tests/test_optional_imports.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/tests/test_relay_delivery.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/tests/test_remote_config.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/tests/test_schema_validation.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/tests/test_smoke_script.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/tests/test_structlog_filtering.py +0 -0
- {debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/tests/test_trigger_token.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: debugbundle-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: DebugBundle SDK for Python
|
|
5
5
|
Author: DebugBundle
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -17,6 +17,8 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
22
|
Classifier: Typing :: Typed
|
|
21
23
|
Requires-Python: >=3.10
|
|
22
24
|
Description-Content-Type: text/markdown
|
|
@@ -24,7 +26,7 @@ License-File: LICENSE
|
|
|
24
26
|
Requires-Dist: httpx<0.29,>=0.27
|
|
25
27
|
Provides-Extra: dev
|
|
26
28
|
Requires-Dist: build<2,>=1; extra == "dev"
|
|
27
|
-
Requires-Dist: django<
|
|
29
|
+
Requires-Dist: django<7,>=5; extra == "dev"
|
|
28
30
|
Requires-Dist: fastapi<1,>=0.115; extra == "dev"
|
|
29
31
|
Requires-Dist: flask<4,>=3; extra == "dev"
|
|
30
32
|
Requires-Dist: jsonschema<5,>=4.23; extra == "dev"
|
|
@@ -263,8 +265,8 @@ There is no zero-install fallback for the Python SDK itself in V1. The nearest l
|
|
|
263
265
|
|
|
264
266
|
| Surface | Minimum compatibility version | Recommended production version | Installed-base compatibility lane | Rolling CI lane | Out of scope |
|
|
265
267
|
| --- | --- | --- | --- | --- | --- |
|
|
266
|
-
| Python runtime | 3.10 | 3.
|
|
267
|
-
| Django | 5.x | latest
|
|
268
|
+
| Python runtime | 3.10 | 3.14 | 3.10 through 3.13 remain supported for installed-base coverage | 3.10 through 3.14 | 3.9 and older |
|
|
269
|
+
| Django | 5.x | latest 6.1 patch | 5.x compatibility support | Python 3.14 suite exercises Django 6.1 | Django 4.x and older |
|
|
268
270
|
| Flask | 3.x | latest 3.x patch | 3.x compatibility support | repo release smoke installs Flask 3.x | Flask 2.x and older |
|
|
269
271
|
| FastAPI | 0.115+ | latest 0.115+ patch line | 0.115+ compatibility support | repo tests install FastAPI 0.115+ | standalone Starlette, older FastAPI lines |
|
|
270
272
|
|
|
@@ -275,7 +277,7 @@ Post-V1 planned expansions from `spec/sdk-language-targets.md` remain out of sco
|
|
|
275
277
|
`debugbundle-python` ships as one package in V1, so there is no multi-package version-alignment step like a BOM or plugin family lock.
|
|
276
278
|
|
|
277
279
|
- Pin one `debugbundle-python` version across your service and worker repos when you want identical SDK behavior everywhere.
|
|
278
|
-
- Keep framework dependencies inside the supported lanes above: Django 5.x, Flask 3.x, and FastAPI 0.115+.
|
|
280
|
+
- Keep framework dependencies inside the supported lanes above: Django 5.x or 6.x, Flask 3.x, and FastAPI 0.115+.
|
|
279
281
|
- The packaged HTTP client dependency is `httpx>=0.27,<0.29`; if you override transport behavior in tests or wrappers, stay inside that range unless you retest the SDK.
|
|
280
282
|
|
|
281
283
|
## Safety Defaults
|
|
@@ -224,8 +224,8 @@ There is no zero-install fallback for the Python SDK itself in V1. The nearest l
|
|
|
224
224
|
|
|
225
225
|
| Surface | Minimum compatibility version | Recommended production version | Installed-base compatibility lane | Rolling CI lane | Out of scope |
|
|
226
226
|
| --- | --- | --- | --- | --- | --- |
|
|
227
|
-
| Python runtime | 3.10 | 3.
|
|
228
|
-
| Django | 5.x | latest
|
|
227
|
+
| Python runtime | 3.10 | 3.14 | 3.10 through 3.13 remain supported for installed-base coverage | 3.10 through 3.14 | 3.9 and older |
|
|
228
|
+
| Django | 5.x | latest 6.1 patch | 5.x compatibility support | Python 3.14 suite exercises Django 6.1 | Django 4.x and older |
|
|
229
229
|
| Flask | 3.x | latest 3.x patch | 3.x compatibility support | repo release smoke installs Flask 3.x | Flask 2.x and older |
|
|
230
230
|
| FastAPI | 0.115+ | latest 0.115+ patch line | 0.115+ compatibility support | repo tests install FastAPI 0.115+ | standalone Starlette, older FastAPI lines |
|
|
231
231
|
|
|
@@ -236,7 +236,7 @@ Post-V1 planned expansions from `spec/sdk-language-targets.md` remain out of sco
|
|
|
236
236
|
`debugbundle-python` ships as one package in V1, so there is no multi-package version-alignment step like a BOM or plugin family lock.
|
|
237
237
|
|
|
238
238
|
- Pin one `debugbundle-python` version across your service and worker repos when you want identical SDK behavior everywhere.
|
|
239
|
-
- Keep framework dependencies inside the supported lanes above: Django 5.x, Flask 3.x, and FastAPI 0.115+.
|
|
239
|
+
- Keep framework dependencies inside the supported lanes above: Django 5.x or 6.x, Flask 3.x, and FastAPI 0.115+.
|
|
240
240
|
- The packaged HTTP client dependency is `httpx>=0.27,<0.29`; if you override transport behavior in tests or wrappers, stay inside that range unless you retest the SDK.
|
|
241
241
|
|
|
242
242
|
## Safety Defaults
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "debugbundle-python"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.5.0"
|
|
8
8
|
description = "DebugBundle SDK for Python"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -21,6 +21,8 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3.10",
|
|
22
22
|
"Programming Language :: Python :: 3.11",
|
|
23
23
|
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
24
26
|
"Typing :: Typed"
|
|
25
27
|
]
|
|
26
28
|
dependencies = [
|
|
@@ -30,7 +32,7 @@ dependencies = [
|
|
|
30
32
|
[project.optional-dependencies]
|
|
31
33
|
dev = [
|
|
32
34
|
"build>=1,<2",
|
|
33
|
-
"django>=5,<
|
|
35
|
+
"django>=5,<7",
|
|
34
36
|
"fastapi>=0.115,<1",
|
|
35
37
|
"flask>=3,<4",
|
|
36
38
|
"jsonschema>=4.23,<5",
|
|
@@ -45,7 +45,13 @@ from .event_support import (
|
|
|
45
45
|
time_now,
|
|
46
46
|
)
|
|
47
47
|
from .logger_integrations import attach_optional_integrations
|
|
48
|
-
from .redaction import
|
|
48
|
+
from .redaction import (
|
|
49
|
+
DEFAULT_REDACT_FIELDS,
|
|
50
|
+
UnsafeTelemetry,
|
|
51
|
+
has_safe_event_identity,
|
|
52
|
+
redact_value,
|
|
53
|
+
sanitize_telemetry,
|
|
54
|
+
)
|
|
49
55
|
from .suppression import EventSuppressionTracker
|
|
50
56
|
from .transport import HttpTransport, Transport, coerce_transport_response
|
|
51
57
|
from .trigger_token import resolve_request_trigger_directives
|
|
@@ -322,7 +328,12 @@ class DebugBundleSdk:
|
|
|
322
328
|
|
|
323
329
|
def set_context(self, key: str, value: object) -> None:
|
|
324
330
|
with self._lock:
|
|
325
|
-
|
|
331
|
+
try:
|
|
332
|
+
protected = sanitize_telemetry({key: value}, self._redact_fields)
|
|
333
|
+
if isinstance(protected, dict):
|
|
334
|
+
self._context[key] = protected.get(key)
|
|
335
|
+
except UnsafeTelemetry:
|
|
336
|
+
return
|
|
326
337
|
|
|
327
338
|
def _bind_scoped_context(self, context: Mapping[str, object]) -> Token[dict[str, object] | None]:
|
|
328
339
|
scoped_context = dict(self._scoped_context.get() or {})
|
|
@@ -330,7 +341,11 @@ class DebugBundleSdk:
|
|
|
330
341
|
if value is None:
|
|
331
342
|
continue
|
|
332
343
|
scoped_context[str(key)] = value
|
|
333
|
-
|
|
344
|
+
try:
|
|
345
|
+
protected = sanitize_telemetry(scoped_context, self._redact_fields)
|
|
346
|
+
return self._scoped_context.set(cast(dict[str, object], protected))
|
|
347
|
+
except UnsafeTelemetry:
|
|
348
|
+
return self._scoped_context.set({})
|
|
334
349
|
|
|
335
350
|
def _reset_scoped_context(self, token: Token[dict[str, object] | None]) -> None:
|
|
336
351
|
self._scoped_context.reset(token)
|
|
@@ -433,11 +448,17 @@ class DebugBundleSdk:
|
|
|
433
448
|
if label not in self._probe_buffers and len(self._probe_buffers) >= self._max_probe_labels:
|
|
434
449
|
return
|
|
435
450
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
value =
|
|
439
|
-
|
|
440
|
-
|
|
451
|
+
try:
|
|
452
|
+
protected_label = sanitize_telemetry(label, self._redact_fields)
|
|
453
|
+
value = data() if callable(data) else data
|
|
454
|
+
if not isinstance(value, Mapping):
|
|
455
|
+
value = {"value": value}
|
|
456
|
+
redacted_value = sanitize_telemetry(dict(value), self._redact_fields)
|
|
457
|
+
if not isinstance(protected_label, str) or not isinstance(redacted_value, dict):
|
|
458
|
+
return
|
|
459
|
+
label = protected_label
|
|
460
|
+
except Exception:
|
|
461
|
+
return
|
|
441
462
|
|
|
442
463
|
if is_heavy:
|
|
443
464
|
self._emit_probe_events(label, redacted_value, matching_directives)
|
|
@@ -613,8 +634,23 @@ class DebugBundleSdk:
|
|
|
613
634
|
merged.update({str(key): value for key, value in context.items()})
|
|
614
635
|
return cast(dict[str, object], redact_value(merged, self._redact_fields))
|
|
615
636
|
|
|
637
|
+
def _protect_event(self, event: dict[str, object]) -> dict[str, object] | None:
|
|
638
|
+
try:
|
|
639
|
+
if not has_safe_event_identity(event, self._redact_fields):
|
|
640
|
+
return None
|
|
641
|
+
candidate = dict(event)
|
|
642
|
+
for key in ("payload", "context", "service"):
|
|
643
|
+
if key in candidate:
|
|
644
|
+
candidate[key] = sanitize_telemetry(candidate[key], self._redact_fields)
|
|
645
|
+
return candidate
|
|
646
|
+
except UnsafeTelemetry:
|
|
647
|
+
return None
|
|
648
|
+
|
|
616
649
|
def _enqueue_event(self, event: dict[str, object]) -> None:
|
|
617
|
-
self.
|
|
650
|
+
protected = self._protect_event(event)
|
|
651
|
+
if protected is None:
|
|
652
|
+
return
|
|
653
|
+
self._buffer.append(protected)
|
|
618
654
|
if len(self._buffer) >= self._batch_size:
|
|
619
655
|
self.flush()
|
|
620
656
|
return
|
|
@@ -638,14 +674,20 @@ class DebugBundleSdk:
|
|
|
638
674
|
aggregate.update(self._base_event(event_type, cast(dict[str, object], payload)))
|
|
639
675
|
prepared = self._apply_before_send_event(aggregate)
|
|
640
676
|
if prepared is not None:
|
|
641
|
-
self.
|
|
677
|
+
protected = self._protect_event(prepared)
|
|
678
|
+
if protected is not None:
|
|
679
|
+
self._buffer.append(protected)
|
|
642
680
|
|
|
643
681
|
def _apply_before_send_event(self, event: dict[str, object]) -> dict[str, object] | None:
|
|
644
|
-
|
|
645
|
-
|
|
682
|
+
protected = self._protect_event(event)
|
|
683
|
+
if protected is None:
|
|
684
|
+
return None
|
|
685
|
+
result = apply_before_send(
|
|
686
|
+
protected,
|
|
646
687
|
self._before_send,
|
|
647
688
|
lambda code, message: self._emit_diagnostic(code, message),
|
|
648
689
|
)
|
|
690
|
+
return self._protect_event(result) if result is not None else None
|
|
649
691
|
|
|
650
692
|
def _build_probe_data(self) -> dict[str, object] | None:
|
|
651
693
|
items: list[dict[str, object]] = []
|
|
@@ -780,7 +822,10 @@ class DebugBundleSdk:
|
|
|
780
822
|
return
|
|
781
823
|
diagnostic: dict[str, object] = {"code": code, "message": message}
|
|
782
824
|
if metadata is not None:
|
|
783
|
-
|
|
825
|
+
try:
|
|
826
|
+
diagnostic["metadata"] = sanitize_telemetry(metadata, self._redact_fields)
|
|
827
|
+
except UnsafeTelemetry:
|
|
828
|
+
pass
|
|
784
829
|
try:
|
|
785
830
|
self._on_diagnostic(diagnostic)
|
|
786
831
|
except Exception:
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import parse_qsl, unquote, urlencode, urlsplit, urlunsplit
|
|
8
|
+
|
|
9
|
+
DEFAULT_REDACT_FIELDS = {
|
|
10
|
+
"authorization",
|
|
11
|
+
"cookie",
|
|
12
|
+
"credit_card",
|
|
13
|
+
"password",
|
|
14
|
+
"secret",
|
|
15
|
+
"ssn",
|
|
16
|
+
"token",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
REDACTED_VALUE = "[REDACTED]"
|
|
20
|
+
MANDATORY_FIELDS = DEFAULT_REDACT_FIELDS | {
|
|
21
|
+
"api_key", "apikey", "access_token", "refresh_token", "private_key", "bearer", "session_id",
|
|
22
|
+
"passwd", "card_number", "cvv", "cvc", "pin", "expiry", "phone", "otp", "verification_code",
|
|
23
|
+
}
|
|
24
|
+
_HEADER = re.compile(r"\b(Authorization|Proxy-Authorization|Cookie|Set-Cookie)\s*:\s*[^\r\n]*", re.I)
|
|
25
|
+
_BEARER = re.compile(r"\b(Bearer|Basic)\s+[A-Za-z0-9._~+/-]{6,}", re.I)
|
|
26
|
+
_TOKEN = re.compile(r"\bdbundle_(?:proj|mem|probe|agent)_[A-Za-z0-9_-]+\b")
|
|
27
|
+
_PEM = re.compile(
|
|
28
|
+
r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----",
|
|
29
|
+
re.I,
|
|
30
|
+
)
|
|
31
|
+
_PEM_START = re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----", re.I)
|
|
32
|
+
_CARD = re.compile(r"(?<![A-Za-z0-9_-])(?:\d[ -]?){12,18}\d(?![A-Za-z0-9_-])")
|
|
33
|
+
_URL = re.compile(r"\bhttps?://[^\s<>\"']+", re.I)
|
|
34
|
+
_LABELS = MANDATORY_FIELDS | {"client_secret", "x_api_key", "accessToken", "refreshToken", "privateKey", "clientSecret"}
|
|
35
|
+
_ASSIGNMENT = re.compile(
|
|
36
|
+
r"\b(" + "|".join(re.escape(key) for key in sorted(_LABELS, key=len, reverse=True)) +
|
|
37
|
+
r")\b([\"']?\s*[:=]\s*)(?:\"[^\"]*\"|'[^']*'|[^\s&,;]+)", re.I,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UnsafeTelemetry(ValueError):
|
|
42
|
+
"""A fixed safe failure marker; never includes caller content."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _sensitive_key(key: str, fields: set[str]) -> bool:
|
|
46
|
+
normalized = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", key.strip()).lower()
|
|
47
|
+
segments = [segment for segment in re.split(r"[^a-z0-9]+", normalized) if segment]
|
|
48
|
+
canonical = {re.sub(r"[^a-z0-9]", "", field.lower()) for field in fields}
|
|
49
|
+
for start in range(len(segments)):
|
|
50
|
+
combined = ""
|
|
51
|
+
for segment in segments[start:]:
|
|
52
|
+
combined += segment
|
|
53
|
+
if combined in canonical:
|
|
54
|
+
return True
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _luhn(candidate: str) -> bool:
|
|
59
|
+
digits = re.sub(r"[^0-9]", "", candidate)
|
|
60
|
+
if not 13 <= len(digits) <= 19 or len(set(digits)) == 1:
|
|
61
|
+
return False
|
|
62
|
+
total = 0
|
|
63
|
+
for index, char in enumerate(reversed(digits)):
|
|
64
|
+
digit = int(char)
|
|
65
|
+
if index % 2:
|
|
66
|
+
digit *= 2
|
|
67
|
+
if digit > 9:
|
|
68
|
+
digit -= 9
|
|
69
|
+
total += digit
|
|
70
|
+
return total % 10 == 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _scrub_url(raw: str, fields: set[str]) -> str:
|
|
74
|
+
try:
|
|
75
|
+
parsed = urlsplit(raw)
|
|
76
|
+
if not parsed.scheme or not parsed.hostname:
|
|
77
|
+
raise ValueError("invalid_url")
|
|
78
|
+
_ = parsed.port # Invalid ports make the entire URL unscannable.
|
|
79
|
+
host = parsed.netloc.rsplit("@", 1)[-1]
|
|
80
|
+
authority = f"REDACTED@{host}" if parsed.username or parsed.password else host
|
|
81
|
+
query = urlencode([
|
|
82
|
+
(key, REDACTED_VALUE if _sensitive_key(key, fields)
|
|
83
|
+
or _scrub_text(value, fields, False) != value else value)
|
|
84
|
+
for key, value in parse_qsl(parsed.query, keep_blank_values=True)
|
|
85
|
+
if len(key) <= 128 and _scrub_text(key, fields, False) == key
|
|
86
|
+
])
|
|
87
|
+
return urlunsplit((parsed.scheme.lower(), authority, parsed.path or "/", query, ""))
|
|
88
|
+
except ValueError:
|
|
89
|
+
return REDACTED_VALUE
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _scrub_text(value: str, extra: set[str], scan_urls: bool = True) -> str:
|
|
93
|
+
if _PEM_START.search(value) and not _PEM.search(value):
|
|
94
|
+
return REDACTED_VALUE
|
|
95
|
+
if re.search(r"(?:password|token|secret|authorization|cookie)%3[ad]", value, re.I):
|
|
96
|
+
try:
|
|
97
|
+
value = unquote(value, errors="strict")
|
|
98
|
+
except UnicodeError:
|
|
99
|
+
return REDACTED_VALUE
|
|
100
|
+
result = _PEM.sub(REDACTED_VALUE, value)
|
|
101
|
+
result = _HEADER.sub(lambda match: f"{match.group(1)}: {REDACTED_VALUE}", result)
|
|
102
|
+
result = _BEARER.sub(lambda match: f"{match.group(1)} {REDACTED_VALUE}", result)
|
|
103
|
+
result = _TOKEN.sub(REDACTED_VALUE, result)
|
|
104
|
+
result = _ASSIGNMENT.sub(lambda match: f"{match.group(1)}{match.group(2)}{REDACTED_VALUE}", result)
|
|
105
|
+
for field in extra:
|
|
106
|
+
named = re.compile(r"\b(" + re.escape(field) + r")\b([\"']?\s*[:=]\s*)(?:\"[^\"]*\"|'[^']*'|[^\s&,;]+)", re.I)
|
|
107
|
+
result = named.sub(lambda match: f"{match.group(1)}{match.group(2)}{REDACTED_VALUE}", result)
|
|
108
|
+
result = _CARD.sub(lambda match: REDACTED_VALUE if _luhn(match.group(0)) else match.group(0), result)
|
|
109
|
+
|
|
110
|
+
if not scan_urls:
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
def replace_url(match: re.Match[str]) -> str:
|
|
114
|
+
raw = match.group(0)
|
|
115
|
+
suffix = re.search(r"[).,;]+$", raw)
|
|
116
|
+
trailing = suffix.group(0) if suffix else ""
|
|
117
|
+
return _scrub_url(raw[:len(raw) - len(trailing)] if trailing else raw, MANDATORY_FIELDS | extra) + trailing
|
|
118
|
+
|
|
119
|
+
return _URL.sub(replace_url, result)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def sanitize_telemetry(value: Any, additional_fields: set[str] | None = None) -> Any:
|
|
123
|
+
"""Mandatory bounded telemetry policy. Failure withholds the input, never returns its original."""
|
|
124
|
+
extra = additional_fields or set()
|
|
125
|
+
if len(extra) > 128 or any(not isinstance(field, str) or not 0 < len(field) <= 64 for field in extra):
|
|
126
|
+
raise UnsafeTelemetry("unsafe_input")
|
|
127
|
+
state = {"nodes": 0, "bytes": 0}
|
|
128
|
+
seen: set[int] = set()
|
|
129
|
+
fields = MANDATORY_FIELDS | extra
|
|
130
|
+
|
|
131
|
+
def visit(candidate: Any, depth: int, structured: bool) -> Any:
|
|
132
|
+
state["nodes"] += 1
|
|
133
|
+
if state["nodes"] > 4096:
|
|
134
|
+
raise UnsafeTelemetry("budget_exceeded")
|
|
135
|
+
if depth > 16:
|
|
136
|
+
return REDACTED_VALUE
|
|
137
|
+
if isinstance(candidate, str):
|
|
138
|
+
if len(candidate) > 16 * 1024:
|
|
139
|
+
return REDACTED_VALUE
|
|
140
|
+
try:
|
|
141
|
+
length = len(candidate.encode("utf-8"))
|
|
142
|
+
except UnicodeError as error:
|
|
143
|
+
raise UnsafeTelemetry("unsafe_input") from error
|
|
144
|
+
if length > 16 * 1024:
|
|
145
|
+
return REDACTED_VALUE
|
|
146
|
+
state["bytes"] += length
|
|
147
|
+
if state["bytes"] > 256 * 1024:
|
|
148
|
+
raise UnsafeTelemetry("budget_exceeded")
|
|
149
|
+
if structured and candidate.startswith(("{", "[")):
|
|
150
|
+
try:
|
|
151
|
+
nested = json.loads(candidate)
|
|
152
|
+
except (ValueError, TypeError):
|
|
153
|
+
nested = None
|
|
154
|
+
if isinstance(nested, (dict, list)):
|
|
155
|
+
return json.dumps(visit(nested, 0, False), separators=(",", ":"), ensure_ascii=False)
|
|
156
|
+
result = _scrub_text(candidate, extra)
|
|
157
|
+
if candidate.startswith(("{", "[")) and result == candidate and re.search(
|
|
158
|
+
r"(?:password|token|secret|authorization|cookie)[\"']?\s*[:=]", candidate, re.I
|
|
159
|
+
):
|
|
160
|
+
return REDACTED_VALUE
|
|
161
|
+
return result
|
|
162
|
+
if candidate is None or isinstance(candidate, (bool, int, float)):
|
|
163
|
+
return candidate
|
|
164
|
+
identity = id(candidate)
|
|
165
|
+
if identity in seen:
|
|
166
|
+
return "[Circular]"
|
|
167
|
+
seen.add(identity)
|
|
168
|
+
try:
|
|
169
|
+
if isinstance(candidate, (list, tuple)):
|
|
170
|
+
if len(candidate) > 256:
|
|
171
|
+
return REDACTED_VALUE
|
|
172
|
+
return [visit(item, depth + 1, structured) for item in candidate]
|
|
173
|
+
if isinstance(candidate, Mapping):
|
|
174
|
+
if len(candidate) > 256:
|
|
175
|
+
return REDACTED_VALUE
|
|
176
|
+
output: dict[str, Any] = {}
|
|
177
|
+
for key, nested in candidate.items():
|
|
178
|
+
if not isinstance(key, str):
|
|
179
|
+
raise UnsafeTelemetry("unsafe_input")
|
|
180
|
+
if len(key) > 128:
|
|
181
|
+
continue
|
|
182
|
+
state["bytes"] += len(key.encode("utf-8"))
|
|
183
|
+
if state["bytes"] > 256 * 1024:
|
|
184
|
+
raise UnsafeTelemetry("budget_exceeded")
|
|
185
|
+
if _scrub_text(key, extra) != key:
|
|
186
|
+
continue
|
|
187
|
+
output[key] = (
|
|
188
|
+
REDACTED_VALUE if _sensitive_key(key, fields) else visit(nested, depth + 1, structured)
|
|
189
|
+
)
|
|
190
|
+
return output
|
|
191
|
+
raise UnsafeTelemetry("unsafe_input")
|
|
192
|
+
finally:
|
|
193
|
+
seen.remove(identity)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
sanitized = visit(value, 0, True)
|
|
197
|
+
if len(json.dumps(sanitized, separators=(",", ":"), ensure_ascii=False).encode("utf-8")) > 256 * 1024:
|
|
198
|
+
raise UnsafeTelemetry("budget_exceeded")
|
|
199
|
+
return sanitized
|
|
200
|
+
except UnsafeTelemetry:
|
|
201
|
+
raise
|
|
202
|
+
except (TypeError, ValueError, UnicodeError, RuntimeError, OverflowError) as error:
|
|
203
|
+
raise UnsafeTelemetry("unsafe_input") from error
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def has_safe_event_identity(event: Mapping[str, Any], extra: set[str] | None = None) -> bool:
|
|
207
|
+
correlation = event.get("correlation") or {}
|
|
208
|
+
if not isinstance(correlation, dict) or len(correlation) > 8:
|
|
209
|
+
return False
|
|
210
|
+
values = [event.get(key) for key in ("schema_version", "sdk_name", "sdk_version")]
|
|
211
|
+
values.extend(correlation.values())
|
|
212
|
+
return all(value is None or isinstance(value, str)
|
|
213
|
+
and sanitize_telemetry(value, extra) == value for value in values)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def redact_value(value: Any, redact_fields: set[str]) -> Any:
|
|
217
|
+
if isinstance(value, Mapping):
|
|
218
|
+
return {
|
|
219
|
+
key: REDACTED_VALUE if key.lower() in redact_fields else redact_value(nested_value, redact_fields)
|
|
220
|
+
for key, nested_value in value.items()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
|
224
|
+
return [redact_value(item, redact_fields) for item in value]
|
|
225
|
+
|
|
226
|
+
return value
|
|
@@ -6,6 +6,7 @@ from collections.abc import Callable, Mapping
|
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
+
from .redaction import UnsafeTelemetry, has_safe_event_identity, sanitize_telemetry
|
|
9
10
|
from .relay_delivery import (
|
|
10
11
|
AtomicRelayFileTransport,
|
|
11
12
|
RelayForwardTransport,
|
|
@@ -392,4 +393,11 @@ def _sanitize_event(
|
|
|
392
393
|
if normalized_correlation:
|
|
393
394
|
sanitized["correlation"] = normalized_correlation
|
|
394
395
|
|
|
396
|
+
try:
|
|
397
|
+
if not has_safe_event_identity(sanitized):
|
|
398
|
+
return None
|
|
399
|
+
sanitized["payload"] = sanitize_telemetry(sanitized["payload"])
|
|
400
|
+
sanitized["service"] = sanitize_telemetry(sanitized["service"])
|
|
401
|
+
except UnsafeTelemetry:
|
|
402
|
+
return None
|
|
395
403
|
return sanitized
|
{debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle_python.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: debugbundle-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: DebugBundle SDK for Python
|
|
5
5
|
Author: DebugBundle
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -17,6 +17,8 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
22
|
Classifier: Typing :: Typed
|
|
21
23
|
Requires-Python: >=3.10
|
|
22
24
|
Description-Content-Type: text/markdown
|
|
@@ -24,7 +26,7 @@ License-File: LICENSE
|
|
|
24
26
|
Requires-Dist: httpx<0.29,>=0.27
|
|
25
27
|
Provides-Extra: dev
|
|
26
28
|
Requires-Dist: build<2,>=1; extra == "dev"
|
|
27
|
-
Requires-Dist: django<
|
|
29
|
+
Requires-Dist: django<7,>=5; extra == "dev"
|
|
28
30
|
Requires-Dist: fastapi<1,>=0.115; extra == "dev"
|
|
29
31
|
Requires-Dist: flask<4,>=3; extra == "dev"
|
|
30
32
|
Requires-Dist: jsonschema<5,>=4.23; extra == "dev"
|
|
@@ -263,8 +265,8 @@ There is no zero-install fallback for the Python SDK itself in V1. The nearest l
|
|
|
263
265
|
|
|
264
266
|
| Surface | Minimum compatibility version | Recommended production version | Installed-base compatibility lane | Rolling CI lane | Out of scope |
|
|
265
267
|
| --- | --- | --- | --- | --- | --- |
|
|
266
|
-
| Python runtime | 3.10 | 3.
|
|
267
|
-
| Django | 5.x | latest
|
|
268
|
+
| Python runtime | 3.10 | 3.14 | 3.10 through 3.13 remain supported for installed-base coverage | 3.10 through 3.14 | 3.9 and older |
|
|
269
|
+
| Django | 5.x | latest 6.1 patch | 5.x compatibility support | Python 3.14 suite exercises Django 6.1 | Django 4.x and older |
|
|
268
270
|
| Flask | 3.x | latest 3.x patch | 3.x compatibility support | repo release smoke installs Flask 3.x | Flask 2.x and older |
|
|
269
271
|
| FastAPI | 0.115+ | latest 0.115+ patch line | 0.115+ compatibility support | repo tests install FastAPI 0.115+ | standalone Starlette, older FastAPI lines |
|
|
270
272
|
|
|
@@ -275,7 +277,7 @@ Post-V1 planned expansions from `spec/sdk-language-targets.md` remain out of sco
|
|
|
275
277
|
`debugbundle-python` ships as one package in V1, so there is no multi-package version-alignment step like a BOM or plugin family lock.
|
|
276
278
|
|
|
277
279
|
- Pin one `debugbundle-python` version across your service and worker repos when you want identical SDK behavior everywhere.
|
|
278
|
-
- Keep framework dependencies inside the supported lanes above: Django 5.x, Flask 3.x, and FastAPI 0.115+.
|
|
280
|
+
- Keep framework dependencies inside the supported lanes above: Django 5.x or 6.x, Flask 3.x, and FastAPI 0.115+.
|
|
279
281
|
- The packaged HTTP client dependency is `httpx>=0.27,<0.29`; if you override transport behavior in tests or wrappers, stay inside that range unless you retest the SDK.
|
|
280
282
|
|
|
281
283
|
## Safety Defaults
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from debugbundle.redaction import UnsafeTelemetry, has_safe_event_identity, sanitize_telemetry
|
|
5
|
+
|
|
6
|
+
CORPUS = json.loads((Path(__file__).parent / "fixtures" / "privacy-conformance.json").read_text())
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_policy_corpus() -> None:
|
|
10
|
+
assert CORPUS["policy"] == "telemetry-privacy-v1"
|
|
11
|
+
for case in CORPUS["cases"]:
|
|
12
|
+
original = json.dumps(case["input"])
|
|
13
|
+
result = sanitize_telemetry(case["input"])
|
|
14
|
+
assert result == case["expected"], case["id"]
|
|
15
|
+
assert json.dumps(case["input"]) == original, case["id"]
|
|
16
|
+
assert sanitize_telemetry(result) == result, case["id"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_additive_baseline_and_failure() -> None:
|
|
20
|
+
assert sanitize_telemetry({"password": "secret", "businessField": 42}, {"businessField"}) == {
|
|
21
|
+
"password": "[REDACTED]", "businessField": "[REDACTED]",
|
|
22
|
+
}
|
|
23
|
+
try:
|
|
24
|
+
sanitize_telemetry({"many": {f"key_{i}": [0] * 20 for i in range(220)}})
|
|
25
|
+
except UnsafeTelemetry as error:
|
|
26
|
+
assert str(error) == "budget_exceeded"
|
|
27
|
+
else:
|
|
28
|
+
raise AssertionError("unscannable input must fail closed")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_protocol_identity_values_are_checked_without_masking_identity_names() -> None:
|
|
32
|
+
assert has_safe_event_identity({"correlation": {"session_id": "session-123"}})
|
|
33
|
+
assert not has_safe_event_identity({"correlation": {"trace_id": "dbundle_proj_SYNTHETIC_SECRET"}})
|
|
34
|
+
assert not has_safe_event_identity({"sdk_version": "password=SYNTHETIC_SECRET"})
|
|
@@ -72,6 +72,17 @@ def test_accepts_valid_batch() -> None:
|
|
|
72
72
|
assert response.body["rejected"] == 0
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
def test_relay_scrubs_old_browser_payload_before_acceptance() -> None:
|
|
76
|
+
accepted: list[BrowserRelayAcceptedBatch] = []
|
|
77
|
+
handler = BrowserRelayHandler(allowed_origins=["https://example.com"], on_accept=accepted.append)
|
|
78
|
+
event = _valid_event()
|
|
79
|
+
event["payload"]["message"] = "Authorization: Bearer RELAY_SECRET"
|
|
80
|
+
event["payload"]["data"] = {"password": "RELAY_SECRET"}
|
|
81
|
+
response = handler.handle(_make_request({"batch": [event]}))
|
|
82
|
+
assert response.status == 202
|
|
83
|
+
assert "RELAY_SECRET" not in json.dumps(accepted[0].events)
|
|
84
|
+
|
|
85
|
+
|
|
75
86
|
def test_accepts_analytics_events_and_preserves_analytics_correlation() -> None:
|
|
76
87
|
fixture = _relay_compliance_fixture("valid-analytics-event")
|
|
77
88
|
accepted: list[BrowserRelayAcceptedBatch] = []
|
|
@@ -70,7 +70,7 @@ def test_standalone_ci_workflow_covers_python_sdk_checks() -> None:
|
|
|
70
70
|
workflow = (REPO_ROOT / ".github/workflows/ci.yml").read_text(encoding="utf-8")
|
|
71
71
|
|
|
72
72
|
assert "actions/setup-python@v6" in workflow
|
|
73
|
-
assert 'python-version: ["3.10", "3.11", "3.12"]' in workflow
|
|
73
|
+
assert 'python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]' in workflow
|
|
74
74
|
assert "python-version: ${{ matrix.python-version }}" in workflow
|
|
75
75
|
assert "python -m pip install -e .[dev]" in workflow
|
|
76
76
|
assert "ruff check src tests" in workflow
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import platform
|
|
4
5
|
import sys
|
|
5
6
|
import uuid
|
|
@@ -93,6 +94,28 @@ def test_before_send_mutates_after_redaction_and_before_queueing() -> None:
|
|
|
93
94
|
assert events[0]["payload"]["message"] == "mutated"
|
|
94
95
|
|
|
95
96
|
|
|
97
|
+
def test_hook_cannot_reintroduce_credentials_and_context_is_scrubbed_before_buffering() -> None:
|
|
98
|
+
transport = FakeTransport()
|
|
99
|
+
observed: list[str] = []
|
|
100
|
+
|
|
101
|
+
def hook(event: dict[str, object]) -> dict[str, object]:
|
|
102
|
+
observed.append(json.dumps(event))
|
|
103
|
+
payload = event["payload"]
|
|
104
|
+
assert isinstance(payload, dict)
|
|
105
|
+
payload["message"] = "Bearer POST_HOOK_SECRET"
|
|
106
|
+
return event
|
|
107
|
+
|
|
108
|
+
sdk = DebugBundleSdk(transport=transport)
|
|
109
|
+
sdk.init(project_token="dbundle_proj_test", before_send=hook, redact_fields=[])
|
|
110
|
+
sdk.set_context("password", "PREBUFFER_SECRET")
|
|
111
|
+
assert "PREBUFFER_SECRET" not in json.dumps(sdk._context)
|
|
112
|
+
sdk.capture_message("Authorization: Bearer ORIGINAL_SECRET", level="error")
|
|
113
|
+
sdk.flush()
|
|
114
|
+
assert "ORIGINAL_SECRET" not in observed[0]
|
|
115
|
+
assert "POST_HOOK_SECRET" not in json.dumps(transport.calls)
|
|
116
|
+
assert "PREBUFFER_SECRET" not in json.dumps(transport.calls)
|
|
117
|
+
|
|
118
|
+
|
|
96
119
|
def test_before_send_drop_invalid_failure_and_sampling_are_safe() -> None:
|
|
97
120
|
transport = FakeTransport()
|
|
98
121
|
diagnostics: list[dict[str, object]] = []
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from collections.abc import Mapping, Sequence
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
DEFAULT_REDACT_FIELDS = {
|
|
7
|
-
"authorization",
|
|
8
|
-
"cookie",
|
|
9
|
-
"credit_card",
|
|
10
|
-
"password",
|
|
11
|
-
"secret",
|
|
12
|
-
"ssn",
|
|
13
|
-
"token",
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
REDACTED_VALUE = "[REDACTED]"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def redact_value(value: Any, redact_fields: set[str]) -> Any:
|
|
20
|
-
if isinstance(value, Mapping):
|
|
21
|
-
return {
|
|
22
|
-
key: REDACTED_VALUE if key.lower() in redact_fields else redact_value(nested_value, redact_fields)
|
|
23
|
-
for key, nested_value in value.items()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
|
27
|
-
return [redact_value(item, redact_fields) for item in value]
|
|
28
|
-
|
|
29
|
-
return value
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/integrations/__init__.py
RENAMED
|
File without changes
|
{debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/integrations/common.py
RENAMED
|
File without changes
|
{debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/integrations/django.py
RENAMED
|
File without changes
|
{debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/integrations/fastapi.py
RENAMED
|
File without changes
|
|
File without changes
|
{debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/integrations/relay_django.py
RENAMED
|
File without changes
|
{debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/integrations/relay_fastapi.py
RENAMED
|
File without changes
|
{debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/integrations/relay_flask.py
RENAMED
|
File without changes
|
{debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/logger_integrations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle/structlog_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-1.4.2 → debugbundle_python-1.5.0}/src/debugbundle_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|