omnibase_infra 0.3.1__py3-none-any.whl → 0.4.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.
Files changed (117) 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/errors/__init__.py +4 -0
  6. omnibase_infra/errors/error_infra.py +60 -0
  7. omnibase_infra/handlers/__init__.py +3 -0
  8. omnibase_infra/handlers/handler_slack_webhook.py +426 -0
  9. omnibase_infra/handlers/models/__init__.py +14 -0
  10. omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
  11. omnibase_infra/handlers/models/model_slack_alert.py +24 -0
  12. omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
  13. omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
  14. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
  15. omnibase_infra/mixins/__init__.py +14 -0
  16. omnibase_infra/mixins/mixin_node_introspection.py +42 -20
  17. omnibase_infra/mixins/mixin_postgres_error_response.py +314 -0
  18. omnibase_infra/mixins/mixin_postgres_op_executor.py +298 -0
  19. omnibase_infra/models/__init__.py +3 -0
  20. omnibase_infra/models/discovery/model_dependency_spec.py +1 -0
  21. omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
  22. omnibase_infra/models/discovery/model_introspection_config.py +28 -1
  23. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
  24. omnibase_infra/models/discovery/model_introspection_task_config.py +1 -0
  25. omnibase_infra/{nodes/effects/models → models}/model_backend_result.py +22 -6
  26. omnibase_infra/models/projection/__init__.py +11 -0
  27. omnibase_infra/models/projection/model_contract_projection.py +170 -0
  28. omnibase_infra/models/projection/model_topic_projection.py +148 -0
  29. omnibase_infra/models/runtime/__init__.py +4 -0
  30. omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
  31. omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
  32. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
  33. omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
  34. omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
  35. omnibase_infra/nodes/effects/__init__.py +1 -1
  36. omnibase_infra/nodes/effects/models/__init__.py +6 -4
  37. omnibase_infra/nodes/effects/models/model_registry_response.py +1 -1
  38. omnibase_infra/nodes/effects/protocol_consul_client.py +1 -1
  39. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +1 -1
  40. omnibase_infra/nodes/effects/registry_effect.py +1 -1
  41. omnibase_infra/nodes/node_contract_persistence_effect/__init__.py +101 -0
  42. omnibase_infra/nodes/node_contract_persistence_effect/contract.yaml +490 -0
  43. omnibase_infra/nodes/node_contract_persistence_effect/handlers/__init__.py +74 -0
  44. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_cleanup_topics.py +217 -0
  45. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_contract_upsert.py +242 -0
  46. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py +194 -0
  47. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_heartbeat.py +243 -0
  48. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_mark_stale.py +208 -0
  49. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py +298 -0
  50. omnibase_infra/nodes/node_contract_persistence_effect/models/__init__.py +15 -0
  51. omnibase_infra/nodes/node_contract_persistence_effect/models/model_persistence_result.py +52 -0
  52. omnibase_infra/nodes/node_contract_persistence_effect/node.py +131 -0
  53. omnibase_infra/nodes/node_contract_persistence_effect/registry/__init__.py +27 -0
  54. omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +251 -0
  55. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
  56. omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
  57. omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
  58. omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
  59. omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
  60. omnibase_infra/projectors/__init__.py +6 -0
  61. omnibase_infra/projectors/projection_reader_contract.py +1301 -0
  62. omnibase_infra/runtime/__init__.py +12 -0
  63. omnibase_infra/runtime/baseline_subscriptions.py +13 -6
  64. omnibase_infra/runtime/contract_dependency_resolver.py +455 -0
  65. omnibase_infra/runtime/contract_registration_event_router.py +500 -0
  66. omnibase_infra/runtime/db/__init__.py +4 -0
  67. omnibase_infra/runtime/db/models/__init__.py +15 -10
  68. omnibase_infra/runtime/db/models/model_db_operation.py +40 -0
  69. omnibase_infra/runtime/db/models/model_db_param.py +24 -0
  70. omnibase_infra/runtime/db/models/model_db_repository_contract.py +40 -0
  71. omnibase_infra/runtime/db/models/model_db_return.py +26 -0
  72. omnibase_infra/runtime/db/models/model_db_safety_policy.py +32 -0
  73. omnibase_infra/runtime/emit_daemon/event_registry.py +34 -22
  74. omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
  75. omnibase_infra/runtime/intent_execution_router.py +430 -0
  76. omnibase_infra/runtime/models/__init__.py +6 -0
  77. omnibase_infra/runtime/models/model_contract_registry_config.py +41 -0
  78. omnibase_infra/runtime/models/model_intent_execution_summary.py +79 -0
  79. omnibase_infra/runtime/models/model_runtime_config.py +8 -0
  80. omnibase_infra/runtime/protocols/__init__.py +16 -0
  81. omnibase_infra/runtime/protocols/protocol_intent_executor.py +107 -0
  82. omnibase_infra/runtime/publisher_topic_scoped.py +16 -11
  83. omnibase_infra/runtime/registry_policy.py +29 -15
  84. omnibase_infra/runtime/request_response_wiring.py +793 -0
  85. omnibase_infra/runtime/service_kernel.py +295 -8
  86. omnibase_infra/runtime/service_runtime_host_process.py +149 -5
  87. omnibase_infra/runtime/util_version.py +5 -1
  88. omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
  89. omnibase_infra/services/contract_publisher/config.py +4 -4
  90. omnibase_infra/services/contract_publisher/service.py +8 -5
  91. omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
  92. omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
  93. omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
  94. omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
  95. omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
  96. omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
  97. omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
  98. omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
  99. omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
  100. omnibase_infra/services/registry_api/models/__init__.py +25 -0
  101. omnibase_infra/services/registry_api/models/model_contract_ref.py +44 -0
  102. omnibase_infra/services/registry_api/models/model_contract_view.py +81 -0
  103. omnibase_infra/services/registry_api/models/model_response_contracts.py +50 -0
  104. omnibase_infra/services/registry_api/models/model_response_topics.py +50 -0
  105. omnibase_infra/services/registry_api/models/model_topic_summary.py +57 -0
  106. omnibase_infra/services/registry_api/models/model_topic_view.py +63 -0
  107. omnibase_infra/services/registry_api/routes.py +205 -6
  108. omnibase_infra/services/registry_api/service.py +528 -1
  109. omnibase_infra/utils/__init__.py +7 -0
  110. omnibase_infra/utils/util_db_error_context.py +292 -0
  111. omnibase_infra/validation/infra_validators.py +3 -1
  112. omnibase_infra/validation/validation_exemptions.yaml +65 -0
  113. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +3 -3
  114. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +117 -58
  115. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
  116. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
  117. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -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"]
@@ -0,0 +1,57 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Topic summary 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
+ from omnibase_infra.models.projection.model_topic_projection import TopicDirection
16
+
17
+
18
+ class ModelTopicSummary(BaseModel):
19
+ """Topic summary for list view.
20
+
21
+ Provides a compact view of a topic suitable for list endpoints,
22
+ including direction (publish/subscribe) and contract count.
23
+
24
+ Attributes:
25
+ topic_suffix: Topic suffix (without environment prefix)
26
+ direction: Relationship direction ('publish' or 'subscribe')
27
+ contract_count: Number of contracts with this topic relationship
28
+ last_seen_at: Timestamp of last activity on this topic
29
+ is_active: Whether the topic has active contracts
30
+ """
31
+
32
+ model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True)
33
+
34
+ topic_suffix: str = Field(
35
+ ...,
36
+ description="Topic suffix (without environment prefix)",
37
+ )
38
+ direction: TopicDirection = Field(
39
+ ...,
40
+ description="Relationship direction ('publish' or 'subscribe')",
41
+ )
42
+ contract_count: int = Field(
43
+ ...,
44
+ ge=0,
45
+ description="Number of contracts with this topic relationship",
46
+ )
47
+ last_seen_at: datetime = Field(
48
+ ...,
49
+ description="Timestamp of last activity on this topic",
50
+ )
51
+ is_active: bool = Field(
52
+ ...,
53
+ description="Whether the topic has active contracts",
54
+ )
55
+
56
+
57
+ __all__ = ["ModelTopicSummary"]
@@ -0,0 +1,63 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Topic 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
+ from omnibase_infra.services.registry_api.models.model_contract_ref import (
16
+ ModelContractRef,
17
+ )
18
+
19
+
20
+ class ModelTopicView(BaseModel):
21
+ """Topic detail for API responses.
22
+
23
+ Represents a topic with its publishers and subscribers,
24
+ providing full detail for topic inspection endpoints.
25
+
26
+ Attributes:
27
+ topic_suffix: Topic suffix (without environment prefix)
28
+ publishers: List of contracts that publish to this topic
29
+ subscribers: List of contracts that subscribe to this topic
30
+ first_seen_at: Timestamp when topic was first observed
31
+ last_seen_at: Timestamp of last activity on this topic
32
+ is_active: Whether the topic has active publishers or subscribers
33
+ """
34
+
35
+ model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True)
36
+
37
+ topic_suffix: str = Field(
38
+ ...,
39
+ description="Topic suffix (without environment prefix)",
40
+ )
41
+ publishers: list[ModelContractRef] = Field(
42
+ default_factory=list,
43
+ description="List of contracts that publish to this topic",
44
+ )
45
+ subscribers: list[ModelContractRef] = Field(
46
+ default_factory=list,
47
+ description="List of contracts that subscribe to this topic",
48
+ )
49
+ first_seen_at: datetime = Field(
50
+ ...,
51
+ description="Timestamp when topic was first observed",
52
+ )
53
+ last_seen_at: datetime = Field(
54
+ ...,
55
+ description="Timestamp of last activity on this topic",
56
+ )
57
+ is_active: bool = Field(
58
+ ...,
59
+ description="Whether the topic has active publishers or subscribers",
60
+ )
61
+
62
+
63
+ __all__ = ["ModelTopicView"]
@@ -6,15 +6,20 @@ FastAPI route handlers for the Registry API. Routes are defined as an
6
6
  APIRouter for easy mounting into the main FastAPI application.
7
7
 
8
8
  Endpoint Summary:
9
- GET /registry/discovery - Full dashboard payload
10
- GET /registry/nodes - Node list with pagination
11
- GET /registry/nodes/{id} - Single node detail
12
- GET /registry/instances - Live Consul instances
13
- GET /registry/widgets/mapping - Widget mapping configuration
14
- GET /registry/health - Service health check
9
+ GET /registry/discovery - Full dashboard payload
10
+ GET /registry/nodes - Node list with pagination
11
+ GET /registry/nodes/{id} - Single node detail
12
+ GET /registry/instances - Live Consul instances
13
+ GET /registry/widgets/mapping - Widget mapping configuration
14
+ GET /registry/health - Service health check
15
+ GET /registry/contracts - Contract list with pagination
16
+ GET /registry/contracts/{id} - Single contract detail
17
+ GET /registry/topics - Topic list with pagination
18
+ GET /registry/topics/{suffix} - Single topic detail
15
19
 
16
20
  Related Tickets:
17
21
  - OMN-1278: Contract-Driven Dashboard - Registry Discovery
22
+ - OMN-1845: Contract Registry Persistence
18
23
  """
19
24
 
20
25
  from __future__ import annotations
@@ -26,11 +31,15 @@ from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, s
26
31
 
27
32
  from omnibase_infra.enums import EnumRegistrationState
28
33
  from omnibase_infra.services.registry_api.models import (
34
+ ModelContractView,
29
35
  ModelRegistryDiscoveryResponse,
30
36
  ModelRegistryHealthResponse,
31
37
  ModelRegistryNodeView,
38
+ ModelResponseListContracts,
32
39
  ModelResponseListInstances,
33
40
  ModelResponseListNodes,
41
+ ModelResponseListTopics,
42
+ ModelTopicView,
34
43
  ModelWidgetMapping,
35
44
  )
36
45
 
@@ -368,4 +377,194 @@ async def health_check(
368
377
  return response
369
378
 
370
379
 
380
+ # ============================================================
381
+ # Contract Registry Endpoints
382
+ # ============================================================
383
+
384
+
385
+ @router.get(
386
+ "/contracts",
387
+ response_model=ModelResponseListContracts,
388
+ summary="List Registered Contracts",
389
+ description=(
390
+ "Returns a paginated list of registered contracts from the contract registry. "
391
+ "Supports filtering by node name and active status."
392
+ ),
393
+ responses={
394
+ 400: {"description": "Bad request (e.g., invalid correlation ID format)"},
395
+ 200: {"description": "Successful response with contract list"},
396
+ },
397
+ )
398
+ async def list_contracts(
399
+ service: Annotated[ServiceRegistryDiscovery, Depends(get_service)],
400
+ correlation_id: Annotated[UUID, Depends(get_correlation_id)],
401
+ limit: Annotated[
402
+ int,
403
+ Query(ge=1, le=1000, description="Maximum number of contracts to return"),
404
+ ] = 100,
405
+ offset: Annotated[
406
+ int,
407
+ Query(ge=0, description="Number of contracts to skip for pagination"),
408
+ ] = 0,
409
+ active_only: Annotated[
410
+ bool,
411
+ Query(description="If true, return only active contracts"),
412
+ ] = True,
413
+ node_name: Annotated[
414
+ str | None,
415
+ Query(description="Filter by node name"),
416
+ ] = None,
417
+ ) -> ModelResponseListContracts:
418
+ """List registered contracts with pagination and optional filtering."""
419
+ contracts, pagination, warnings = await service.list_contracts(
420
+ limit=limit,
421
+ offset=offset,
422
+ active_only=active_only,
423
+ node_name=node_name,
424
+ correlation_id=correlation_id,
425
+ )
426
+
427
+ return ModelResponseListContracts(
428
+ contracts=contracts,
429
+ pagination=pagination,
430
+ warnings=warnings,
431
+ )
432
+
433
+
434
+ @router.get(
435
+ "/contracts/{contract_id:path}",
436
+ response_model=ModelContractView,
437
+ summary="Get Contract Details",
438
+ description=(
439
+ "Returns detailed information for a single contract by ID. "
440
+ "Includes lists of topics the contract publishes to and subscribes from."
441
+ ),
442
+ responses={
443
+ 400: {"description": "Bad request (e.g., invalid correlation ID format)"},
444
+ 200: {"description": "Successful response with contract details"},
445
+ 404: {"description": "Contract not found"},
446
+ },
447
+ )
448
+ async def get_contract(
449
+ contract_id: str,
450
+ service: Annotated[ServiceRegistryDiscovery, Depends(get_service)],
451
+ correlation_id: Annotated[UUID, Depends(get_correlation_id)],
452
+ ) -> ModelContractView:
453
+ """Get a single contract by ID."""
454
+ contract, warnings = await service.get_contract(
455
+ contract_id=contract_id,
456
+ correlation_id=correlation_id,
457
+ )
458
+
459
+ if contract is None:
460
+ # Check if it was a service error or genuinely not found
461
+ if warnings:
462
+ raise HTTPException(
463
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
464
+ detail=f"Service error: {warnings[0].message}",
465
+ headers={"X-Correlation-ID": str(correlation_id)},
466
+ )
467
+ raise HTTPException(
468
+ status_code=status.HTTP_404_NOT_FOUND,
469
+ detail=f"Contract not found: {contract_id}",
470
+ headers={"X-Correlation-ID": str(correlation_id)},
471
+ )
472
+
473
+ return contract
474
+
475
+
476
+ @router.get(
477
+ "/topics",
478
+ response_model=ModelResponseListTopics,
479
+ summary="List Event Topics",
480
+ description=(
481
+ "Returns a paginated list of event topics from the contract registry. "
482
+ "Supports filtering by direction (publish/subscribe)."
483
+ ),
484
+ responses={
485
+ 400: {"description": "Bad request (e.g., invalid correlation ID format)"},
486
+ 200: {"description": "Successful response with topic list"},
487
+ },
488
+ )
489
+ async def list_topics(
490
+ service: Annotated[ServiceRegistryDiscovery, Depends(get_service)],
491
+ correlation_id: Annotated[UUID, Depends(get_correlation_id)],
492
+ direction: Annotated[
493
+ str | None,
494
+ Query(description="Filter by direction ('publish' or 'subscribe')"),
495
+ ] = None,
496
+ limit: Annotated[
497
+ int,
498
+ Query(ge=1, le=1000, description="Maximum number of topics to return"),
499
+ ] = 100,
500
+ offset: Annotated[
501
+ int,
502
+ Query(ge=0, description="Number of topics to skip for pagination"),
503
+ ] = 0,
504
+ ) -> ModelResponseListTopics:
505
+ """List event topics with pagination and optional filtering."""
506
+ # Validate direction if provided
507
+ if direction is not None and direction not in ("publish", "subscribe"):
508
+ raise HTTPException(
509
+ status_code=status.HTTP_400_BAD_REQUEST,
510
+ detail=f"Invalid direction: {direction}. Must be 'publish' or 'subscribe'.",
511
+ headers={"X-Correlation-ID": str(correlation_id)},
512
+ )
513
+
514
+ topics, pagination, warnings = await service.list_topics(
515
+ direction=direction,
516
+ limit=limit,
517
+ offset=offset,
518
+ correlation_id=correlation_id,
519
+ )
520
+
521
+ return ModelResponseListTopics(
522
+ topics=topics,
523
+ pagination=pagination,
524
+ warnings=warnings,
525
+ )
526
+
527
+
528
+ @router.get(
529
+ "/topics/{topic_suffix:path}",
530
+ response_model=ModelTopicView,
531
+ summary="Get Topic Details",
532
+ description=(
533
+ "Returns detailed information for a single topic by suffix. "
534
+ "Includes lists of publishers and subscribers with contract references."
535
+ ),
536
+ responses={
537
+ 400: {"description": "Bad request (e.g., invalid correlation ID format)"},
538
+ 200: {"description": "Successful response with topic details"},
539
+ 404: {"description": "Topic not found"},
540
+ },
541
+ )
542
+ async def get_topic(
543
+ topic_suffix: str,
544
+ service: Annotated[ServiceRegistryDiscovery, Depends(get_service)],
545
+ correlation_id: Annotated[UUID, Depends(get_correlation_id)],
546
+ ) -> ModelTopicView:
547
+ """Get a single topic by suffix."""
548
+ topic, warnings = await service.get_topic_detail(
549
+ topic_suffix=topic_suffix,
550
+ correlation_id=correlation_id,
551
+ )
552
+
553
+ if topic is None:
554
+ # Check if it was a service error or genuinely not found
555
+ if warnings:
556
+ raise HTTPException(
557
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
558
+ detail=f"Service error: {warnings[0].message}",
559
+ headers={"X-Correlation-ID": str(correlation_id)},
560
+ )
561
+ raise HTTPException(
562
+ status_code=status.HTTP_404_NOT_FOUND,
563
+ detail=f"Topic not found: {topic_suffix}",
564
+ headers={"X-Correlation-ID": str(correlation_id)},
565
+ )
566
+
567
+ return topic
568
+
569
+
371
570
  __all__ = ["router", "get_service", "get_correlation_id"]