debugbundle-python 0.1.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.
Files changed (38) hide show
  1. debugbundle_python-0.1.0/LICENSE +17 -0
  2. debugbundle_python-0.1.0/PKG-INFO +66 -0
  3. debugbundle_python-0.1.0/README.md +27 -0
  4. debugbundle_python-0.1.0/pyproject.toml +82 -0
  5. debugbundle_python-0.1.0/setup.cfg +4 -0
  6. debugbundle_python-0.1.0/src/debugbundle/__init__.py +165 -0
  7. debugbundle_python-0.1.0/src/debugbundle/config.py +192 -0
  8. debugbundle_python-0.1.0/src/debugbundle/core.py +811 -0
  9. debugbundle_python-0.1.0/src/debugbundle/integrations/__init__.py +16 -0
  10. debugbundle_python-0.1.0/src/debugbundle/integrations/common.py +77 -0
  11. debugbundle_python-0.1.0/src/debugbundle/integrations/django.py +53 -0
  12. debugbundle_python-0.1.0/src/debugbundle/integrations/fastapi.py +94 -0
  13. debugbundle_python-0.1.0/src/debugbundle/integrations/flask.py +67 -0
  14. debugbundle_python-0.1.0/src/debugbundle/integrations/relay_django.py +50 -0
  15. debugbundle_python-0.1.0/src/debugbundle/integrations/relay_fastapi.py +46 -0
  16. debugbundle_python-0.1.0/src/debugbundle/integrations/relay_flask.py +46 -0
  17. debugbundle_python-0.1.0/src/debugbundle/logger_integrations.py +154 -0
  18. debugbundle_python-0.1.0/src/debugbundle/py.typed +0 -0
  19. debugbundle_python-0.1.0/src/debugbundle/redaction.py +29 -0
  20. debugbundle_python-0.1.0/src/debugbundle/relay.py +261 -0
  21. debugbundle_python-0.1.0/src/debugbundle/suppression.py +124 -0
  22. debugbundle_python-0.1.0/src/debugbundle/transport.py +59 -0
  23. debugbundle_python-0.1.0/src/debugbundle/trigger_token.py +143 -0
  24. debugbundle_python-0.1.0/src/debugbundle_python.egg-info/PKG-INFO +66 -0
  25. debugbundle_python-0.1.0/src/debugbundle_python.egg-info/SOURCES.txt +36 -0
  26. debugbundle_python-0.1.0/src/debugbundle_python.egg-info/dependency_links.txt +1 -0
  27. debugbundle_python-0.1.0/src/debugbundle_python.egg-info/requires.txt +15 -0
  28. debugbundle_python-0.1.0/src/debugbundle_python.egg-info/top_level.txt +1 -0
  29. debugbundle_python-0.1.0/tests/test_contracts.py +169 -0
  30. debugbundle_python-0.1.0/tests/test_framework_integrations.py +279 -0
  31. debugbundle_python-0.1.0/tests/test_hooks.py +306 -0
  32. debugbundle_python-0.1.0/tests/test_http_integration.py +165 -0
  33. debugbundle_python-0.1.0/tests/test_relay.py +195 -0
  34. debugbundle_python-0.1.0/tests/test_remote_config.py +226 -0
  35. debugbundle_python-0.1.0/tests/test_repository_metadata.py +48 -0
  36. debugbundle_python-0.1.0/tests/test_schema_validation.py +100 -0
  37. debugbundle_python-0.1.0/tests/test_sdk.py +364 -0
  38. debugbundle_python-0.1.0/tests/test_trigger_token.py +174 -0
@@ -0,0 +1,17 @@
1
+ GNU AFFERO GENERAL PUBLIC LICENSE
2
+ Version 3, 19 November 2007
3
+
4
+ Copyright (C) 2024-present DebugBundle
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Affero General Public License as
8
+ published by the Free Software Foundation, either version 3 of the
9
+ License, or (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Affero General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Affero General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: debugbundle-python
3
+ Version: 0.1.0
4
+ Summary: DebugBundle SDK for Python
5
+ Author: DebugBundle
6
+ License-Expression: AGPL-3.0-only
7
+ Project-URL: Homepage, https://debugbundle.com/docs/sdk-python
8
+ Project-URL: Repository, https://github.com/debugbundle/debugbundle-python
9
+ Project-URL: Issues, https://github.com/debugbundle/debugbundle-python/issues
10
+ Keywords: debugbundle,debugging,ai-agent,error-tracking
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Framework :: Django
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Framework :: Flask
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: httpx<0.29,>=0.27
25
+ Provides-Extra: dev
26
+ Requires-Dist: build<2,>=1; extra == "dev"
27
+ Requires-Dist: django<6,>=5; extra == "dev"
28
+ Requires-Dist: fastapi<1,>=0.115; extra == "dev"
29
+ Requires-Dist: flask<4,>=3; extra == "dev"
30
+ Requires-Dist: jsonschema<5,>=4.23; extra == "dev"
31
+ Requires-Dist: loguru<1,>=0.7; extra == "dev"
32
+ Requires-Dist: mypy<2,>=1.15; extra == "dev"
33
+ Requires-Dist: pytest<9,>=8.3; extra == "dev"
34
+ Requires-Dist: pytest-cov<7,>=5; extra == "dev"
35
+ Requires-Dist: ruff<0.12,>=0.11; extra == "dev"
36
+ Requires-Dist: structlog<26,>=24; extra == "dev"
37
+ Requires-Dist: twine<7,>=5; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ # debugbundle-python
41
+
42
+ DebugBundle SDK for Python.
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install debugbundle-python
48
+ ```
49
+
50
+ ## Quick Start
51
+
52
+ ```python
53
+ import debugbundle
54
+
55
+ debugbundle.init(project_token="dbundle_proj_test", service="checkout-api")
56
+ debugbundle.capture_exception(RuntimeError("boom"))
57
+ debugbundle.flush()
58
+ ```
59
+
60
+ ## Status
61
+
62
+ This repository currently contains the full Phase 18 Python SDK scope in eleven implementation slices: core SDK surface, buffering, redaction, duplicate suppression, probe buffering, vanilla runtime hooks, framework integrations for Django, Flask, and FastAPI, remote config polling and capture-policy enforcement, optional `structlog` and `loguru` auto-detection when `capture_logging()` is enabled, contract-aligned `EventEnvelope` emission for log, request, exception, suppression, and probe payloads, explicit public wrapper signatures and a validated buildable typed package artifact, real HTTP integration coverage against a lightweight mock ingestion server, vendored machine-readable schema validation for all event types the Python SDK currently emits, a standalone CI workflow that validates Ruff, mypy, pytest, and package builds for the Python 3.10+ support floor actually used by the package, an enforced per-file coverage gate that keeps every shipped Python SDK module at or above the required 80% minimum, and request-local framework correlation binding so `X-DebugBundle-Trace-Id` flows through Django, Flask, and FastAPI into the emitted event correlation metadata for cross-context linking.
63
+
64
+ ## Docs
65
+
66
+ https://debugbundle.com/docs/sdk-python
@@ -0,0 +1,27 @@
1
+ # debugbundle-python
2
+
3
+ DebugBundle SDK for Python.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install debugbundle-python
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ import debugbundle
15
+
16
+ debugbundle.init(project_token="dbundle_proj_test", service="checkout-api")
17
+ debugbundle.capture_exception(RuntimeError("boom"))
18
+ debugbundle.flush()
19
+ ```
20
+
21
+ ## Status
22
+
23
+ This repository currently contains the full Phase 18 Python SDK scope in eleven implementation slices: core SDK surface, buffering, redaction, duplicate suppression, probe buffering, vanilla runtime hooks, framework integrations for Django, Flask, and FastAPI, remote config polling and capture-policy enforcement, optional `structlog` and `loguru` auto-detection when `capture_logging()` is enabled, contract-aligned `EventEnvelope` emission for log, request, exception, suppression, and probe payloads, explicit public wrapper signatures and a validated buildable typed package artifact, real HTTP integration coverage against a lightweight mock ingestion server, vendored machine-readable schema validation for all event types the Python SDK currently emits, a standalone CI workflow that validates Ruff, mypy, pytest, and package builds for the Python 3.10+ support floor actually used by the package, an enforced per-file coverage gate that keeps every shipped Python SDK module at or above the required 80% minimum, and request-local framework correlation binding so `X-DebugBundle-Trace-Id` flows through Django, Flask, and FastAPI into the emitted event correlation metadata for cross-context linking.
24
+
25
+ ## Docs
26
+
27
+ https://debugbundle.com/docs/sdk-python
@@ -0,0 +1,82 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "debugbundle-python"
7
+ version = "0.1.0"
8
+ description = "DebugBundle SDK for Python"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "AGPL-3.0-only"
12
+ authors = [{ name = "DebugBundle" }]
13
+ keywords = ["debugbundle", "debugging", "ai-agent", "error-tracking"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Framework :: Django",
17
+ "Framework :: FastAPI",
18
+ "Framework :: Flask",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Typing :: Typed"
25
+ ]
26
+ dependencies = [
27
+ "httpx>=0.27,<0.29"
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ "build>=1,<2",
33
+ "django>=5,<6",
34
+ "fastapi>=0.115,<1",
35
+ "flask>=3,<4",
36
+ "jsonschema>=4.23,<5",
37
+ "loguru>=0.7,<1",
38
+ "mypy>=1.15,<2",
39
+ "pytest>=8.3,<9",
40
+ "pytest-cov>=5,<7",
41
+ "ruff>=0.11,<0.12",
42
+ "structlog>=24,<26",
43
+ "twine>=5,<7"
44
+ ]
45
+
46
+ [project.urls]
47
+ Homepage = "https://debugbundle.com/docs/sdk-python"
48
+ Repository = "https://github.com/debugbundle/debugbundle-python"
49
+ Issues = "https://github.com/debugbundle/debugbundle-python/issues"
50
+
51
+ [tool.setuptools]
52
+ package-dir = {"" = "src"}
53
+ include-package-data = true
54
+
55
+ [tool.setuptools.packages.find]
56
+ where = ["src"]
57
+
58
+ [tool.pytest.ini_options]
59
+ testpaths = ["tests"]
60
+ pythonpath = ["src"]
61
+
62
+ [tool.ruff]
63
+ line-length = 120
64
+ target-version = "py310"
65
+
66
+ [tool.ruff.lint]
67
+ select = ["E", "F", "I"]
68
+
69
+ [tool.mypy]
70
+ python_version = "3.10"
71
+ files = ["src"]
72
+ check_untyped_defs = true
73
+ no_implicit_optional = true
74
+ warn_unused_ignores = true
75
+ warn_redundant_casts = true
76
+ pretty = true
77
+
78
+ [tool.coverage.run]
79
+ source = ["src/debugbundle"]
80
+
81
+ [tool.coverage.report]
82
+ show_missing = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,165 @@
1
+ """DebugBundle Python SDK."""
2
+
3
+ import asyncio
4
+ import importlib
5
+ import logging
6
+ from collections.abc import Callable, Mapping
7
+ from contextvars import Token
8
+ from typing import Any
9
+
10
+ from .config import RemoteProbeDirective
11
+ from .core import ConfigFetchResponse, DebugBundleSdk
12
+ from .relay import BrowserRelayAcceptedBatch, BrowserRelayHandler, BrowserRelayResponse
13
+
14
+ _sdk = DebugBundleSdk()
15
+
16
+
17
+ def init(
18
+ project_token: str,
19
+ environment: str | None = None,
20
+ service: str | None = None,
21
+ enabled: bool = True,
22
+ redact_fields: list[str] | None = None,
23
+ sample_rate: float = 1.0,
24
+ batch_size: int = 25,
25
+ flush_interval: float = 5.0,
26
+ endpoint: str = "https://api.debugbundle.com/v1/events",
27
+ log_level: str = "warning",
28
+ max_probe_labels: int = 50,
29
+ max_probe_entries_per_label: int = 10,
30
+ probe_flush_on_error: bool = True,
31
+ fetch_impl: Callable[[str, dict[str, object]], ConfigFetchResponse] | None = None,
32
+ on_diagnostic: Callable[[dict[str, object]], None] | None = None,
33
+ probes_poll_interval: int = 60_000,
34
+ ) -> None:
35
+ _sdk.init(
36
+ project_token=project_token,
37
+ environment=environment,
38
+ service=service,
39
+ enabled=enabled,
40
+ redact_fields=redact_fields,
41
+ sample_rate=sample_rate,
42
+ batch_size=batch_size,
43
+ flush_interval=flush_interval,
44
+ endpoint=endpoint,
45
+ log_level=log_level,
46
+ max_probe_labels=max_probe_labels,
47
+ max_probe_entries_per_label=max_probe_entries_per_label,
48
+ probe_flush_on_error=probe_flush_on_error,
49
+ fetch_impl=fetch_impl,
50
+ on_diagnostic=on_diagnostic,
51
+ probes_poll_interval=probes_poll_interval,
52
+ )
53
+
54
+
55
+ def capture_exception(error: BaseException, context: dict[str, object] | None = None) -> None:
56
+ _sdk.capture_exception(error, context=context)
57
+
58
+
59
+ def capture_error(error: BaseException, context: dict[str, object] | None = None) -> None:
60
+ _sdk.capture_error(error, context=context)
61
+
62
+
63
+ def capture_log(message: str, level: str = "warning", context: dict[str, object] | None = None) -> None:
64
+ _sdk.capture_log(message, level=level, context=context)
65
+
66
+
67
+ def capture_request(
68
+ request: Mapping[str, object],
69
+ response: Mapping[str, object] | None = None,
70
+ context: Mapping[str, object] | None = None,
71
+ ) -> None:
72
+ _sdk.capture_request(request, response=response, context=context)
73
+
74
+
75
+ def capture_message(message: str, level: str | None = None, context: dict[str, object] | None = None) -> None:
76
+ _sdk.capture_message(message, level=level, context=context)
77
+
78
+
79
+ def set_context(key: str, value: object) -> None:
80
+ _sdk.set_context(key, value)
81
+
82
+
83
+ def flush() -> None:
84
+ _sdk.flush()
85
+
86
+
87
+ def get_status() -> str:
88
+ return _sdk.status
89
+
90
+
91
+ def get_last_event_at() -> float | None:
92
+ return _sdk.last_event_at
93
+
94
+
95
+ def probe(label: str, data: object | Callable[[], object], opts: Mapping[str, object] | None = None) -> None:
96
+ _sdk.probe(label, data, opts=opts)
97
+
98
+
99
+ def capture_exceptions() -> None:
100
+ _sdk.capture_exceptions()
101
+
102
+
103
+ def capture_logging(logger: logging.Logger | None = None) -> None:
104
+ _sdk.capture_logging(logger=logger)
105
+
106
+
107
+ def capture_async(loop: asyncio.AbstractEventLoop | None = None) -> None:
108
+ _sdk.capture_async(loop=loop)
109
+
110
+
111
+ def begin_request(request: dict) -> object:
112
+ return _sdk.begin_request(request)
113
+
114
+
115
+ def end_request(token: Token[list[RemoteProbeDirective] | None]) -> None:
116
+ _sdk.end_request(token)
117
+
118
+
119
+ _OPTIONAL_EXPORTS = {
120
+ "DebugBundleDjangoMiddleware": (".integrations.django", "DebugBundleDjangoMiddleware"),
121
+ "DebugBundleFastAPIMiddleware": (".integrations.fastapi", "DebugBundleFastAPIMiddleware"),
122
+ "create_django_relay_view": (".integrations.relay_django", "create_django_relay_view"),
123
+ "create_fastapi_relay_handler": (".integrations.relay_fastapi", "create_fastapi_relay_handler"),
124
+ "create_flask_relay_handler": (".integrations.relay_flask", "create_flask_relay_handler"),
125
+ "instrument_fastapi": (".integrations.fastapi", "instrument_fastapi"),
126
+ "instrument_flask": (".integrations.flask", "instrument_flask"),
127
+ }
128
+
129
+
130
+ def __getattr__(name: str) -> Any:
131
+ if name not in _OPTIONAL_EXPORTS:
132
+ raise AttributeError(f"module 'debugbundle' has no attribute {name!r}")
133
+
134
+ module_name, attribute_name = _OPTIONAL_EXPORTS[name]
135
+ module = importlib.import_module(module_name, __name__)
136
+ return getattr(module, attribute_name)
137
+
138
+
139
+ __all__ = [
140
+ "BrowserRelayAcceptedBatch",
141
+ "BrowserRelayHandler",
142
+ "BrowserRelayResponse",
143
+ "DebugBundleSdk",
144
+ "DebugBundleDjangoMiddleware",
145
+ "DebugBundleFastAPIMiddleware",
146
+ "begin_request",
147
+ "capture_async",
148
+ "capture_error",
149
+ "capture_exception",
150
+ "capture_exceptions",
151
+ "capture_log",
152
+ "capture_logging",
153
+ "capture_message",
154
+ "capture_request",
155
+ "create_django_relay_view",
156
+ "create_fastapi_relay_handler",
157
+ "create_flask_relay_handler",
158
+ "end_request",
159
+ "flush",
160
+ "init",
161
+ "instrument_fastapi",
162
+ "instrument_flask",
163
+ "probe",
164
+ "set_context",
165
+ ]
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ DEFAULT_PROBES_POLL_INTERVAL_MS = 60_000
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class CapturePolicy:
11
+ preset: str
12
+ capture_logs: str
13
+ capture_request_events: str
14
+ capture_breadcrumbs: str
15
+ capture_probe_events: str
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class RemoteProbeDirective:
20
+ id: str
21
+ label_pattern: str
22
+ service: str
23
+ environment: str
24
+ expires_at: str
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class RemoteConfigSnapshot:
29
+ probes_enabled: bool
30
+ remote_probes_enabled: bool
31
+ directives: list[RemoteProbeDirective]
32
+ poll_interval_ms: int
33
+ capture_policy: CapturePolicy
34
+ trigger_token_key: str | None = None
35
+
36
+
37
+ BALANCED_CAPTURE_POLICY = CapturePolicy(
38
+ preset="balanced",
39
+ capture_logs="warning",
40
+ capture_request_events="all",
41
+ capture_breadcrumbs="local_only",
42
+ capture_probe_events="standalone_when_activated",
43
+ )
44
+
45
+ MINIMAL_CAPTURE_POLICY = CapturePolicy(
46
+ preset="minimal",
47
+ capture_logs="warning",
48
+ capture_request_events="off",
49
+ capture_breadcrumbs="local_only",
50
+ capture_probe_events="buffer_only",
51
+ )
52
+
53
+
54
+ def parse_remote_config(payload: object, fallback_poll_interval_ms: int, now_ms: int) -> RemoteConfigSnapshot | None:
55
+ if not isinstance(payload, dict):
56
+ return None
57
+
58
+ probes_enabled = payload.get("probes_enabled") is True
59
+ remote_probes_enabled = payload.get("remote_probes_enabled") is True
60
+ poll_interval_candidate = payload.get("poll_interval_ms")
61
+ poll_interval_ms = (
62
+ int(poll_interval_candidate)
63
+ if isinstance(poll_interval_candidate, (int, float)) and int(poll_interval_candidate) > 0
64
+ else fallback_poll_interval_ms
65
+ )
66
+
67
+ directives: list[RemoteProbeDirective] = []
68
+ active_probes = payload.get("active_probes")
69
+ if isinstance(active_probes, list):
70
+ for directive in active_probes:
71
+ parsed = _parse_directive(directive)
72
+ if parsed is not None and _expires_at_ms(parsed.expires_at) > now_ms:
73
+ directives.append(parsed)
74
+
75
+ capture_policy = _parse_capture_policy(payload.get("capture_policy"))
76
+ if capture_policy is None:
77
+ return None
78
+
79
+ return RemoteConfigSnapshot(
80
+ probes_enabled=probes_enabled,
81
+ remote_probes_enabled=remote_probes_enabled,
82
+ directives=directives,
83
+ poll_interval_ms=poll_interval_ms if remote_probes_enabled else DEFAULT_PROBES_POLL_INTERVAL_MS,
84
+ capture_policy=capture_policy,
85
+ trigger_token_key=_as_non_empty_string(payload.get("trigger_token_key")),
86
+ )
87
+
88
+
89
+ def find_matching_remote_probe_directives(
90
+ directives: list[RemoteProbeDirective],
91
+ label: str,
92
+ service: str,
93
+ environment: str,
94
+ now_ms: int,
95
+ ) -> list[RemoteProbeDirective]:
96
+ matches: list[RemoteProbeDirective] = []
97
+ for directive in directives:
98
+ if _expires_at_ms(directive.expires_at) <= now_ms:
99
+ continue
100
+ if directive.service != "*" and directive.service != service:
101
+ continue
102
+ if directive.environment != "*" and directive.environment != environment:
103
+ continue
104
+ if _matches_label_pattern(directive.label_pattern, label):
105
+ matches.append(directive)
106
+ return matches
107
+
108
+
109
+ def _parse_capture_policy(payload: object) -> CapturePolicy | None:
110
+ if payload is None:
111
+ return BALANCED_CAPTURE_POLICY
112
+ if not isinstance(payload, dict):
113
+ return None
114
+
115
+ preset = _as_non_empty_string(payload.get("preset")) or BALANCED_CAPTURE_POLICY.preset
116
+ capture_logs = _as_non_empty_string(payload.get("capture_logs"))
117
+ capture_request_events = _as_non_empty_string(payload.get("capture_request_events"))
118
+ capture_breadcrumbs = _as_non_empty_string(payload.get("capture_breadcrumbs"))
119
+ capture_probe_events = _as_non_empty_string(payload.get("capture_probe_events"))
120
+
121
+ if capture_logs not in {"off", "error", "warning", "info"}:
122
+ return None
123
+ if capture_request_events not in {"off", "failures_only", "filtered", "all"}:
124
+ return None
125
+ if capture_breadcrumbs not in {"local_only", "exception_only", "standalone"}:
126
+ return None
127
+ if capture_probe_events not in {"buffer_only", "standalone_when_activated"}:
128
+ return None
129
+
130
+ return CapturePolicy(
131
+ preset=preset,
132
+ capture_logs=capture_logs,
133
+ capture_request_events=capture_request_events,
134
+ capture_breadcrumbs=capture_breadcrumbs,
135
+ capture_probe_events=capture_probe_events,
136
+ )
137
+
138
+
139
+ def _parse_directive(payload: object) -> RemoteProbeDirective | None:
140
+ if not isinstance(payload, dict):
141
+ return None
142
+
143
+ directive_id = _as_non_empty_string(payload.get("id"))
144
+ label_pattern = _as_non_empty_string(payload.get("label_pattern"))
145
+ service = _as_non_empty_string(payload.get("service"))
146
+ environment = _as_non_empty_string(payload.get("environment"))
147
+ expires_at = _as_non_empty_string(payload.get("expires_at"))
148
+
149
+ if directive_id is None:
150
+ return None
151
+ if label_pattern is None:
152
+ return None
153
+ if service is None:
154
+ return None
155
+ if environment is None:
156
+ return None
157
+ if expires_at is None:
158
+ return None
159
+ if _expires_at_ms(expires_at) == 0:
160
+ return None
161
+
162
+ return RemoteProbeDirective(
163
+ id=directive_id,
164
+ label_pattern=label_pattern,
165
+ service=service,
166
+ environment=environment,
167
+ expires_at=expires_at,
168
+ )
169
+
170
+
171
+ def _matches_label_pattern(pattern: str, label: str) -> bool:
172
+ if pattern == "*":
173
+ return True
174
+ if pattern.endswith(".*"):
175
+ prefix = pattern[:-2]
176
+ return label == prefix or label.startswith(f"{prefix}.")
177
+ return pattern == label
178
+
179
+
180
+ def _expires_at_ms(value: str) -> int:
181
+ from datetime import datetime
182
+
183
+ try:
184
+ if value.endswith("Z"):
185
+ value = value[:-1] + "+00:00"
186
+ return int(datetime.fromisoformat(value).timestamp() * 1000)
187
+ except ValueError:
188
+ return 0
189
+
190
+
191
+ def _as_non_empty_string(value: Any) -> str | None:
192
+ return value if isinstance(value, str) and len(value) > 0 else None