Repository navigation
feat(OMN-1752): Extract ContractPublisher service to omnibase_infra #223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
a992557
feat(OMN-1752): Extract ContractPublisher service to omnibase_infra
jonahgabriel e73f3c8
fix(OMN-1752): Fix ContractPublisher dedup tracking and add comprehen…
jonahgabriel e0e56e2
fix(OMN-1752): Improve ContractPublisher consistency and efficiency
jonahgabriel dfa74ce
fix(OMN-1752): Improve ContractPublisher error handling and documenta…
jonahgabriel 1111796
fix(OMN-1752): Throw errors for configuration issues instead of silen…
jonahgabriel 29e2de4
Merge origin/main into jonah/omn-1752-extract-contract-publisher
jonahgabriel 306fb75
fix(OMN-1752): Address code review issues in ContractPublisher
jonahgabriel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
95 changes: 95 additions & 0 deletions
95
src/omnibase_infra/services/contract_publisher/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| # SPDX-License-Identifier: MIT | ||
| # Copyright (c) 2025 OmniNode Team | ||
| """Contract Publisher Service Module. | ||
|
|
||
| Provides infrastructure for bulk contract discovery and publishing to Kafka. | ||
| This service discovers contracts from configured sources (filesystem, package) | ||
| and publishes them to the contract registration topic for dynamic discovery. | ||
|
|
||
| Moved from omniclaude as part of OMN-1752 (ARCH-002 compliance). | ||
|
|
||
| Design Principle: | ||
| Infra standardizes the publishing *engine*; apps provide *source configuration*. | ||
|
|
||
| Flow: | ||
| Source → Validate → Normalize → Publish → Report | ||
| - Sources provide origin (filesystem, package) | ||
| - Event bus provides distribution (broadcast plane) | ||
|
|
||
| Example: | ||
| >>> from omnibase_infra.services.contract_publisher import ( | ||
| ... ServiceContractPublisher, | ||
| ... ModelContractPublisherConfig, | ||
| ... ) | ||
| >>> config = ModelContractPublisherConfig( | ||
| ... mode="filesystem", | ||
| ... filesystem_root=Path("/app/contracts/handlers"), | ||
| ... ) | ||
| >>> publisher = await ServiceContractPublisher.from_container(container, config) | ||
| >>> result = await publisher.publish_all() | ||
| >>> if result: | ||
| ... print(f"Published {len(result.published)} contracts") | ||
| ... else: | ||
| ... print(f"No contracts published, {len(result.contract_errors)} errors") | ||
|
|
||
| .. versionadded:: 0.3.0 | ||
| Created as part of OMN-1752 (ContractPublisher extraction). | ||
| """ | ||
|
|
||
| # Config | ||
| from omnibase_infra.services.contract_publisher.config import ( | ||
| ModelContractPublisherConfig, | ||
| ) | ||
|
|
||
| # Errors | ||
| from omnibase_infra.services.contract_publisher.errors import ( | ||
| ContractPublisherError, | ||
| ContractPublishingInfraError, | ||
| ContractSourceNotConfiguredError, | ||
| NoContractsFoundError, | ||
| ) | ||
|
|
||
| # Result models | ||
| from omnibase_infra.services.contract_publisher.models import ( | ||
| ModelContractError, | ||
| ModelInfraError, | ||
| ModelPublishResult, | ||
| ModelPublishStats, | ||
| ) | ||
|
|
||
| # Service | ||
| from omnibase_infra.services.contract_publisher.service import ( | ||
| ServiceContractPublisher, | ||
| ) | ||
|
|
||
| # Sources | ||
| from omnibase_infra.services.contract_publisher.sources import ( | ||
| ModelDiscoveredContract, | ||
| ProtocolContractPublisherSource, | ||
| SourceContractComposite, | ||
| SourceContractFilesystem, | ||
| SourceContractPackage, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| # Service | ||
| "ServiceContractPublisher", | ||
| # Config | ||
| "ModelContractPublisherConfig", | ||
| # Result models | ||
| "ModelPublishResult", | ||
| "ModelPublishStats", | ||
| "ModelContractError", | ||
| "ModelInfraError", | ||
| # Errors | ||
| "ContractPublisherError", | ||
| "ContractSourceNotConfiguredError", | ||
| "ContractPublishingInfraError", | ||
| "NoContractsFoundError", | ||
| # Sources | ||
| "ProtocolContractPublisherSource", | ||
| "ModelDiscoveredContract", | ||
| "SourceContractFilesystem", | ||
| "SourceContractPackage", | ||
| "SourceContractComposite", | ||
| ] |
199 changes: 199 additions & 0 deletions
199
src/omnibase_infra/services/contract_publisher/config.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| # SPDX-License-Identifier: MIT | ||
| # Copyright (c) 2025 OmniNode Team | ||
| """Contract Publisher Configuration Model. | ||
|
|
||
| This module defines the configuration model for contract publishing operations. | ||
| Configuration is validated at construction time using Pydantic model validators. | ||
|
|
||
| Configuration Options: | ||
| mode: Publishing source mode (filesystem, package, composite) | ||
| filesystem_root: Root directory for filesystem mode | ||
| package_module: Module name for package mode | ||
| fail_fast: Whether to raise on infrastructure errors | ||
| allow_zero_contracts: Whether to allow empty publish results | ||
| environment: Environment prefix for Kafka topics | ||
|
|
||
| Environment Resolution: | ||
| The environment is resolved with precedence: | ||
| 1. config.environment (if provided) | ||
| 2. ONEX_ENV environment variable | ||
| 3. Default "dev" | ||
|
|
||
| Related: | ||
| - OMN-1752: Extract ContractPublisher to omnibase_infra | ||
| - ARCH-002: Runtime owns all Kafka plumbing | ||
|
|
||
| .. versionadded:: 0.3.0 | ||
| Created as part of OMN-1752 (ContractPublisher extraction). | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| from pathlib import Path | ||
| from typing import Literal, Self | ||
|
|
||
| from pydantic import BaseModel, ConfigDict, Field, model_validator | ||
|
|
||
|
|
||
| class ModelContractPublisherConfig(BaseModel): | ||
| """Configuration for contract publishing. | ||
|
|
||
| Defines the source mode and configuration for discovering and | ||
| publishing contracts to Kafka. | ||
|
|
||
| Source Modes: | ||
| filesystem: Discover contracts from a directory tree | ||
| package: Discover contracts from installed package resources | ||
| composite: Merge both sources with conflict detection | ||
|
|
||
| Configuration Rules (enforced by validator): | ||
| - filesystem mode requires filesystem_root | ||
| - package mode requires package_module | ||
| - composite mode requires at least one of filesystem_root or package_module | ||
|
|
||
| Attributes: | ||
| mode: Publishing source mode | ||
| filesystem_root: Root directory for filesystem discovery | ||
| package_module: Module name for package resource discovery | ||
| fail_fast: If True, raise immediately on infrastructure errors | ||
| allow_zero_contracts: If True, allow empty publish results | ||
| environment: Environment prefix for topics (defaults via resolve_environment) | ||
|
|
||
| Example: | ||
| >>> config = ModelContractPublisherConfig( | ||
| ... mode="filesystem", | ||
| ... filesystem_root=Path("/app/contracts/handlers"), | ||
| ... fail_fast=True, | ||
| ... allow_zero_contracts=False, | ||
| ... ) | ||
| >>> env = config.resolve_environment() | ||
| >>> print(f"Publishing to {env}.onex.evt.contract-registered.v1") | ||
|
|
||
| .. versionadded:: 0.3.0 | ||
| """ | ||
|
|
||
| model_config = ConfigDict(frozen=True, extra="forbid") | ||
|
|
||
| mode: Literal["filesystem", "package", "composite"] = Field( | ||
| description="Publishing source mode" | ||
| ) | ||
| filesystem_root: Path | None = Field( | ||
| default=None, | ||
| description="Root directory for filesystem discovery", | ||
| ) | ||
| package_module: str | None = Field( | ||
| default=None, | ||
| description="Module name for package resource discovery (e.g., 'myapp.contracts')", | ||
| ) | ||
| fail_fast: bool = Field( | ||
| default=True, | ||
| description="If True, raise immediately on infrastructure errors", | ||
| ) | ||
| allow_zero_contracts: bool = Field( | ||
| default=False, | ||
| description="If True, allow empty publish results without raising", | ||
| ) | ||
| environment: str | None = Field( | ||
| default=None, | ||
| description="Environment prefix for topics (resolved via resolve_environment)", | ||
| ) | ||
|
|
||
| @model_validator(mode="after") | ||
| def validate_source_configured(self) -> Self: | ||
| """Validate that required source fields are configured for the mode. | ||
|
|
||
| Rules: | ||
| - filesystem mode requires filesystem_root | ||
| - package mode requires package_module | ||
| - composite mode requires at least one source | ||
|
|
||
| Returns: | ||
| Self if validation passes | ||
|
|
||
| Raises: | ||
| ValueError: If required source configuration is missing | ||
| """ | ||
| match self.mode: | ||
| case "filesystem": | ||
| if not self.filesystem_root: | ||
| raise ValueError("filesystem mode requires filesystem_root") | ||
| case "package": | ||
| if not self.package_module: | ||
| raise ValueError("package mode requires package_module") | ||
| case "composite": | ||
| if not self.filesystem_root and not self.package_module: | ||
| raise ValueError( | ||
| "composite mode requires at least one source " | ||
| "(filesystem_root or package_module)" | ||
| ) | ||
| return self | ||
|
|
||
| def resolve_environment(self) -> str: | ||
| """Resolve environment with precedence: config > env var > 'dev'. | ||
|
|
||
| Resolution Order: | ||
| 1. self.environment (if provided and non-empty after normalization) | ||
| 2. ONEX_ENV environment variable (if set and non-empty after normalization) | ||
| 3. Default "dev" | ||
|
|
||
| Normalization: | ||
| - Whitespace is stripped | ||
| - Trailing dots are removed (to prevent "dev..topic" issues) | ||
|
|
||
| Note: | ||
| Whitespace-only strings (e.g., " ") are treated as empty and | ||
| fall through to the next priority level. | ||
|
|
||
| Returns: | ||
| Resolved environment string, normalized | ||
|
|
||
| Example: | ||
| >>> config = ModelContractPublisherConfig( | ||
| ... mode="filesystem", | ||
| ... filesystem_root=Path("/app"), | ||
| ... environment="prod", | ||
| ... ) | ||
| >>> config.resolve_environment() | ||
| 'prod' | ||
|
|
||
| >>> # With no config.environment and ONEX_ENV=staging | ||
| >>> config2 = ModelContractPublisherConfig( | ||
| ... mode="filesystem", | ||
| ... filesystem_root=Path("/app"), | ||
| ... ) | ||
| >>> # Returns "staging" if ONEX_ENV is set, else "dev" | ||
| """ | ||
| # Priority 1: Explicit config (if non-empty after normalization) | ||
| if self.environment: | ||
| normalized = self._normalize_environment(self.environment) | ||
| if normalized: | ||
| return normalized | ||
|
|
||
| # Priority 2: Environment variable (if non-empty after normalization) | ||
| env_var = os.getenv("ONEX_ENV", "") | ||
| if env_var: | ||
| normalized = self._normalize_environment(env_var) | ||
| if normalized: | ||
| return normalized | ||
|
|
||
| # Priority 3: Default | ||
| return "dev" | ||
|
|
||
| @staticmethod | ||
| def _normalize_environment(value: str) -> str: | ||
| """Normalize environment string. | ||
|
|
||
| Strips whitespace and removes trailing dots to prevent | ||
| topic formatting issues like "dev..topic". | ||
|
|
||
| Args: | ||
| value: Raw environment value | ||
|
|
||
| Returns: | ||
| Normalized environment string | ||
| """ | ||
| return value.strip().rstrip(".") | ||
|
|
||
|
|
||
| __all__ = ["ModelContractPublisherConfig"] | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 6489
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 8011
🏁 Script executed:
# Check imports in the config.py file head -50 src/omnibase_infra/services/contract_publisher/config.pyRepository: OmniNode-ai/omnibase_infra
Length of output: 1724
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 543
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 7127
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 2237
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 288
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 1975
🏁 Script executed:
# Check if there are any config-specific error classes in the errors module ls -la src/omnibase_infra/errors/Repository: OmniNode-ai/omnibase_infra
Length of output: 1079
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 561
🏁 Script executed:
# Also check the __init__.py in errors to see what's exported head -100 src/omnibase_infra/errors/__init__.pyRepository: OmniNode-ai/omnibase_infra
Length of output: 4413
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 7909
🏁 Script executed:
# Check how emit_daemon/config.py uses OnexError with validators or config head -100 src/omnibase_infra/runtime/emit_daemon/config.pyRepository: OmniNode-ai/omnibase_infra
Length of output: 3772
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 2482
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 1054
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 482
🏁 Script executed:
# Check what errors are imported in config_consumer.py head -40 src/omnibase_infra/services/session/config_consumer.pyRepository: OmniNode-ai/omnibase_infra
Length of output: 1291
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 648
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 3086
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 411
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 926
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 5073
🏁 Script executed:
Repository: OmniNode-ai/omnibase_infra
Length of output: 1491
Replace ValueError with ProtocolConfigurationError and include error chaining.
Current code violates the mandatory error policy by raising ValueError. Use
ProtocolConfigurationError(the config-specific subclass) with the required error chaining pattern.Corrected pattern
Per coding guidelines: Only raise
OnexErroror its subclasses with error chaining patternraise ... from e.🤖 Prompt for AI Agents