span-panel-api 2.4.1__tar.gz → 2.4.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 (81) hide show
  1. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.github/workflows/ci.yml +6 -7
  2. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.github/workflows/dependabot-auto-approve.yml +1 -1
  3. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.github/workflows/dependabot-auto-merge.yml +1 -1
  4. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.github/workflows/release.yml +2 -2
  5. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/CHANGELOG.md +7 -0
  6. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/PKG-INFO +1 -4
  7. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/README.md +0 -3
  8. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/pyproject.toml +1 -1
  9. span_panel_api-2.4.2/src/span_panel_api/_http.py +66 -0
  10. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/conftest.py +9 -0
  11. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/test_auth_and_homie_helpers.py +5 -2
  12. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/test_detection_auth.py +15 -3
  13. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/uv.lock +47 -47
  14. span_panel_api-2.4.1/src/span_panel_api/_http.py +0 -27
  15. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.codefactor +0 -0
  16. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.codefactor.yml +0 -0
  17. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.deps-installed +0 -0
  18. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  19. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  20. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.github/dependabot.yml +0 -0
  21. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.gitignore +0 -0
  22. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.markdownlint-cli2.jsonc +0 -0
  23. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.markdownlint.json +0 -0
  24. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.pre-commit-config.yaml +0 -0
  25. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.prettierrc.json +0 -0
  26. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.vscode/extensions.json +0 -0
  27. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/.vscode/tasks.json +0 -0
  28. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/DEVELOPMENT.md +0 -0
  29. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/LICENSE +0 -0
  30. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/SECURITY.md +0 -0
  31. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/conftest.py +0 -0
  32. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/coverage_output.log +0 -0
  33. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/debug_unmapped.py +0 -0
  34. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/developer_attribute_readme.md +0 -0
  35. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/openapi.json +0 -0
  36. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/pytest.ini +0 -0
  37. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/pytest_output.log +0 -0
  38. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/scripts/__init__.py +0 -0
  39. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/scripts/coverage.py +0 -0
  40. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/scripts/format.sh +0 -0
  41. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/scripts/format_markdown.py +0 -0
  42. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/scripts/test_live_auth.py +0 -0
  43. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/setup-hooks.sh +0 -0
  44. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/__init__.py +0 -0
  45. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/auth.py +0 -0
  46. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/const.py +0 -0
  47. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/detection.py +0 -0
  48. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/exceptions.py +0 -0
  49. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/factory.py +0 -0
  50. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/models.py +0 -0
  51. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/mqtt/__init__.py +0 -0
  52. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/mqtt/async_client.py +0 -0
  53. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/mqtt/client.py +0 -0
  54. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/mqtt/connection.py +0 -0
  55. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/mqtt/const.py +0 -0
  56. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/mqtt/field_metadata.py +0 -0
  57. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/mqtt/homie.py +0 -0
  58. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/mqtt/models.py +0 -0
  59. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/phase_validation.py +0 -0
  60. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/protocol.py +0 -0
  61. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/src/span_panel_api/py.typed +0 -0
  62. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
  63. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
  64. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
  65. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/fixtures/v2/README.md +0 -0
  66. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/fixtures/v2/homie_schema.json +0 -0
  67. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/fixtures/v2/status.json +0 -0
  68. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/simulation_fixtures/circuits.response.txt +0 -0
  69. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/simulation_fixtures/panel.response.txt +0 -0
  70. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/simulation_fixtures/soe.response.txt +0 -0
  71. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/simulation_fixtures/status.response.txt +0 -0
  72. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/test_async_mqtt_client.py +0 -0
  73. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/test_field_metadata.py +0 -0
  74. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/test_mqtt_bridge.py +0 -0
  75. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/test_mqtt_connect_flow.py +0 -0
  76. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/test_mqtt_debounce.py +0 -0
  77. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/test_mqtt_homie.py +0 -0
  78. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/test_phase_validation_configs.py +0 -0
  79. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/test_phase_validation_errors.py +0 -0
  80. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/test_protocol_conformance.py +0 -0
  81. {span_panel_api-2.4.1 → span_panel_api-2.4.2}/tests/test_protocol_models.py +0 -0
@@ -11,7 +11,7 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  strategy:
13
13
  matrix:
14
- python-version: ["3.13", "3.14"]
14
+ python-version: ["3.14"]
15
15
 
16
16
  steps:
17
17
  - name: Checkout code
@@ -21,10 +21,9 @@ jobs:
21
21
  uses: actions/setup-python@v6
22
22
  with:
23
23
  python-version: ${{ matrix.python-version }}
24
- allow-prereleases: true
25
24
 
26
25
  - name: Install uv
27
- uses: astral-sh/setup-uv@v5
26
+ uses: astral-sh/setup-uv@v7
28
27
  with:
29
28
  enable-cache: true
30
29
 
@@ -50,10 +49,10 @@ jobs:
50
49
  - name: Set up Python
51
50
  uses: actions/setup-python@v6
52
51
  with:
53
- python-version: "3.13"
52
+ python-version: "3.14"
54
53
 
55
54
  - name: Install uv
56
- uses: astral-sh/setup-uv@v5
55
+ uses: astral-sh/setup-uv@v7
57
56
  with:
58
57
  enable-cache: true
59
58
 
@@ -79,10 +78,10 @@ jobs:
79
78
  - name: Set up Python
80
79
  uses: actions/setup-python@v6
81
80
  with:
82
- python-version: "3.13"
81
+ python-version: "3.14"
83
82
 
84
83
  - name: Install uv
85
- uses: astral-sh/setup-uv@v5
84
+ uses: astral-sh/setup-uv@v7
86
85
  with:
87
86
  enable-cache: true
88
87
 
@@ -15,7 +15,7 @@ jobs:
15
15
  steps:
16
16
  - name: Dependabot metadata
17
17
  id: metadata
18
- uses: dependabot/fetch-metadata@v2.5.0
18
+ uses: dependabot/fetch-metadata@v3.0.0
19
19
  with:
20
20
  github-token: "${{ secrets.GITHUB_TOKEN }}"
21
21
 
@@ -15,7 +15,7 @@ jobs:
15
15
  steps:
16
16
  - name: Dependabot metadata
17
17
  id: metadata
18
- uses: dependabot/fetch-metadata@v2.5.0
18
+ uses: dependabot/fetch-metadata@v3.0.0
19
19
  with:
20
20
  github-token: "${{ secrets.GITHUB_TOKEN }}"
21
21
 
@@ -18,10 +18,10 @@ jobs:
18
18
  - name: Set up Python
19
19
  uses: actions/setup-python@v6
20
20
  with:
21
- python-version: "3.13"
21
+ python-version: "3.14"
22
22
 
23
23
  - name: Install uv
24
- uses: astral-sh/setup-uv@v5
24
+ uses: astral-sh/setup-uv@v7
25
25
  with:
26
26
  enable-cache: true
27
27
 
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.4.2] - 03/2026
8
+
9
+ ### Fixed
10
+
11
+ - **Moved SSL context creation to executor** — `httpx.AsyncClient()` eagerly calls `ssl.SSLContext.load_verify_locations()` with the system CA bundle, which is a blocking file I/O operation that triggers Home Assistant's event loop protection. The SSL
12
+ context is now created in an executor thread and passed to httpx via `verify=ctx`.
13
+
7
14
  ## [2.4.1] - 03/2026
8
15
 
9
16
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: span-panel-api
3
- Version: 2.4.1
3
+ Version: 2.4.2
4
4
  Summary: A client library for SPAN Panel API
5
5
  Project-URL: Homepage, https://github.com/SpanPanel/span-panel-api
6
6
  Project-URL: Issues, https://github.com/SpanPanel/span-panel-api/issues
@@ -22,7 +22,6 @@ Description-Content-Type: text/markdown
22
22
  [![CI Status](https://img.shields.io/github/actions/workflow/status/SpanPanel/span-panel-api/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/SpanPanel/span-panel-api/actions/workflows/ci.yml)
23
23
 
24
24
  [![Code Quality](https://img.shields.io/codefactor/grade/github/SpanPanel/span-panel-api?style=flat-square)](https://www.codefactor.io/repository/github/spanpanel/span-panel-api)
25
- [![Security](https://img.shields.io/snyk/vulnerabilities/github/SpanPanel/span-panel-api?style=flat-square)](https://snyk.io/test/github/SpanPanel/span-panel-api)
26
25
 
27
26
  [![Pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&style=flat-square)](https://github.com/pre-commit/pre-commit)
28
27
  [![Linting: Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json&style=flat-square)](https://github.com/astral-sh/ruff)
@@ -51,8 +50,6 @@ pip install span-panel-api
51
50
 
52
51
  ## Architecture
53
52
 
54
- v2.0.0 is a ground-up rewrite. The package connects to the SPAN Panel's on-device MQTT broker using the [Homie v5](https://homieiot.github.io/) convention. All panel and circuit state is delivered via retained MQTT messages — no polling required.
55
-
56
53
  ### Transport
57
54
 
58
55
  The `SpanMqttClient` connects to the panel's MQTT broker (MQTTS or WebSocket) and subscribes to the Homie device tree. A `HomieDeviceConsumer` state machine parses incoming topic updates into typed `SpanPanelSnapshot` dataclasses. Changes are pushed to
@@ -7,7 +7,6 @@
7
7
  [![CI Status](https://img.shields.io/github/actions/workflow/status/SpanPanel/span-panel-api/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/SpanPanel/span-panel-api/actions/workflows/ci.yml)
8
8
 
9
9
  [![Code Quality](https://img.shields.io/codefactor/grade/github/SpanPanel/span-panel-api?style=flat-square)](https://www.codefactor.io/repository/github/spanpanel/span-panel-api)
10
- [![Security](https://img.shields.io/snyk/vulnerabilities/github/SpanPanel/span-panel-api?style=flat-square)](https://snyk.io/test/github/SpanPanel/span-panel-api)
11
10
 
12
11
  [![Pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&style=flat-square)](https://github.com/pre-commit/pre-commit)
13
12
  [![Linting: Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json&style=flat-square)](https://github.com/astral-sh/ruff)
@@ -36,8 +35,6 @@ pip install span-panel-api
36
35
 
37
36
  ## Architecture
38
37
 
39
- v2.0.0 is a ground-up rewrite. The package connects to the SPAN Panel's on-device MQTT broker using the [Homie v5](https://homieiot.github.io/) convention. All panel and circuit state is delivered via retained MQTT messages — no polling required.
40
-
41
38
  ### Transport
42
39
 
43
40
  The `SpanMqttClient` connects to the panel's MQTT broker (MQTTS or WebSocket) and subscribes to the Homie device tree. A `HomieDeviceConsumer` state machine parses incoming topic updates into typed `SpanPanelSnapshot` dataclasses. Changes are pushed to
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "span-panel-api"
3
- version = "2.4.1"
3
+ version = "2.4.2"
4
4
  description = "A client library for SPAN Panel API"
5
5
  authors = [
6
6
  {name = "SpanPanel"}
@@ -0,0 +1,66 @@
1
+ """Shared HTTP helpers for SPAN Panel bootstrap REST calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import AsyncIterator
7
+ from contextlib import asynccontextmanager
8
+ from dataclasses import dataclass, field
9
+ import ssl
10
+
11
+ import httpx
12
+
13
+
14
+ @dataclass
15
+ class _SSLCache:
16
+ """Mutable container for the cached SSLContext and its async lock."""
17
+
18
+ context: ssl.SSLContext | None = None
19
+ lock: asyncio.Lock | None = field(default=None, repr=False)
20
+
21
+ def get_lock(self) -> asyncio.Lock:
22
+ """Return the async lock, creating it lazily."""
23
+ if self.lock is None:
24
+ self.lock = asyncio.Lock()
25
+ return self.lock
26
+
27
+
28
+ _ssl_cache = _SSLCache()
29
+
30
+
31
+ def _build_url(host: str, port: int, path: str) -> str:
32
+ """Build an HTTP URL, omitting the port when it is the default (80)."""
33
+ if port == 80:
34
+ return f"http://{host}{path}"
35
+ return f"http://{host}:{port}{path}"
36
+
37
+
38
+ async def _create_ssl_context() -> ssl.SSLContext:
39
+ """Return a cached default SSL context, creating it in an executor on first call.
40
+
41
+ ``ssl.create_default_context()`` calls ``load_verify_locations`` which
42
+ performs blocking file I/O on the system CA bundle. The resulting context
43
+ is thread-safe and reusable, so we cache it for the lifetime of the process.
44
+ """
45
+ if _ssl_cache.context is not None:
46
+ return _ssl_cache.context
47
+ async with _ssl_cache.get_lock():
48
+ # Double-check after acquiring the lock.
49
+ if _ssl_cache.context is not None:
50
+ return _ssl_cache.context
51
+ loop = asyncio.get_running_loop()
52
+ _ssl_cache.context = await loop.run_in_executor(None, ssl.create_default_context)
53
+ return _ssl_cache.context
54
+
55
+
56
+ @asynccontextmanager
57
+ async def _get_client(
58
+ httpx_client: httpx.AsyncClient | None,
59
+ timeout: float,
60
+ ) -> AsyncIterator[httpx.AsyncClient]:
61
+ if httpx_client is not None:
62
+ yield httpx_client
63
+ return
64
+ ctx = await _create_ssl_context()
65
+ async with httpx.AsyncClient(timeout=timeout, verify=ctx) as client:
66
+ yield client
@@ -13,9 +13,18 @@ import pytest
13
13
  from paho.mqtt.client import ConnectFlags
14
14
  from paho.mqtt.reasoncodes import ReasonCode
15
15
 
16
+ import span_panel_api._http as _http_mod
16
17
  from span_panel_api.models import V2HomieSchema
17
18
  from span_panel_api.mqtt.const import TOPIC_PREFIX, TYPE_CORE
18
19
 
20
+
21
+ @pytest.fixture(autouse=True)
22
+ def _reset_ssl_cache() -> None:
23
+ """Ensure the module-level SSL context cache doesn't leak between tests."""
24
+ _http_mod._ssl_cache.context = None
25
+ _http_mod._ssl_cache.lock = None
26
+
27
+
19
28
  # ---------------------------------------------------------------------------
20
29
  # Constants shared across MQTT tests
21
30
  # ---------------------------------------------------------------------------
@@ -145,7 +145,10 @@ class TestHttpxClientInjectionAuthHelpers:
145
145
  headers={"content-type": "text/plain"},
146
146
  request=httpx.Request("GET", "http://test"),
147
147
  )
148
- with patch("span_panel_api._http.httpx.AsyncClient") as cls:
148
+ with (
149
+ patch("span_panel_api._http.httpx.AsyncClient") as cls,
150
+ patch("span_panel_api._http._create_ssl_context", new_callable=AsyncMock) as mock_ctx,
151
+ ):
149
152
  mock_client = AsyncMock()
150
153
  mock_client.get.return_value = mock_response
151
154
  mock_client.__aenter__ = AsyncMock(return_value=mock_client)
@@ -154,7 +157,7 @@ class TestHttpxClientInjectionAuthHelpers:
154
157
 
155
158
  await download_ca_cert("192.168.1.1", timeout=88.5)
156
159
 
157
- cls.assert_called_once_with(timeout=88.5)
160
+ cls.assert_called_once_with(timeout=88.5, verify=mock_ctx.return_value)
158
161
 
159
162
  @pytest.mark.asyncio
160
163
  async def test_get_homie_schema_injected_skips_constructor(self) -> None:
@@ -52,7 +52,10 @@ class TestHttpHelpers:
52
52
 
53
53
  @pytest.mark.asyncio
54
54
  async def test_get_client_creates_and_closes_fallback_client(self) -> None:
55
- with patch("span_panel_api._http.httpx.AsyncClient") as mock_cls:
55
+ with (
56
+ patch("span_panel_api._http.httpx.AsyncClient") as mock_cls,
57
+ patch("span_panel_api._http._create_ssl_context", new_callable=AsyncMock) as mock_ctx,
58
+ ):
56
59
  mock_instance = AsyncMock()
57
60
  mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
58
61
  mock_instance.__aexit__ = AsyncMock(return_value=False)
@@ -63,6 +66,7 @@ class TestHttpHelpers:
63
66
 
64
67
  mock_cls.assert_called_once_with(
65
68
  timeout=12.5,
69
+ verify=mock_ctx.return_value,
66
70
  )
67
71
  mock_instance.__aenter__.assert_awaited_once()
68
72
  mock_instance.__aexit__.assert_awaited_once()
@@ -135,7 +139,10 @@ class TestHttpxClientInjection:
135
139
  @pytest.mark.asyncio
136
140
  async def test_fallback_client_uses_register_v2_timeout(self) -> None:
137
141
  mock_response = _mock_response(200, V2_AUTH_JSON)
138
- with patch("span_panel_api._http.httpx.AsyncClient") as mock_client_cls:
142
+ with (
143
+ patch("span_panel_api._http.httpx.AsyncClient") as mock_client_cls,
144
+ patch("span_panel_api._http._create_ssl_context", new_callable=AsyncMock) as mock_ctx,
145
+ ):
139
146
  mock_client = AsyncMock()
140
147
  mock_client.post.return_value = mock_response
141
148
  mock_client.__aenter__ = AsyncMock(return_value=mock_client)
@@ -146,6 +153,7 @@ class TestHttpxClientInjection:
146
153
 
147
154
  mock_client_cls.assert_called_once_with(
148
155
  timeout=42.5,
156
+ verify=mock_ctx.return_value,
149
157
  )
150
158
 
151
159
  @pytest.mark.asyncio
@@ -179,7 +187,10 @@ class TestHttpxClientInjection:
179
187
  @pytest.mark.asyncio
180
188
  async def test_detect_api_version_fallback_uses_timeout(self) -> None:
181
189
  mock_response = _mock_response(200, V2_STATUS_JSON)
182
- with patch("span_panel_api._http.httpx.AsyncClient") as mock_client_cls:
190
+ with (
191
+ patch("span_panel_api._http.httpx.AsyncClient") as mock_client_cls,
192
+ patch("span_panel_api._http._create_ssl_context", new_callable=AsyncMock) as mock_ctx,
193
+ ):
183
194
  mock_client = AsyncMock()
184
195
  mock_client.get.return_value = mock_response
185
196
  mock_client.__aenter__ = AsyncMock(return_value=mock_client)
@@ -190,6 +201,7 @@ class TestHttpxClientInjection:
190
201
 
191
202
  mock_client_cls.assert_called_once_with(
192
203
  timeout=3.25,
204
+ verify=mock_ctx.return_value,
193
205
  )
194
206
 
195
207
 
@@ -427,51 +427,51 @@ toml = [
427
427
 
428
428
  [[package]]
429
429
  name = "cryptography"
430
- version = "46.0.5"
430
+ version = "46.0.6"
431
431
  source = { registry = "https://pypi.org/simple" }
432
432
  dependencies = [
433
433
  { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
434
434
  { name = "typing-extensions", marker = "python_full_version < '3.11'" },
435
435
  ]
436
- sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
437
- wheels = [
438
- { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
439
- { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
440
- { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
441
- { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
442
- { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
443
- { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
444
- { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
445
- { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
446
- { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
447
- { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
448
- { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
449
- { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" },
450
- { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" },
451
- { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" },
452
- { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" },
453
- { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" },
454
- { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" },
455
- { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" },
456
- { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" },
457
- { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" },
458
- { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" },
459
- { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" },
460
- { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
461
- { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
462
- { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
463
- { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
464
- { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
465
- { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
466
- { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
467
- { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
468
- { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
469
- { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
470
- { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
471
- { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" },
472
- { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" },
473
- { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" },
474
- { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" },
436
+ sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" }
437
+ wheels = [
438
+ { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" },
439
+ { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" },
440
+ { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" },
441
+ { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" },
442
+ { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" },
443
+ { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" },
444
+ { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" },
445
+ { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" },
446
+ { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" },
447
+ { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" },
448
+ { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" },
449
+ { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" },
450
+ { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" },
451
+ { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" },
452
+ { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" },
453
+ { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" },
454
+ { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" },
455
+ { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" },
456
+ { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" },
457
+ { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" },
458
+ { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" },
459
+ { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" },
460
+ { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" },
461
+ { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" },
462
+ { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" },
463
+ { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" },
464
+ { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" },
465
+ { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" },
466
+ { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" },
467
+ { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" },
468
+ { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" },
469
+ { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" },
470
+ { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" },
471
+ { url = "https://files.pythonhosted.org/packages/bc/1f/4c926f50df7749f000f20eede0c896769509895e2648db5da0ed55db711d/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8", size = 4218227, upload-time = "2026-03-25T23:34:40.871Z" },
472
+ { url = "https://files.pythonhosted.org/packages/c6/65/707be3ffbd5f786028665c3223e86e11c4cda86023adbc56bd72b1b6bab5/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0", size = 4381399, upload-time = "2026-03-25T23:34:42.609Z" },
473
+ { url = "https://files.pythonhosted.org/packages/f3/6d/73557ed0ef7d73d04d9aba745d2c8e95218213687ee5e76b7d236a5030fc/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b", size = 4217595, upload-time = "2026-03-25T23:34:44.205Z" },
474
+ { url = "https://files.pythonhosted.org/packages/9e/c5/e1594c4eec66a567c3ac4400008108a415808be2ce13dcb9a9045c92f1a0/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a", size = 4380912, upload-time = "2026-03-25T23:34:46.328Z" },
475
475
  ]
476
476
 
477
477
  [[package]]
@@ -988,11 +988,11 @@ wheels = [
988
988
 
989
989
  [[package]]
990
990
  name = "pygments"
991
- version = "2.19.2"
991
+ version = "2.20.0"
992
992
  source = { registry = "https://pypi.org/simple" }
993
- sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
993
+ sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
994
994
  wheels = [
995
- { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
995
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
996
996
  ]
997
997
 
998
998
  [[package]]
@@ -1214,7 +1214,7 @@ wheels = [
1214
1214
 
1215
1215
  [[package]]
1216
1216
  name = "requests"
1217
- version = "2.32.5"
1217
+ version = "2.33.0"
1218
1218
  source = { registry = "https://pypi.org/simple" }
1219
1219
  dependencies = [
1220
1220
  { name = "certifi" },
@@ -1222,9 +1222,9 @@ dependencies = [
1222
1222
  { name = "idna" },
1223
1223
  { name = "urllib3" },
1224
1224
  ]
1225
- sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
1225
+ sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" }
1226
1226
  wheels = [
1227
- { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
1227
+ { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" },
1228
1228
  ]
1229
1229
 
1230
1230
  [[package]]
@@ -1310,7 +1310,7 @@ wheels = [
1310
1310
 
1311
1311
  [[package]]
1312
1312
  name = "span-panel-api"
1313
- version = "2.4.1"
1313
+ version = "2.4.2"
1314
1314
  source = { editable = "." }
1315
1315
  dependencies = [
1316
1316
  { name = "httpx" },
@@ -1,27 +0,0 @@
1
- """Shared HTTP helpers for SPAN Panel bootstrap REST calls."""
2
-
3
- from __future__ import annotations
4
-
5
- from collections.abc import AsyncIterator
6
- from contextlib import asynccontextmanager
7
-
8
- import httpx
9
-
10
-
11
- def _build_url(host: str, port: int, path: str) -> str:
12
- """Build an HTTP URL, omitting the port when it is the default (80)."""
13
- if port == 80:
14
- return f"http://{host}{path}"
15
- return f"http://{host}:{port}{path}"
16
-
17
-
18
- @asynccontextmanager
19
- async def _get_client(
20
- httpx_client: httpx.AsyncClient | None,
21
- timeout: float,
22
- ) -> AsyncIterator[httpx.AsyncClient]:
23
- if httpx_client is not None:
24
- yield httpx_client
25
- return
26
- async with httpx.AsyncClient(timeout=timeout) as client:
27
- yield client
File without changes