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.
Files changed (72) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/enums/__init__.py +3 -0
  3. omnibase_infra/enums/enum_consumer_group_purpose.py +9 -0
  4. omnibase_infra/enums/enum_postgres_error_code.py +188 -0
  5. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
  6. omnibase_infra/mixins/__init__.py +14 -0
  7. omnibase_infra/mixins/mixin_postgres_error_response.py +314 -0
  8. omnibase_infra/mixins/mixin_postgres_op_executor.py +298 -0
  9. omnibase_infra/models/__init__.py +3 -0
  10. omnibase_infra/{nodes/effects/models → models}/model_backend_result.py +22 -6
  11. omnibase_infra/models/projection/__init__.py +11 -0
  12. omnibase_infra/models/projection/model_contract_projection.py +170 -0
  13. omnibase_infra/models/projection/model_topic_projection.py +148 -0
  14. omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
  15. omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
  16. omnibase_infra/nodes/effects/__init__.py +1 -1
  17. omnibase_infra/nodes/effects/models/__init__.py +6 -4
  18. omnibase_infra/nodes/effects/models/model_registry_response.py +1 -1
  19. omnibase_infra/nodes/effects/protocol_consul_client.py +1 -1
  20. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +1 -1
  21. omnibase_infra/nodes/effects/registry_effect.py +1 -1
  22. omnibase_infra/nodes/node_contract_persistence_effect/__init__.py +101 -0
  23. omnibase_infra/nodes/node_contract_persistence_effect/contract.yaml +490 -0
  24. omnibase_infra/nodes/node_contract_persistence_effect/handlers/__init__.py +74 -0
  25. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_cleanup_topics.py +217 -0
  26. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_contract_upsert.py +242 -0
  27. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py +194 -0
  28. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_heartbeat.py +243 -0
  29. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_mark_stale.py +208 -0
  30. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py +298 -0
  31. omnibase_infra/nodes/node_contract_persistence_effect/models/__init__.py +15 -0
  32. omnibase_infra/nodes/node_contract_persistence_effect/models/model_persistence_result.py +52 -0
  33. omnibase_infra/nodes/node_contract_persistence_effect/node.py +114 -0
  34. omnibase_infra/nodes/node_contract_persistence_effect/registry/__init__.py +27 -0
  35. omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +220 -0
  36. omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
  37. omnibase_infra/projectors/__init__.py +6 -0
  38. omnibase_infra/projectors/projection_reader_contract.py +1301 -0
  39. omnibase_infra/runtime/__init__.py +5 -0
  40. omnibase_infra/runtime/contract_registration_event_router.py +500 -0
  41. omnibase_infra/runtime/db/__init__.py +4 -0
  42. omnibase_infra/runtime/db/models/__init__.py +15 -10
  43. omnibase_infra/runtime/db/models/model_db_operation.py +40 -0
  44. omnibase_infra/runtime/db/models/model_db_param.py +24 -0
  45. omnibase_infra/runtime/db/models/model_db_repository_contract.py +40 -0
  46. omnibase_infra/runtime/db/models/model_db_return.py +26 -0
  47. omnibase_infra/runtime/db/models/model_db_safety_policy.py +32 -0
  48. omnibase_infra/runtime/intent_execution_router.py +430 -0
  49. omnibase_infra/runtime/models/__init__.py +6 -0
  50. omnibase_infra/runtime/models/model_contract_registry_config.py +41 -0
  51. omnibase_infra/runtime/models/model_intent_execution_summary.py +79 -0
  52. omnibase_infra/runtime/models/model_runtime_config.py +8 -0
  53. omnibase_infra/runtime/protocols/__init__.py +16 -0
  54. omnibase_infra/runtime/protocols/protocol_intent_executor.py +107 -0
  55. omnibase_infra/runtime/request_response_wiring.py +785 -0
  56. omnibase_infra/runtime/service_kernel.py +295 -8
  57. omnibase_infra/services/registry_api/models/__init__.py +25 -0
  58. omnibase_infra/services/registry_api/models/model_contract_ref.py +44 -0
  59. omnibase_infra/services/registry_api/models/model_contract_view.py +81 -0
  60. omnibase_infra/services/registry_api/models/model_response_contracts.py +50 -0
  61. omnibase_infra/services/registry_api/models/model_response_topics.py +50 -0
  62. omnibase_infra/services/registry_api/models/model_topic_summary.py +57 -0
  63. omnibase_infra/services/registry_api/models/model_topic_view.py +63 -0
  64. omnibase_infra/services/registry_api/routes.py +205 -6
  65. omnibase_infra/services/registry_api/service.py +528 -1
  66. omnibase_infra/validation/infra_validators.py +3 -1
  67. omnibase_infra/validation/validation_exemptions.yaml +54 -0
  68. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/METADATA +3 -3
  69. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/RECORD +72 -34
  70. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/WHEEL +0 -0
  71. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/entry_points.txt +0 -0
  72. {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
- # Explicit bool evaluation (not truthy string) for kafka usage.
469
- # KAFKA_BOOTSTRAP_SERVERS env var takes precedence over config.event_bus.type.
470
- # This prevents implicit "kafka but localhost" fallback scenarios.
471
- use_kafka: bool = (
472
- bool(kafka_bootstrap_servers) or config.event_bus.type == "kafka"
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} {config.output_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"]