sonar-tracing 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.
- sonar_tracing-0.1.0/.gitignore +17 -0
- sonar_tracing-0.1.0/LICENSE +21 -0
- sonar_tracing-0.1.0/PKG-INFO +76 -0
- sonar_tracing-0.1.0/README.md +48 -0
- sonar_tracing-0.1.0/pyproject.toml +46 -0
- sonar_tracing-0.1.0/src/sonar_tracing/__init__.py +35 -0
- sonar_tracing-0.1.0/src/sonar_tracing/adapters/__init__.py +10 -0
- sonar_tracing-0.1.0/src/sonar_tracing/adapters/base.py +135 -0
- sonar_tracing-0.1.0/src/sonar_tracing/adapters/registry.py +75 -0
- sonar_tracing-0.1.0/src/sonar_tracing/bootstrap.py +331 -0
- sonar_tracing-0.1.0/src/sonar_tracing/config.py +376 -0
- sonar_tracing-0.1.0/src/sonar_tracing/context.py +122 -0
- sonar_tracing-0.1.0/src/sonar_tracing/exporters/__init__.py +3 -0
- sonar_tracing-0.1.0/src/sonar_tracing/exporters/redacting.py +120 -0
- sonar_tracing-0.1.0/src/sonar_tracing/processors/__init__.py +3 -0
- sonar_tracing-0.1.0/src/sonar_tracing/processors/identity.py +77 -0
- sonar_tracing-0.1.0/src/sonar_tracing/py.typed +0 -0
- sonar_tracing-0.1.0/src/sonar_tracing/redaction.py +164 -0
- sonar_tracing-0.1.0/src/sonar_tracing/safety.py +131 -0
- sonar_tracing-0.1.0/src/sonar_tracing/sampling.py +84 -0
- sonar_tracing-0.1.0/src/sonar_tracing/semconv.py +40 -0
- sonar_tracing-0.1.0/src/sonar_tracing/testing/__init__.py +75 -0
- sonar_tracing-0.1.0/src/sonar_tracing/version.py +1 -0
- sonar_tracing-0.1.0/tests/test_adapters.py +122 -0
- sonar_tracing-0.1.0/tests/test_headers.py +97 -0
- sonar_tracing-0.1.0/tests/test_init.py +290 -0
- sonar_tracing-0.1.0/tests/test_pipeline.py +255 -0
- sonar_tracing-0.1.0/tests/test_redaction.py +194 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
.venv/
|
|
5
|
+
venv/
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
.ruff_cache/
|
|
8
|
+
.mypy_cache/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
.env
|
|
12
|
+
*.local.yaml
|
|
13
|
+
.DS_Store
|
|
14
|
+
|
|
15
|
+
# Collector persistent queue (runtime state)
|
|
16
|
+
collector/queue/*
|
|
17
|
+
!collector/queue/.gitkeep
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew Ahmon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sonar-tracing
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One-line OpenTelemetry tracing for AI agents, across frameworks.
|
|
5
|
+
Project-URL: Homepage, https://github.com/deep-dive-mexico/sonar
|
|
6
|
+
Project-URL: Repository, https://github.com/deep-dive-mexico/sonar
|
|
7
|
+
Project-URL: Issues, https://github.com/deep-dive-mexico/sonar/issues
|
|
8
|
+
Author-email: Matthew Ahmon <matthew.ahmon@gmail.com>
|
|
9
|
+
Maintainer-email: Matthew Ahmon <matthew.ahmon@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,ai,llm,observability,opentelemetry,otel,tracing
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: System :: Monitoring
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: opentelemetry-api>=1.30
|
|
23
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.30
|
|
24
|
+
Requires-Dist: opentelemetry-sdk>=1.30
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# sonar-tracing
|
|
30
|
+
|
|
31
|
+
One-line OpenTelemetry tracing for AI agents, across frameworks.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import sonar_tracing as tracing
|
|
35
|
+
|
|
36
|
+
tracing.init(service_name="support-agent", framework="strands")
|
|
37
|
+
|
|
38
|
+
with tracing.identity(tenant_id="acme", session_id=session_id):
|
|
39
|
+
result = agent(prompt)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
That is the whole integration. No exporter wiring, no `TracerProvider`, no OTLP
|
|
43
|
+
setup — `init()` builds the production processor/exporter chain for you and
|
|
44
|
+
`identity()` stamps tenant/session context onto every span in scope.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install sonar-tracing
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Framework support ships as a separate package. For AWS Strands, also install the
|
|
53
|
+
adapter — it registers itself, so there is nothing to import:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install sonar-tracing sonar-tracing-strands
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## What it does
|
|
60
|
+
|
|
61
|
+
- **`init()`** — stands up a `TracerProvider` wired to an OTLP HTTP exporter,
|
|
62
|
+
with redaction and identity-stamping built into the pipeline.
|
|
63
|
+
- **`identity()`** — a context manager that attaches tenant/session attributes
|
|
64
|
+
to spans created within it.
|
|
65
|
+
- **Framework support is a plugin, not a branch.** Adapters are discovered from
|
|
66
|
+
the `sonar_tracing.adapters` entry point group, so a new framework ships as a
|
|
67
|
+
new package — core never changes.
|
|
68
|
+
|
|
69
|
+
## Requirements
|
|
70
|
+
|
|
71
|
+
- Python 3.11+
|
|
72
|
+
- An OTLP-compatible collector endpoint
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# sonar-tracing
|
|
2
|
+
|
|
3
|
+
One-line OpenTelemetry tracing for AI agents, across frameworks.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import sonar_tracing as tracing
|
|
7
|
+
|
|
8
|
+
tracing.init(service_name="support-agent", framework="strands")
|
|
9
|
+
|
|
10
|
+
with tracing.identity(tenant_id="acme", session_id=session_id):
|
|
11
|
+
result = agent(prompt)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
That is the whole integration. No exporter wiring, no `TracerProvider`, no OTLP
|
|
15
|
+
setup — `init()` builds the production processor/exporter chain for you and
|
|
16
|
+
`identity()` stamps tenant/session context onto every span in scope.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install sonar-tracing
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Framework support ships as a separate package. For AWS Strands, also install the
|
|
25
|
+
adapter — it registers itself, so there is nothing to import:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install sonar-tracing sonar-tracing-strands
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## What it does
|
|
32
|
+
|
|
33
|
+
- **`init()`** — stands up a `TracerProvider` wired to an OTLP HTTP exporter,
|
|
34
|
+
with redaction and identity-stamping built into the pipeline.
|
|
35
|
+
- **`identity()`** — a context manager that attaches tenant/session attributes
|
|
36
|
+
to spans created within it.
|
|
37
|
+
- **Framework support is a plugin, not a branch.** Adapters are discovered from
|
|
38
|
+
the `sonar_tracing.adapters` entry point group, so a new framework ships as a
|
|
39
|
+
new package — core never changes.
|
|
40
|
+
|
|
41
|
+
## Requirements
|
|
42
|
+
|
|
43
|
+
- Python 3.11+
|
|
44
|
+
- An OTLP-compatible collector endpoint
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sonar-tracing"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "One-line OpenTelemetry tracing for AI agents, across frameworks."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
authors = [{ name = "Matthew Ahmon", email = "matthew.ahmon@gmail.com" }]
|
|
10
|
+
maintainers = [{ name = "Matthew Ahmon", email = "matthew.ahmon@gmail.com" }]
|
|
11
|
+
keywords = ["opentelemetry", "tracing", "observability", "ai", "agents", "llm", "otel"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
"Topic :: System :: Monitoring",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"opentelemetry-api>=1.30",
|
|
24
|
+
"opentelemetry-sdk>=1.30",
|
|
25
|
+
"opentelemetry-exporter-otlp-proto-http>=1.30",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/deep-dive-mexico/sonar"
|
|
30
|
+
Repository = "https://github.com/deep-dive-mexico/sonar"
|
|
31
|
+
Issues = "https://github.com/deep-dive-mexico/sonar/issues"
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = ["pytest>=8"]
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["hatchling"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.version]
|
|
41
|
+
path = "src/sonar_tracing/version.py"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
# testing/ ships: it's the public adapter-contract helper (assert_adapter_contract,
|
|
45
|
+
# build_pipeline) that third-party adapter authors import to verify their adapter.
|
|
46
|
+
packages = ["src/sonar_tracing"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""sonar-tracing - unified OpenTelemetry tracing for AI agents.
|
|
2
|
+
|
|
3
|
+
import sonar_tracing as tracing
|
|
4
|
+
|
|
5
|
+
tracing.init(service_name="support-agent", framework="strands")
|
|
6
|
+
|
|
7
|
+
with tracing.identity(tenant_id="acme", session_id=session):
|
|
8
|
+
result = agent(prompt)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .bootstrap import (
|
|
12
|
+
TracingState,
|
|
13
|
+
current_state,
|
|
14
|
+
force_flush,
|
|
15
|
+
handles,
|
|
16
|
+
init,
|
|
17
|
+
shutdown,
|
|
18
|
+
)
|
|
19
|
+
from .context import Identity, current_identity, identity
|
|
20
|
+
from .safety import diagnostics
|
|
21
|
+
from .version import __version__
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Identity",
|
|
25
|
+
"TracingState",
|
|
26
|
+
"__version__",
|
|
27
|
+
"current_identity",
|
|
28
|
+
"current_state",
|
|
29
|
+
"diagnostics",
|
|
30
|
+
"force_flush",
|
|
31
|
+
"handles",
|
|
32
|
+
"identity",
|
|
33
|
+
"init",
|
|
34
|
+
"shutdown",
|
|
35
|
+
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""The adapter contract.
|
|
2
|
+
|
|
3
|
+
Frameworks integrate with OpenTelemetry in at least four different shapes:
|
|
4
|
+
|
|
5
|
+
native the framework is OTel-aware and only needs a global provider
|
|
6
|
+
to exist (Strands)
|
|
7
|
+
instrumentor a third-party package monkey-patches the framework and needs
|
|
8
|
+
the provider to already exist (LangChain via OpenInference)
|
|
9
|
+
processor the framework has its own tracing bus and accepts a processor
|
|
10
|
+
(OpenAI Agents SDK)
|
|
11
|
+
noop nothing to do
|
|
12
|
+
|
|
13
|
+
The one thing that does not generalise is *ordering*, and that is why this is a
|
|
14
|
+
two-phase interface rather than a single ``install()`` call:
|
|
15
|
+
|
|
16
|
+
phase 1 requirements() runs BEFORE the TracerProvider is built, and before
|
|
17
|
+
the framework is touched. Frameworks that read
|
|
18
|
+
configuration from the environment at import time
|
|
19
|
+
can only be influenced here.
|
|
20
|
+
phase 2 attach() runs AFTER the provider exists, for anything that
|
|
21
|
+
needs to be handed a provider.
|
|
22
|
+
|
|
23
|
+
This is not theoretical. Strands' Tracer reads OTEL_SEMCONV_STABILITY_OPT_IN in
|
|
24
|
+
its constructor and caches the result for the process lifetime, while
|
|
25
|
+
OpenInference's instrumentor must be given a live provider. A single-phase
|
|
26
|
+
interface cannot satisfy both.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from logging import Logger
|
|
33
|
+
from typing import Any, Protocol
|
|
34
|
+
|
|
35
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
36
|
+
|
|
37
|
+
from ..config import Settings
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class Requirements:
|
|
42
|
+
"""What an adapter needs in place before the provider is built."""
|
|
43
|
+
|
|
44
|
+
# Applied only if the variable is currently unset. Never clobber an operator.
|
|
45
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
46
|
+
resource_attributes: dict[str, str] = field(default_factory=dict)
|
|
47
|
+
warnings: tuple[str, ...] = ()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class AttachContext:
|
|
52
|
+
tracer_provider: TracerProvider
|
|
53
|
+
settings: Settings
|
|
54
|
+
logger: Logger
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class AttachResult:
|
|
59
|
+
ok: bool
|
|
60
|
+
mechanism: str # native | instrumentor | processor | noop
|
|
61
|
+
detail: str = ""
|
|
62
|
+
emitted_span_kinds: frozenset[str] = frozenset()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Adapter(Protocol):
|
|
66
|
+
"""Implement this in your own package and publish it as an entry point.
|
|
67
|
+
|
|
68
|
+
Adding a framework must not require touching anything outside
|
|
69
|
+
``adapters/``, a mapping pack, and a conformance fixture.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
name: str
|
|
73
|
+
semconv_family: str
|
|
74
|
+
mapping_pack: str
|
|
75
|
+
|
|
76
|
+
def detect(self) -> bool:
|
|
77
|
+
"""Is this framework installed and plausibly in use?"""
|
|
78
|
+
|
|
79
|
+
def requirements(self) -> Requirements: ...
|
|
80
|
+
|
|
81
|
+
def attach(self, ctx: AttachContext) -> AttachResult: ...
|
|
82
|
+
|
|
83
|
+
def detach(self) -> None:
|
|
84
|
+
"""Undo attach(). Must be idempotent; used by tests and re-init."""
|
|
85
|
+
|
|
86
|
+
def handles(self) -> list[Any]:
|
|
87
|
+
"""Objects the application must wire in by hand, if any.
|
|
88
|
+
|
|
89
|
+
Empty for auto-instrumenting frameworks. Non-empty for frameworks where
|
|
90
|
+
some call sites take explicit callbacks (LangGraph), so that
|
|
91
|
+
``tracing.handles()`` can hand them over without the application
|
|
92
|
+
importing an instrumentor directly.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class BaseAdapter:
|
|
97
|
+
"""Optional convenience base so a minimal adapter is a few lines."""
|
|
98
|
+
|
|
99
|
+
name: str = "unnamed"
|
|
100
|
+
semconv_family: str = "unknown"
|
|
101
|
+
mapping_pack: str = "unknown@1"
|
|
102
|
+
|
|
103
|
+
def detect(self) -> bool:
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
def requirements(self) -> Requirements:
|
|
107
|
+
return Requirements()
|
|
108
|
+
|
|
109
|
+
def attach(self, ctx: AttachContext) -> AttachResult:
|
|
110
|
+
return AttachResult(ok=True, mechanism="noop")
|
|
111
|
+
|
|
112
|
+
def detach(self) -> None:
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
def handles(self) -> list[Any]:
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
_REQUIRED_ATTRS = ("name", "semconv_family", "mapping_pack")
|
|
120
|
+
_REQUIRED_METHODS = ("detect", "requirements", "attach", "detach", "handles")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def validate_adapter(candidate: Any) -> None:
|
|
124
|
+
"""Structural check with a useful error message.
|
|
125
|
+
|
|
126
|
+
``runtime_checkable`` Protocols cannot verify non-method members, and a
|
|
127
|
+
third-party adapter missing ``mapping_pack`` would otherwise fail much later
|
|
128
|
+
at ingest, as unmapped spans.
|
|
129
|
+
"""
|
|
130
|
+
missing = [a for a in _REQUIRED_ATTRS if not isinstance(getattr(candidate, a, None), str)]
|
|
131
|
+
if missing:
|
|
132
|
+
raise TypeError(f"{candidate!r} is missing string attributes: {missing}")
|
|
133
|
+
not_callable = [m for m in _REQUIRED_METHODS if not callable(getattr(candidate, m, None))]
|
|
134
|
+
if not_callable:
|
|
135
|
+
raise TypeError(f"{candidate!r} is missing methods: {not_callable}")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Adapter discovery.
|
|
2
|
+
|
|
3
|
+
Framework support is a plugin, not a branch. There is no ``if framework ==
|
|
4
|
+
"strands"`` here or anywhere else in core: adapters are resolved from the
|
|
5
|
+
``sonar_tracing.adapters`` entry point group, so a new framework ships as a new
|
|
6
|
+
package.
|
|
7
|
+
|
|
8
|
+
The builtin table is a fallback for running from a source checkout where package
|
|
9
|
+
metadata may not be installed. It maps name -> import target, never name ->
|
|
10
|
+
behaviour, and is empty by default: every adapter ships as its own package and
|
|
11
|
+
registers an entry point.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import importlib
|
|
17
|
+
from importlib.metadata import entry_points
|
|
18
|
+
|
|
19
|
+
from ..safety import rate_limited, safe
|
|
20
|
+
from .base import Adapter, validate_adapter
|
|
21
|
+
|
|
22
|
+
ENTRY_POINT_GROUP = "sonar_tracing.adapters"
|
|
23
|
+
|
|
24
|
+
_BUILTIN: dict[str, str] = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def available() -> dict[str, str]:
|
|
28
|
+
"""name -> "module:attr" for every discoverable adapter."""
|
|
29
|
+
found = dict(_BUILTIN)
|
|
30
|
+
try:
|
|
31
|
+
for entry_point in entry_points(group=ENTRY_POINT_GROUP):
|
|
32
|
+
found[entry_point.name] = entry_point.value
|
|
33
|
+
except Exception as exc: # noqa: BLE001 - discovery must not break init
|
|
34
|
+
rate_limited.error("registry.discover", f"entry point discovery failed: {exc!r}", exc)
|
|
35
|
+
return found
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@safe("registry.load", default=None)
|
|
39
|
+
def load(name: str) -> Adapter | None:
|
|
40
|
+
target = available().get(name)
|
|
41
|
+
if target is None:
|
|
42
|
+
rate_limited.warn_once(
|
|
43
|
+
f"registry.unknown.{name}",
|
|
44
|
+
f"unknown framework {name!r}; known adapters: {sorted(available())}",
|
|
45
|
+
)
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
module_name, _, attr = target.partition(":")
|
|
49
|
+
adapter = getattr(importlib.import_module(module_name), attr)()
|
|
50
|
+
validate_adapter(adapter)
|
|
51
|
+
return adapter
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve(frameworks: tuple[str, ...]) -> list[Adapter]:
|
|
55
|
+
"""Turn the ``framework=`` argument into concrete adapters.
|
|
56
|
+
|
|
57
|
+
``("auto",)`` loads every discoverable adapter and keeps the ones whose
|
|
58
|
+
``detect()`` says the framework is present.
|
|
59
|
+
"""
|
|
60
|
+
if frameworks == ("auto",) or "auto" in frameworks:
|
|
61
|
+
resolved: list[Adapter] = []
|
|
62
|
+
for name in sorted(available()):
|
|
63
|
+
adapter = load(name)
|
|
64
|
+
if adapter is None:
|
|
65
|
+
continue
|
|
66
|
+
try:
|
|
67
|
+
if adapter.detect():
|
|
68
|
+
resolved.append(adapter)
|
|
69
|
+
except Exception as exc: # noqa: BLE001
|
|
70
|
+
rate_limited.error(
|
|
71
|
+
f"registry.detect.{name}", f"adapter {name!r} detect() failed: {exc!r}", exc
|
|
72
|
+
)
|
|
73
|
+
return resolved
|
|
74
|
+
|
|
75
|
+
return [a for a in (load(name) for name in frameworks) if a is not None]
|