From ed2ff2d4e9a6edce2cb694b76cd1a11e80b49823 Mon Sep 17 00:00:00 2001 From: Khaled Salhab Date: Sun, 14 Jun 2026 04:16:12 +0300 Subject: [PATCH 1/2] test(streaming): add empty-monitors edge case for stream_alerts (#0db470) Guard the path where stream_alerts receives an empty monitors list on first poll: verify nothing is yielded, asyncio.sleep is still called (the loop continues), and monitors appearing on the second poll set the baseline without triggering transition alerts. --- tests/unit/test_streaming.py | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/unit/test_streaming.py b/tests/unit/test_streaming.py index 96f39dd..1b6b8ed 100644 --- a/tests/unit/test_streaming.py +++ b/tests/unit/test_streaming.py @@ -290,6 +290,63 @@ async def capturing_sleep(interval: float) -> None: assert recorded_intervals[0] == 5.0 + async def test_stream_alerts_empty_monitors_yields_nothing( + self, async_client: AsyncHyperpingClient + ) -> None: + """Empty monitors list on first poll yields nothing.""" + with ( + patch.object(async_client, "_request", new=AsyncMock(return_value=[])), + patch("asyncio.sleep", new=AsyncMock(side_effect=asyncio.CancelledError)), + ): + results = await _drain( + async_client.stream_alerts(poll_interval=0.0), until_cancelled=True + ) + + assert results == [] + + async def test_stream_alerts_empty_monitors_sleep_still_called( + self, async_client: AsyncHyperpingClient + ) -> None: + """asyncio.sleep is still called when the monitors list is empty.""" + recorded_intervals: list[float] = [] + + async def capturing_sleep(interval: float) -> None: + recorded_intervals.append(interval) + raise asyncio.CancelledError + + with ( + patch.object(async_client, "_request", new=AsyncMock(return_value=[])), + patch("asyncio.sleep", new=capturing_sleep), + ): + await _drain(async_client.stream_alerts(poll_interval=0.0), until_cancelled=True) + + assert len(recorded_intervals) == 1 + + async def test_stream_alerts_empty_then_monitors_appear_no_transition( + self, async_client: AsyncHyperpingClient + ) -> None: + """Monitors appearing after an empty first poll set the baseline without yielding.""" + poll1: list = [] + poll2 = [_monitor_dict("mon_1", "Test", down=False)] + + sleep_calls = 0 + + async def controlled_sleep(_interval: float) -> None: + nonlocal sleep_calls + sleep_calls += 1 + if sleep_calls >= 2: + raise asyncio.CancelledError + + with ( + patch.object(async_client, "_request", new=AsyncMock(side_effect=[poll1, poll2])), + patch("asyncio.sleep", new=controlled_sleep), + ): + results = await _drain( + async_client.stream_alerts(poll_interval=0.0), until_cancelled=True + ) + + assert results == [] + # --------------------------------------------------------------------------- # stream_incident_updates tests From 88c7c880d42bd142573a9c25d2a43aebc25ae738 Mon Sep 17 00:00:00 2001 From: Khaled Salhab Date: Sun, 14 Jun 2026 14:35:47 +0300 Subject: [PATCH 2/2] remove premature OTel scaffolding from streaming test branch Same as the parent async-streaming branch: _otel.py and test_otel.py require the PY-11 client wiring that is not present here, so they were dead code plus a failing standalone test. Remove them and the opentelemetry deps. OTel is delivered solely by the PY-11 PR. --- pyproject.toml | 5 - src/hyperping/_otel.py | 97 --------- tests/unit/test_otel.py | 429 ---------------------------------------- uv.lock | 49 +---- 4 files changed, 1 insertion(+), 579 deletions(-) delete mode 100644 src/hyperping/_otel.py delete mode 100644 tests/unit/test_otel.py diff --git a/pyproject.toml b/pyproject.toml index e4a7289..af584e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,9 +32,6 @@ dependencies = [ [project.optional-dependencies] cli = ["typer[all]>=0.12,<1.0"] -otel = [ - "opentelemetry-api>=1.20", -] dev = [ "pytest>=9.0.3", "pytest-cov", @@ -44,8 +41,6 @@ dev = [ "pydantic", "pip-audit>=2.7", "typer[all]>=0.12,<1.0", - "opentelemetry-api>=1.20", - "opentelemetry-sdk>=1.20", ] [project.scripts] diff --git a/src/hyperping/_otel.py b/src/hyperping/_otel.py deleted file mode 100644 index 59d858e..0000000 --- a/src/hyperping/_otel.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Optional OpenTelemetry instrumentation for the Hyperping SDK. - -When opentelemetry-api is not installed, all functions are no-ops and the SDK -behaves identically to today with zero overhead. - -Install ``hyperping[otel]`` to enable tracing. -""" - -from __future__ import annotations - -import contextlib -from collections.abc import Generator -from typing import TYPE_CHECKING -from urllib.parse import urlsplit - -try: - from opentelemetry import trace - from opentelemetry.trace import Span, Status, StatusCode, Tracer # noqa: F401 - - HAS_OTEL: bool = True -except ImportError: - HAS_OTEL = False - -if TYPE_CHECKING: - from opentelemetry.trace import Span, Tracer - -from hyperping._version import __version__ - - -def get_tracer() -> Tracer | None: - """Return a Tracer for the hyperping SDK, or None when OTel is not installed.""" - if not HAS_OTEL: - return None - return trace.get_tracer("hyperping", __version__) - - -@contextlib.contextmanager -def start_request_span( - tracer: Tracer | None, - method: str, - path: str, - base_url: str, -) -> Generator[Span | None, None, None]: - """Context manager wrapping a REST API call in an OTel span. - - Yields None (no-op) when tracer is None. Compatible with both sync and - async callers because the span lifecycle is synchronous regardless. - """ - if tracer is None: - yield None - return - - span_name = f"hyperping {method} {path}" - server_address = urlsplit(base_url).hostname or base_url - - with tracer.start_as_current_span(span_name) as span: - span.set_attribute("http.request.method", method) - span.set_attribute("url.full", base_url.rstrip("/") + path) - span.set_attribute("server.address", server_address) - span.set_attribute("hyperping.sdk.version", __version__) - yield span - - -@contextlib.contextmanager -def start_rpc_span( - tracer: Tracer | None, - rpc_method: str, - url: str, -) -> Generator[Span | None, None, None]: - """Context manager wrapping an MCP JSON-RPC call in an OTel span. - - Yields None (no-op) when tracer is None. - """ - if tracer is None: - yield None - return - - span_name = f"hyperping.mcp {rpc_method}" - server_address = urlsplit(url).hostname or url - - with tracer.start_as_current_span(span_name) as span: - span.set_attribute("rpc.method", rpc_method) - span.set_attribute("rpc.system", "jsonrpc") - span.set_attribute("server.address", server_address) - span.set_attribute("hyperping.sdk.version", __version__) - yield span - - -def record_error(span: Span | None, exception: BaseException) -> None: - """Record an exception on a span and set its status to ERROR. - - No-op when span is None. - """ - if span is None or not HAS_OTEL: - return - span.set_status(Status(StatusCode.ERROR)) - span.record_exception(exception) diff --git a/tests/unit/test_otel.py b/tests/unit/test_otel.py deleted file mode 100644 index 0f5a28f..0000000 --- a/tests/unit/test_otel.py +++ /dev/null @@ -1,429 +0,0 @@ -"""Tests for OpenTelemetry instrumentation in the Hyperping SDK.""" - -from __future__ import annotations - -import json as _json -from collections.abc import Generator -from typing import Any -from unittest.mock import patch - -import httpx -import pytest -import respx -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SimpleSpanProcessor -from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter -from opentelemetry.trace import StatusCode - -from hyperping._async_client import AsyncHyperpingClient -from hyperping._async_mcp_transport import AsyncMcpTransport -from hyperping._mcp_transport import McpTransport -from hyperping._version import __version__ as _SDK_VERSION # noqa: N812 -from hyperping.client import HyperpingClient, RetryConfig -from hyperping.endpoints import API_BASE, MCP_URL, Endpoint -from hyperping.exceptions import HyperpingAPIError, HyperpingAuthError - -# ── Helpers ──────────────────────────────────────────────────────────────────── - - -def _tool_resp(data: dict[str, Any], *, req_id: int = 2) -> httpx.Response: - return httpx.Response( - 200, - json={ - "jsonrpc": "2.0", - "id": req_id, - "result": {"content": [{"type": "text", "text": _json.dumps(data)}]}, - }, - ) - - -# ── Fixtures ─────────────────────────────────────────────────────────────────── - - -@pytest.fixture -def _span_provider() -> Generator[tuple[InMemorySpanExporter, TracerProvider], None, None]: - """Create an isolated local TracerProvider and InMemorySpanExporter. - - Does NOT modify the global OTel TracerProvider (which can only be set once - per process). Each test gets its own provider and exporter. - """ - exporter = InMemorySpanExporter() - provider = TracerProvider() - provider.add_span_processor(SimpleSpanProcessor(exporter)) - yield exporter, provider - exporter.clear() - - -@pytest.fixture -def span_exporter( - _span_provider: tuple[InMemorySpanExporter, TracerProvider], -) -> InMemorySpanExporter: - """Expose just the InMemorySpanExporter from the local provider.""" - exporter, _ = _span_provider - return exporter - - -@pytest.fixture -def otel_tracer( - _span_provider: tuple[InMemorySpanExporter, TracerProvider], -) -> Any: - """Return a test-local tracer from the isolated TracerProvider.""" - _, provider = _span_provider - return provider.get_tracer("hyperping", _SDK_VERSION) - - -@pytest.fixture -def otel_client(otel_tracer: Any) -> Generator[HyperpingClient, None, None]: - """HyperpingClient with the test-local tracer injected (bypasses global OTel).""" - c = HyperpingClient( - api_key="sk_test_key", - base_url=API_BASE, - retry_config=RetryConfig(max_retries=0), - ) - c._tracer = otel_tracer # type: ignore[assignment] - yield c - c.close() - - -@pytest.fixture -def otel_async_client(otel_tracer: Any) -> Generator[AsyncHyperpingClient, None, None]: - """AsyncHyperpingClient with the test-local tracer injected.""" - c = AsyncHyperpingClient( - api_key="sk_test_key", - base_url=API_BASE, - retry_config=RetryConfig(max_retries=0), - ) - c._tracer = otel_tracer # type: ignore[assignment] - yield c - - -@pytest.fixture -def otel_mcp(otel_tracer: Any) -> Generator[McpTransport, None, None]: - """McpTransport with test-local tracer and pre-initialized state.""" - t = McpTransport(api_key="sk_test", base_url=MCP_URL) - t._tracer = otel_tracer # type: ignore[assignment] - t._initialized = True - t._init_result = {"protocolVersion": "2025-03-26"} - yield t - t.close() - - -@pytest.fixture -def otel_async_mcp(otel_tracer: Any) -> Generator[AsyncMcpTransport, None, None]: - """AsyncMcpTransport with test-local tracer and pre-initialized state.""" - t = AsyncMcpTransport(api_key="sk_test", base_url=MCP_URL) - t._tracer = otel_tracer # type: ignore[assignment] - t._initialized = True - t._init_result = {"protocolVersion": "2025-03-26"} - yield t - - -# ── TestOtelModule ───────────────────────────────────────────────────────────── - - -class TestOtelModule: - def test_has_otel_flag_true_when_installed(self) -> None: - """HAS_OTEL is True in the test environment (opentelemetry-api is installed).""" - from hyperping._otel import HAS_OTEL - - assert HAS_OTEL is True - - def test_get_tracer_returns_tracer(self) -> None: - """get_tracer() returns a non-None tracer when OTel is installed.""" - from hyperping._otel import get_tracer - - t = get_tracer() - assert t is not None - - def test_get_tracer_returns_none_when_missing(self) -> None: - """get_tracer() returns None when HAS_OTEL is False.""" - import hyperping._otel as otel_mod - - with patch.object(otel_mod, "HAS_OTEL", False): - t = otel_mod.get_tracer() - assert t is None - - -# ── TestSyncClientSpans ──────────────────────────────────────────────────────── - - -class TestSyncClientSpans: - @respx.mock - def test_request_creates_span( - self, otel_client: HyperpingClient, span_exporter: InMemorySpanExporter - ) -> None: - """GET /v1/monitors emits a span named 'hyperping GET /v1/monitors'.""" - respx.get(f"{API_BASE}{Endpoint.MONITORS}").mock(return_value=httpx.Response(200, json=[])) - otel_client.list_monitors() - spans = span_exporter.get_finished_spans() - assert any(s.name == f"hyperping GET {Endpoint.MONITORS}" for s in spans) - - @respx.mock - def test_span_has_http_method_attribute( - self, otel_client: HyperpingClient, span_exporter: InMemorySpanExporter - ) -> None: - """Span carries http.request.method = 'GET'.""" - respx.get(f"{API_BASE}{Endpoint.MONITORS}").mock(return_value=httpx.Response(200, json=[])) - otel_client.list_monitors() - spans = [s for s in span_exporter.get_finished_spans() if "GET" in s.name] - assert spans, "No matching span found" - assert spans[0].attributes.get("http.request.method") == "GET" # type: ignore[union-attr] - - @respx.mock - def test_span_has_url_attribute( - self, otel_client: HyperpingClient, span_exporter: InMemorySpanExporter - ) -> None: - """Span carries url.full.""" - respx.get(f"{API_BASE}{Endpoint.MONITORS}").mock(return_value=httpx.Response(200, json=[])) - otel_client.list_monitors() - spans = [s for s in span_exporter.get_finished_spans() if "GET" in s.name] - assert spans - assert "url.full" in (spans[0].attributes or {}) - - @respx.mock - def test_span_has_status_ok_on_success( - self, otel_client: HyperpingClient, span_exporter: InMemorySpanExporter - ) -> None: - """Span status is UNSET (interpreted as OK) on successful 200 response.""" - respx.get(f"{API_BASE}{Endpoint.MONITORS}").mock(return_value=httpx.Response(200, json=[])) - otel_client.list_monitors() - spans = [s for s in span_exporter.get_finished_spans() if "GET" in s.name] - assert spans - assert spans[0].status.status_code == StatusCode.UNSET - - @respx.mock - def test_span_records_error_on_api_error( - self, otel_client: HyperpingClient, span_exporter: InMemorySpanExporter - ) -> None: - """Span status is ERROR and exception is recorded on 500 response.""" - respx.get(f"{API_BASE}{Endpoint.MONITORS}").mock( - return_value=httpx.Response(500, json={"error": "Internal Server Error"}) - ) - with pytest.raises(HyperpingAPIError): - otel_client.list_monitors() - spans = [s for s in span_exporter.get_finished_spans() if "GET" in s.name] - assert spans - assert spans[0].status.status_code == StatusCode.ERROR - - @respx.mock - def test_span_records_error_on_auth_error( - self, otel_client: HyperpingClient, span_exporter: InMemorySpanExporter - ) -> None: - """Span status is ERROR on 401 (authentication failure).""" - respx.get(f"{API_BASE}{Endpoint.MONITORS}").mock( - return_value=httpx.Response(401, json={"error": "Unauthorized"}) - ) - with pytest.raises(HyperpingAuthError): - otel_client.list_monitors() - spans = [s for s in span_exporter.get_finished_spans() if "GET" in s.name] - assert spans - assert spans[0].status.status_code == StatusCode.ERROR - - @respx.mock - def test_no_span_when_tracer_is_none(self, span_exporter: InMemorySpanExporter) -> None: - """When _tracer is None, no span is emitted and the client works normally.""" - c = HyperpingClient( - api_key="sk_test_key", - base_url=API_BASE, - retry_config=RetryConfig(max_retries=0), - ) - c._tracer = None # type: ignore[assignment] - respx.get(f"{API_BASE}{Endpoint.MONITORS}").mock(return_value=httpx.Response(200, json=[])) - c.list_monitors() - spans = [s for s in span_exporter.get_finished_spans() if "hyperping" in s.name] - assert len(spans) == 0 - c.close() - - -# ── TestAsyncClientSpans ─────────────────────────────────────────────────────── - - -class TestAsyncClientSpans: - @respx.mock - async def test_async_request_creates_span( - self, otel_async_client: AsyncHyperpingClient, span_exporter: InMemorySpanExporter - ) -> None: - """Async GET /v1/monitors emits a span.""" - respx.get(f"{API_BASE}{Endpoint.MONITORS}").mock(return_value=httpx.Response(200, json=[])) - await otel_async_client.list_monitors() - spans = span_exporter.get_finished_spans() - assert any(s.name == f"hyperping GET {Endpoint.MONITORS}" for s in spans) - await otel_async_client.close() - - @respx.mock - async def test_async_span_has_attributes( - self, otel_async_client: AsyncHyperpingClient, span_exporter: InMemorySpanExporter - ) -> None: - """Async span carries http.request.method and url.full.""" - respx.get(f"{API_BASE}{Endpoint.MONITORS}").mock(return_value=httpx.Response(200, json=[])) - await otel_async_client.list_monitors() - spans = [s for s in span_exporter.get_finished_spans() if "GET" in s.name] - assert spans - attrs = spans[0].attributes or {} - assert attrs.get("http.request.method") == "GET" - assert "url.full" in attrs - await otel_async_client.close() - - @respx.mock - async def test_async_span_records_error( - self, otel_async_client: AsyncHyperpingClient, span_exporter: InMemorySpanExporter - ) -> None: - """Async span status is ERROR on 500.""" - respx.get(f"{API_BASE}{Endpoint.MONITORS}").mock( - return_value=httpx.Response(500, json={"error": "Server Error"}) - ) - with pytest.raises(HyperpingAPIError): - await otel_async_client.list_monitors() - spans = [s for s in span_exporter.get_finished_spans() if "GET" in s.name] - assert spans - assert spans[0].status.status_code == StatusCode.ERROR - await otel_async_client.close() - - -# ── TestMcpTransportSpans ────────────────────────────────────────────────────── - - -class TestMcpTransportSpans: - @respx.mock - def test_mcp_rpc_creates_span( - self, otel_mcp: McpTransport, span_exporter: InMemorySpanExporter - ) -> None: - """tools/call emits a span named 'hyperping.mcp tools/call'.""" - respx.post(MCP_URL).mock(return_value=_tool_resp({"ok": True})) - otel_mcp.call_tool("test_tool") - spans = span_exporter.get_finished_spans() - assert any(s.name == "hyperping.mcp tools/call" for s in spans) - - @respx.mock - def test_mcp_span_has_rpc_method_attribute( - self, otel_mcp: McpTransport, span_exporter: InMemorySpanExporter - ) -> None: - """MCP span carries rpc.method = 'tools/call'.""" - respx.post(MCP_URL).mock(return_value=_tool_resp({"ok": True})) - otel_mcp.call_tool("test_tool") - spans = [s for s in span_exporter.get_finished_spans() if "tools/call" in s.name] - assert spans - assert spans[0].attributes.get("rpc.method") == "tools/call" # type: ignore[union-attr] - - @respx.mock - def test_mcp_span_has_rpc_system_attribute( - self, otel_mcp: McpTransport, span_exporter: InMemorySpanExporter - ) -> None: - """MCP span carries rpc.system = 'jsonrpc'.""" - respx.post(MCP_URL).mock(return_value=_tool_resp({"ok": True})) - otel_mcp.call_tool("test_tool") - spans = [s for s in span_exporter.get_finished_spans() if "tools/call" in s.name] - assert spans - assert spans[0].attributes.get("rpc.system") == "jsonrpc" # type: ignore[union-attr] - - @respx.mock - def test_mcp_span_records_error_on_rpc_error( - self, otel_mcp: McpTransport, span_exporter: InMemorySpanExporter - ) -> None: - """MCP span status is ERROR when JSON-RPC returns an error.""" - respx.post(MCP_URL).mock( - return_value=httpx.Response( - 200, - json={"jsonrpc": "2.0", "id": 2, "error": {"code": -32600, "message": "Bad req"}}, - ) - ) - with pytest.raises(HyperpingAPIError): - otel_mcp.call_tool("test_tool") - spans = [s for s in span_exporter.get_finished_spans() if "tools/call" in s.name] - assert spans - assert spans[0].status.status_code == StatusCode.ERROR - - @respx.mock - def test_mcp_span_records_error_on_http_error( - self, otel_mcp: McpTransport, span_exporter: InMemorySpanExporter - ) -> None: - """MCP span status is ERROR on HTTP 500.""" - respx.post(MCP_URL).mock(return_value=httpx.Response(500)) - with pytest.raises(HyperpingAPIError): - otel_mcp.call_tool("test_tool") - spans = [s for s in span_exporter.get_finished_spans() if "tools/call" in s.name] - assert spans - assert spans[0].status.status_code == StatusCode.ERROR - - -# ── TestAsyncMcpTransportSpans ───────────────────────────────────────────────── - - -class TestAsyncMcpTransportSpans: - @respx.mock - async def test_async_mcp_rpc_creates_span( - self, otel_async_mcp: AsyncMcpTransport, span_exporter: InMemorySpanExporter - ) -> None: - """Async tools/call emits 'hyperping.mcp tools/call'.""" - respx.post(MCP_URL).mock(return_value=_tool_resp({"ok": True})) - await otel_async_mcp.call_tool("test_tool") - spans = span_exporter.get_finished_spans() - assert any(s.name == "hyperping.mcp tools/call" for s in spans) - await otel_async_mcp.close() - - @respx.mock - async def test_async_mcp_span_records_error( - self, otel_async_mcp: AsyncMcpTransport, span_exporter: InMemorySpanExporter - ) -> None: - """Async MCP span status is ERROR on HTTP 500.""" - respx.post(MCP_URL).mock(return_value=httpx.Response(500)) - with pytest.raises(HyperpingAPIError): - await otel_async_mcp.call_tool("test_tool") - spans = [s for s in span_exporter.get_finished_spans() if "tools/call" in s.name] - assert spans - assert spans[0].status.status_code == StatusCode.ERROR - await otel_async_mcp.close() - - -# ── TestNoopWhenUninstalled ──────────────────────────────────────────────────── - - -class TestNoopWhenUninstalled: - @respx.mock - def test_sync_client_works_without_otel(self) -> None: - """Client works normally when _tracer is None (simulates HAS_OTEL=False).""" - import hyperping._otel as otel_mod - - with patch.object(otel_mod, "HAS_OTEL", False): - c = HyperpingClient( - api_key="sk_test_key", - base_url=API_BASE, - retry_config=RetryConfig(max_retries=0), - ) - assert c._tracer is None - respx.get(f"{API_BASE}{Endpoint.MONITORS}").mock(return_value=httpx.Response(200, json=[])) - result = c.list_monitors() - assert result == [] - c.close() - - @respx.mock - async def test_async_client_works_without_otel(self) -> None: - """Async client works normally when _tracer is None.""" - import hyperping._otel as otel_mod - - with patch.object(otel_mod, "HAS_OTEL", False): - c = AsyncHyperpingClient( - api_key="sk_test_key", - base_url=API_BASE, - retry_config=RetryConfig(max_retries=0), - ) - assert c._tracer is None - respx.get(f"{API_BASE}{Endpoint.MONITORS}").mock(return_value=httpx.Response(200, json=[])) - result = await c.list_monitors() - assert result == [] - await c.close() - - @respx.mock - def test_mcp_transport_works_without_otel(self) -> None: - """MCP transport works normally when _tracer is None.""" - import hyperping._otel as otel_mod - - with patch.object(otel_mod, "HAS_OTEL", False): - t = McpTransport(api_key="sk_test", base_url=MCP_URL) - assert t._tracer is None - t._initialized = True - t._init_result = {"protocolVersion": "2025-03-26"} - respx.post(MCP_URL).mock(return_value=_tool_resp({"ok": True})) - result = t.call_tool("test_tool") - assert result == {"ok": True} - t.close() diff --git a/uv.lock b/uv.lock index 87c5bde..f4435d2 100644 --- a/uv.lock +++ b/uv.lock @@ -386,8 +386,6 @@ cli = [ ] dev = [ { name = "mypy" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, { name = "pip-audit" }, { name = "pydantic" }, { name = "pytest" }, @@ -396,9 +394,6 @@ dev = [ { name = "ruff" }, { name = "typer" }, ] -otel = [ - { name = "opentelemetry-api" }, -] [package.dev-dependencies] dev = [ @@ -409,9 +404,6 @@ dev = [ requires-dist = [ { name = "httpx2", specifier = ">=2.4,<3.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10" }, - { name = "opentelemetry-api", marker = "extra == 'dev'", specifier = ">=1.20" }, - { name = "opentelemetry-api", marker = "extra == 'otel'", specifier = ">=1.20" }, - { name = "opentelemetry-sdk", marker = "extra == 'dev'", specifier = ">=1.20" }, { name = "pip-audit", marker = "extra == 'dev'", specifier = ">=2.7" }, { name = "pydantic", specifier = ">=2.0,<3.0" }, { name = "pydantic", marker = "extra == 'dev'" }, @@ -422,7 +414,7 @@ requires-dist = [ { name = "typer", extras = ["all"], marker = "extra == 'cli'", specifier = ">=0.12,<1.0" }, { name = "typer", extras = ["all"], marker = "extra == 'dev'", specifier = ">=0.12,<1.0" }, ] -provides-extras = ["cli", "otel", "dev"] +provides-extras = ["cli", "dev"] [package.metadata.requires-dev] dev = [{ name = "pytest-asyncio", specifier = ">=0.23.0" }] @@ -652,45 +644,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] -[[package]] -name = "opentelemetry-api" -version = "1.42.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, -] - -[[package]] -name = "opentelemetry-sdk" -version = "1.42.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" }, -] - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.63b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" }, -] - [[package]] name = "packageurl-python" version = "0.17.6"