omnibase_infra 0.2.1__py3-none-any.whl → 0.2.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/adapters/adapter_onex_tool_execution.py +446 -0
- omnibase_infra/cli/commands.py +1 -1
- omnibase_infra/configs/widget_mapping.yaml +176 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +4 -1
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +4 -1
- omnibase_infra/errors/error_compute_registry.py +4 -1
- omnibase_infra/errors/error_event_bus_registry.py +4 -1
- omnibase_infra/errors/error_infra.py +3 -1
- omnibase_infra/errors/error_policy_registry.py +4 -1
- omnibase_infra/handlers/handler_db.py +2 -1
- omnibase_infra/handlers/handler_graph.py +10 -5
- omnibase_infra/handlers/handler_mcp.py +736 -63
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
- omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
- omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +301 -4
- omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
- omnibase_infra/mixins/mixin_node_introspection.py +24 -7
- omnibase_infra/mixins/mixin_retry_execution.py +1 -1
- omnibase_infra/models/handlers/__init__.py +10 -0
- omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
- omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
- omnibase_infra/models/mcp/__init__.py +15 -0
- omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
- omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
- omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
- omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
- omnibase_infra/models/registration/model_node_capabilities.py +11 -0
- omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
- omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
- omnibase_infra/nodes/effects/contract.yaml +0 -5
- omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
- omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
- omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
- omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +46 -25
- omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
- omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
- omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +24 -19
- omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
- omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
- omnibase_infra/plugins/plugin_compute_base.py +16 -2
- omnibase_infra/protocols/protocol_event_projector.py +1 -1
- omnibase_infra/runtime/__init__.py +51 -1
- omnibase_infra/runtime/binding_config_resolver.py +102 -37
- omnibase_infra/runtime/constants_notification.py +75 -0
- omnibase_infra/runtime/contract_handler_discovery.py +6 -1
- omnibase_infra/runtime/handler_bootstrap_source.py +514 -0
- omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
- omnibase_infra/runtime/handler_contract_source.py +289 -167
- omnibase_infra/runtime/handler_plugin_loader.py +4 -2
- omnibase_infra/runtime/mixin_semver_cache.py +25 -1
- omnibase_infra/runtime/mixins/__init__.py +7 -0
- omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
- omnibase_infra/runtime/models/__init__.py +24 -0
- omnibase_infra/runtime/models/model_health_check_result.py +2 -1
- omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
- omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
- omnibase_infra/runtime/projector_plugin_loader.py +1 -1
- omnibase_infra/runtime/projector_shell.py +229 -1
- omnibase_infra/runtime/protocols/__init__.py +10 -0
- omnibase_infra/runtime/registry/registry_protocol_binding.py +3 -2
- omnibase_infra/runtime/registry_policy.py +9 -326
- omnibase_infra/runtime/secret_resolver.py +4 -2
- omnibase_infra/runtime/service_kernel.py +10 -2
- omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
- omnibase_infra/runtime/service_runtime_host_process.py +225 -15
- omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
- omnibase_infra/runtime/transition_notification_publisher.py +764 -0
- omnibase_infra/runtime/util_container_wiring.py +6 -5
- omnibase_infra/runtime/util_wiring.py +5 -1
- omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
- omnibase_infra/services/mcp/__init__.py +31 -0
- omnibase_infra/services/mcp/mcp_server_lifecycle.py +443 -0
- omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
- omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
- omnibase_infra/services/registry_api/__init__.py +40 -0
- omnibase_infra/services/registry_api/main.py +243 -0
- omnibase_infra/services/registry_api/models/__init__.py +66 -0
- omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
- omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
- omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
- omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
- omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
- omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
- omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
- omnibase_infra/services/registry_api/models/model_warning.py +49 -0
- omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
- omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
- omnibase_infra/services/registry_api/routes.py +371 -0
- omnibase_infra/services/registry_api/service.py +846 -0
- omnibase_infra/services/service_capability_query.py +4 -4
- omnibase_infra/services/service_health.py +3 -2
- omnibase_infra/services/service_timeout_emitter.py +13 -2
- omnibase_infra/utils/util_dsn_validation.py +1 -1
- omnibase_infra/validation/__init__.py +3 -19
- omnibase_infra/validation/contracts/security.validation.yaml +114 -0
- omnibase_infra/validation/infra_validators.py +35 -24
- omnibase_infra/validation/validation_exemptions.yaml +113 -9
- omnibase_infra/validation/validator_chain_propagation.py +2 -2
- omnibase_infra/validation/validator_runtime_shape.py +1 -1
- omnibase_infra/validation/validator_security.py +473 -370
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/METADATA +2 -2
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/RECORD +116 -74
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Registry summary model for aggregate statistics.
|
|
4
|
+
|
|
5
|
+
Related Tickets:
|
|
6
|
+
- OMN-1278: Contract-Driven Dashboard - Registry Discovery
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelRegistrySummary(BaseModel):
|
|
15
|
+
"""Summary statistics for the registry.
|
|
16
|
+
|
|
17
|
+
Provides aggregate counts for dashboard widgets.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
total_nodes: Total number of registered nodes
|
|
21
|
+
active_nodes: Number of nodes in ACTIVE state
|
|
22
|
+
healthy_instances: Number of passing health check instances
|
|
23
|
+
unhealthy_instances: Number of failing health check instances
|
|
24
|
+
by_node_type: Count of nodes by type
|
|
25
|
+
by_state: Count of nodes by registration state
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
29
|
+
|
|
30
|
+
total_nodes: int = Field(
|
|
31
|
+
...,
|
|
32
|
+
ge=0,
|
|
33
|
+
description="Total number of registered nodes",
|
|
34
|
+
)
|
|
35
|
+
active_nodes: int = Field(
|
|
36
|
+
...,
|
|
37
|
+
ge=0,
|
|
38
|
+
description="Number of nodes in ACTIVE state",
|
|
39
|
+
)
|
|
40
|
+
healthy_instances: int = Field(
|
|
41
|
+
...,
|
|
42
|
+
ge=0,
|
|
43
|
+
description="Number of passing health check instances",
|
|
44
|
+
)
|
|
45
|
+
unhealthy_instances: int = Field(
|
|
46
|
+
...,
|
|
47
|
+
ge=0,
|
|
48
|
+
description="Number of failing health check instances",
|
|
49
|
+
)
|
|
50
|
+
by_node_type: dict[str, int] = Field(
|
|
51
|
+
default_factory=dict,
|
|
52
|
+
description="Count of nodes by type",
|
|
53
|
+
)
|
|
54
|
+
by_state: dict[str, int] = Field(
|
|
55
|
+
default_factory=dict,
|
|
56
|
+
description="Count of nodes by registration state",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
__all__ = ["ModelRegistrySummary"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Response model for list_instances endpoint.
|
|
4
|
+
|
|
5
|
+
Related Tickets:
|
|
6
|
+
- OMN-1278: Contract-Driven Dashboard - Registry Discovery
|
|
7
|
+
- PR #182: Add Pydantic response models for API endpoints
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
13
|
+
|
|
14
|
+
from omnibase_infra.services.registry_api.models.model_registry_instance_view import (
|
|
15
|
+
ModelRegistryInstanceView,
|
|
16
|
+
)
|
|
17
|
+
from omnibase_infra.services.registry_api.models.model_warning import ModelWarning
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ModelResponseListInstances(BaseModel):
|
|
21
|
+
"""Response model for the GET /registry/instances endpoint.
|
|
22
|
+
|
|
23
|
+
Provides a list of live Consul service instances with optional warnings
|
|
24
|
+
for partial success scenarios.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
instances: List of live Consul service instances
|
|
28
|
+
warnings: List of warnings for partial success scenarios
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
32
|
+
|
|
33
|
+
instances: list[ModelRegistryInstanceView] = Field(
|
|
34
|
+
default_factory=list,
|
|
35
|
+
description="List of live Consul service instances",
|
|
36
|
+
)
|
|
37
|
+
warnings: list[ModelWarning] = Field(
|
|
38
|
+
default_factory=list,
|
|
39
|
+
description="Warnings for partial success scenarios",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = ["ModelResponseListInstances"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Response model for list_nodes endpoint.
|
|
4
|
+
|
|
5
|
+
Related Tickets:
|
|
6
|
+
- OMN-1278: Contract-Driven Dashboard - Registry Discovery
|
|
7
|
+
- PR #182: Add Pydantic response models for API endpoints
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
13
|
+
|
|
14
|
+
from omnibase_infra.services.registry_api.models.model_pagination_info import (
|
|
15
|
+
ModelPaginationInfo,
|
|
16
|
+
)
|
|
17
|
+
from omnibase_infra.services.registry_api.models.model_registry_node_view import (
|
|
18
|
+
ModelRegistryNodeView,
|
|
19
|
+
)
|
|
20
|
+
from omnibase_infra.services.registry_api.models.model_warning import ModelWarning
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ModelResponseListNodes(BaseModel):
|
|
24
|
+
"""Response model for the GET /registry/nodes endpoint.
|
|
25
|
+
|
|
26
|
+
Provides a paginated list of registered nodes with optional warnings
|
|
27
|
+
for partial success scenarios.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
nodes: List of registered nodes matching the query
|
|
31
|
+
pagination: Pagination information for the result set
|
|
32
|
+
warnings: List of warnings for partial success scenarios
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
36
|
+
|
|
37
|
+
nodes: list[ModelRegistryNodeView] = Field(
|
|
38
|
+
default_factory=list,
|
|
39
|
+
description="List of registered nodes matching the query",
|
|
40
|
+
)
|
|
41
|
+
pagination: ModelPaginationInfo = Field(
|
|
42
|
+
...,
|
|
43
|
+
description="Pagination information for the result set",
|
|
44
|
+
)
|
|
45
|
+
warnings: list[ModelWarning] = Field(
|
|
46
|
+
default_factory=list,
|
|
47
|
+
description="Warnings for partial success scenarios",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = ["ModelResponseListNodes"]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Warning model for partial success scenarios.
|
|
4
|
+
|
|
5
|
+
Related Tickets:
|
|
6
|
+
- OMN-1278: Contract-Driven Dashboard - Registry Discovery
|
|
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 ModelWarning(BaseModel):
|
|
17
|
+
"""Warning message for partial success scenarios.
|
|
18
|
+
|
|
19
|
+
Used when one backend succeeds but another fails, allowing the
|
|
20
|
+
API to return partial results with appropriate warnings.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
source: Source of the warning (e.g., "consul", "postgres")
|
|
24
|
+
message: Human-readable warning message
|
|
25
|
+
code: Optional error code for programmatic handling
|
|
26
|
+
timestamp: When the warning was generated
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
30
|
+
|
|
31
|
+
source: str = Field(
|
|
32
|
+
...,
|
|
33
|
+
description="Source of the warning",
|
|
34
|
+
)
|
|
35
|
+
message: str = Field(
|
|
36
|
+
...,
|
|
37
|
+
description="Human-readable warning message",
|
|
38
|
+
)
|
|
39
|
+
code: str | None = Field(
|
|
40
|
+
default=None,
|
|
41
|
+
description="Optional error code",
|
|
42
|
+
)
|
|
43
|
+
timestamp: datetime = Field(
|
|
44
|
+
...,
|
|
45
|
+
description="When the warning was generated",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ["ModelWarning"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Widget defaults model for dashboard configuration.
|
|
4
|
+
|
|
5
|
+
Related Tickets:
|
|
6
|
+
- OMN-1278: Contract-Driven Dashboard - Registry Discovery
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelWidgetDefaults(BaseModel):
|
|
15
|
+
"""Default configuration for a widget type.
|
|
16
|
+
|
|
17
|
+
Attributes vary by widget type and provide sensible defaults.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
model_config = ConfigDict(frozen=True, extra="allow")
|
|
21
|
+
|
|
22
|
+
# Common fields - all optional
|
|
23
|
+
show_timestamp: bool | None = None
|
|
24
|
+
max_items: int | None = None
|
|
25
|
+
refresh_interval_seconds: int | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = ["ModelWidgetDefaults"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Widget mapping model for complete dashboard configuration.
|
|
4
|
+
|
|
5
|
+
Related Tickets:
|
|
6
|
+
- OMN-1278: Contract-Driven Dashboard - Registry Discovery
|
|
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_capability_widget_mapping import (
|
|
14
|
+
ModelCapabilityWidgetMapping,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ModelWidgetMapping(BaseModel):
|
|
19
|
+
"""Complete widget mapping configuration.
|
|
20
|
+
|
|
21
|
+
Loaded from widget_mapping.yaml, provides the mapping from
|
|
22
|
+
capabilities and semantic roles to widget types.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
version: Configuration version for compatibility checking
|
|
26
|
+
capability_mappings: Map of capability tags to widget configs
|
|
27
|
+
semantic_mappings: Map of semantic roles to widget configs
|
|
28
|
+
fallback: Default widget config when no mapping matches
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
32
|
+
|
|
33
|
+
version: str = Field(
|
|
34
|
+
...,
|
|
35
|
+
description="Configuration version",
|
|
36
|
+
)
|
|
37
|
+
capability_mappings: dict[str, ModelCapabilityWidgetMapping] = Field(
|
|
38
|
+
default_factory=dict,
|
|
39
|
+
description="Map of capability tags to widget configs",
|
|
40
|
+
)
|
|
41
|
+
semantic_mappings: dict[str, ModelCapabilityWidgetMapping] = Field(
|
|
42
|
+
default_factory=dict,
|
|
43
|
+
description="Map of semantic roles to widget configs",
|
|
44
|
+
)
|
|
45
|
+
fallback: ModelCapabilityWidgetMapping = Field(
|
|
46
|
+
...,
|
|
47
|
+
description="Default widget config when no mapping matches",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = ["ModelWidgetMapping"]
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Registry API Routes.
|
|
4
|
+
|
|
5
|
+
FastAPI route handlers for the Registry API. Routes are defined as an
|
|
6
|
+
APIRouter for easy mounting into the main FastAPI application.
|
|
7
|
+
|
|
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
|
|
15
|
+
|
|
16
|
+
Related Tickets:
|
|
17
|
+
- OMN-1278: Contract-Driven Dashboard - Registry Discovery
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import TYPE_CHECKING, Annotated
|
|
23
|
+
from uuid import UUID, uuid4
|
|
24
|
+
|
|
25
|
+
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status
|
|
26
|
+
|
|
27
|
+
from omnibase_infra.enums import EnumRegistrationState
|
|
28
|
+
from omnibase_infra.services.registry_api.models import (
|
|
29
|
+
ModelRegistryDiscoveryResponse,
|
|
30
|
+
ModelRegistryHealthResponse,
|
|
31
|
+
ModelRegistryNodeView,
|
|
32
|
+
ModelResponseListInstances,
|
|
33
|
+
ModelResponseListNodes,
|
|
34
|
+
ModelWidgetMapping,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from omnibase_infra.services.registry_api.service import ServiceRegistryDiscovery
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_correlation_id(
|
|
42
|
+
x_correlation_id: Annotated[
|
|
43
|
+
str | None,
|
|
44
|
+
Header(
|
|
45
|
+
alias="X-Correlation-ID",
|
|
46
|
+
description="Correlation ID for distributed tracing. Must be a valid UUID if provided.",
|
|
47
|
+
),
|
|
48
|
+
] = None,
|
|
49
|
+
) -> UUID:
|
|
50
|
+
"""FastAPI dependency to extract and validate correlation ID from HTTP header.
|
|
51
|
+
|
|
52
|
+
Extracts the X-Correlation-ID header value and validates it as a UUID.
|
|
53
|
+
If no header is provided, generates a new UUID for the request.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
x_correlation_id: Optional correlation ID string from X-Correlation-ID header.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Parsed UUID from header or newly generated UUID if not provided.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
HTTPException: 400 Bad Request if correlation ID is provided but not a valid UUID.
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
Valid header: X-Correlation-ID: 550e8400-e29b-41d4-a716-446655440000
|
|
66
|
+
Invalid header: X-Correlation-ID: not-a-uuid (returns 400)
|
|
67
|
+
Missing header: Generates new UUID automatically
|
|
68
|
+
"""
|
|
69
|
+
if x_correlation_id is None:
|
|
70
|
+
return uuid4()
|
|
71
|
+
try:
|
|
72
|
+
return UUID(x_correlation_id)
|
|
73
|
+
except ValueError:
|
|
74
|
+
raise HTTPException(
|
|
75
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
76
|
+
detail=f"Invalid X-Correlation-ID header format: '{x_correlation_id}'. Must be a valid UUID (e.g., '550e8400-e29b-41d4-a716-446655440000').",
|
|
77
|
+
) from None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Create router with prefix
|
|
81
|
+
router = APIRouter(
|
|
82
|
+
prefix="/registry",
|
|
83
|
+
tags=["registry"],
|
|
84
|
+
responses={
|
|
85
|
+
400: {"description": "Bad request (e.g., invalid correlation ID format)"},
|
|
86
|
+
500: {"description": "Internal server error"},
|
|
87
|
+
503: {"description": "Service unavailable"},
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_service(request: Request) -> ServiceRegistryDiscovery:
|
|
93
|
+
"""Dependency to get the registry discovery service from app state.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
request: FastAPI request object.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
ServiceRegistryDiscovery instance from app state.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
HTTPException: If service is not configured in app state.
|
|
103
|
+
"""
|
|
104
|
+
service: ServiceRegistryDiscovery | None = getattr(
|
|
105
|
+
request.app.state, "registry_service", None
|
|
106
|
+
)
|
|
107
|
+
if service is None:
|
|
108
|
+
raise HTTPException(
|
|
109
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
110
|
+
detail="Registry service not configured",
|
|
111
|
+
)
|
|
112
|
+
return service
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@router.get(
|
|
116
|
+
"/discovery",
|
|
117
|
+
response_model=ModelRegistryDiscoveryResponse,
|
|
118
|
+
summary="Full Dashboard Payload",
|
|
119
|
+
description=(
|
|
120
|
+
"Returns the complete dashboard payload including nodes, live instances, "
|
|
121
|
+
"and summary statistics. This is the primary endpoint for dashboard "
|
|
122
|
+
"consumption, providing all needed data in a single request."
|
|
123
|
+
),
|
|
124
|
+
responses={
|
|
125
|
+
400: {"description": "Bad request (e.g., invalid correlation ID format)"},
|
|
126
|
+
200: {
|
|
127
|
+
"description": "Successful response with full discovery data",
|
|
128
|
+
"content": {
|
|
129
|
+
"application/json": {
|
|
130
|
+
"example": {
|
|
131
|
+
"timestamp": "2025-01-21T10:00:00Z",
|
|
132
|
+
"warnings": [],
|
|
133
|
+
"summary": {
|
|
134
|
+
"total_nodes": 10,
|
|
135
|
+
"active_nodes": 8,
|
|
136
|
+
"healthy_instances": 5,
|
|
137
|
+
"unhealthy_instances": 2,
|
|
138
|
+
"by_node_type": {"EFFECT": 5, "COMPUTE": 3, "REDUCER": 2},
|
|
139
|
+
"by_state": {"active": 8, "pending_registration": 2},
|
|
140
|
+
},
|
|
141
|
+
"nodes": [],
|
|
142
|
+
"live_instances": [],
|
|
143
|
+
"pagination": {
|
|
144
|
+
"total": 10,
|
|
145
|
+
"limit": 100,
|
|
146
|
+
"offset": 0,
|
|
147
|
+
"has_more": False,
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
async def get_discovery(
|
|
156
|
+
service: Annotated[ServiceRegistryDiscovery, Depends(get_service)],
|
|
157
|
+
correlation_id: Annotated[UUID, Depends(get_correlation_id)],
|
|
158
|
+
limit: Annotated[
|
|
159
|
+
int,
|
|
160
|
+
Query(ge=1, le=1000, description="Maximum number of nodes to return"),
|
|
161
|
+
] = 100,
|
|
162
|
+
offset: Annotated[
|
|
163
|
+
int,
|
|
164
|
+
Query(ge=0, description="Number of nodes to skip for pagination"),
|
|
165
|
+
] = 0,
|
|
166
|
+
) -> ModelRegistryDiscoveryResponse:
|
|
167
|
+
"""Get full dashboard payload with nodes, instances, and summary."""
|
|
168
|
+
response = await service.get_discovery(
|
|
169
|
+
limit=limit,
|
|
170
|
+
offset=offset,
|
|
171
|
+
correlation_id=correlation_id,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return response
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@router.get(
|
|
178
|
+
"/nodes",
|
|
179
|
+
response_model=ModelResponseListNodes,
|
|
180
|
+
summary="List Registered Nodes",
|
|
181
|
+
description=(
|
|
182
|
+
"Returns a paginated list of registered nodes from the PostgreSQL "
|
|
183
|
+
"projection store. Supports filtering by state and node type."
|
|
184
|
+
),
|
|
185
|
+
responses={
|
|
186
|
+
400: {"description": "Bad request (e.g., invalid correlation ID format)"},
|
|
187
|
+
200: {
|
|
188
|
+
"description": "Successful response with node list",
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
)
|
|
192
|
+
async def list_nodes(
|
|
193
|
+
service: Annotated[ServiceRegistryDiscovery, Depends(get_service)],
|
|
194
|
+
correlation_id: Annotated[UUID, Depends(get_correlation_id)],
|
|
195
|
+
limit: Annotated[
|
|
196
|
+
int,
|
|
197
|
+
Query(ge=1, le=1000, description="Maximum number of nodes to return"),
|
|
198
|
+
] = 100,
|
|
199
|
+
offset: Annotated[
|
|
200
|
+
int,
|
|
201
|
+
Query(ge=0, description="Number of nodes to skip for pagination"),
|
|
202
|
+
] = 0,
|
|
203
|
+
state: Annotated[
|
|
204
|
+
str | None,
|
|
205
|
+
Query(
|
|
206
|
+
description="Filter by registration state (e.g., 'active', 'pending_registration')"
|
|
207
|
+
),
|
|
208
|
+
] = None,
|
|
209
|
+
node_type: Annotated[
|
|
210
|
+
str | None,
|
|
211
|
+
Query(
|
|
212
|
+
description="Filter by node type (effect, compute, reducer, orchestrator)"
|
|
213
|
+
),
|
|
214
|
+
] = None,
|
|
215
|
+
) -> ModelResponseListNodes:
|
|
216
|
+
"""List registered nodes with pagination and optional filtering."""
|
|
217
|
+
# Parse state filter
|
|
218
|
+
state_filter: EnumRegistrationState | None = None
|
|
219
|
+
if state is not None:
|
|
220
|
+
try:
|
|
221
|
+
state_filter = EnumRegistrationState(state)
|
|
222
|
+
except ValueError:
|
|
223
|
+
raise HTTPException(
|
|
224
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
225
|
+
detail=f"Invalid state value: {state}. Valid values: {[s.value for s in EnumRegistrationState]}",
|
|
226
|
+
) from None
|
|
227
|
+
|
|
228
|
+
nodes, pagination, warnings = await service.list_nodes(
|
|
229
|
+
limit=limit,
|
|
230
|
+
offset=offset,
|
|
231
|
+
state=state_filter,
|
|
232
|
+
node_type=node_type,
|
|
233
|
+
correlation_id=correlation_id,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return ModelResponseListNodes(
|
|
237
|
+
nodes=nodes,
|
|
238
|
+
pagination=pagination,
|
|
239
|
+
warnings=warnings,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@router.get(
|
|
244
|
+
"/nodes/{node_id}",
|
|
245
|
+
response_model=ModelRegistryNodeView,
|
|
246
|
+
summary="Get Node Details",
|
|
247
|
+
description="Returns detailed information for a single registered node by ID.",
|
|
248
|
+
responses={
|
|
249
|
+
400: {"description": "Bad request (e.g., invalid correlation ID format)"},
|
|
250
|
+
200: {"description": "Successful response with node details"},
|
|
251
|
+
404: {"description": "Node not found"},
|
|
252
|
+
},
|
|
253
|
+
)
|
|
254
|
+
async def get_node(
|
|
255
|
+
node_id: UUID,
|
|
256
|
+
service: Annotated[ServiceRegistryDiscovery, Depends(get_service)],
|
|
257
|
+
correlation_id: Annotated[UUID, Depends(get_correlation_id)],
|
|
258
|
+
) -> ModelRegistryNodeView:
|
|
259
|
+
"""Get a single node by ID."""
|
|
260
|
+
|
|
261
|
+
node, warnings = await service.get_node(
|
|
262
|
+
node_id=node_id,
|
|
263
|
+
correlation_id=correlation_id,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if node is None:
|
|
267
|
+
# Check if it was a service error or genuinely not found
|
|
268
|
+
if warnings:
|
|
269
|
+
raise HTTPException(
|
|
270
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
271
|
+
detail=f"Service error: {warnings[0].message}",
|
|
272
|
+
)
|
|
273
|
+
raise HTTPException(
|
|
274
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
275
|
+
detail=f"Node not found: {node_id}",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return node
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@router.get(
|
|
282
|
+
"/instances",
|
|
283
|
+
response_model=ModelResponseListInstances,
|
|
284
|
+
summary="List Live Consul Instances",
|
|
285
|
+
description=(
|
|
286
|
+
"Returns a list of live service instances from Consul. "
|
|
287
|
+
"Includes health status and metadata for each instance."
|
|
288
|
+
),
|
|
289
|
+
responses={
|
|
290
|
+
400: {"description": "Bad request (e.g., invalid correlation ID format)"},
|
|
291
|
+
200: {"description": "Successful response with instance list"},
|
|
292
|
+
},
|
|
293
|
+
)
|
|
294
|
+
async def list_instances(
|
|
295
|
+
service: Annotated[ServiceRegistryDiscovery, Depends(get_service)],
|
|
296
|
+
correlation_id: Annotated[UUID, Depends(get_correlation_id)],
|
|
297
|
+
service_name: Annotated[
|
|
298
|
+
str | None,
|
|
299
|
+
Query(description="Filter by service name"),
|
|
300
|
+
] = None,
|
|
301
|
+
include_unhealthy: Annotated[
|
|
302
|
+
bool,
|
|
303
|
+
Query(description="Include unhealthy instances in results"),
|
|
304
|
+
] = False,
|
|
305
|
+
) -> ModelResponseListInstances:
|
|
306
|
+
"""List live Consul service instances."""
|
|
307
|
+
instances, warnings = await service.list_instances(
|
|
308
|
+
service_name=service_name,
|
|
309
|
+
include_unhealthy=include_unhealthy,
|
|
310
|
+
correlation_id=correlation_id,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
return ModelResponseListInstances(
|
|
314
|
+
instances=instances,
|
|
315
|
+
warnings=warnings,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@router.get(
|
|
320
|
+
"/widgets/mapping",
|
|
321
|
+
response_model=ModelWidgetMapping,
|
|
322
|
+
summary="Widget Mapping Configuration",
|
|
323
|
+
description=(
|
|
324
|
+
"Returns the capability-to-widget mapping configuration. "
|
|
325
|
+
"Used by dashboards to determine which widget type to render "
|
|
326
|
+
"for each node capability."
|
|
327
|
+
),
|
|
328
|
+
responses={
|
|
329
|
+
200: {"description": "Successful response with widget mapping"},
|
|
330
|
+
503: {"description": "Configuration not available"},
|
|
331
|
+
},
|
|
332
|
+
)
|
|
333
|
+
async def get_widget_mapping(
|
|
334
|
+
service: Annotated[ServiceRegistryDiscovery, Depends(get_service)],
|
|
335
|
+
) -> ModelWidgetMapping:
|
|
336
|
+
"""Get widget mapping configuration."""
|
|
337
|
+
mapping, warnings = service.get_widget_mapping()
|
|
338
|
+
|
|
339
|
+
if mapping is None:
|
|
340
|
+
raise HTTPException(
|
|
341
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
342
|
+
detail=f"Widget mapping not available: {warnings[0].message if warnings else 'Unknown error'}",
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
return mapping
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@router.get(
|
|
349
|
+
"/health",
|
|
350
|
+
response_model=ModelRegistryHealthResponse,
|
|
351
|
+
summary="Service Health Check",
|
|
352
|
+
description=(
|
|
353
|
+
"Performs a health check on all backend components (PostgreSQL, Consul, config) "
|
|
354
|
+
"and returns the overall service health status."
|
|
355
|
+
),
|
|
356
|
+
responses={
|
|
357
|
+
400: {"description": "Bad request (e.g., invalid correlation ID format)"},
|
|
358
|
+
200: {"description": "Health check response (may indicate degraded/unhealthy)"},
|
|
359
|
+
},
|
|
360
|
+
)
|
|
361
|
+
async def health_check(
|
|
362
|
+
service: Annotated[ServiceRegistryDiscovery, Depends(get_service)],
|
|
363
|
+
correlation_id: Annotated[UUID, Depends(get_correlation_id)],
|
|
364
|
+
) -> ModelRegistryHealthResponse:
|
|
365
|
+
"""Perform health check on all backend components."""
|
|
366
|
+
response = await service.health_check(correlation_id=correlation_id)
|
|
367
|
+
|
|
368
|
+
return response
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
__all__ = ["router", "get_service", "get_correlation_id"]
|