debugbundle-python 1.0.0__tar.gz → 1.1.2__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.
Files changed (42) hide show
  1. {debugbundle_python-1.0.0/src/debugbundle_python.egg-info → debugbundle_python-1.1.2}/PKG-INFO +1 -1
  2. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/pyproject.toml +1 -1
  3. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/config.py +65 -1
  4. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/core.py +79 -16
  5. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2/src/debugbundle_python.egg-info}/PKG-INFO +1 -1
  6. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_remote_config.py +48 -2
  7. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_repository_metadata.py +13 -2
  8. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_smoke_script.py +2 -2
  9. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/LICENSE +0 -0
  10. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/README.md +0 -0
  11. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/setup.cfg +0 -0
  12. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/__init__.py +0 -0
  13. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/__init__.py +0 -0
  14. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/common.py +0 -0
  15. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/django.py +0 -0
  16. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/fastapi.py +0 -0
  17. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/flask.py +0 -0
  18. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/relay_django.py +0 -0
  19. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/relay_fastapi.py +0 -0
  20. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/relay_flask.py +0 -0
  21. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/logger_integrations.py +0 -0
  22. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/py.typed +0 -0
  23. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/redaction.py +0 -0
  24. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/relay.py +0 -0
  25. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/relay_delivery.py +0 -0
  26. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/suppression.py +0 -0
  27. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/transport.py +0 -0
  28. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/trigger_token.py +0 -0
  29. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle_python.egg-info/SOURCES.txt +0 -0
  30. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle_python.egg-info/dependency_links.txt +0 -0
  31. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle_python.egg-info/requires.txt +0 -0
  32. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle_python.egg-info/top_level.txt +0 -0
  33. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_contracts.py +0 -0
  34. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_framework_integrations.py +0 -0
  35. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_hooks.py +0 -0
  36. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_http_integration.py +0 -0
  37. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_optional_imports.py +0 -0
  38. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_relay.py +0 -0
  39. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_relay_delivery.py +0 -0
  40. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_schema_validation.py +0 -0
  41. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_sdk.py +0 -0
  42. {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/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.0.0
3
+ Version: 1.1.2
4
4
  Summary: DebugBundle SDK for Python
5
5
  Author: DebugBundle
6
6
  License-Expression: AGPL-3.0-only
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "debugbundle-python"
7
- version = "1.0.0"
7
+ version = "1.1.2"
8
8
  description = "DebugBundle SDK for Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -6,6 +6,13 @@ from typing import Any
6
6
  DEFAULT_PROBES_POLL_INTERVAL_MS = 60_000
7
7
 
8
8
 
9
+ @dataclass(frozen=True)
10
+ class ImmediateClientErrorPathRule:
11
+ status_code: int
12
+ path_pattern: str
13
+ methods: tuple[str, ...]
14
+
15
+
9
16
  @dataclass(frozen=True)
10
17
  class CapturePolicy:
11
18
  preset: str
@@ -14,6 +21,7 @@ class CapturePolicy:
14
21
  capture_breadcrumbs: str
15
22
  capture_probe_events: str
16
23
  immediate_client_error_statuses: tuple[int, ...]
24
+ immediate_client_error_path_rules: tuple[ImmediateClientErrorPathRule, ...] = ()
17
25
 
18
26
 
19
27
  @dataclass(frozen=True)
@@ -42,6 +50,7 @@ BALANCED_CAPTURE_POLICY = CapturePolicy(
42
50
  capture_breadcrumbs="exception_only",
43
51
  capture_probe_events="buffer_only",
44
52
  immediate_client_error_statuses=(),
53
+ immediate_client_error_path_rules=(),
45
54
  )
46
55
 
47
56
  MINIMAL_CAPTURE_POLICY = CapturePolicy(
@@ -51,6 +60,7 @@ MINIMAL_CAPTURE_POLICY = CapturePolicy(
51
60
  capture_breadcrumbs="local_only",
52
61
  capture_probe_events="buffer_only",
53
62
  immediate_client_error_statuses=(),
63
+ immediate_client_error_path_rules=(),
54
64
  )
55
65
 
56
66
 
@@ -123,6 +133,9 @@ def _parse_capture_policy(payload: object) -> CapturePolicy | None:
123
133
  immediate_client_error_statuses = _parse_immediate_client_error_statuses(
124
134
  payload.get("immediate_client_error_statuses")
125
135
  )
136
+ immediate_client_error_path_rules = _parse_immediate_client_error_path_rules(
137
+ payload.get("immediate_client_error_path_rules")
138
+ )
126
139
 
127
140
  if capture_logs not in {"off", "error", "warning", "info"}:
128
141
  return None
@@ -132,7 +145,7 @@ def _parse_capture_policy(payload: object) -> CapturePolicy | None:
132
145
  return None
133
146
  if capture_probe_events not in {"buffer_only", "standalone_when_activated"}:
134
147
  return None
135
- if immediate_client_error_statuses is None:
148
+ if immediate_client_error_statuses is None or immediate_client_error_path_rules is None:
136
149
  return None
137
150
 
138
151
  return CapturePolicy(
@@ -142,6 +155,7 @@ def _parse_capture_policy(payload: object) -> CapturePolicy | None:
142
155
  capture_breadcrumbs=capture_breadcrumbs,
143
156
  capture_probe_events=capture_probe_events,
144
157
  immediate_client_error_statuses=immediate_client_error_statuses,
158
+ immediate_client_error_path_rules=immediate_client_error_path_rules,
145
159
  )
146
160
 
147
161
 
@@ -160,6 +174,56 @@ def _parse_immediate_client_error_statuses(value: object) -> tuple[int, ...] | N
160
174
  return tuple(sorted(set(statuses)))
161
175
 
162
176
 
177
+ def _parse_immediate_client_error_path_rules(value: object) -> tuple[ImmediateClientErrorPathRule, ...] | None:
178
+ if value is None:
179
+ return ()
180
+ if not isinstance(value, list) or len(value) > 25:
181
+ return None
182
+
183
+ rules: list[ImmediateClientErrorPathRule] = []
184
+ for item in value:
185
+ if not isinstance(item, dict):
186
+ return None
187
+ status_code = item.get("status_code")
188
+ path_pattern = item.get("path_pattern")
189
+ raw_methods = item.get("methods", [])
190
+ if (
191
+ not isinstance(status_code, int)
192
+ or isinstance(status_code, bool)
193
+ or status_code < 400
194
+ or status_code > 499
195
+ or not isinstance(path_pattern, str)
196
+ or not _is_valid_path_pattern(path_pattern)
197
+ or not isinstance(raw_methods, list)
198
+ or len(raw_methods) > 7
199
+ ):
200
+ return None
201
+
202
+ methods: list[str] = []
203
+ for raw_method in raw_methods:
204
+ method = raw_method.upper() if isinstance(raw_method, str) else ""
205
+ if method not in {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}:
206
+ return None
207
+ if method not in methods:
208
+ methods.append(method)
209
+ rules.append(
210
+ ImmediateClientErrorPathRule(
211
+ status_code=status_code,
212
+ path_pattern=path_pattern,
213
+ methods=tuple(methods),
214
+ )
215
+ )
216
+
217
+ return tuple(rules)
218
+
219
+
220
+ def _is_valid_path_pattern(value: str) -> bool:
221
+ if not value.startswith("/") or len(value) == 0 or len(value) > 256 or "?" in value or "#" in value:
222
+ return False
223
+ wildcard_index = value.find("*")
224
+ return wildcard_index == -1 or wildcard_index == len(value) - 1
225
+
226
+
163
227
  def _parse_directive(payload: object) -> RemoteProbeDirective | None:
164
228
  if not isinstance(payload, dict):
165
229
  return None
@@ -55,9 +55,6 @@ LEVEL_RANKS = {
55
55
  }
56
56
  BALANCED_IMMEDIATE_REQUEST_STATUSES = {408, 423, 424, 425, 429}
57
57
  INVESTIGATIVE_IMMEDIATE_REQUEST_STATUSES = BALANCED_IMMEDIATE_REQUEST_STATUSES | {409}
58
- BALANCED_STANDARD_ANOMALY_STATUSES = {401, 403, 404, 409, 422}
59
- BALANCED_HIGH_VOLUME_ANOMALY_STATUSES = {400, 410}
60
- INVESTIGATIVE_ANOMALY_STATUSES = BALANCED_STANDARD_ANOMALY_STATUSES | BALANCED_HIGH_VOLUME_ANOMALY_STATUSES
61
58
 
62
59
 
63
60
  @dataclass
@@ -286,7 +283,12 @@ class DebugBundleSdk:
286
283
  context: Mapping[str, object] | None = None,
287
284
  ) -> None:
288
285
  with self._lock:
289
- if not self._enabled or not self._passes_sample_rate() or not self._should_capture_request_event(response):
286
+ should_capture = (
287
+ self._enabled
288
+ and self._passes_sample_rate()
289
+ and self._should_capture_request_event(request, response)
290
+ )
291
+ if not should_capture:
290
292
  return
291
293
  payload = _request_event_payload(
292
294
  _redact_mapping(dict(request), self._redact_fields),
@@ -526,23 +528,27 @@ class DebugBundleSdk:
526
528
  payload: dict[str, object],
527
529
  context: Mapping[str, object] | None = None,
528
530
  ) -> dict[str, object]:
529
- return {
531
+ merged_context = self._merged_context(context)
532
+ event: dict[str, object] = {
530
533
  "schema_version": SCHEMA_VERSION,
531
534
  "event_id": str(uuid.uuid4()),
532
535
  "event_type": event_type,
533
536
  "occurred_at": _iso_now(self._time_provider),
534
537
  "sdk_name": "debugbundle-python",
535
538
  "sdk_version": _sdk_version(),
536
- "sdk_language": "python",
537
539
  "service": {
538
540
  "name": self._service,
539
541
  "runtime": "python",
540
542
  "framework": None,
541
543
  "environment": self._environment,
542
544
  },
543
- "correlation": _correlation_payload(self._merged_context(context)),
545
+ "correlation": _correlation_payload(merged_context),
544
546
  "payload": payload,
545
547
  }
548
+ envelope_context = _event_context(merged_context)
549
+ if envelope_context:
550
+ event["context"] = envelope_context
551
+ return event
546
552
 
547
553
  def _merged_context(self, context: Mapping[str, object] | None = None) -> dict[str, object]:
548
554
  merged = dict(self._context)
@@ -602,17 +608,31 @@ class DebugBundleSdk:
602
608
  policy_threshold = self._capture_policy.capture_logs
603
609
  return self._log_level if LEVEL_RANKS[self._log_level] >= LEVEL_RANKS[policy_threshold] else policy_threshold
604
610
 
605
- def _should_capture_request_event(self, response: Mapping[str, object] | None) -> bool:
611
+ def _should_capture_request_event(
612
+ self,
613
+ request: Mapping[str, object] | None,
614
+ response: Mapping[str, object] | None,
615
+ ) -> bool:
606
616
  policy = self._capture_policy.capture_request_events
607
617
  status_code = None
608
618
  if response is not None:
609
619
  candidate = response.get("status_code") or response.get("response_status")
610
620
  if isinstance(candidate, int):
611
621
  status_code = candidate
622
+ request_path = None
623
+ http_method = None
624
+ if request is not None:
625
+ path_candidate = request.get("path") or request.get("url")
626
+ method_candidate = request.get("method")
627
+ request_path = path_candidate if isinstance(path_candidate, str) else None
628
+ http_method = method_candidate if isinstance(method_candidate, str) else None
612
629
  if _is_immediate_request_incident_status(
613
630
  status_code,
614
631
  self._capture_policy.preset,
615
632
  self._capture_policy.immediate_client_error_statuses,
633
+ request_path,
634
+ http_method,
635
+ self._capture_policy.immediate_client_error_path_rules,
616
636
  ):
617
637
  return True
618
638
  if policy == "off":
@@ -624,7 +644,7 @@ class DebugBundleSdk:
624
644
  if status_code is None:
625
645
  return policy == "filtered"
626
646
  if policy == "failures_only":
627
- return _is_request_anomaly_candidate_status(status_code, self._capture_policy.preset)
647
+ return status_code >= 500
628
648
  if policy == "filtered":
629
649
  return False
630
650
  return True
@@ -845,6 +865,14 @@ def _correlation_payload(context: Mapping[str, object]) -> dict[str, str | None]
845
865
  }
846
866
 
847
867
 
868
+ def _event_context(context: Mapping[str, object]) -> dict[str, object]:
869
+ return {
870
+ str(key): value
871
+ for key, value in context.items()
872
+ if key not in {"request", "response", "correlation", "request_id", "trace_id", "session_id", "user_id_hash"}
873
+ }
874
+
875
+
848
876
  def _coerce_optional_string(value: object) -> str | None:
849
877
  if value is None:
850
878
  return None
@@ -859,6 +887,9 @@ def _is_immediate_request_incident_status(
859
887
  status_code: int | None,
860
888
  preset: str,
861
889
  immediate_client_error_statuses: tuple[int, ...],
890
+ request_path: str | None = None,
891
+ http_method: str | None = None,
892
+ immediate_client_error_path_rules: tuple[object, ...] = (),
862
893
  ) -> bool:
863
894
  if status_code is None:
864
895
  return False
@@ -866,6 +897,13 @@ def _is_immediate_request_incident_status(
866
897
  return True
867
898
  if status_code in immediate_client_error_statuses:
868
899
  return True
900
+ if _matches_immediate_client_error_path_rule(
901
+ status_code,
902
+ request_path,
903
+ http_method,
904
+ immediate_client_error_path_rules,
905
+ ):
906
+ return True
869
907
  if preset == "investigative":
870
908
  return status_code in INVESTIGATIVE_IMMEDIATE_REQUEST_STATUSES
871
909
  if preset == "balanced":
@@ -873,16 +911,41 @@ def _is_immediate_request_incident_status(
873
911
  return False
874
912
 
875
913
 
876
- def _is_request_anomaly_candidate_status(status_code: int | None, preset: str) -> bool:
877
- if status_code is None or status_code < 400 or status_code >= 500:
914
+ def _matches_immediate_client_error_path_rule(
915
+ status_code: int,
916
+ request_path: str | None,
917
+ http_method: str | None,
918
+ rules: tuple[object, ...],
919
+ ) -> bool:
920
+ if status_code < 400 or status_code > 499 or request_path is None:
878
921
  return False
879
- if preset == "investigative":
880
- return status_code in INVESTIGATIVE_ANOMALY_STATUSES
881
- if preset == "balanced":
882
- return status_code in BALANCED_STANDARD_ANOMALY_STATUSES or status_code in BALANCED_HIGH_VOLUME_ANOMALY_STATUSES
922
+ normalized_path = _normalize_request_path(request_path)
923
+ normalized_method = http_method.upper() if isinstance(http_method, str) else None
924
+ for rule in rules:
925
+ rule_status = getattr(rule, "status_code", None)
926
+ path_pattern = getattr(rule, "path_pattern", None)
927
+ methods = getattr(rule, "methods", ())
928
+ if rule_status != status_code or not isinstance(path_pattern, str):
929
+ continue
930
+ if methods and (normalized_method is None or normalized_method not in methods):
931
+ continue
932
+ if path_pattern.endswith("*"):
933
+ if normalized_path.startswith(path_pattern[:-1]):
934
+ return True
935
+ elif normalized_path == path_pattern:
936
+ return True
883
937
  return False
884
938
 
885
939
 
940
+ def _normalize_request_path(value: str) -> str:
941
+ from urllib.parse import urlparse
942
+
943
+ parsed = urlparse(value)
944
+ if parsed.path:
945
+ return parsed.path
946
+ return value.split("?", 1)[0].split("#", 1)[0] if value.startswith("/") else "/"
947
+
948
+
886
949
  def _time_now() -> float:
887
950
  return datetime.now(tz=timezone.utc).timestamp()
888
951
 
@@ -891,7 +954,7 @@ def _sdk_version() -> str:
891
954
  try:
892
955
  return metadata.version("debugbundle-python")
893
956
  except metadata.PackageNotFoundError:
894
- return "1.0.0"
957
+ return "1.1.2"
895
958
 
896
959
 
897
960
  def _sdk_config_endpoint(events_endpoint: str) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: debugbundle-python
3
- Version: 1.0.0
3
+ Version: 1.1.2
4
4
  Summary: DebugBundle SDK for Python
5
5
  Author: DebugBundle
6
6
  License-Expression: AGPL-3.0-only
@@ -229,7 +229,7 @@ def test_capture_policy_filters_logs_and_request_events_from_remote_config() ->
229
229
  assert events[1]["payload"]["response_status"] == 503
230
230
 
231
231
 
232
- def test_balanced_capture_policy_keeps_request_failure_anomaly_candidates() -> None:
232
+ def test_balanced_capture_policy_keeps_immediate_failures_but_not_unconfigured_4xx() -> None:
233
233
  clock = ManualClock()
234
234
  fetch = FakeFetch(
235
235
  responses=[
@@ -271,7 +271,53 @@ def test_balanced_capture_policy_keeps_request_failure_anomaly_candidates() -> N
271
271
  for event in events
272
272
  if event["event_type"] == "request_event"
273
273
  ]
274
- assert request_statuses == [429, 404, 409]
274
+ assert request_statuses == [429]
275
+
276
+
277
+ def test_capture_policy_promotes_configured_client_error_path_rules_when_request_capture_is_off() -> None:
278
+ clock = ManualClock()
279
+ fetch = FakeFetch(
280
+ responses=[
281
+ FakeConfigResponse(
282
+ 200,
283
+ {
284
+ "probes_enabled": True,
285
+ "remote_probes_enabled": True,
286
+ "active_probes": [],
287
+ "poll_interval_ms": 15000,
288
+ "capture_policy": {
289
+ "preset": "minimal",
290
+ "capture_logs": "error",
291
+ "capture_request_events": "off",
292
+ "capture_breadcrumbs": "local_only",
293
+ "capture_probe_events": "buffer_only",
294
+ "immediate_client_error_path_rules": [
295
+ {"status_code": 404, "path_pattern": "/checkout/*", "methods": ["POST"]}
296
+ ],
297
+ },
298
+ },
299
+ )
300
+ ]
301
+ )
302
+ transport = FakeTransport()
303
+ sdk = DebugBundleSdk(transport=transport, time_provider=clock.time)
304
+ sdk.init(
305
+ project_token="dbundle_proj_test",
306
+ service="checkout-api",
307
+ environment="production",
308
+ fetch_impl=fetch,
309
+ )
310
+
311
+ sdk.capture_request({"method": "POST", "path": "/checkout/cart", "headers": {}}, {"status_code": 404})
312
+ sdk.capture_request({"method": "GET", "path": "/checkout/cart", "headers": {}}, {"status_code": 404})
313
+ sdk.capture_request({"method": "POST", "path": "/robots.txt", "headers": {}}, {"status_code": 404})
314
+ sdk.flush()
315
+
316
+ events = _events(transport)
317
+ request_events = [event for event in events if event["event_type"] == "request_event"]
318
+ assert len(request_events) == 1
319
+ assert request_events[0]["payload"]["path"] == "/checkout/cart"
320
+ assert request_events[0]["payload"]["response_status"] == 404
275
321
 
276
322
 
277
323
  def test_investigative_capture_policy_promotes_409_even_when_request_capture_is_off() -> None:
@@ -30,14 +30,25 @@ def test_standalone_changelog_and_security_policy_are_launch_ready() -> None:
30
30
  security = (REPO_ROOT / "SECURITY.md").read_text(encoding="utf-8")
31
31
 
32
32
  assert "## [Unreleased]" in changelog
33
- assert "## [1.0.0] - 2026-05-31" in changelog
33
+ assert (
34
+ "## [1.1.2] - 2026-06-19\n\n"
35
+ "### Fixed\n"
36
+ "- Release packaging quality gates so the published Python SDK patch can ship cleanly "
37
+ "without changing runtime behavior.\n\n"
38
+ "## [1.1.1] - 2026-06-19\n\n"
39
+ "### Fixed\n"
40
+ "- Normalized canonical event-envelope emission so custom app context now stays in envelope `context`, "
41
+ "request events avoid legacy payload extras, and installed projects stop tripping malformed ingestion rejects "
42
+ "after upgrade.\n\n"
43
+ "## [1.1.0] - 2026-06-08" in changelog
44
+ )
34
45
  assert "https://github.com/debugbundle/debugbundle-python/security/advisories/new" in security
35
46
 
36
47
 
37
48
  def test_standalone_ci_workflow_covers_python_sdk_checks() -> None:
38
49
  workflow = (REPO_ROOT / ".github/workflows/ci.yml").read_text(encoding="utf-8")
39
50
 
40
- assert "actions/setup-python@v5" in workflow
51
+ assert "actions/setup-python@v6" in workflow
41
52
  assert 'python-version: "3.12"' in workflow
42
53
  assert "python -m pip install -e .[dev]" in workflow
43
54
  assert "ruff check src tests" in workflow
@@ -32,7 +32,7 @@ def test_install_with_retry_retries_until_success(monkeypatch) -> None:
32
32
  monkeypatch.setattr(SMOKE.time, "sleep", sleeps.append)
33
33
 
34
34
  SMOKE._install_with_retry(
35
- ["python", "-m", "pip", "install", "debugbundle-python==1.0.0"],
35
+ ["python", "-m", "pip", "install", "debugbundle-python==1.1.2"],
36
36
  retries=3,
37
37
  retry_delay_seconds=7,
38
38
  )
@@ -50,7 +50,7 @@ def test_install_with_retry_raises_after_final_attempt(monkeypatch) -> None:
50
50
 
51
51
  try:
52
52
  SMOKE._install_with_retry(
53
- ["python", "-m", "pip", "install", "debugbundle-python==1.0.0"],
53
+ ["python", "-m", "pip", "install", "debugbundle-python==1.1.2"],
54
54
  retries=2,
55
55
  retry_delay_seconds=1,
56
56
  )