piphi-runtime-kit-python 0.3.0__py3-none-any.whl
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/__init__.py +180 -0
- piphi_runtime_kit_python/adapters/__init__.py +13 -0
- piphi_runtime_kit_python/adapters/fastapi.py +73 -0
- piphi_runtime_kit_python/fastapi.py +13 -0
- piphi_runtime_kit_python/runtime/__init__.py +124 -0
- piphi_runtime_kit_python/runtime/auth.py +134 -0
- piphi_runtime_kit_python/runtime/config_sync.py +157 -0
- piphi_runtime_kit_python/runtime/configuration.py +112 -0
- piphi_runtime_kit_python/runtime/context.py +29 -0
- piphi_runtime_kit_python/runtime/discovery.py +71 -0
- piphi_runtime_kit_python/runtime/dispatch.py +197 -0
- piphi_runtime_kit_python/runtime/errors.py +131 -0
- piphi_runtime_kit_python/runtime/events.py +194 -0
- piphi_runtime_kit_python/runtime/health.py +54 -0
- piphi_runtime_kit_python/runtime/lifespan.py +79 -0
- piphi_runtime_kit_python/runtime/mqtt.py +213 -0
- piphi_runtime_kit_python/runtime/registry.py +77 -0
- piphi_runtime_kit_python/runtime/starter.py +96 -0
- piphi_runtime_kit_python/runtime/state.py +22 -0
- piphi_runtime_kit_python/runtime/tasks.py +42 -0
- piphi_runtime_kit_python/runtime/telemetry.py +130 -0
- piphi_runtime_kit_python/schemas.py +145 -0
- piphi_runtime_kit_python-0.3.0.dist-info/METADATA +875 -0
- piphi_runtime_kit_python-0.3.0.dist-info/RECORD +27 -0
- piphi_runtime_kit_python-0.3.0.dist-info/WHEEL +4 -0
- piphi_runtime_kit_python-0.3.0.dist-info/entry_points.txt +4 -0
- piphi_runtime_kit_python-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from .runtime.auth import (
|
|
2
|
+
RUNTIME_CONTAINER_ID_HEADER_NAME,
|
|
3
|
+
RUNTIME_INTERNAL_TOKEN_HEADER_NAME,
|
|
4
|
+
RuntimeAuthContext,
|
|
5
|
+
RuntimeAuthHeaders,
|
|
6
|
+
build_runtime_auth_headers,
|
|
7
|
+
extract_runtime_auth_headers,
|
|
8
|
+
format_runtime_auth_sync_log,
|
|
9
|
+
mask_token,
|
|
10
|
+
)
|
|
11
|
+
from .runtime.config_sync import ConfigSyncCoordinator, build_sync_response, reconcile_config_ids
|
|
12
|
+
from .runtime.configuration import (
|
|
13
|
+
build_config_apply_response,
|
|
14
|
+
build_config_remove_response,
|
|
15
|
+
format_config_apply_log,
|
|
16
|
+
redact_config_secrets,
|
|
17
|
+
validate_typed_config,
|
|
18
|
+
validate_typed_configs,
|
|
19
|
+
)
|
|
20
|
+
from .runtime.context import RuntimeContext
|
|
21
|
+
from .runtime.discovery import (
|
|
22
|
+
build_discovery_response,
|
|
23
|
+
format_discovery_attempt_log,
|
|
24
|
+
normalize_discovery_inputs,
|
|
25
|
+
)
|
|
26
|
+
from .runtime.errors import (
|
|
27
|
+
CoreAuthError,
|
|
28
|
+
CoreDeliveryError,
|
|
29
|
+
CoreRouteNotFoundError,
|
|
30
|
+
CoreServerError,
|
|
31
|
+
CoreTimeoutError,
|
|
32
|
+
CoreUnavailableError,
|
|
33
|
+
CoreUnexpectedResponseError,
|
|
34
|
+
classify_core_delivery_error,
|
|
35
|
+
)
|
|
36
|
+
from .runtime.dispatch import (
|
|
37
|
+
build_local_event_record,
|
|
38
|
+
dispatch_event_delivery,
|
|
39
|
+
dispatch_telemetry_delivery,
|
|
40
|
+
schedule_event_delivery,
|
|
41
|
+
schedule_telemetry_delivery,
|
|
42
|
+
)
|
|
43
|
+
from .runtime.events import (
|
|
44
|
+
EventClient,
|
|
45
|
+
build_core_event_payload,
|
|
46
|
+
build_event_ingest_response,
|
|
47
|
+
build_event_list_response,
|
|
48
|
+
format_event_log,
|
|
49
|
+
normalize_event_payload,
|
|
50
|
+
)
|
|
51
|
+
from .runtime.health import build_runtime_diagnostics_response, build_runtime_health_response
|
|
52
|
+
from .runtime.lifespan import (
|
|
53
|
+
DEFAULT_CORE_CLIENT_TIMEOUT_SECONDS,
|
|
54
|
+
DEFAULT_RUNTIME_CONTAINER_ID_ENV_NAME,
|
|
55
|
+
DEFAULT_RUNTIME_INTERNAL_TOKEN_ENV_NAME,
|
|
56
|
+
bind_core_http_client,
|
|
57
|
+
bootstrap_runtime_auth_from_env,
|
|
58
|
+
runtime_lifespan,
|
|
59
|
+
)
|
|
60
|
+
from .runtime.mqtt import (
|
|
61
|
+
DEFAULT_SOURCE_TOPIC_PREFIX,
|
|
62
|
+
MqttBrokerConfig,
|
|
63
|
+
MqttJsonClient,
|
|
64
|
+
build_source_errors_topic,
|
|
65
|
+
build_source_model_packets_topic,
|
|
66
|
+
build_source_packet_envelope,
|
|
67
|
+
build_source_packets_topic,
|
|
68
|
+
build_source_status_topic,
|
|
69
|
+
build_source_topic_root,
|
|
70
|
+
)
|
|
71
|
+
from .runtime.registry import RuntimeRegistry
|
|
72
|
+
from .runtime.state import RuntimeProcessState
|
|
73
|
+
from .runtime.starter import RuntimeStarter, create_runtime_starter
|
|
74
|
+
from .runtime.tasks import create_tracked_task, shutdown_background_tasks, track_background_task
|
|
75
|
+
from .runtime.telemetry import TelemetryClient, build_core_auth_headers
|
|
76
|
+
from .schemas import (
|
|
77
|
+
CoreEventPayload,
|
|
78
|
+
EventSeverity,
|
|
79
|
+
EventTransport,
|
|
80
|
+
IntegrationCommandRequest,
|
|
81
|
+
IntegrationDiscoveryRequest,
|
|
82
|
+
IntegrationDiscoveryResponse,
|
|
83
|
+
IntegrationEventIngestResponse,
|
|
84
|
+
IntegrationEventListResponse,
|
|
85
|
+
IntegrationEventRequest,
|
|
86
|
+
RuntimeConfig,
|
|
87
|
+
RuntimeConfigApplyResponse,
|
|
88
|
+
RuntimeConfigRemoveResponse,
|
|
89
|
+
RuntimeConfigSnapshot,
|
|
90
|
+
RuntimeConfigSyncResponse,
|
|
91
|
+
RuntimeDiagnosticsResponse,
|
|
92
|
+
RuntimeHealthResponse,
|
|
93
|
+
TelemetryPayload,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
__all__ = [
|
|
97
|
+
"ConfigSyncCoordinator",
|
|
98
|
+
"CoreAuthError",
|
|
99
|
+
"CoreDeliveryError",
|
|
100
|
+
"CoreRouteNotFoundError",
|
|
101
|
+
"CoreServerError",
|
|
102
|
+
"CoreTimeoutError",
|
|
103
|
+
"CoreUnavailableError",
|
|
104
|
+
"CoreUnexpectedResponseError",
|
|
105
|
+
"DEFAULT_CORE_CLIENT_TIMEOUT_SECONDS",
|
|
106
|
+
"DEFAULT_RUNTIME_CONTAINER_ID_ENV_NAME",
|
|
107
|
+
"DEFAULT_RUNTIME_INTERNAL_TOKEN_ENV_NAME",
|
|
108
|
+
"RUNTIME_CONTAINER_ID_HEADER_NAME",
|
|
109
|
+
"RUNTIME_INTERNAL_TOKEN_HEADER_NAME",
|
|
110
|
+
"IntegrationCommandRequest",
|
|
111
|
+
"IntegrationDiscoveryRequest",
|
|
112
|
+
"IntegrationDiscoveryResponse",
|
|
113
|
+
"IntegrationEventRequest",
|
|
114
|
+
"RuntimeAuthContext",
|
|
115
|
+
"RuntimeAuthHeaders",
|
|
116
|
+
"RuntimeConfig",
|
|
117
|
+
"RuntimeConfigApplyResponse",
|
|
118
|
+
"RuntimeConfigRemoveResponse",
|
|
119
|
+
"RuntimeConfigSnapshot",
|
|
120
|
+
"RuntimeConfigSyncResponse",
|
|
121
|
+
"RuntimeContext",
|
|
122
|
+
"RuntimeRegistry",
|
|
123
|
+
"RuntimeProcessState",
|
|
124
|
+
"RuntimeStarter",
|
|
125
|
+
"RuntimeDiagnosticsResponse",
|
|
126
|
+
"RuntimeHealthResponse",
|
|
127
|
+
"TelemetryClient",
|
|
128
|
+
"EventClient",
|
|
129
|
+
"TelemetryPayload",
|
|
130
|
+
"CoreEventPayload",
|
|
131
|
+
"EventSeverity",
|
|
132
|
+
"EventTransport",
|
|
133
|
+
"IntegrationEventIngestResponse",
|
|
134
|
+
"IntegrationEventListResponse",
|
|
135
|
+
"build_discovery_response",
|
|
136
|
+
"classify_core_delivery_error",
|
|
137
|
+
"build_core_event_payload",
|
|
138
|
+
"build_event_ingest_response",
|
|
139
|
+
"build_event_list_response",
|
|
140
|
+
"build_local_event_record",
|
|
141
|
+
"build_core_auth_headers",
|
|
142
|
+
"build_config_apply_response",
|
|
143
|
+
"build_config_remove_response",
|
|
144
|
+
"build_runtime_auth_headers",
|
|
145
|
+
"build_runtime_diagnostics_response",
|
|
146
|
+
"build_runtime_health_response",
|
|
147
|
+
"build_sync_response",
|
|
148
|
+
"bind_core_http_client",
|
|
149
|
+
"bootstrap_runtime_auth_from_env",
|
|
150
|
+
"create_tracked_task",
|
|
151
|
+
"create_runtime_starter",
|
|
152
|
+
"dispatch_event_delivery",
|
|
153
|
+
"dispatch_telemetry_delivery",
|
|
154
|
+
"extract_runtime_auth_headers",
|
|
155
|
+
"format_config_apply_log",
|
|
156
|
+
"format_discovery_attempt_log",
|
|
157
|
+
"format_event_log",
|
|
158
|
+
"format_runtime_auth_sync_log",
|
|
159
|
+
"mask_token",
|
|
160
|
+
"MqttBrokerConfig",
|
|
161
|
+
"MqttJsonClient",
|
|
162
|
+
"normalize_discovery_inputs",
|
|
163
|
+
"normalize_event_payload",
|
|
164
|
+
"redact_config_secrets",
|
|
165
|
+
"reconcile_config_ids",
|
|
166
|
+
"runtime_lifespan",
|
|
167
|
+
"schedule_event_delivery",
|
|
168
|
+
"schedule_telemetry_delivery",
|
|
169
|
+
"shutdown_background_tasks",
|
|
170
|
+
"track_background_task",
|
|
171
|
+
"validate_typed_config",
|
|
172
|
+
"validate_typed_configs",
|
|
173
|
+
"DEFAULT_SOURCE_TOPIC_PREFIX",
|
|
174
|
+
"build_source_errors_topic",
|
|
175
|
+
"build_source_model_packets_topic",
|
|
176
|
+
"build_source_packet_envelope",
|
|
177
|
+
"build_source_packets_topic",
|
|
178
|
+
"build_source_status_topic",
|
|
179
|
+
"build_source_topic_root",
|
|
180
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Optional framework adapters layered on top of the core runtime helpers."""
|
|
2
|
+
|
|
3
|
+
from .fastapi import (
|
|
4
|
+
get_payload_container_id,
|
|
5
|
+
sync_runtime_auth_from_fastapi_payload,
|
|
6
|
+
sync_runtime_auth_from_fastapi_request,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"get_payload_container_id",
|
|
11
|
+
"sync_runtime_auth_from_fastapi_payload",
|
|
12
|
+
"sync_runtime_auth_from_fastapi_request",
|
|
13
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Optional FastAPI-facing helpers for common PiPhi runtime patterns."""
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Mapping
|
|
6
|
+
|
|
7
|
+
from ..runtime.auth import RuntimeAuthHeaders
|
|
8
|
+
from ..runtime.context import RuntimeContext
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from fastapi import Request
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_payload_container_id(
|
|
15
|
+
payload: object | Mapping[str, Any] | None,
|
|
16
|
+
*,
|
|
17
|
+
attribute_name: str = "container_id",
|
|
18
|
+
) -> str | None:
|
|
19
|
+
"""Return a normalized container id from a payload object or mapping.
|
|
20
|
+
|
|
21
|
+
This lets integrations share one helper across Pydantic models, dataclasses,
|
|
22
|
+
or plain dictionaries when wiring request auth to PiPhi container scope.
|
|
23
|
+
"""
|
|
24
|
+
if payload is None:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
value: Any
|
|
28
|
+
if isinstance(payload, Mapping):
|
|
29
|
+
value = payload.get(attribute_name)
|
|
30
|
+
else:
|
|
31
|
+
value = getattr(payload, attribute_name, None)
|
|
32
|
+
|
|
33
|
+
normalized = str(value or "").strip()
|
|
34
|
+
return normalized or None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def sync_runtime_auth_from_fastapi_request(
|
|
38
|
+
runtime_context: RuntimeContext,
|
|
39
|
+
request: "Request",
|
|
40
|
+
*,
|
|
41
|
+
payload_container_id: str | None = None,
|
|
42
|
+
) -> RuntimeAuthHeaders:
|
|
43
|
+
"""Sync runtime auth from a FastAPI request's headers into the context.
|
|
44
|
+
|
|
45
|
+
Typical usage:
|
|
46
|
+
parsed = sync_runtime_auth_from_fastapi_request(
|
|
47
|
+
runtime_context,
|
|
48
|
+
request,
|
|
49
|
+
payload_container_id=payload.container_id,
|
|
50
|
+
)
|
|
51
|
+
"""
|
|
52
|
+
return runtime_context.auth.sync_from_headers(
|
|
53
|
+
request.headers,
|
|
54
|
+
payload_container_id=payload_container_id,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def sync_runtime_auth_from_fastapi_payload(
|
|
59
|
+
runtime_context: RuntimeContext,
|
|
60
|
+
request: "Request",
|
|
61
|
+
payload: object | Mapping[str, Any] | None,
|
|
62
|
+
*,
|
|
63
|
+
attribute_name: str = "container_id",
|
|
64
|
+
) -> RuntimeAuthHeaders:
|
|
65
|
+
"""Sync request auth using a payload object that carries container scope."""
|
|
66
|
+
return sync_runtime_auth_from_fastapi_request(
|
|
67
|
+
runtime_context,
|
|
68
|
+
request,
|
|
69
|
+
payload_container_id=get_payload_container_id(
|
|
70
|
+
payload,
|
|
71
|
+
attribute_name=attribute_name,
|
|
72
|
+
),
|
|
73
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Convenience re-exports for the optional FastAPI adapter layer."""
|
|
2
|
+
|
|
3
|
+
from .adapters.fastapi import (
|
|
4
|
+
get_payload_container_id,
|
|
5
|
+
sync_runtime_auth_from_fastapi_payload,
|
|
6
|
+
sync_runtime_auth_from_fastapi_request,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"get_payload_container_id",
|
|
11
|
+
"sync_runtime_auth_from_fastapi_payload",
|
|
12
|
+
"sync_runtime_auth_from_fastapi_request",
|
|
13
|
+
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from .auth import (
|
|
2
|
+
RUNTIME_CONTAINER_ID_HEADER_NAME,
|
|
3
|
+
RUNTIME_INTERNAL_TOKEN_HEADER_NAME,
|
|
4
|
+
RuntimeAuthContext,
|
|
5
|
+
RuntimeAuthHeaders,
|
|
6
|
+
build_runtime_auth_headers,
|
|
7
|
+
extract_runtime_auth_headers,
|
|
8
|
+
format_runtime_auth_sync_log,
|
|
9
|
+
mask_token,
|
|
10
|
+
)
|
|
11
|
+
from .config_sync import ConfigSyncCoordinator, build_sync_response, reconcile_config_ids
|
|
12
|
+
from .configuration import (
|
|
13
|
+
build_config_apply_response,
|
|
14
|
+
build_config_remove_response,
|
|
15
|
+
format_config_apply_log,
|
|
16
|
+
redact_config_secrets,
|
|
17
|
+
validate_typed_config,
|
|
18
|
+
validate_typed_configs,
|
|
19
|
+
)
|
|
20
|
+
from .context import RuntimeContext
|
|
21
|
+
from .discovery import (
|
|
22
|
+
build_discovery_response,
|
|
23
|
+
format_discovery_attempt_log,
|
|
24
|
+
normalize_discovery_inputs,
|
|
25
|
+
)
|
|
26
|
+
from .errors import (
|
|
27
|
+
CoreAuthError,
|
|
28
|
+
CoreDeliveryError,
|
|
29
|
+
CoreRouteNotFoundError,
|
|
30
|
+
CoreServerError,
|
|
31
|
+
CoreTimeoutError,
|
|
32
|
+
CoreUnavailableError,
|
|
33
|
+
CoreUnexpectedResponseError,
|
|
34
|
+
classify_core_delivery_error,
|
|
35
|
+
)
|
|
36
|
+
from .dispatch import (
|
|
37
|
+
build_local_event_record,
|
|
38
|
+
dispatch_event_delivery,
|
|
39
|
+
dispatch_telemetry_delivery,
|
|
40
|
+
schedule_event_delivery,
|
|
41
|
+
schedule_telemetry_delivery,
|
|
42
|
+
)
|
|
43
|
+
from .events import (
|
|
44
|
+
EventClient,
|
|
45
|
+
build_core_event_payload,
|
|
46
|
+
build_event_ingest_response,
|
|
47
|
+
build_event_list_response,
|
|
48
|
+
format_event_log,
|
|
49
|
+
normalize_event_payload,
|
|
50
|
+
)
|
|
51
|
+
from .health import build_runtime_diagnostics_response, build_runtime_health_response
|
|
52
|
+
from .lifespan import (
|
|
53
|
+
DEFAULT_CORE_CLIENT_TIMEOUT_SECONDS,
|
|
54
|
+
DEFAULT_RUNTIME_CONTAINER_ID_ENV_NAME,
|
|
55
|
+
DEFAULT_RUNTIME_INTERNAL_TOKEN_ENV_NAME,
|
|
56
|
+
bind_core_http_client,
|
|
57
|
+
bootstrap_runtime_auth_from_env,
|
|
58
|
+
runtime_lifespan,
|
|
59
|
+
)
|
|
60
|
+
from .registry import RuntimeRegistry
|
|
61
|
+
from .state import RuntimeProcessState
|
|
62
|
+
from .starter import RuntimeStarter, create_runtime_starter
|
|
63
|
+
from .tasks import create_tracked_task, shutdown_background_tasks, track_background_task
|
|
64
|
+
from .telemetry import TelemetryClient, build_core_auth_headers
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"ConfigSyncCoordinator",
|
|
68
|
+
"CoreAuthError",
|
|
69
|
+
"CoreDeliveryError",
|
|
70
|
+
"CoreRouteNotFoundError",
|
|
71
|
+
"CoreServerError",
|
|
72
|
+
"CoreTimeoutError",
|
|
73
|
+
"CoreUnavailableError",
|
|
74
|
+
"CoreUnexpectedResponseError",
|
|
75
|
+
"RUNTIME_CONTAINER_ID_HEADER_NAME",
|
|
76
|
+
"RUNTIME_INTERNAL_TOKEN_HEADER_NAME",
|
|
77
|
+
"RuntimeAuthContext",
|
|
78
|
+
"RuntimeAuthHeaders",
|
|
79
|
+
"RuntimeContext",
|
|
80
|
+
"RuntimeRegistry",
|
|
81
|
+
"RuntimeProcessState",
|
|
82
|
+
"RuntimeStarter",
|
|
83
|
+
"TelemetryClient",
|
|
84
|
+
"EventClient",
|
|
85
|
+
"build_local_event_record",
|
|
86
|
+
"build_config_apply_response",
|
|
87
|
+
"build_config_remove_response",
|
|
88
|
+
"build_discovery_response",
|
|
89
|
+
"build_core_event_payload",
|
|
90
|
+
"build_event_ingest_response",
|
|
91
|
+
"build_event_list_response",
|
|
92
|
+
"build_core_auth_headers",
|
|
93
|
+
"build_runtime_auth_headers",
|
|
94
|
+
"build_sync_response",
|
|
95
|
+
"bind_core_http_client",
|
|
96
|
+
"bootstrap_runtime_auth_from_env",
|
|
97
|
+
"classify_core_delivery_error",
|
|
98
|
+
"create_tracked_task",
|
|
99
|
+
"create_runtime_starter",
|
|
100
|
+
"dispatch_event_delivery",
|
|
101
|
+
"dispatch_telemetry_delivery",
|
|
102
|
+
"DEFAULT_CORE_CLIENT_TIMEOUT_SECONDS",
|
|
103
|
+
"DEFAULT_RUNTIME_CONTAINER_ID_ENV_NAME",
|
|
104
|
+
"DEFAULT_RUNTIME_INTERNAL_TOKEN_ENV_NAME",
|
|
105
|
+
"extract_runtime_auth_headers",
|
|
106
|
+
"format_config_apply_log",
|
|
107
|
+
"format_discovery_attempt_log",
|
|
108
|
+
"format_event_log",
|
|
109
|
+
"format_runtime_auth_sync_log",
|
|
110
|
+
"mask_token",
|
|
111
|
+
"build_runtime_diagnostics_response",
|
|
112
|
+
"build_runtime_health_response",
|
|
113
|
+
"normalize_discovery_inputs",
|
|
114
|
+
"normalize_event_payload",
|
|
115
|
+
"redact_config_secrets",
|
|
116
|
+
"reconcile_config_ids",
|
|
117
|
+
"runtime_lifespan",
|
|
118
|
+
"schedule_event_delivery",
|
|
119
|
+
"schedule_telemetry_delivery",
|
|
120
|
+
"shutdown_background_tasks",
|
|
121
|
+
"track_background_task",
|
|
122
|
+
"validate_typed_config",
|
|
123
|
+
"validate_typed_configs",
|
|
124
|
+
]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Helpers for resolving PiPhi runtime auth across requests and background work."""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DEFAULT_INTERNAL_TOKEN_ENV_NAME = "PIPHI_INTEGRATION_INTERNAL_TOKEN"
|
|
11
|
+
RUNTIME_CONTAINER_ID_HEADER_NAME = "X-Container-Id"
|
|
12
|
+
RUNTIME_INTERNAL_TOKEN_HEADER_NAME = "X-PiPhi-Integration-Token"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def mask_token(token: str | None) -> str:
|
|
16
|
+
"""Return a log-safe representation of an internal runtime token."""
|
|
17
|
+
normalized = (token or "").strip()
|
|
18
|
+
if not normalized:
|
|
19
|
+
return "missing"
|
|
20
|
+
if len(normalized) <= 10:
|
|
21
|
+
return "present"
|
|
22
|
+
return f"{normalized[:6]}...{normalized[-4:]}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_header_value(headers: Mapping[str, Any], name: str) -> str:
|
|
26
|
+
for key, value in headers.items():
|
|
27
|
+
if str(key).lower() == name.lower():
|
|
28
|
+
return str(value or "").strip()
|
|
29
|
+
return ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True, frozen=True)
|
|
33
|
+
class RuntimeAuthHeaders:
|
|
34
|
+
"""Normalized PiPhi runtime auth values extracted from request headers."""
|
|
35
|
+
container_id: str = ""
|
|
36
|
+
internal_token: str = ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def extract_runtime_auth_headers(headers: Mapping[str, Any]) -> RuntimeAuthHeaders:
|
|
40
|
+
"""Extract PiPhi runtime auth headers from any case-insensitive header mapping."""
|
|
41
|
+
return RuntimeAuthHeaders(
|
|
42
|
+
container_id=_get_header_value(headers, RUNTIME_CONTAINER_ID_HEADER_NAME),
|
|
43
|
+
internal_token=_get_header_value(headers, RUNTIME_INTERNAL_TOKEN_HEADER_NAME),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_runtime_auth_headers(
|
|
48
|
+
*,
|
|
49
|
+
container_id: str,
|
|
50
|
+
internal_token: str | None,
|
|
51
|
+
) -> dict[str, str]:
|
|
52
|
+
"""Build the standard PiPhi runtime auth headers for outbound Core calls."""
|
|
53
|
+
headers = {RUNTIME_CONTAINER_ID_HEADER_NAME: container_id}
|
|
54
|
+
if internal_token:
|
|
55
|
+
headers[RUNTIME_INTERNAL_TOKEN_HEADER_NAME] = internal_token
|
|
56
|
+
return headers
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def format_runtime_auth_sync_log(
|
|
60
|
+
parsed_headers: RuntimeAuthHeaders,
|
|
61
|
+
*,
|
|
62
|
+
payload_container_id: str | None = None,
|
|
63
|
+
) -> str:
|
|
64
|
+
"""Build a consistent log message for request-scoped runtime auth sync.
|
|
65
|
+
|
|
66
|
+
This keeps token masking and field naming consistent across integrations
|
|
67
|
+
that want to log the incoming runtime auth context for debugging.
|
|
68
|
+
"""
|
|
69
|
+
return (
|
|
70
|
+
"runtime_internal_auth "
|
|
71
|
+
f"header_container_id={parsed_headers.container_id or 'missing'} "
|
|
72
|
+
f"payload_container_id={payload_container_id or 'missing'} "
|
|
73
|
+
f"token={mask_token(parsed_headers.internal_token)}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(slots=True)
|
|
78
|
+
class RuntimeAuthContext:
|
|
79
|
+
"""In-memory auth context shared by a runtime process.
|
|
80
|
+
|
|
81
|
+
Integrations can keep the most recent container scope and internal token here,
|
|
82
|
+
then reuse it for outbound telemetry or config sync calls.
|
|
83
|
+
"""
|
|
84
|
+
container_id: str = ""
|
|
85
|
+
internal_token: str = ""
|
|
86
|
+
|
|
87
|
+
def update(
|
|
88
|
+
self,
|
|
89
|
+
*,
|
|
90
|
+
container_id: str | None = None,
|
|
91
|
+
internal_token: str | None = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Update the stored auth context with any non-empty incoming values."""
|
|
94
|
+
if container_id is not None:
|
|
95
|
+
resolved_container_id = str(container_id).strip()
|
|
96
|
+
if resolved_container_id:
|
|
97
|
+
self.container_id = resolved_container_id
|
|
98
|
+
|
|
99
|
+
if internal_token is not None:
|
|
100
|
+
resolved_internal_token = str(internal_token).strip()
|
|
101
|
+
if resolved_internal_token:
|
|
102
|
+
self.internal_token = resolved_internal_token
|
|
103
|
+
|
|
104
|
+
def sync_from_headers(
|
|
105
|
+
self,
|
|
106
|
+
headers: Mapping[str, Any],
|
|
107
|
+
*,
|
|
108
|
+
payload_container_id: str | None = None,
|
|
109
|
+
) -> RuntimeAuthHeaders:
|
|
110
|
+
"""Update the context from request headers and return the parsed values.
|
|
111
|
+
|
|
112
|
+
Typical usage:
|
|
113
|
+
parsed = runtime_context.auth.sync_from_headers(
|
|
114
|
+
request.headers,
|
|
115
|
+
payload_container_id=payload.container_id,
|
|
116
|
+
)
|
|
117
|
+
"""
|
|
118
|
+
parsed_headers = extract_runtime_auth_headers(headers)
|
|
119
|
+
self.update(
|
|
120
|
+
container_id=parsed_headers.container_id or payload_container_id,
|
|
121
|
+
internal_token=parsed_headers.internal_token,
|
|
122
|
+
)
|
|
123
|
+
return parsed_headers
|
|
124
|
+
|
|
125
|
+
def resolve(
|
|
126
|
+
self,
|
|
127
|
+
*,
|
|
128
|
+
container_id: str | None = None,
|
|
129
|
+
token_env_name: str = DEFAULT_INTERNAL_TOKEN_ENV_NAME,
|
|
130
|
+
) -> tuple[str, str]:
|
|
131
|
+
"""Resolve the effective container scope and internal token for a request."""
|
|
132
|
+
resolved_container_id = str(container_id or "").strip() or self.container_id
|
|
133
|
+
resolved_internal_token = self.internal_token or (os.getenv(token_env_name) or "").strip()
|
|
134
|
+
return resolved_container_id, resolved_internal_token
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Helpers for basic config-sync orchestration and generation tracking."""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Awaitable, Callable, TypeVar
|
|
7
|
+
|
|
8
|
+
from ..schemas import RuntimeConfig, RuntimeConfigSnapshot, RuntimeConfigSyncResponse
|
|
9
|
+
from .state import RuntimeProcessState
|
|
10
|
+
|
|
11
|
+
TConfig = TypeVar("TConfig", bound=RuntimeConfig)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def reconcile_config_ids(
|
|
15
|
+
*,
|
|
16
|
+
active_ids: list[str],
|
|
17
|
+
incoming_ids: list[str],
|
|
18
|
+
) -> tuple[list[str], list[str]]:
|
|
19
|
+
"""Return removed ids and unchanged ids from two config-id lists."""
|
|
20
|
+
active_set = set(active_ids)
|
|
21
|
+
incoming_set = set(incoming_ids)
|
|
22
|
+
removed_ids = sorted(active_set - incoming_set)
|
|
23
|
+
unchanged_ids = sorted(active_set & incoming_set)
|
|
24
|
+
return removed_ids, unchanged_ids
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_sync_response(
|
|
28
|
+
*,
|
|
29
|
+
status: str,
|
|
30
|
+
container_id: str,
|
|
31
|
+
reason: str | None = None,
|
|
32
|
+
generation: int | None = None,
|
|
33
|
+
applied: list[str] | None = None,
|
|
34
|
+
removed: list[str] | None = None,
|
|
35
|
+
active_config_ids: list[str] | None = None,
|
|
36
|
+
metadata: dict | None = None,
|
|
37
|
+
) -> RuntimeConfigSyncResponse:
|
|
38
|
+
"""Build a standard PiPhi config-sync response payload."""
|
|
39
|
+
return RuntimeConfigSyncResponse(
|
|
40
|
+
status=status,
|
|
41
|
+
container_id=container_id,
|
|
42
|
+
reason=reason,
|
|
43
|
+
generation=generation,
|
|
44
|
+
applied=applied or [],
|
|
45
|
+
removed=removed or [],
|
|
46
|
+
active_config_ids=active_config_ids or [],
|
|
47
|
+
metadata=metadata or {},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(slots=True)
|
|
52
|
+
class ConfigSyncCoordinator:
|
|
53
|
+
"""Track config generations and common sync response patterns.
|
|
54
|
+
|
|
55
|
+
This class intentionally stays small. It helps integrations decide whether
|
|
56
|
+
an incoming snapshot is stale and keeps generation tracking in one place.
|
|
57
|
+
"""
|
|
58
|
+
process_state: RuntimeProcessState
|
|
59
|
+
|
|
60
|
+
def get_current_generation(self) -> int | None:
|
|
61
|
+
"""Return the latest applied config generation for the process."""
|
|
62
|
+
return self.process_state.current_generation
|
|
63
|
+
|
|
64
|
+
def mark_generation(self, generation: int | None) -> int | None:
|
|
65
|
+
"""Persist a new generation and return the effective stored value."""
|
|
66
|
+
if generation is not None:
|
|
67
|
+
self.process_state.current_generation = int(generation)
|
|
68
|
+
return self.process_state.current_generation
|
|
69
|
+
|
|
70
|
+
def is_stale(self, snapshot: RuntimeConfigSnapshot) -> bool:
|
|
71
|
+
"""Return whether an incoming snapshot is older than the current state."""
|
|
72
|
+
incoming_generation = snapshot.generation
|
|
73
|
+
current_generation = self.process_state.current_generation
|
|
74
|
+
return bool(
|
|
75
|
+
incoming_generation is not None
|
|
76
|
+
and current_generation is not None
|
|
77
|
+
and int(incoming_generation) < int(current_generation)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def build_stale_response(
|
|
81
|
+
self,
|
|
82
|
+
*,
|
|
83
|
+
snapshot: RuntimeConfigSnapshot,
|
|
84
|
+
active_config_ids: list[str],
|
|
85
|
+
) -> RuntimeConfigSyncResponse:
|
|
86
|
+
"""Build the standard response for an ignored stale snapshot."""
|
|
87
|
+
return build_sync_response(
|
|
88
|
+
status="stale_ignored",
|
|
89
|
+
container_id=snapshot.container_id,
|
|
90
|
+
reason=snapshot.reason,
|
|
91
|
+
generation=self.process_state.current_generation,
|
|
92
|
+
active_config_ids=active_config_ids,
|
|
93
|
+
metadata={
|
|
94
|
+
"stale_generation_ignored": True,
|
|
95
|
+
"incoming_generation": snapshot.generation,
|
|
96
|
+
"current_generation": self.process_state.current_generation,
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
async def apply_snapshot(
|
|
101
|
+
self,
|
|
102
|
+
*,
|
|
103
|
+
snapshot: RuntimeConfigSnapshot,
|
|
104
|
+
active_config_ids: list[str],
|
|
105
|
+
apply_config: Callable[[RuntimeConfig], Awaitable[None]],
|
|
106
|
+
remove_config: Callable[[str], Awaitable[bool]],
|
|
107
|
+
get_active_config_ids: Callable[[], list[str]] | None = None,
|
|
108
|
+
) -> RuntimeConfigSyncResponse:
|
|
109
|
+
"""Apply a config snapshot using caller-provided apply/remove callbacks.
|
|
110
|
+
|
|
111
|
+
This keeps the integration-specific logic in the callbacks while the SDK
|
|
112
|
+
handles the generic PiPhi sync flow: stale detection, diffing, generation
|
|
113
|
+
tracking, and standard response payloads.
|
|
114
|
+
"""
|
|
115
|
+
if self.is_stale(snapshot):
|
|
116
|
+
return self.build_stale_response(
|
|
117
|
+
snapshot=snapshot,
|
|
118
|
+
active_config_ids=active_config_ids,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
incoming_ids = [config.id for config in snapshot.configs]
|
|
122
|
+
removed_ids, _unchanged_ids = reconcile_config_ids(
|
|
123
|
+
active_ids=active_config_ids,
|
|
124
|
+
incoming_ids=incoming_ids,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
removed: list[str] = []
|
|
128
|
+
applied: list[str] = []
|
|
129
|
+
|
|
130
|
+
for config_id in removed_ids:
|
|
131
|
+
was_removed = await remove_config(config_id)
|
|
132
|
+
if was_removed:
|
|
133
|
+
removed.append(config_id)
|
|
134
|
+
|
|
135
|
+
for config in snapshot.configs:
|
|
136
|
+
await apply_config(config)
|
|
137
|
+
applied.append(config.id)
|
|
138
|
+
|
|
139
|
+
current_generation = self.mark_generation(snapshot.generation)
|
|
140
|
+
next_active_config_ids = (
|
|
141
|
+
get_active_config_ids() if get_active_config_ids is not None else incoming_ids
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return build_sync_response(
|
|
145
|
+
status="synced",
|
|
146
|
+
container_id=snapshot.container_id,
|
|
147
|
+
reason=snapshot.reason,
|
|
148
|
+
generation=current_generation,
|
|
149
|
+
applied=applied,
|
|
150
|
+
removed=removed,
|
|
151
|
+
active_config_ids=next_active_config_ids,
|
|
152
|
+
metadata={
|
|
153
|
+
"applied_count": len(applied),
|
|
154
|
+
"removed_count": len(removed),
|
|
155
|
+
"current_generation": current_generation,
|
|
156
|
+
},
|
|
157
|
+
)
|