span-panel-api 3.0.0b6__tar.gz → 3.0.0b7__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.
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/CHANGELOG.md +9 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/PKG-INFO +1 -1
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/pyproject.toml +1 -1
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/factory.py +13 -3
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/mqtt/client.py +17 -2
- span_panel_api-3.0.0b7/tests/test_shared_http_client.py +121 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/.gitignore +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/LICENSE +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/README.md +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/__init__.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/_http.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/adapters.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/auth.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/const.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/detection.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/dispatch.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/exceptions.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/models.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/mqtt/__init__.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/mqtt/async_client.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/mqtt/connection.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/mqtt/const.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/mqtt/models.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/phase_validation.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/protocol.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/py.typed +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/reference_payloads/README.md +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/reference_payloads/__init__.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/reference_payloads/homie_schema.json +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/schema_drift.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/conftest.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/fixtures/flat_wire.json +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/fixtures/panelbench_unvalued_by_both.json +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/fixtures/v2/README.md +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/fixtures/v2/status.json +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/simulation_fixtures/circuits.response.txt +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/simulation_fixtures/panel.response.txt +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/simulation_fixtures/soe.response.txt +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/simulation_fixtures/status.response.txt +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_accumulator.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_adapters_discovery.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_adopted_control.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_adoption.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_async_mqtt_client.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_auth_and_homie_helpers.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_catalog_divergence.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_detection_auth.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_exceptions.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_factory_dispatch.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_field_metadata.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_live_flat_differential.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_mqtt_bridge.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_mqtt_client_connection.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_mqtt_connect_flow.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_mqtt_debounce.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_mqtt_homie.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_packaging.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_phase_validation_configs.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_phase_validation_errors.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_protocol_conformance.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_protocol_models.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_public_api_unchanged.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_redispatch_on_reconnect.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_reference_tree_values.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_generation_cross_check.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_migration_delta.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_one_adapter.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_one_against_simulator.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_one_charge_limit.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_one_circuits.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_one_conformance.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_one_connection_health.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_one_devices.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_one_discovery.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_one_panel.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_one_pcs.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_one_service_entrance.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_one_shed_forecast.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_one_snapshot.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_one_transport.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_provenance.py +0 -0
- {span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_zero_adapter.py +0 -0
|
@@ -4,6 +4,15 @@ 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
|
+
## [3.0.0b7]
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **`SpanMqttClient` accepts an `httpx_client`, and so does `create_span_client`.** Four config-flow-facing entry points already took an injected client; the runtime path was the one that did not, so every schema read built a throwaway — including the
|
|
12
|
+
retry loop that runs during a firmware upgrade, which built one per attempt at exactly the moment the panel was mid-reboot. Optional and defaulted, so nothing outside Home Assistant changes. The ownership rule is the one the existing entry points already
|
|
13
|
+
state: a client handed in is never closed here, and its timeouts, limits and headers are the caller's, which is why the per-call `timeout` defaults are ignored when one is given. Home Assistant's shared client carries httpx's default timeout rather than
|
|
14
|
+
this library's 10 s, and that is the caller exercising the policy it owns rather than a setting being lost.
|
|
15
|
+
|
|
7
16
|
## [3.0.0b6]
|
|
8
17
|
|
|
9
18
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: span-panel-api
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0b7
|
|
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
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import logging
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
11
12
|
|
|
12
13
|
from .adapters import resolve_adapter
|
|
13
14
|
from .auth import get_homie_schema, register_v2
|
|
@@ -17,6 +18,9 @@ from .exceptions import SpanPanelAuthError
|
|
|
17
18
|
from .mqtt.client import SpanMqttClient
|
|
18
19
|
from .mqtt.models import MqttClientConfig
|
|
19
20
|
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
20
24
|
_LOGGER = logging.getLogger(__name__)
|
|
21
25
|
|
|
22
26
|
_V2_CLIENT_NAME = "span-panel-api"
|
|
@@ -28,6 +32,7 @@ async def create_span_client(
|
|
|
28
32
|
mqtt_config: MqttClientConfig | None = None,
|
|
29
33
|
serial_number: str | None = None,
|
|
30
34
|
port: int = 80,
|
|
35
|
+
httpx_client: httpx.AsyncClient | None = None,
|
|
31
36
|
) -> SpanMqttClient:
|
|
32
37
|
"""Create a SPAN Panel MQTT client.
|
|
33
38
|
|
|
@@ -37,6 +42,10 @@ async def create_span_client(
|
|
|
37
42
|
mqtt_config: Pre-built MQTT broker configuration.
|
|
38
43
|
serial_number: Panel serial number (extracted from detection/registration if omitted).
|
|
39
44
|
port: HTTP port of the panel bootstrap API used for registration and detection.
|
|
45
|
+
httpx_client: Optional shared ``httpx.AsyncClient``, used for every request this
|
|
46
|
+
makes and handed to the client it builds. Not closed here; its timeouts and
|
|
47
|
+
limits are the caller's, which is why the per-call ``timeout`` defaults are
|
|
48
|
+
ignored when one is given.
|
|
40
49
|
|
|
41
50
|
Returns:
|
|
42
51
|
A connected-ready SpanMqttClient instance.
|
|
@@ -54,7 +63,7 @@ async def create_span_client(
|
|
|
54
63
|
if mqtt_config is None:
|
|
55
64
|
if passphrase is None:
|
|
56
65
|
raise SpanPanelAuthError("Neither mqtt_config nor passphrase provided")
|
|
57
|
-
auth_response = await register_v2(host, _V2_CLIENT_NAME, passphrase, port=port)
|
|
66
|
+
auth_response = await register_v2(host, _V2_CLIENT_NAME, passphrase, port=port, httpx_client=httpx_client)
|
|
58
67
|
mqtt_config = MqttClientConfig(
|
|
59
68
|
broker_host=auth_response.ebus_broker_host,
|
|
60
69
|
username=auth_response.ebus_broker_username,
|
|
@@ -68,7 +77,7 @@ async def create_span_client(
|
|
|
68
77
|
|
|
69
78
|
if serial_number is None:
|
|
70
79
|
# Try to detect from panel status
|
|
71
|
-
result = await detect_api_version(host, port=port)
|
|
80
|
+
result = await detect_api_version(host, port=port, httpx_client=httpx_client)
|
|
72
81
|
if result.status_info is not None:
|
|
73
82
|
serial_number = result.status_info.serial_number
|
|
74
83
|
|
|
@@ -80,7 +89,7 @@ async def create_span_client(
|
|
|
80
89
|
# flat-versus-parent/child signal, mirroring MQTT's `info/data-model-version`
|
|
81
90
|
# — so the parser is chosen before a single message is consumed, rather than
|
|
82
91
|
# a wrong parser being discovered by its output.
|
|
83
|
-
schema = await get_homie_schema(host, port=port)
|
|
92
|
+
schema = await get_homie_schema(host, port=port, httpx_client=httpx_client)
|
|
84
93
|
adapter_key, dispatch_reason = select_adapter_key(schema.data_model_version)
|
|
85
94
|
# In a thread: resolution reads distribution metadata and imports the adapter
|
|
86
95
|
# package, and this is the first call in the process to do either. See
|
|
@@ -96,6 +105,7 @@ async def create_span_client(
|
|
|
96
105
|
data_model_version=schema.data_model_version,
|
|
97
106
|
schema_dispatch_reason=dispatch_reason,
|
|
98
107
|
schema=schema,
|
|
108
|
+
httpx_client=httpx_client,
|
|
99
109
|
)
|
|
100
110
|
await client.connect()
|
|
101
111
|
return client
|
|
@@ -13,6 +13,7 @@ import contextlib
|
|
|
13
13
|
from importlib.metadata import version
|
|
14
14
|
import logging
|
|
15
15
|
import time
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
16
17
|
|
|
17
18
|
from span_panel_api.schema_drift import log_schema_drift
|
|
18
19
|
|
|
@@ -34,6 +35,9 @@ from .connection import AsyncMqttBridge
|
|
|
34
35
|
from .const import MQTT_READY_TIMEOUT_S
|
|
35
36
|
from .models import MqttClientConfig
|
|
36
37
|
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
import httpx
|
|
40
|
+
|
|
37
41
|
_LOGGER = logging.getLogger(__name__)
|
|
38
42
|
|
|
39
43
|
# How long to wait for circuit name properties after device ready.
|
|
@@ -74,6 +78,7 @@ class SpanMqttClient:
|
|
|
74
78
|
data_model_version: str | None = None,
|
|
75
79
|
schema_dispatch_reason: str | None = None,
|
|
76
80
|
schema: V2HomieSchema | None = None,
|
|
81
|
+
httpx_client: httpx.AsyncClient | None = None,
|
|
77
82
|
) -> None:
|
|
78
83
|
self._host = host
|
|
79
84
|
self._serial_number = serial_number
|
|
@@ -81,6 +86,12 @@ class SpanMqttClient:
|
|
|
81
86
|
self._snapshot_interval = snapshot_interval
|
|
82
87
|
self._panel_http_port = panel_http_port
|
|
83
88
|
self._adapter_factory = adapter_factory
|
|
89
|
+
# Shared by the caller, owned by the caller: never closed here, and its
|
|
90
|
+
# policy -- timeouts, limits, headers -- is whatever the caller set. That
|
|
91
|
+
# is the same rule the four config-flow entry points already state, and
|
|
92
|
+
# the reason this exists at all is that the runtime path was the one place
|
|
93
|
+
# left without it. See `_get_client`.
|
|
94
|
+
self._httpx_client = httpx_client
|
|
84
95
|
|
|
85
96
|
self._bridge: AsyncMqttBridge | None = None
|
|
86
97
|
self._adapter: SchemaAdapter | None = None
|
|
@@ -287,7 +298,11 @@ class SpanMqttClient:
|
|
|
287
298
|
# would be a second call to the same unauthenticated endpoint for a
|
|
288
299
|
# value that cannot have changed. A directly-constructed client has no
|
|
289
300
|
# schema yet, so it fetches here and dispatches in _build_adapter.
|
|
290
|
-
schema =
|
|
301
|
+
schema = (
|
|
302
|
+
self._schema
|
|
303
|
+
if self._schema is not None
|
|
304
|
+
else await get_homie_schema(self._host, port=self._panel_http_port, httpx_client=self._httpx_client)
|
|
305
|
+
)
|
|
291
306
|
self._schema = schema
|
|
292
307
|
await self._preload_adapter(schema)
|
|
293
308
|
adapter = self._build_adapter(schema)
|
|
@@ -807,7 +822,7 @@ class SpanMqttClient:
|
|
|
807
822
|
last: Exception | None = None
|
|
808
823
|
for _ in range(_REDISPATCH_RETRY_ATTEMPTS):
|
|
809
824
|
try:
|
|
810
|
-
return await get_homie_schema(self._host, port=self._panel_http_port)
|
|
825
|
+
return await get_homie_schema(self._host, port=self._panel_http_port, httpx_client=self._httpx_client)
|
|
811
826
|
except (SpanPanelConnectionError, SpanPanelTimeoutError) as exc:
|
|
812
827
|
last = exc
|
|
813
828
|
await asyncio.sleep(delay)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""The runtime path uses the caller's HTTP client, not one of its own.
|
|
2
|
+
|
|
3
|
+
Four config-flow-facing entry points already take an injected
|
|
4
|
+
`httpx.AsyncClient`; `SpanMqttClient` was the one runtime entry point without
|
|
5
|
+
it, so every schema fetch built a throwaway client -- including the retry loop
|
|
6
|
+
that runs during a firmware upgrade, which builds one per attempt at exactly the
|
|
7
|
+
moment the panel is mid-reboot.
|
|
8
|
+
|
|
9
|
+
Home Assistant is the caller that cares. It owns a shared client, closes it at
|
|
10
|
+
shutdown, and the integration's own `quality_scale.yaml` claims
|
|
11
|
+
`inject-websession: done` -- a claim that was true of the config flow and not of
|
|
12
|
+
anything that ran afterwards.
|
|
13
|
+
|
|
14
|
+
**Ownership is the whole contract.** A client handed in is never closed here, and
|
|
15
|
+
its policy is the caller's: timeouts, limits and headers are whatever the caller
|
|
16
|
+
configured, which is why the per-call `timeout` arguments are documented as
|
|
17
|
+
ignored when a client is injected. Home Assistant's shared client carries
|
|
18
|
+
httpx's default timeout rather than this library's, and that is the caller
|
|
19
|
+
exercising the policy it owns rather than a setting being lost.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import contextlib
|
|
26
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
27
|
+
|
|
28
|
+
import pytest
|
|
29
|
+
|
|
30
|
+
from span_panel_api.mqtt import MqttClientConfig
|
|
31
|
+
from span_panel_api.mqtt.client import SpanMqttClient
|
|
32
|
+
|
|
33
|
+
SERIAL = "sp3-242424-001"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _Schema:
|
|
37
|
+
def __init__(self, version: str | None) -> None:
|
|
38
|
+
self.data_model_version = version
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _client(injected: object | None) -> SpanMqttClient:
|
|
42
|
+
return SpanMqttClient(
|
|
43
|
+
host="192.168.1.1",
|
|
44
|
+
serial_number=SERIAL,
|
|
45
|
+
broker_config=MqttClientConfig(broker_host="broker.local", username="u", password="p"),
|
|
46
|
+
httpx_client=injected, # type: ignore[arg-type]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_the_connect_fetch_uses_the_injected_client() -> None:
|
|
52
|
+
"""The first schema read of a session, and the one every install makes."""
|
|
53
|
+
sentinel = MagicMock(name="shared-client")
|
|
54
|
+
client = _client(sentinel)
|
|
55
|
+
|
|
56
|
+
fetch = AsyncMock(return_value=_Schema("1.0"))
|
|
57
|
+
with (
|
|
58
|
+
patch("span_panel_api.mqtt.client.get_homie_schema", fetch),
|
|
59
|
+
patch.object(client, "_preload_adapter", AsyncMock()),
|
|
60
|
+
patch.object(client, "_build_adapter", MagicMock()),
|
|
61
|
+
patch.object(client, "_connect_bridge", AsyncMock(), create=True),
|
|
62
|
+
):
|
|
63
|
+
# Connect goes on to bring up the MQTT bridge, which has nothing to do
|
|
64
|
+
# with this assertion and no broker to reach. The fetch is what is under
|
|
65
|
+
# test, and the await-count assertion below is what keeps that from
|
|
66
|
+
# passing vacuously if it never happened at all.
|
|
67
|
+
with contextlib.suppress(Exception):
|
|
68
|
+
await client.connect()
|
|
69
|
+
|
|
70
|
+
assert fetch.await_count >= 1
|
|
71
|
+
assert fetch.await_args.kwargs["httpx_client"] is sentinel
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.asyncio
|
|
75
|
+
async def test_the_upgrade_refetch_uses_the_injected_client() -> None:
|
|
76
|
+
"""The path that mattered most, because it builds one client per retry attempt.
|
|
77
|
+
|
|
78
|
+
A panel accepts MQTT before it serves HTTP, so this loop can run several times
|
|
79
|
+
in a row while the panel finishes booting -- each one previously a fresh
|
|
80
|
+
client, a fresh connection pool, thrown away on the next attempt.
|
|
81
|
+
"""
|
|
82
|
+
sentinel = MagicMock(name="shared-client")
|
|
83
|
+
client = _client(sentinel)
|
|
84
|
+
client._loop = asyncio.get_running_loop()
|
|
85
|
+
|
|
86
|
+
fetch = AsyncMock(return_value=_Schema("1.0"))
|
|
87
|
+
with patch("span_panel_api.mqtt.client.get_homie_schema", fetch):
|
|
88
|
+
assert await client._fetch_schema_with_retry() is not None
|
|
89
|
+
|
|
90
|
+
assert fetch.await_args.kwargs["httpx_client"] is sentinel
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@pytest.mark.asyncio
|
|
94
|
+
async def test_an_injected_client_is_never_closed_here() -> None:
|
|
95
|
+
"""It belongs to the caller, and the caller may still be using it.
|
|
96
|
+
|
|
97
|
+
Home Assistant hands out one shared client to every integration and closes it
|
|
98
|
+
at shutdown; closing it from here would take the others down with it. HA
|
|
99
|
+
guards its own copy, but a library that relies on the caller guarding it is
|
|
100
|
+
relying on the caller.
|
|
101
|
+
"""
|
|
102
|
+
sentinel = MagicMock(name="shared-client")
|
|
103
|
+
sentinel.aclose = AsyncMock()
|
|
104
|
+
client = _client(sentinel)
|
|
105
|
+
|
|
106
|
+
await client.close()
|
|
107
|
+
|
|
108
|
+
sentinel.aclose.assert_not_awaited()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@pytest.mark.asyncio
|
|
112
|
+
async def test_no_injected_client_still_works() -> None:
|
|
113
|
+
"""The default has to stay the default: this library is not Home Assistant's alone."""
|
|
114
|
+
client = _client(None)
|
|
115
|
+
client._loop = asyncio.get_running_loop()
|
|
116
|
+
|
|
117
|
+
fetch = AsyncMock(return_value=_Schema("1.0"))
|
|
118
|
+
with patch("span_panel_api.mqtt.client.get_homie_schema", fetch):
|
|
119
|
+
assert await client._fetch_schema_with_retry() is not None
|
|
120
|
+
|
|
121
|
+
assert fetch.await_args.kwargs["httpx_client"] is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/reference_payloads/README.md
RENAMED
|
File without changes
|
{span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/src/span_panel_api/reference_payloads/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/fixtures/panelbench_unvalued_by_both.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/simulation_fixtures/circuits.response.txt
RENAMED
|
File without changes
|
{span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/simulation_fixtures/panel.response.txt
RENAMED
|
File without changes
|
{span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/simulation_fixtures/soe.response.txt
RENAMED
|
File without changes
|
{span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/simulation_fixtures/status.response.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_generation_cross_check.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_one_against_simulator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{span_panel_api-3.0.0b6 → span_panel_api-3.0.0b7}/tests/test_schema_one_connection_health.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|