Source code for kelvin.sdk.container
"""Dependency wiring (composition root).
This module is responsible for creating and wiring all services.
It's the single place where dependencies are assembled.
"""
from __future__ import annotations
from functools import cached_property
from typing import TYPE_CHECKING
from kelvin.sdk.output import Output
from kelvin.sdk.services import (
AuthService,
CredentialStore,
DockerService,
SchemaService,
SessionService,
TemplateService,
)
from kelvin.sdk.services.agent_sync import AgentService
from kelvin.sdk.services.config import ConfigService
if TYPE_CHECKING:
from kelvin.api.client import Client
from kelvin.sdk.commands.app import AppCommands
from kelvin.sdk.commands.app_images import AppImageCommands
from kelvin.sdk.commands.apps import AppsCommands
from kelvin.sdk.commands.auth import AuthCommands
from kelvin.sdk.commands.mlflow import MLflowCommands
from kelvin.sdk.commands.secret import SecretCommands
from kelvin.sdk.commands.workload import WorkloadCommands
from kelvin.sdk.services.mlflow import MLflowService
# ============ Services Container ============
[docs]
class Services:
"""Container for all services with lazy initialization.
Services are created on first access, not at startup. This means:
- Faster CLI startup
- Commands that don't need auth won't fail if no session exists
- Services that depend on other services are created in the right order
This is passed via Click context to all commands.
Add new services here as your CLI grows.
"""
def __init__(
self,
*,
json_mode: bool = False,
assume_yes: bool = False,
verbose: bool = False,
) -> None:
self._json_mode: bool = json_mode
self._assume_yes: bool = assume_yes
self._verbose: bool = verbose
@cached_property
def output(self) -> Output:
"""Output service - always safe to access."""
return Output(
json_mode=self._json_mode,
assume_yes=self._assume_yes,
verbose=self._verbose,
)
# ============ Flag Setters ============
[docs]
def set_verbose(self, value: bool) -> None:
"""Set verbose flag, propagating to output if already initialized."""
self._verbose = value
# cached_property stores its result in the instance __dict__ under the property name.
# If "output" is not there yet, the next access will read self._verbose naturally.
if "output" in self.__dict__:
self.output.verbose = value
[docs]
def set_json_mode(self, value: bool) -> None:
"""Set json_mode flag, propagating to output if already initialized."""
self._json_mode = value
if "output" in self.__dict__:
self.output.json_mode = value
[docs]
def set_assume_yes(self, value: bool) -> None:
"""Set assume_yes flag, propagating to output if already initialized."""
self._assume_yes = value
if "output" in self.__dict__:
self.output.assume_yes = value
# ============ Core Infrastructure Services (Leaf Services) ============
@cached_property
def credential_store(self) -> CredentialStore:
"""Credential store - manages secure token storage."""
return CredentialStore()
@cached_property
def auth(self) -> AuthService:
"""Auth service - handles OAuth/PKCE authentication flow."""
return AuthService()
@cached_property
def session(self) -> SessionService:
"""Session service - manages session state and platform metadata."""
return SessionService()
@cached_property
def docker(self) -> DockerService:
"""Docker service - manages Docker operations."""
return DockerService()
@cached_property
def schema(self) -> SchemaService:
"""Schema service - manages app configuration schemas."""
return SchemaService()
@cached_property
def template(self) -> TemplateService:
"""Template service - manages application templates."""
return TemplateService()
@cached_property
def agent(self) -> AgentService:
"""Agent service - syncs agent bundles into applications."""
return AgentService()
@cached_property
def config_service(self) -> ConfigService:
"""Config service - manages CLI configuration file."""
return ConfigService()
# ============ Business Logic Commands ============
@cached_property
def auth_commands(self) -> AuthCommands:
"""Auth commands - orchestrates auth workflow."""
from kelvin.sdk.commands.auth import AuthCommands
return AuthCommands(
auth=self.auth,
credentials=self.credential_store,
session=self.session,
docker=self.docker,
)
@cached_property
def workload_commands(self) -> WorkloadCommands:
"""Workload commands - manages workload operations."""
from kelvin.sdk.commands.workload import WorkloadCommands
return WorkloadCommands(client=self.api_client, schema_service=self.schema)
@cached_property
def app_commands(self) -> AppCommands:
"""App commands - manages app creation and operations.
Note: api_client and registry_url are NOT passed here to avoid
requiring authentication for local-only operations (create, build).
The upload command receives them directly from the CLI layer.
"""
from kelvin.sdk.commands.app import AppCommands
return AppCommands(
template_service=self.template,
docker_service=self.docker,
schema_service=self.schema,
)
@cached_property
def app_image_commands(self) -> AppImageCommands:
"""App image commands - manages local Docker image operations.
These are local-only operations that don't require authentication.
"""
from kelvin.sdk.commands.app_images import AppImageCommands
return AppImageCommands(docker_service=self.docker)
@cached_property
def apps_commands(self) -> AppsCommands:
"""Apps commands - manages platform app registry operations.
These are API-based operations that require authentication.
"""
from kelvin.sdk.commands.apps import AppsCommands
return AppsCommands(client=self.api_client)
@cached_property
def secret_commands(self) -> SecretCommands:
"""Secret commands - manages platform secret operations.
These are API-based operations that require authentication.
"""
from kelvin.sdk.commands.secret import SecretCommands
return SecretCommands(client=self.api_client)
@cached_property
def mlflow_service(self) -> MLflowService:
"""MLflow service - wraps MLflow registry operations.
This is an optional service that requires the mlflow package.
"""
from kelvin.sdk.services.mlflow import MLflowService
return MLflowService()
@cached_property
def mlflow_commands(self) -> MLflowCommands:
"""MLflow commands - manages MLflow app creation and model import.
These are local operations that don't require authentication,
but require the mlflow package to be installed.
"""
from kelvin.sdk.commands.mlflow import MLflowCommands
return MLflowCommands(
mlflow_service=self.mlflow_service,
template_service=self.template,
)
# ============ API Client ============
@cached_property
def api_client(self) -> Client:
"""API client - requires valid session and credentials.
Cached after first access. Token is validated and refreshed
if needed when the client is first created.
Raises
------
NotLoggedInError
If no active session exists.
CredentialsExpiredError
If credentials are expired and cannot be refreshed.
"""
from kelvin.sdk.services.api_client import APIClientFactory
return APIClientFactory(
session=self.session,
credentials=self.credential_store,
auth=self.auth,
).create(verbose=self._verbose)
# ============ Factory Function ============
[docs]
def create_services(
json_mode: bool = False,
assume_yes: bool = False,
verbose: bool = False,
) -> Services:
"""Create services container (lazy initialization).
Services are not created here - they're created on first access.
This makes startup fast and allows commands that don't need
certain services to run without them.
Args:
json_mode: Enable JSON output mode.
assume_yes: Skip confirmation prompts.
verbose: Enable verbose/debug output.
Returns:
Services container with lazy initialization.
"""
return Services(
json_mode=json_mode,
assume_yes=assume_yes,
verbose=verbose,
)