piphi-runtime-kit-python 0.4.2__tar.gz → 0.4.3__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.
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/PKG-INFO +1 -1
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/pyproject.toml +1 -1
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/__init__.py +19 -1
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/config_sync.py +74 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/health.py +1 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/schemas.py +22 -0
- piphi_runtime_kit_python-0.4.3/tests/test_config_sync.py +138 -0
- piphi_runtime_kit_python-0.4.2/tests/test_config_sync.py +0 -74
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/LICENSE +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/README.md +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/adapters/__init__.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/adapters/fastapi.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/fastapi.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/__init__.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/auth.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/configuration.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/context.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/discovery.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/dispatch.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/errors.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/events.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/identity.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/lifespan.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/mqtt.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/registry.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/starter.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/state.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/tasks.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/telemetry.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/__init__.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_fastapi_adapter.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_auth.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_auth_matrix.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_config_sync_matrix.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_configuration.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_configuration_matrix.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_delivery_errors.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_discovery.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_discovery_matrix.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_dispatch.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_dispatch_matrix.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_errors_matrix.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_events.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_events_matrix.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_health.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_identity.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_lifespan.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_mqtt.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_mqtt_matrix.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_registry.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_registry_matrix.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_starter.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_support_matrix.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_tasks.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_telemetry.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_telemetry_matrix.py +0 -0
- {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_schemas_matrix.py +0 -0
|
@@ -8,7 +8,18 @@ from .runtime.auth import (
|
|
|
8
8
|
format_runtime_auth_sync_log,
|
|
9
9
|
mask_token,
|
|
10
10
|
)
|
|
11
|
-
from .runtime.config_sync import
|
|
11
|
+
from .runtime.config_sync import (
|
|
12
|
+
CORE_BASE_URL_ENV_NAME,
|
|
13
|
+
DEFAULT_RUNTIME_CONFIG_SNAPSHOT_FILENAME,
|
|
14
|
+
DEFAULT_RUNTIME_VOLUME_DIR,
|
|
15
|
+
RUNTIME_CONFIG_SNAPSHOT_PATH_ENV_NAME,
|
|
16
|
+
ConfigSyncCoordinator,
|
|
17
|
+
build_sync_response,
|
|
18
|
+
load_runtime_config_snapshot,
|
|
19
|
+
reconcile_config_ids,
|
|
20
|
+
resolve_core_base_url,
|
|
21
|
+
resolve_runtime_config_snapshot_path,
|
|
22
|
+
)
|
|
12
23
|
from .runtime.configuration import (
|
|
13
24
|
build_config_apply_response,
|
|
14
25
|
build_config_remove_response,
|
|
@@ -109,6 +120,10 @@ __all__ = [
|
|
|
109
120
|
"CoreUnavailableError",
|
|
110
121
|
"CoreUnexpectedResponseError",
|
|
111
122
|
"DEFAULT_CORE_CLIENT_TIMEOUT_SECONDS",
|
|
123
|
+
"CORE_BASE_URL_ENV_NAME",
|
|
124
|
+
"DEFAULT_RUNTIME_CONFIG_SNAPSHOT_FILENAME",
|
|
125
|
+
"DEFAULT_RUNTIME_VOLUME_DIR",
|
|
126
|
+
"RUNTIME_CONFIG_SNAPSHOT_PATH_ENV_NAME",
|
|
112
127
|
"DEFAULT_RUNTIME_CONTAINER_ID_ENV_NAME",
|
|
113
128
|
"DEFAULT_RUNTIME_INTERNAL_TOKEN_ENV_NAME",
|
|
114
129
|
"RUNTIME_CONTAINER_ID_HEADER_NAME",
|
|
@@ -169,6 +184,7 @@ __all__ = [
|
|
|
169
184
|
"format_discovery_attempt_log",
|
|
170
185
|
"format_event_log",
|
|
171
186
|
"format_runtime_auth_sync_log",
|
|
187
|
+
"load_runtime_config_snapshot",
|
|
172
188
|
"mask_token",
|
|
173
189
|
"MqttBrokerConfig",
|
|
174
190
|
"MqttJsonClient",
|
|
@@ -176,6 +192,8 @@ __all__ = [
|
|
|
176
192
|
"normalize_event_payload",
|
|
177
193
|
"redact_config_secrets",
|
|
178
194
|
"reconcile_config_ids",
|
|
195
|
+
"resolve_core_base_url",
|
|
196
|
+
"resolve_runtime_config_snapshot_path",
|
|
179
197
|
"runtime_lifespan",
|
|
180
198
|
"schedule_event_delivery",
|
|
181
199
|
"schedule_telemetry_delivery",
|
|
@@ -3,6 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
"""Helpers for basic config-sync orchestration and generation tracking."""
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
6
9
|
from typing import Awaitable, Callable, TypeVar
|
|
7
10
|
|
|
8
11
|
from ..schemas import RuntimeConfig, RuntimeConfigSnapshot, RuntimeConfigSyncResponse
|
|
@@ -10,6 +13,77 @@ from .state import RuntimeProcessState
|
|
|
10
13
|
|
|
11
14
|
TConfig = TypeVar("TConfig", bound=RuntimeConfig)
|
|
12
15
|
|
|
16
|
+
CORE_BASE_URL_ENV_NAME = "PIPHI_CORE_BASE_URL"
|
|
17
|
+
RUNTIME_CONTAINER_ID_ENV_NAME = "PIPHI_CONTAINER_ID"
|
|
18
|
+
RUNTIME_CONFIG_SNAPSHOT_PATH_ENV_NAME = "PIPHI_CONFIG_SNAPSHOT_PATH"
|
|
19
|
+
DEFAULT_RUNTIME_VOLUME_DIR = "/.piphinetwork"
|
|
20
|
+
DEFAULT_RUNTIME_CONFIG_SNAPSHOT_FILENAME = "configs.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def resolve_core_base_url(default: str | None = None) -> str | None:
|
|
24
|
+
"""Resolve Core's base URL from the managed-runtime environment."""
|
|
25
|
+
value = (os.getenv(CORE_BASE_URL_ENV_NAME) or "").strip().rstrip("/")
|
|
26
|
+
if value:
|
|
27
|
+
return value
|
|
28
|
+
return default.rstrip("/") if isinstance(default, str) and default.strip() else default
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resolve_runtime_config_snapshot_path(
|
|
32
|
+
*,
|
|
33
|
+
container_id: str | None = None,
|
|
34
|
+
volume_dir: str | Path = DEFAULT_RUNTIME_VOLUME_DIR,
|
|
35
|
+
) -> Path:
|
|
36
|
+
"""Resolve the local config snapshot path mounted by PiPhi Core."""
|
|
37
|
+
explicit_path = (os.getenv(RUNTIME_CONFIG_SNAPSHOT_PATH_ENV_NAME) or "").strip()
|
|
38
|
+
if explicit_path:
|
|
39
|
+
return Path(explicit_path)
|
|
40
|
+
|
|
41
|
+
resolved_container_id = (
|
|
42
|
+
str(container_id or "").strip()
|
|
43
|
+
or (os.getenv(RUNTIME_CONTAINER_ID_ENV_NAME) or "").strip()
|
|
44
|
+
)
|
|
45
|
+
if resolved_container_id:
|
|
46
|
+
return Path(volume_dir).joinpath(f"{resolved_container_id}.json")
|
|
47
|
+
return Path(volume_dir).joinpath(DEFAULT_RUNTIME_CONFIG_SNAPSHOT_FILENAME)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_runtime_config_snapshot(
|
|
51
|
+
*,
|
|
52
|
+
path: str | Path | None = None,
|
|
53
|
+
container_id: str | None = None,
|
|
54
|
+
volume_dir: str | Path = DEFAULT_RUNTIME_VOLUME_DIR,
|
|
55
|
+
) -> RuntimeConfigSnapshot | None:
|
|
56
|
+
"""Load Core's last known config snapshot from the mounted runtime volume.
|
|
57
|
+
|
|
58
|
+
Missing, unreadable, or invalid snapshots return ``None`` so integrations can
|
|
59
|
+
treat snapshot rehydration as a safe startup fallback rather than a crash
|
|
60
|
+
trigger.
|
|
61
|
+
"""
|
|
62
|
+
snapshot_path = (
|
|
63
|
+
Path(path)
|
|
64
|
+
if path is not None
|
|
65
|
+
else resolve_runtime_config_snapshot_path(
|
|
66
|
+
container_id=container_id,
|
|
67
|
+
volume_dir=volume_dir,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
try:
|
|
71
|
+
raw_payload = snapshot_path.read_text(encoding="utf-8")
|
|
72
|
+
except (FileNotFoundError, OSError):
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
payload = json.loads(raw_payload)
|
|
77
|
+
except json.JSONDecodeError:
|
|
78
|
+
return None
|
|
79
|
+
if not isinstance(payload, dict):
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
return RuntimeConfigSnapshot.model_validate(payload)
|
|
84
|
+
except ValueError:
|
|
85
|
+
return None
|
|
86
|
+
|
|
13
87
|
|
|
14
88
|
def reconcile_config_ids(
|
|
15
89
|
*,
|
|
@@ -15,6 +15,7 @@ def _build_runtime_snapshot(runtime_context: RuntimeContext) -> dict[str, Any]:
|
|
|
15
15
|
"core_auth_present": bool(runtime_context.auth.internal_token),
|
|
16
16
|
"core_http_client_bound": process_state.core_http_client is not None,
|
|
17
17
|
"current_generation": process_state.current_generation,
|
|
18
|
+
"config_generation": process_state.current_generation,
|
|
18
19
|
"pending_task_count": len(
|
|
19
20
|
[task for task in process_state.background_tasks if not task.done()]
|
|
20
21
|
),
|
|
@@ -35,12 +35,34 @@ class RuntimeConfig(BaseModel):
|
|
|
35
35
|
|
|
36
36
|
class RuntimeConfigSnapshot(BaseModel):
|
|
37
37
|
"""Snapshot payload sent from PiPhi Core to an integration runtime."""
|
|
38
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
39
|
+
|
|
40
|
+
schema_version: int | None = Field(
|
|
41
|
+
default=None,
|
|
42
|
+
validation_alias=AliasChoices("schema_version", "schemaVersion"),
|
|
43
|
+
)
|
|
38
44
|
container_id: str
|
|
39
45
|
integration_id: str | None = None
|
|
40
46
|
driver_pid: int | None = None
|
|
41
47
|
reason: str | None = None
|
|
42
48
|
generation: int | None = None
|
|
49
|
+
updated_at: str | None = Field(
|
|
50
|
+
default=None,
|
|
51
|
+
validation_alias=AliasChoices("updated_at", "updatedAt"),
|
|
52
|
+
)
|
|
43
53
|
configs: list[RuntimeConfig] = Field(default_factory=list)
|
|
54
|
+
deleted_config_ids: list[str] = Field(
|
|
55
|
+
default_factory=list,
|
|
56
|
+
validation_alias=AliasChoices("deleted_config_ids", "deletedConfigIds"),
|
|
57
|
+
)
|
|
58
|
+
config_hash: str | None = Field(
|
|
59
|
+
default=None,
|
|
60
|
+
validation_alias=AliasChoices("config_hash", "configHash"),
|
|
61
|
+
)
|
|
62
|
+
internal_token: str | None = Field(
|
|
63
|
+
default=None,
|
|
64
|
+
validation_alias=AliasChoices("internal_token", "internalToken"),
|
|
65
|
+
)
|
|
44
66
|
|
|
45
67
|
|
|
46
68
|
class RuntimeConfigSyncResponse(BaseModel):
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from piphi_runtime_kit_python import (
|
|
4
|
+
ConfigSyncCoordinator,
|
|
5
|
+
RuntimeConfig,
|
|
6
|
+
RuntimeConfigSnapshot,
|
|
7
|
+
RuntimeProcessState,
|
|
8
|
+
load_runtime_config_snapshot,
|
|
9
|
+
reconcile_config_ids,
|
|
10
|
+
resolve_core_base_url,
|
|
11
|
+
resolve_runtime_config_snapshot_path,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_reconcile_config_ids_returns_removed_and_unchanged_ids() -> None:
|
|
16
|
+
removed_ids, unchanged_ids = reconcile_config_ids(
|
|
17
|
+
active_ids=["a", "b", "c"],
|
|
18
|
+
incoming_ids=["b", "c", "d"],
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
assert removed_ids == ["a"]
|
|
22
|
+
assert unchanged_ids == ["b", "c"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_config_sync_coordinator_detects_stale_snapshot() -> None:
|
|
26
|
+
state = RuntimeProcessState(current_generation=10)
|
|
27
|
+
coordinator = ConfigSyncCoordinator(process_state=state)
|
|
28
|
+
snapshot = RuntimeConfigSnapshot(
|
|
29
|
+
container_id="container-1",
|
|
30
|
+
generation=9,
|
|
31
|
+
configs=[RuntimeConfig(id="device-1")],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
assert coordinator.is_stale(snapshot) is True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_config_sync_coordinator_updates_generation() -> None:
|
|
38
|
+
state = RuntimeProcessState()
|
|
39
|
+
coordinator = ConfigSyncCoordinator(process_state=state)
|
|
40
|
+
|
|
41
|
+
assert coordinator.mark_generation(42) == 42
|
|
42
|
+
assert state.current_generation == 42
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_resolve_core_base_url_uses_managed_runtime_env(monkeypatch) -> None:
|
|
46
|
+
monkeypatch.setenv("PIPHI_CORE_BASE_URL", "http://127.0.0.1:31419/")
|
|
47
|
+
|
|
48
|
+
assert resolve_core_base_url() == "http://127.0.0.1:31419"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_resolve_runtime_config_snapshot_path_uses_container_id(monkeypatch, tmp_path) -> None:
|
|
52
|
+
monkeypatch.delenv("PIPHI_CONFIG_SNAPSHOT_PATH", raising=False)
|
|
53
|
+
monkeypatch.setenv("PIPHI_CONTAINER_ID", "container-1")
|
|
54
|
+
|
|
55
|
+
assert resolve_runtime_config_snapshot_path(volume_dir=tmp_path) == tmp_path / "container-1.json"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_resolve_runtime_config_snapshot_path_prefers_explicit_path(monkeypatch, tmp_path) -> None:
|
|
59
|
+
explicit_path = tmp_path / "custom.json"
|
|
60
|
+
monkeypatch.setenv("PIPHI_CONFIG_SNAPSHOT_PATH", str(explicit_path))
|
|
61
|
+
|
|
62
|
+
assert resolve_runtime_config_snapshot_path(volume_dir=tmp_path) == explicit_path
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_load_runtime_config_snapshot_reads_core_volume_contract(tmp_path) -> None:
|
|
66
|
+
snapshot_path = tmp_path / "container-1.json"
|
|
67
|
+
snapshot_path.write_text(
|
|
68
|
+
"""
|
|
69
|
+
{
|
|
70
|
+
"schema_version": 1,
|
|
71
|
+
"container_id": "container-1",
|
|
72
|
+
"integration_id": "integration-1",
|
|
73
|
+
"reason": "startup",
|
|
74
|
+
"generation": 42,
|
|
75
|
+
"updated_at": "2026-04-27T12:00:00+00:00",
|
|
76
|
+
"configs": [{"id": "device-1", "serial": "abc123"}],
|
|
77
|
+
"deleted_config_ids": ["device-2"],
|
|
78
|
+
"config_hash": "sha256:abc",
|
|
79
|
+
"internal_token": "runtime-token"
|
|
80
|
+
}
|
|
81
|
+
""",
|
|
82
|
+
encoding="utf-8",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
snapshot = load_runtime_config_snapshot(path=snapshot_path)
|
|
86
|
+
|
|
87
|
+
assert snapshot is not None
|
|
88
|
+
assert snapshot.schema_version == 1
|
|
89
|
+
assert snapshot.container_id == "container-1"
|
|
90
|
+
assert snapshot.generation == 42
|
|
91
|
+
assert snapshot.configs[0].id == "device-1"
|
|
92
|
+
assert snapshot.deleted_config_ids == ["device-2"]
|
|
93
|
+
assert snapshot.config_hash == "sha256:abc"
|
|
94
|
+
assert snapshot.internal_token == "runtime-token"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_load_runtime_config_snapshot_returns_none_for_missing_or_invalid_snapshot(tmp_path) -> None:
|
|
98
|
+
assert load_runtime_config_snapshot(path=tmp_path / "missing.json") is None
|
|
99
|
+
|
|
100
|
+
invalid_path = tmp_path / "invalid.json"
|
|
101
|
+
invalid_path.write_text("not-json", encoding="utf-8")
|
|
102
|
+
|
|
103
|
+
assert load_runtime_config_snapshot(path=invalid_path) is None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@pytest.mark.asyncio
|
|
107
|
+
async def test_config_sync_coordinator_apply_snapshot_orchestrates_callbacks() -> None:
|
|
108
|
+
state = RuntimeProcessState(current_generation=1)
|
|
109
|
+
coordinator = ConfigSyncCoordinator(process_state=state)
|
|
110
|
+
snapshot = RuntimeConfigSnapshot(
|
|
111
|
+
container_id="container-1",
|
|
112
|
+
generation=2,
|
|
113
|
+
configs=[RuntimeConfig(id="device-2"), RuntimeConfig(id="device-3")],
|
|
114
|
+
)
|
|
115
|
+
active_ids = ["device-1", "device-2"]
|
|
116
|
+
applied: list[str] = []
|
|
117
|
+
removed: list[str] = []
|
|
118
|
+
|
|
119
|
+
async def apply_config(config: RuntimeConfig) -> None:
|
|
120
|
+
applied.append(config.id)
|
|
121
|
+
|
|
122
|
+
async def remove_config(config_id: str) -> bool:
|
|
123
|
+
removed.append(config_id)
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
response = await coordinator.apply_snapshot(
|
|
127
|
+
snapshot=snapshot,
|
|
128
|
+
active_config_ids=active_ids,
|
|
129
|
+
apply_config=apply_config,
|
|
130
|
+
remove_config=remove_config,
|
|
131
|
+
get_active_config_ids=lambda: ["device-2", "device-3"],
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
assert applied == ["device-2", "device-3"]
|
|
135
|
+
assert removed == ["device-1"]
|
|
136
|
+
assert response.status == "synced"
|
|
137
|
+
assert response.generation == 2
|
|
138
|
+
assert response.active_config_ids == ["device-2", "device-3"]
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from piphi_runtime_kit_python import (
|
|
4
|
-
ConfigSyncCoordinator,
|
|
5
|
-
RuntimeConfig,
|
|
6
|
-
RuntimeConfigSnapshot,
|
|
7
|
-
RuntimeProcessState,
|
|
8
|
-
reconcile_config_ids,
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def test_reconcile_config_ids_returns_removed_and_unchanged_ids() -> None:
|
|
13
|
-
removed_ids, unchanged_ids = reconcile_config_ids(
|
|
14
|
-
active_ids=["a", "b", "c"],
|
|
15
|
-
incoming_ids=["b", "c", "d"],
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
assert removed_ids == ["a"]
|
|
19
|
-
assert unchanged_ids == ["b", "c"]
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def test_config_sync_coordinator_detects_stale_snapshot() -> None:
|
|
23
|
-
state = RuntimeProcessState(current_generation=10)
|
|
24
|
-
coordinator = ConfigSyncCoordinator(process_state=state)
|
|
25
|
-
snapshot = RuntimeConfigSnapshot(
|
|
26
|
-
container_id="container-1",
|
|
27
|
-
generation=9,
|
|
28
|
-
configs=[RuntimeConfig(id="device-1")],
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
assert coordinator.is_stale(snapshot) is True
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def test_config_sync_coordinator_updates_generation() -> None:
|
|
35
|
-
state = RuntimeProcessState()
|
|
36
|
-
coordinator = ConfigSyncCoordinator(process_state=state)
|
|
37
|
-
|
|
38
|
-
assert coordinator.mark_generation(42) == 42
|
|
39
|
-
assert state.current_generation == 42
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@pytest.mark.asyncio
|
|
43
|
-
async def test_config_sync_coordinator_apply_snapshot_orchestrates_callbacks() -> None:
|
|
44
|
-
state = RuntimeProcessState(current_generation=1)
|
|
45
|
-
coordinator = ConfigSyncCoordinator(process_state=state)
|
|
46
|
-
snapshot = RuntimeConfigSnapshot(
|
|
47
|
-
container_id="container-1",
|
|
48
|
-
generation=2,
|
|
49
|
-
configs=[RuntimeConfig(id="device-2"), RuntimeConfig(id="device-3")],
|
|
50
|
-
)
|
|
51
|
-
active_ids = ["device-1", "device-2"]
|
|
52
|
-
applied: list[str] = []
|
|
53
|
-
removed: list[str] = []
|
|
54
|
-
|
|
55
|
-
async def apply_config(config: RuntimeConfig) -> None:
|
|
56
|
-
applied.append(config.id)
|
|
57
|
-
|
|
58
|
-
async def remove_config(config_id: str) -> bool:
|
|
59
|
-
removed.append(config_id)
|
|
60
|
-
return True
|
|
61
|
-
|
|
62
|
-
response = await coordinator.apply_snapshot(
|
|
63
|
-
snapshot=snapshot,
|
|
64
|
-
active_config_ids=active_ids,
|
|
65
|
-
apply_config=apply_config,
|
|
66
|
-
remove_config=remove_config,
|
|
67
|
-
get_active_config_ids=lambda: ["device-2", "device-3"],
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
assert applied == ["device-2", "device-3"]
|
|
71
|
-
assert removed == ["device-1"]
|
|
72
|
-
assert response.status == "synced"
|
|
73
|
-
assert response.generation == 2
|
|
74
|
-
assert response.active_config_ids == ["device-2", "device-3"]
|
|
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
|
{piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_fastapi_adapter.py
RENAMED
|
File without changes
|
{piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_auth.py
RENAMED
|
File without changes
|
{piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_auth_matrix.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_discovery.py
RENAMED
|
File without changes
|
|
File without changes
|
{piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_dispatch.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_events.py
RENAMED
|
File without changes
|
|
File without changes
|
{piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_health.py
RENAMED
|
File without changes
|
{piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_identity.py
RENAMED
|
File without changes
|
{piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_lifespan.py
RENAMED
|
File without changes
|
{piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_mqtt.py
RENAMED
|
File without changes
|
{piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_mqtt_matrix.py
RENAMED
|
File without changes
|
{piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
{piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_starter.py
RENAMED
|
File without changes
|
|
File without changes
|
{piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_tasks.py
RENAMED
|
File without changes
|
{piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_telemetry.py
RENAMED
|
File without changes
|
|
File without changes
|
{piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_schemas_matrix.py
RENAMED
|
File without changes
|