omnibase_infra 0.3.1__py3-none-any.whl → 0.3.2__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.
- omnibase_infra/__init__.py +1 -1
- omnibase_infra/enums/__init__.py +3 -0
- omnibase_infra/enums/enum_consumer_group_purpose.py +9 -0
- omnibase_infra/enums/enum_postgres_error_code.py +188 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
- omnibase_infra/mixins/__init__.py +14 -0
- omnibase_infra/mixins/mixin_postgres_error_response.py +314 -0
- omnibase_infra/mixins/mixin_postgres_op_executor.py +298 -0
- omnibase_infra/models/__init__.py +3 -0
- omnibase_infra/{nodes/effects/models → models}/model_backend_result.py +22 -6
- omnibase_infra/models/projection/__init__.py +11 -0
- omnibase_infra/models/projection/model_contract_projection.py +170 -0
- omnibase_infra/models/projection/model_topic_projection.py +148 -0
- omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
- omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
- omnibase_infra/nodes/effects/__init__.py +1 -1
- omnibase_infra/nodes/effects/models/__init__.py +6 -4
- omnibase_infra/nodes/effects/models/model_registry_response.py +1 -1
- omnibase_infra/nodes/effects/protocol_consul_client.py +1 -1
- omnibase_infra/nodes/effects/protocol_postgres_adapter.py +1 -1
- omnibase_infra/nodes/effects/registry_effect.py +1 -1
- omnibase_infra/nodes/node_contract_persistence_effect/__init__.py +101 -0
- omnibase_infra/nodes/node_contract_persistence_effect/contract.yaml +490 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/__init__.py +74 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_cleanup_topics.py +217 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_contract_upsert.py +242 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py +194 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_heartbeat.py +243 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_mark_stale.py +208 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py +298 -0
- omnibase_infra/nodes/node_contract_persistence_effect/models/__init__.py +15 -0
- omnibase_infra/nodes/node_contract_persistence_effect/models/model_persistence_result.py +52 -0
- omnibase_infra/nodes/node_contract_persistence_effect/node.py +114 -0
- omnibase_infra/nodes/node_contract_persistence_effect/registry/__init__.py +27 -0
- omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +220 -0
- omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
- omnibase_infra/projectors/__init__.py +6 -0
- omnibase_infra/projectors/projection_reader_contract.py +1301 -0
- omnibase_infra/runtime/__init__.py +5 -0
- omnibase_infra/runtime/contract_registration_event_router.py +500 -0
- omnibase_infra/runtime/db/__init__.py +4 -0
- omnibase_infra/runtime/db/models/__init__.py +15 -10
- omnibase_infra/runtime/db/models/model_db_operation.py +40 -0
- omnibase_infra/runtime/db/models/model_db_param.py +24 -0
- omnibase_infra/runtime/db/models/model_db_repository_contract.py +40 -0
- omnibase_infra/runtime/db/models/model_db_return.py +26 -0
- omnibase_infra/runtime/db/models/model_db_safety_policy.py +32 -0
- omnibase_infra/runtime/intent_execution_router.py +430 -0
- omnibase_infra/runtime/models/__init__.py +6 -0
- omnibase_infra/runtime/models/model_contract_registry_config.py +41 -0
- omnibase_infra/runtime/models/model_intent_execution_summary.py +79 -0
- omnibase_infra/runtime/models/model_runtime_config.py +8 -0
- omnibase_infra/runtime/protocols/__init__.py +16 -0
- omnibase_infra/runtime/protocols/protocol_intent_executor.py +107 -0
- omnibase_infra/runtime/request_response_wiring.py +785 -0
- omnibase_infra/runtime/service_kernel.py +295 -8
- omnibase_infra/services/registry_api/models/__init__.py +25 -0
- omnibase_infra/services/registry_api/models/model_contract_ref.py +44 -0
- omnibase_infra/services/registry_api/models/model_contract_view.py +81 -0
- omnibase_infra/services/registry_api/models/model_response_contracts.py +50 -0
- omnibase_infra/services/registry_api/models/model_response_topics.py +50 -0
- omnibase_infra/services/registry_api/models/model_topic_summary.py +57 -0
- omnibase_infra/services/registry_api/models/model_topic_view.py +63 -0
- omnibase_infra/services/registry_api/routes.py +205 -6
- omnibase_infra/services/registry_api/service.py +528 -1
- omnibase_infra/validation/infra_validators.py +3 -1
- omnibase_infra/validation/validation_exemptions.yaml +54 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/METADATA +3 -3
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/RECORD +72 -34
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -55,6 +55,7 @@ import signal
|
|
|
55
55
|
import sys
|
|
56
56
|
import time
|
|
57
57
|
from collections.abc import Awaitable, Callable
|
|
58
|
+
from functools import partial
|
|
58
59
|
from importlib.metadata import version as get_package_version
|
|
59
60
|
from pathlib import Path
|
|
60
61
|
from typing import cast
|
|
@@ -75,7 +76,15 @@ from omnibase_infra.errors import (
|
|
|
75
76
|
from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
|
|
76
77
|
from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
|
|
77
78
|
from omnibase_infra.event_bus.models.config import ModelKafkaEventBusConfig
|
|
79
|
+
from omnibase_infra.event_bus.models.model_event_message import ModelEventMessage
|
|
78
80
|
from omnibase_infra.models import ModelNodeIdentity
|
|
81
|
+
from omnibase_infra.nodes.contract_registry_reducer.contract_registration_event_router import (
|
|
82
|
+
ContractRegistrationEventRouter,
|
|
83
|
+
ProtocolIntentEffect,
|
|
84
|
+
)
|
|
85
|
+
from omnibase_infra.nodes.contract_registry_reducer.reducer import (
|
|
86
|
+
ContractRegistryReducer,
|
|
87
|
+
)
|
|
79
88
|
from omnibase_infra.nodes.node_registration_orchestrator.dispatchers import (
|
|
80
89
|
DispatcherNodeIntrospected,
|
|
81
90
|
)
|
|
@@ -423,6 +432,11 @@ async def bootstrap() -> int:
|
|
|
423
432
|
health_server: ServiceHealth | None = None
|
|
424
433
|
postgres_pool: asyncpg.Pool | None = None
|
|
425
434
|
introspection_unsubscribe: Callable[[], Awaitable[None]] | None = None
|
|
435
|
+
# Contract registry unsubscribe functions and router
|
|
436
|
+
contract_router: ContractRegistrationEventRouter | None = None
|
|
437
|
+
contract_unsub_registered: Callable[[], Awaitable[None]] | None = None
|
|
438
|
+
contract_unsub_deregistered: Callable[[], Awaitable[None]] | None = None
|
|
439
|
+
contract_unsub_heartbeat: Callable[[], Awaitable[None]] | None = None
|
|
426
440
|
correlation_id = generate_correlation_id()
|
|
427
441
|
bootstrap_start_time = time.time()
|
|
428
442
|
|
|
@@ -458,6 +472,7 @@ async def bootstrap() -> int:
|
|
|
458
472
|
|
|
459
473
|
# 3. Create event bus
|
|
460
474
|
# Dispatch based on configuration or environment variable:
|
|
475
|
+
# - ONEX_EVENT_BUS_TYPE env var overrides config.event_bus.type
|
|
461
476
|
# - If KAFKA_BOOTSTRAP_SERVERS env var is set, use EventBusKafka
|
|
462
477
|
# - If config.event_bus.type == "kafka", use EventBusKafka
|
|
463
478
|
# - Otherwise, use EventBusInmemory for local development/testing
|
|
@@ -465,12 +480,54 @@ async def bootstrap() -> int:
|
|
|
465
480
|
environment = os.getenv("ONEX_ENVIRONMENT") or config.event_bus.environment
|
|
466
481
|
kafka_bootstrap_servers = os.getenv("KAFKA_BOOTSTRAP_SERVERS")
|
|
467
482
|
|
|
468
|
-
#
|
|
469
|
-
#
|
|
470
|
-
#
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
483
|
+
# Check for ONEX_EVENT_BUS_TYPE environment variable override
|
|
484
|
+
# This allows CI/testing environments to force inmemory event bus
|
|
485
|
+
# even when the config file defaults to kafka.
|
|
486
|
+
event_bus_type_override = os.getenv("ONEX_EVENT_BUS_TYPE", "").lower()
|
|
487
|
+
if event_bus_type_override:
|
|
488
|
+
logger.debug(
|
|
489
|
+
"Event bus type override from ONEX_EVENT_BUS_TYPE=%s (correlation_id=%s)",
|
|
490
|
+
event_bus_type_override,
|
|
491
|
+
correlation_id,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# Determine effective event bus type with override precedence:
|
|
495
|
+
# 1. ONEX_EVENT_BUS_TYPE env var (highest priority)
|
|
496
|
+
# 2. KAFKA_BOOTSTRAP_SERVERS env var (if set, implies kafka)
|
|
497
|
+
# 3. config.event_bus.type (from runtime_config.yaml)
|
|
498
|
+
if event_bus_type_override == "inmemory":
|
|
499
|
+
# Explicit inmemory override - use inmemory regardless of other config
|
|
500
|
+
use_kafka = False
|
|
501
|
+
logger.info(
|
|
502
|
+
"Using inmemory event bus (ONEX_EVENT_BUS_TYPE override) (correlation_id=%s)",
|
|
503
|
+
correlation_id,
|
|
504
|
+
)
|
|
505
|
+
elif event_bus_type_override == "kafka":
|
|
506
|
+
# Explicit kafka override - validate that bootstrap_servers is available
|
|
507
|
+
use_kafka = True
|
|
508
|
+
elif event_bus_type_override and event_bus_type_override not in (
|
|
509
|
+
"inmemory",
|
|
510
|
+
"kafka",
|
|
511
|
+
):
|
|
512
|
+
# Invalid override value - warn and fall back to config
|
|
513
|
+
logger.warning(
|
|
514
|
+
"Invalid ONEX_EVENT_BUS_TYPE value '%s', expected 'inmemory' or 'kafka'. "
|
|
515
|
+
"Falling back to config.event_bus.type='%s' (correlation_id=%s)",
|
|
516
|
+
event_bus_type_override,
|
|
517
|
+
config.event_bus.type,
|
|
518
|
+
correlation_id,
|
|
519
|
+
)
|
|
520
|
+
use_kafka = (
|
|
521
|
+
bool(kafka_bootstrap_servers) or config.event_bus.type == "kafka"
|
|
522
|
+
)
|
|
523
|
+
else:
|
|
524
|
+
# No override - use original logic
|
|
525
|
+
# Explicit bool evaluation (not truthy string) for kafka usage.
|
|
526
|
+
# KAFKA_BOOTSTRAP_SERVERS env var takes precedence over config.event_bus.type.
|
|
527
|
+
# This prevents implicit "kafka but localhost" fallback scenarios.
|
|
528
|
+
use_kafka = (
|
|
529
|
+
bool(kafka_bootstrap_servers) or config.event_bus.type == "kafka"
|
|
530
|
+
)
|
|
474
531
|
|
|
475
532
|
# Validate bootstrap_servers is provided when kafka is requested via config
|
|
476
533
|
# This prevents confusing implicit localhost:9092 fallback
|
|
@@ -931,6 +988,81 @@ async def bootstrap() -> int:
|
|
|
931
988
|
},
|
|
932
989
|
)
|
|
933
990
|
|
|
991
|
+
# 4.9. Wire ContractRegistrationEventRouter if contract_registry.enabled
|
|
992
|
+
# This router subscribes to contract lifecycle events (registration,
|
|
993
|
+
# deregistration, heartbeat) and routes them to the ContractRegistryReducer.
|
|
994
|
+
# The router also runs an internal tick timer for staleness computation.
|
|
995
|
+
if config.contract_registry.enabled and postgres_pool is not None:
|
|
996
|
+
# Import postgres handlers for contract persistence
|
|
997
|
+
# Deferred import to avoid loading heavy dependencies when not needed
|
|
998
|
+
from omnibase_infra.nodes.node_contract_persistence_effect.handlers import (
|
|
999
|
+
HandlerPostgresCleanupTopics,
|
|
1000
|
+
HandlerPostgresContractUpsert,
|
|
1001
|
+
HandlerPostgresDeactivate,
|
|
1002
|
+
HandlerPostgresHeartbeat,
|
|
1003
|
+
HandlerPostgresMarkStale,
|
|
1004
|
+
HandlerPostgresTopicUpdate,
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
# Create effect handlers keyed by intent_type
|
|
1008
|
+
# These handlers execute PostgreSQL operations for intents from the reducer
|
|
1009
|
+
# Note: Handlers implement ProtocolIntentEffect duck-typing style with
|
|
1010
|
+
# more specific payload types. Cast tells mypy they satisfy the protocol.
|
|
1011
|
+
contract_effect_handlers: dict[str, ProtocolIntentEffect] = {
|
|
1012
|
+
"postgres.upsert_contract": cast(
|
|
1013
|
+
"ProtocolIntentEffect",
|
|
1014
|
+
HandlerPostgresContractUpsert(postgres_pool),
|
|
1015
|
+
),
|
|
1016
|
+
"postgres.update_topic": cast(
|
|
1017
|
+
"ProtocolIntentEffect",
|
|
1018
|
+
HandlerPostgresTopicUpdate(postgres_pool),
|
|
1019
|
+
),
|
|
1020
|
+
"postgres.mark_stale": cast(
|
|
1021
|
+
"ProtocolIntentEffect",
|
|
1022
|
+
HandlerPostgresMarkStale(postgres_pool),
|
|
1023
|
+
),
|
|
1024
|
+
"postgres.update_heartbeat": cast(
|
|
1025
|
+
"ProtocolIntentEffect",
|
|
1026
|
+
HandlerPostgresHeartbeat(postgres_pool),
|
|
1027
|
+
),
|
|
1028
|
+
"postgres.deactivate_contract": cast(
|
|
1029
|
+
"ProtocolIntentEffect",
|
|
1030
|
+
HandlerPostgresDeactivate(postgres_pool),
|
|
1031
|
+
),
|
|
1032
|
+
"postgres.cleanup_topic_references": cast(
|
|
1033
|
+
"ProtocolIntentEffect",
|
|
1034
|
+
HandlerPostgresCleanupTopics(postgres_pool),
|
|
1035
|
+
),
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
# Create reducer and router
|
|
1039
|
+
contract_reducer = ContractRegistryReducer()
|
|
1040
|
+
contract_router = ContractRegistrationEventRouter(
|
|
1041
|
+
container=container,
|
|
1042
|
+
reducer=contract_reducer,
|
|
1043
|
+
effect_handlers=contract_effect_handlers,
|
|
1044
|
+
event_bus=event_bus,
|
|
1045
|
+
tick_interval_seconds=config.contract_registry.tick_interval_seconds,
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
logger.info(
|
|
1049
|
+
"ContractRegistrationEventRouter created (correlation_id=%s)",
|
|
1050
|
+
correlation_id,
|
|
1051
|
+
extra={
|
|
1052
|
+
"tick_interval_seconds": config.contract_registry.tick_interval_seconds,
|
|
1053
|
+
"handler_count": len(contract_effect_handlers),
|
|
1054
|
+
},
|
|
1055
|
+
)
|
|
1056
|
+
else:
|
|
1057
|
+
logger.debug(
|
|
1058
|
+
"Contract registry disabled or no postgres_pool (correlation_id=%s)",
|
|
1059
|
+
correlation_id,
|
|
1060
|
+
extra={
|
|
1061
|
+
"contract_registry_enabled": config.contract_registry.enabled,
|
|
1062
|
+
"postgres_pool_available": postgres_pool is not None,
|
|
1063
|
+
},
|
|
1064
|
+
)
|
|
1065
|
+
|
|
934
1066
|
except Exception as pool_error:
|
|
935
1067
|
# Log warning but continue without registration support
|
|
936
1068
|
# Use sanitize_error_message to prevent credential leakage in logs
|
|
@@ -1298,6 +1430,84 @@ async def bootstrap() -> int:
|
|
|
1298
1430
|
},
|
|
1299
1431
|
)
|
|
1300
1432
|
|
|
1433
|
+
# 9.6. Start contract registry event consumer if router is available
|
|
1434
|
+
# This consumer subscribes to 3 Kafka topics for contract lifecycle events
|
|
1435
|
+
# and routes them to the ContractRegistryReducer for projection.
|
|
1436
|
+
if contract_router is not None and has_subscribe:
|
|
1437
|
+
# Create typed node identity for contract registry subscriptions
|
|
1438
|
+
contract_node_identity = ModelNodeIdentity(
|
|
1439
|
+
env=environment,
|
|
1440
|
+
service=config.name or "onex-kernel",
|
|
1441
|
+
node_name="contract-registry",
|
|
1442
|
+
version=config.contract_version or "v1",
|
|
1443
|
+
)
|
|
1444
|
+
|
|
1445
|
+
# Subscribe to 3 contract lifecycle topics with same identity
|
|
1446
|
+
contract_subscribe_start_time = time.time()
|
|
1447
|
+
|
|
1448
|
+
# Derive environment-aware topic names (avoid hardcoded "dev." prefix)
|
|
1449
|
+
contract_registered_topic = (
|
|
1450
|
+
f"{environment}.onex.evt.platform.contract-registered.v1"
|
|
1451
|
+
)
|
|
1452
|
+
contract_deregistered_topic = (
|
|
1453
|
+
f"{environment}.onex.evt.platform.contract-deregistered.v1"
|
|
1454
|
+
)
|
|
1455
|
+
node_heartbeat_topic = f"{environment}.onex.evt.platform.node-heartbeat.v1"
|
|
1456
|
+
|
|
1457
|
+
logger.info(
|
|
1458
|
+
"Subscribing to contract registry events on event bus (correlation_id=%s)",
|
|
1459
|
+
correlation_id,
|
|
1460
|
+
extra={
|
|
1461
|
+
"topics": [
|
|
1462
|
+
contract_registered_topic,
|
|
1463
|
+
contract_deregistered_topic,
|
|
1464
|
+
node_heartbeat_topic,
|
|
1465
|
+
],
|
|
1466
|
+
"node_identity": {
|
|
1467
|
+
"env": contract_node_identity.env,
|
|
1468
|
+
"service": contract_node_identity.service,
|
|
1469
|
+
"node_name": contract_node_identity.node_name,
|
|
1470
|
+
"version": contract_node_identity.version,
|
|
1471
|
+
},
|
|
1472
|
+
"purpose": EnumConsumerGroupPurpose.CONTRACT_REGISTRY.value,
|
|
1473
|
+
},
|
|
1474
|
+
)
|
|
1475
|
+
|
|
1476
|
+
contract_unsub_registered = await event_bus.subscribe(
|
|
1477
|
+
topic=contract_registered_topic,
|
|
1478
|
+
node_identity=contract_node_identity,
|
|
1479
|
+
on_message=contract_router.handle_message,
|
|
1480
|
+
purpose=EnumConsumerGroupPurpose.CONTRACT_REGISTRY,
|
|
1481
|
+
)
|
|
1482
|
+
contract_unsub_deregistered = await event_bus.subscribe(
|
|
1483
|
+
topic=contract_deregistered_topic,
|
|
1484
|
+
node_identity=contract_node_identity,
|
|
1485
|
+
on_message=contract_router.handle_message,
|
|
1486
|
+
purpose=EnumConsumerGroupPurpose.CONTRACT_REGISTRY,
|
|
1487
|
+
)
|
|
1488
|
+
contract_unsub_heartbeat = await event_bus.subscribe(
|
|
1489
|
+
topic=node_heartbeat_topic,
|
|
1490
|
+
node_identity=contract_node_identity,
|
|
1491
|
+
on_message=contract_router.handle_message,
|
|
1492
|
+
purpose=EnumConsumerGroupPurpose.CONTRACT_REGISTRY,
|
|
1493
|
+
)
|
|
1494
|
+
|
|
1495
|
+
# Start the router's tick timer
|
|
1496
|
+
await contract_router.start()
|
|
1497
|
+
|
|
1498
|
+
contract_subscribe_duration = time.time() - contract_subscribe_start_time
|
|
1499
|
+
logger.info(
|
|
1500
|
+
"Contract registry event consumers started successfully in %.3fs (correlation_id=%s)",
|
|
1501
|
+
contract_subscribe_duration,
|
|
1502
|
+
correlation_id,
|
|
1503
|
+
extra={
|
|
1504
|
+
"topics_count": 3,
|
|
1505
|
+
"tick_interval_seconds": contract_router.tick_interval_seconds,
|
|
1506
|
+
"subscribe_duration_seconds": contract_subscribe_duration,
|
|
1507
|
+
"event_bus_type": event_bus_type,
|
|
1508
|
+
},
|
|
1509
|
+
)
|
|
1510
|
+
|
|
1301
1511
|
# Calculate total bootstrap time
|
|
1302
1512
|
bootstrap_duration = time.time() - bootstrap_start_time
|
|
1303
1513
|
|
|
@@ -1309,14 +1519,24 @@ async def bootstrap() -> int:
|
|
|
1309
1519
|
registration_status = "enabled (PostgreSQL only)"
|
|
1310
1520
|
else:
|
|
1311
1521
|
registration_status = "disabled"
|
|
1522
|
+
|
|
1523
|
+
# Contract registry status for banner
|
|
1524
|
+
if contract_router is not None:
|
|
1525
|
+
contract_registry_status = (
|
|
1526
|
+
f"enabled (tick: {config.contract_registry.tick_interval_seconds}s)"
|
|
1527
|
+
)
|
|
1528
|
+
else:
|
|
1529
|
+
contract_registry_status = "disabled"
|
|
1530
|
+
|
|
1312
1531
|
banner_lines = [
|
|
1313
1532
|
"=" * 60,
|
|
1314
1533
|
f"ONEX Runtime Kernel v{KERNEL_VERSION}",
|
|
1315
1534
|
f"Environment: {environment}",
|
|
1316
1535
|
f"Contracts: {contracts_dir}",
|
|
1317
1536
|
f"Event Bus: {event_bus_type} (group: {config.consumer_group})",
|
|
1318
|
-
f"Topics: {config.input_topic}
|
|
1537
|
+
f"Topics: {config.input_topic} -> {config.output_topic}",
|
|
1319
1538
|
f"Registration: {registration_status}",
|
|
1539
|
+
f"Contract Registry: {contract_registry_status}",
|
|
1320
1540
|
f"Health endpoint: http://0.0.0.0:{http_port}/health",
|
|
1321
1541
|
f"Bootstrap time: {bootstrap_duration:.3f}s",
|
|
1322
1542
|
f"Correlation ID: {correlation_id}",
|
|
@@ -1367,6 +1587,47 @@ async def bootstrap() -> int:
|
|
|
1367
1587
|
)
|
|
1368
1588
|
introspection_unsubscribe = None
|
|
1369
1589
|
|
|
1590
|
+
# Stop contract registry router and consumers
|
|
1591
|
+
if contract_router is not None:
|
|
1592
|
+
try:
|
|
1593
|
+
await contract_router.stop()
|
|
1594
|
+
logger.debug(
|
|
1595
|
+
"Contract registry router stopped (correlation_id=%s)",
|
|
1596
|
+
correlation_id,
|
|
1597
|
+
)
|
|
1598
|
+
except Exception as router_stop_error:
|
|
1599
|
+
logger.warning(
|
|
1600
|
+
"Failed to stop contract registry router: %s (correlation_id=%s)",
|
|
1601
|
+
sanitize_error_message(router_stop_error),
|
|
1602
|
+
correlation_id,
|
|
1603
|
+
)
|
|
1604
|
+
contract_router = None
|
|
1605
|
+
|
|
1606
|
+
# Unsubscribe from contract registry topics
|
|
1607
|
+
for unsub_name, unsub_func in [
|
|
1608
|
+
("contract-registered", contract_unsub_registered),
|
|
1609
|
+
("contract-deregistered", contract_unsub_deregistered),
|
|
1610
|
+
("node-heartbeat", contract_unsub_heartbeat),
|
|
1611
|
+
]:
|
|
1612
|
+
if unsub_func is not None:
|
|
1613
|
+
try:
|
|
1614
|
+
await unsub_func()
|
|
1615
|
+
logger.debug(
|
|
1616
|
+
"Contract registry consumer %s stopped (correlation_id=%s)",
|
|
1617
|
+
unsub_name,
|
|
1618
|
+
correlation_id,
|
|
1619
|
+
)
|
|
1620
|
+
except Exception as unsub_error:
|
|
1621
|
+
logger.warning(
|
|
1622
|
+
"Failed to stop contract registry consumer %s: %s (correlation_id=%s)",
|
|
1623
|
+
unsub_name,
|
|
1624
|
+
sanitize_error_message(unsub_error),
|
|
1625
|
+
correlation_id,
|
|
1626
|
+
)
|
|
1627
|
+
contract_unsub_registered = None
|
|
1628
|
+
contract_unsub_deregistered = None
|
|
1629
|
+
contract_unsub_heartbeat = None
|
|
1630
|
+
|
|
1370
1631
|
# Stop health server (fast, non-blocking)
|
|
1371
1632
|
if health_server is not None:
|
|
1372
1633
|
try:
|
|
@@ -1492,7 +1753,7 @@ async def bootstrap() -> int:
|
|
|
1492
1753
|
|
|
1493
1754
|
finally:
|
|
1494
1755
|
# Guard cleanup - stop all resources if not already stopped
|
|
1495
|
-
# Order: introspection consumer -> health server -> runtime -> pool
|
|
1756
|
+
# Order: introspection consumer -> contract registry -> health server -> runtime -> pool
|
|
1496
1757
|
|
|
1497
1758
|
if introspection_unsubscribe is not None:
|
|
1498
1759
|
try:
|
|
@@ -1504,6 +1765,32 @@ async def bootstrap() -> int:
|
|
|
1504
1765
|
correlation_id,
|
|
1505
1766
|
)
|
|
1506
1767
|
|
|
1768
|
+
# Cleanup contract registry router and consumers
|
|
1769
|
+
if contract_router is not None:
|
|
1770
|
+
try:
|
|
1771
|
+
await contract_router.stop()
|
|
1772
|
+
except Exception as cleanup_error:
|
|
1773
|
+
logger.warning(
|
|
1774
|
+
"Failed to stop contract registry router during cleanup: %s (correlation_id=%s)",
|
|
1775
|
+
sanitize_error_message(cleanup_error),
|
|
1776
|
+
correlation_id,
|
|
1777
|
+
)
|
|
1778
|
+
|
|
1779
|
+
for unsub_func in [
|
|
1780
|
+
contract_unsub_registered,
|
|
1781
|
+
contract_unsub_deregistered,
|
|
1782
|
+
contract_unsub_heartbeat,
|
|
1783
|
+
]:
|
|
1784
|
+
if unsub_func is not None:
|
|
1785
|
+
try:
|
|
1786
|
+
await unsub_func()
|
|
1787
|
+
except Exception as cleanup_error:
|
|
1788
|
+
logger.warning(
|
|
1789
|
+
"Failed to stop contract registry consumer during cleanup: %s (correlation_id=%s)",
|
|
1790
|
+
sanitize_error_message(cleanup_error),
|
|
1791
|
+
correlation_id,
|
|
1792
|
+
)
|
|
1793
|
+
|
|
1507
1794
|
if health_server is not None:
|
|
1508
1795
|
try:
|
|
1509
1796
|
await health_server.stop()
|
|
@@ -13,11 +13,18 @@ Design Principles:
|
|
|
13
13
|
|
|
14
14
|
Related Tickets:
|
|
15
15
|
- OMN-1278: Contract-Driven Dashboard - Registry Discovery
|
|
16
|
+
- OMN-1845: Contract Registry Persistence
|
|
16
17
|
"""
|
|
17
18
|
|
|
18
19
|
from omnibase_infra.services.registry_api.models.model_capability_widget_mapping import (
|
|
19
20
|
ModelCapabilityWidgetMapping,
|
|
20
21
|
)
|
|
22
|
+
from omnibase_infra.services.registry_api.models.model_contract_ref import (
|
|
23
|
+
ModelContractRef,
|
|
24
|
+
)
|
|
25
|
+
from omnibase_infra.services.registry_api.models.model_contract_view import (
|
|
26
|
+
ModelContractView,
|
|
27
|
+
)
|
|
21
28
|
from omnibase_infra.services.registry_api.models.model_pagination_info import (
|
|
22
29
|
ModelPaginationInfo,
|
|
23
30
|
)
|
|
@@ -36,12 +43,24 @@ from omnibase_infra.services.registry_api.models.model_registry_node_view import
|
|
|
36
43
|
from omnibase_infra.services.registry_api.models.model_registry_summary import (
|
|
37
44
|
ModelRegistrySummary,
|
|
38
45
|
)
|
|
46
|
+
from omnibase_infra.services.registry_api.models.model_response_contracts import (
|
|
47
|
+
ModelResponseListContracts,
|
|
48
|
+
)
|
|
39
49
|
from omnibase_infra.services.registry_api.models.model_response_list_instances import (
|
|
40
50
|
ModelResponseListInstances,
|
|
41
51
|
)
|
|
42
52
|
from omnibase_infra.services.registry_api.models.model_response_list_nodes import (
|
|
43
53
|
ModelResponseListNodes,
|
|
44
54
|
)
|
|
55
|
+
from omnibase_infra.services.registry_api.models.model_response_topics import (
|
|
56
|
+
ModelResponseListTopics,
|
|
57
|
+
)
|
|
58
|
+
from omnibase_infra.services.registry_api.models.model_topic_summary import (
|
|
59
|
+
ModelTopicSummary,
|
|
60
|
+
)
|
|
61
|
+
from omnibase_infra.services.registry_api.models.model_topic_view import (
|
|
62
|
+
ModelTopicView,
|
|
63
|
+
)
|
|
45
64
|
from omnibase_infra.services.registry_api.models.model_warning import ModelWarning
|
|
46
65
|
from omnibase_infra.services.registry_api.models.model_widget_defaults import (
|
|
47
66
|
ModelWidgetDefaults,
|
|
@@ -52,14 +71,20 @@ from omnibase_infra.services.registry_api.models.model_widget_mapping import (
|
|
|
52
71
|
|
|
53
72
|
__all__ = [
|
|
54
73
|
"ModelCapabilityWidgetMapping",
|
|
74
|
+
"ModelContractRef",
|
|
75
|
+
"ModelContractView",
|
|
55
76
|
"ModelPaginationInfo",
|
|
56
77
|
"ModelRegistryDiscoveryResponse",
|
|
57
78
|
"ModelRegistryHealthResponse",
|
|
58
79
|
"ModelRegistryInstanceView",
|
|
59
80
|
"ModelRegistryNodeView",
|
|
60
81
|
"ModelRegistrySummary",
|
|
82
|
+
"ModelResponseListContracts",
|
|
61
83
|
"ModelResponseListInstances",
|
|
62
84
|
"ModelResponseListNodes",
|
|
85
|
+
"ModelResponseListTopics",
|
|
86
|
+
"ModelTopicSummary",
|
|
87
|
+
"ModelTopicView",
|
|
63
88
|
"ModelWarning",
|
|
64
89
|
"ModelWidgetDefaults",
|
|
65
90
|
"ModelWidgetMapping",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Contract reference model for dashboard display.
|
|
4
|
+
|
|
5
|
+
Related Tickets:
|
|
6
|
+
- OMN-1845: Contract Registry Persistence
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelContractRef(BaseModel):
|
|
15
|
+
"""Lightweight contract reference.
|
|
16
|
+
|
|
17
|
+
Used to reference a contract without including full details,
|
|
18
|
+
suitable for embedding in topic views.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
contract_id: Unique identifier in format node_name:major.minor.patch
|
|
22
|
+
node_name: Name of the node this contract belongs to
|
|
23
|
+
version: Semantic version string in format major.minor.patch
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True)
|
|
27
|
+
|
|
28
|
+
# ONEX_EXCLUDE: pattern_validator - contract_id is a derived natural key (name:version), not UUID
|
|
29
|
+
contract_id: str = Field(
|
|
30
|
+
...,
|
|
31
|
+
description="Unique identifier in format node_name:major.minor.patch",
|
|
32
|
+
)
|
|
33
|
+
# ONEX_EXCLUDE: pattern_validator - node_name is the contract name, not an entity reference
|
|
34
|
+
node_name: str = Field(
|
|
35
|
+
...,
|
|
36
|
+
description="Name of the node this contract belongs to",
|
|
37
|
+
)
|
|
38
|
+
version: str = Field(
|
|
39
|
+
...,
|
|
40
|
+
description="Semantic version string in format major.minor.patch",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__all__ = ["ModelContractRef"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Contract view model for dashboard display.
|
|
4
|
+
|
|
5
|
+
Related Tickets:
|
|
6
|
+
- OMN-1845: Contract Registry Persistence
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ModelContractView(BaseModel):
|
|
17
|
+
"""Contract detail for API responses.
|
|
18
|
+
|
|
19
|
+
Represents a registered contract from the contract registry,
|
|
20
|
+
flattened for dashboard consumption.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
contract_id: Unique identifier in format node_name:major.minor.patch
|
|
24
|
+
node_name: Name of the node this contract belongs to
|
|
25
|
+
version: Semantic version string in format major.minor.patch
|
|
26
|
+
contract_hash: SHA-256 hash of contract content for integrity verification
|
|
27
|
+
is_active: Whether the contract is currently active
|
|
28
|
+
registered_at: Timestamp of initial registration
|
|
29
|
+
last_seen_at: Timestamp of last activity (heartbeat or event)
|
|
30
|
+
deregistered_at: Timestamp when contract was deregistered (None if active)
|
|
31
|
+
topics_published: List of topic suffixes this contract publishes to
|
|
32
|
+
topics_subscribed: List of topic suffixes this contract subscribes to
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True)
|
|
36
|
+
|
|
37
|
+
# ONEX_EXCLUDE: pattern_validator - contract_id is a derived natural key (name:version), not UUID
|
|
38
|
+
contract_id: str = Field(
|
|
39
|
+
...,
|
|
40
|
+
description="Unique identifier in format node_name:major.minor.patch",
|
|
41
|
+
)
|
|
42
|
+
# ONEX_EXCLUDE: pattern_validator - node_name is the contract name, not an entity reference
|
|
43
|
+
node_name: str = Field(
|
|
44
|
+
...,
|
|
45
|
+
description="Name of the node this contract belongs to",
|
|
46
|
+
)
|
|
47
|
+
version: str = Field(
|
|
48
|
+
...,
|
|
49
|
+
description="Semantic version string in format major.minor.patch",
|
|
50
|
+
)
|
|
51
|
+
contract_hash: str = Field(
|
|
52
|
+
...,
|
|
53
|
+
description="SHA-256 hash of contract content for integrity verification",
|
|
54
|
+
)
|
|
55
|
+
is_active: bool = Field(
|
|
56
|
+
...,
|
|
57
|
+
description="Whether the contract is currently active",
|
|
58
|
+
)
|
|
59
|
+
registered_at: datetime = Field(
|
|
60
|
+
...,
|
|
61
|
+
description="Timestamp of initial registration",
|
|
62
|
+
)
|
|
63
|
+
last_seen_at: datetime = Field(
|
|
64
|
+
...,
|
|
65
|
+
description="Timestamp of last activity (heartbeat or event)",
|
|
66
|
+
)
|
|
67
|
+
deregistered_at: datetime | None = Field(
|
|
68
|
+
default=None,
|
|
69
|
+
description="Timestamp when contract was deregistered (None if active)",
|
|
70
|
+
)
|
|
71
|
+
topics_published: tuple[str, ...] = Field(
|
|
72
|
+
default_factory=tuple,
|
|
73
|
+
description="List of topic suffixes this contract publishes to",
|
|
74
|
+
)
|
|
75
|
+
topics_subscribed: tuple[str, ...] = Field(
|
|
76
|
+
default_factory=tuple,
|
|
77
|
+
description="List of topic suffixes this contract subscribes to",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ["ModelContractView"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Response model for list_contracts endpoint.
|
|
4
|
+
|
|
5
|
+
Related Tickets:
|
|
6
|
+
- OMN-1845: Contract Registry Persistence
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
from omnibase_infra.services.registry_api.models.model_contract_view import (
|
|
14
|
+
ModelContractView,
|
|
15
|
+
)
|
|
16
|
+
from omnibase_infra.services.registry_api.models.model_pagination_info import (
|
|
17
|
+
ModelPaginationInfo,
|
|
18
|
+
)
|
|
19
|
+
from omnibase_infra.services.registry_api.models.model_warning import ModelWarning
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ModelResponseListContracts(BaseModel):
|
|
23
|
+
"""Response model for the GET /registry/contracts endpoint.
|
|
24
|
+
|
|
25
|
+
Provides a paginated list of registered contracts with optional warnings
|
|
26
|
+
for partial success scenarios.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
contracts: List of registered contracts matching the query
|
|
30
|
+
pagination: Pagination information for the result set
|
|
31
|
+
warnings: List of warnings for partial success scenarios
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
35
|
+
|
|
36
|
+
contracts: list[ModelContractView] = Field(
|
|
37
|
+
default_factory=list,
|
|
38
|
+
description="List of registered contracts matching the query",
|
|
39
|
+
)
|
|
40
|
+
pagination: ModelPaginationInfo = Field(
|
|
41
|
+
...,
|
|
42
|
+
description="Pagination information for the result set",
|
|
43
|
+
)
|
|
44
|
+
warnings: list[ModelWarning] = Field(
|
|
45
|
+
default_factory=list,
|
|
46
|
+
description="Warnings for partial success scenarios",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = ["ModelResponseListContracts"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Response model for list_topics endpoint.
|
|
4
|
+
|
|
5
|
+
Related Tickets:
|
|
6
|
+
- OMN-1845: Contract Registry Persistence
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
from omnibase_infra.services.registry_api.models.model_pagination_info import (
|
|
14
|
+
ModelPaginationInfo,
|
|
15
|
+
)
|
|
16
|
+
from omnibase_infra.services.registry_api.models.model_topic_summary import (
|
|
17
|
+
ModelTopicSummary,
|
|
18
|
+
)
|
|
19
|
+
from omnibase_infra.services.registry_api.models.model_warning import ModelWarning
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ModelResponseListTopics(BaseModel):
|
|
23
|
+
"""Response model for the GET /registry/topics endpoint.
|
|
24
|
+
|
|
25
|
+
Provides a paginated list of topics with optional warnings
|
|
26
|
+
for partial success scenarios.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
topics: List of topic summaries matching the query
|
|
30
|
+
pagination: Pagination information for the result set
|
|
31
|
+
warnings: List of warnings for partial success scenarios
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True)
|
|
35
|
+
|
|
36
|
+
topics: list[ModelTopicSummary] = Field(
|
|
37
|
+
default_factory=list,
|
|
38
|
+
description="List of topic summaries matching the query",
|
|
39
|
+
)
|
|
40
|
+
pagination: ModelPaginationInfo = Field(
|
|
41
|
+
...,
|
|
42
|
+
description="Pagination information for the result set",
|
|
43
|
+
)
|
|
44
|
+
warnings: list[ModelWarning] = Field(
|
|
45
|
+
default_factory=list,
|
|
46
|
+
description="Warnings for partial success scenarios",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = ["ModelResponseListTopics"]
|