debugbundle-python 1.4.2__py3-none-any.whl → 1.5.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.
- debugbundle/core.py +58 -13
- debugbundle/event_support.py +1 -1
- debugbundle/redaction.py +197 -0
- debugbundle/relay.py +8 -0
- {debugbundle_python-1.4.2.dist-info → debugbundle_python-1.5.0.dist-info}/METADATA +7 -5
- {debugbundle_python-1.4.2.dist-info → debugbundle_python-1.5.0.dist-info}/RECORD +9 -9
- {debugbundle_python-1.4.2.dist-info → debugbundle_python-1.5.0.dist-info}/WHEEL +0 -0
- {debugbundle_python-1.4.2.dist-info → debugbundle_python-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {debugbundle_python-1.4.2.dist-info → debugbundle_python-1.5.0.dist-info}/top_level.txt +0 -0
debugbundle/core.py
CHANGED
|
@@ -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:
|
debugbundle/event_support.py
CHANGED
debugbundle/redaction.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
3
5
|
from collections.abc import Mapping, Sequence
|
|
4
6
|
from typing import Any
|
|
7
|
+
from urllib.parse import parse_qsl, unquote, urlencode, urlsplit, urlunsplit
|
|
5
8
|
|
|
6
9
|
DEFAULT_REDACT_FIELDS = {
|
|
7
10
|
"authorization",
|
|
@@ -14,6 +17,200 @@ DEFAULT_REDACT_FIELDS = {
|
|
|
14
17
|
}
|
|
15
18
|
|
|
16
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)
|
|
17
214
|
|
|
18
215
|
|
|
19
216
|
def redact_value(value: Any, redact_fields: set[str]) -> Any:
|
debugbundle/relay.py
CHANGED
|
@@ -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
|
|
@@ -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
|
|
@@ -2,12 +2,12 @@ debugbundle/__init__.py,sha256=QH5RY2Cdp3Ry5PNSAhiV9akOMoS2pERRqowFnZ94roc,5131
|
|
|
2
2
|
debugbundle/acknowledgement.py,sha256=tyI8XLRG9N3-Weo6uiGrAlf_O4tjptVe1x1cG7RjrMc,2314
|
|
3
3
|
debugbundle/before_send.py,sha256=_tmV6ovHUBIxfQPMQ8a0C6r91IAnd4DVs8UcjsKhqX0,7792
|
|
4
4
|
debugbundle/config.py,sha256=ENRSR5jUI7xYdBqUakWws3GIjBk00GWFmwXVE_iQp2Y,9296
|
|
5
|
-
debugbundle/core.py,sha256=
|
|
6
|
-
debugbundle/event_support.py,sha256=
|
|
5
|
+
debugbundle/core.py,sha256=XnaHLLfo9cNHNy8n-Q2rG5SRQx9pXE3t5-hwpw2F93Y,34639
|
|
6
|
+
debugbundle/event_support.py,sha256=TSaqYXla6NACknU1nIofe4gVPQAkUkB-yrav09FTvpA,9046
|
|
7
7
|
debugbundle/logger_integrations.py,sha256=T6LR10_DgDS8C-CUPxtC1kZz3qfX5j0B2YUV7LxDovk,3572
|
|
8
8
|
debugbundle/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
debugbundle/redaction.py,sha256=
|
|
10
|
-
debugbundle/relay.py,sha256=
|
|
9
|
+
debugbundle/redaction.py,sha256=mxJgtILpVDUaj8LAhzswfaX1M4Ppyx87lfkhFqbMR8M,9663
|
|
10
|
+
debugbundle/relay.py,sha256=9HkEEVZpqrebzH6A-sSuFVuFjyLg07JuTMpMG8pu7SY,14726
|
|
11
11
|
debugbundle/relay_delivery.py,sha256=VL-nIgJR6mYXqKy-EbA0USWeY_iS_uAr1KBzrnwBnnk,4676
|
|
12
12
|
debugbundle/structlog_integration.py,sha256=hDwCGXTQ8GAjnx2jwlRbpnvLE0y798kFFMsSB3iqOrM,6061
|
|
13
13
|
debugbundle/suppression.py,sha256=XMn0GfF_-WZk2wHWk3KfbO5_u5QhEunues5h3jOInLs,4098
|
|
@@ -21,8 +21,8 @@ debugbundle/integrations/flask.py,sha256=cgyHbsAH96gpNPf_5IGJYzp2va28JdDgXROBsF-
|
|
|
21
21
|
debugbundle/integrations/relay_django.py,sha256=Wj8BE9D5otU2KmtjgqdKIJ-BvXVcagCKgpLYvnPZ6WE,2281
|
|
22
22
|
debugbundle/integrations/relay_fastapi.py,sha256=-Zl6bvhYMLii__mP6-UvSdwxS5VKsb9OZvk-yy-3hEw,2227
|
|
23
23
|
debugbundle/integrations/relay_flask.py,sha256=VFHkDTJy4LkZzL_Ry_Ba-e_gW6UTG4PBwnLcvB_oXuQ,2011
|
|
24
|
-
debugbundle_python-1.
|
|
25
|
-
debugbundle_python-1.
|
|
26
|
-
debugbundle_python-1.
|
|
27
|
-
debugbundle_python-1.
|
|
28
|
-
debugbundle_python-1.
|
|
24
|
+
debugbundle_python-1.5.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
25
|
+
debugbundle_python-1.5.0.dist-info/METADATA,sha256=X1Q7PLLF5B1H2GYePJjV3wCuvkU2_s_q0ynr46dJO74,14569
|
|
26
|
+
debugbundle_python-1.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
27
|
+
debugbundle_python-1.5.0.dist-info/top_level.txt,sha256=RCB9STTFnl1OKdojxz-xhaks2zkRFs1meZXsKnm18LM,12
|
|
28
|
+
debugbundle_python-1.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|