chapkit 0.4.5__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.
- chapkit/__init__.py +97 -0
- chapkit/alembic_helpers.py +138 -0
- chapkit/api/__init__.py +64 -0
- chapkit/api/dependencies.py +32 -0
- chapkit/api/service_builder.py +348 -0
- chapkit/artifact/__init__.py +18 -0
- chapkit/artifact/manager.py +132 -0
- chapkit/artifact/models.py +36 -0
- chapkit/artifact/repository.py +48 -0
- chapkit/artifact/router.py +126 -0
- chapkit/artifact/schemas.py +67 -0
- chapkit/cli/__init__.py +5 -0
- chapkit/cli/__main__.py +6 -0
- chapkit/cli/cli.py +31 -0
- chapkit/cli/init.py +151 -0
- chapkit/cli/templates/.gitignore +152 -0
- chapkit/cli/templates/Dockerfile.jinja2 +80 -0
- chapkit/cli/templates/README.md.jinja2 +132 -0
- chapkit/cli/templates/compose.monitoring.yml.jinja2 +88 -0
- chapkit/cli/templates/compose.yml.jinja2 +30 -0
- chapkit/cli/templates/main.py.jinja2 +135 -0
- chapkit/cli/templates/monitoring/grafana/dashboards/chapkit-service-metrics.json +1232 -0
- chapkit/cli/templates/monitoring/grafana/provisioning/dashboards/dashboard.yml +13 -0
- chapkit/cli/templates/monitoring/grafana/provisioning/datasources/prometheus.yml +25 -0
- chapkit/cli/templates/monitoring/prometheus/prometheus.yml.jinja2 +13 -0
- chapkit/cli/templates/pyproject.toml.jinja2 +17 -0
- chapkit/config/__init__.py +20 -0
- chapkit/config/manager.py +63 -0
- chapkit/config/models.py +60 -0
- chapkit/config/repository.py +76 -0
- chapkit/config/router.py +112 -0
- chapkit/config/schemas.py +63 -0
- chapkit/ml/__init__.py +29 -0
- chapkit/ml/manager.py +231 -0
- chapkit/ml/router.py +114 -0
- chapkit/ml/runner.py +260 -0
- chapkit/ml/schemas.py +98 -0
- chapkit/py.typed +0 -0
- chapkit/scheduler.py +154 -0
- chapkit/task/__init__.py +20 -0
- chapkit/task/manager.py +300 -0
- chapkit/task/models.py +20 -0
- chapkit/task/registry.py +46 -0
- chapkit/task/repository.py +31 -0
- chapkit/task/router.py +115 -0
- chapkit/task/schemas.py +28 -0
- chapkit/task/validation.py +76 -0
- chapkit-0.4.5.dist-info/METADATA +196 -0
- chapkit-0.4.5.dist-info/RECORD +51 -0
- chapkit-0.4.5.dist-info/WHEEL +4 -0
- chapkit-0.4.5.dist-info/entry_points.txt +3 -0
chapkit/__init__.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Chapkit - ML/data service modules built on servicekit."""
|
|
2
|
+
|
|
3
|
+
# CLI feature
|
|
4
|
+
# Scheduler feature
|
|
5
|
+
# Artifact feature
|
|
6
|
+
from .artifact import (
|
|
7
|
+
Artifact,
|
|
8
|
+
ArtifactHierarchy,
|
|
9
|
+
ArtifactIn,
|
|
10
|
+
ArtifactManager,
|
|
11
|
+
ArtifactOut,
|
|
12
|
+
ArtifactRepository,
|
|
13
|
+
ArtifactRouter,
|
|
14
|
+
ArtifactTreeNode,
|
|
15
|
+
)
|
|
16
|
+
from .cli import app as cli_app
|
|
17
|
+
|
|
18
|
+
# Config feature
|
|
19
|
+
from .config import (
|
|
20
|
+
BaseConfig,
|
|
21
|
+
Config,
|
|
22
|
+
ConfigIn,
|
|
23
|
+
ConfigManager,
|
|
24
|
+
ConfigOut,
|
|
25
|
+
ConfigRepository,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# ML feature
|
|
29
|
+
from .ml import (
|
|
30
|
+
FunctionalModelRunner,
|
|
31
|
+
MLManager,
|
|
32
|
+
MLRouter,
|
|
33
|
+
ModelRunnerProtocol,
|
|
34
|
+
PredictionArtifactData,
|
|
35
|
+
PredictRequest,
|
|
36
|
+
PredictResponse,
|
|
37
|
+
TrainedModelArtifactData,
|
|
38
|
+
TrainRequest,
|
|
39
|
+
TrainResponse,
|
|
40
|
+
)
|
|
41
|
+
from .scheduler import ChapkitJobRecord, ChapkitJobScheduler
|
|
42
|
+
|
|
43
|
+
# Task feature
|
|
44
|
+
from .task import (
|
|
45
|
+
Task,
|
|
46
|
+
TaskIn,
|
|
47
|
+
TaskManager,
|
|
48
|
+
TaskOut,
|
|
49
|
+
TaskRegistry,
|
|
50
|
+
TaskRepository,
|
|
51
|
+
TaskRouter,
|
|
52
|
+
validate_and_disable_orphaned_tasks,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
# CLI
|
|
57
|
+
"cli_app",
|
|
58
|
+
# Scheduler
|
|
59
|
+
"ChapkitJobRecord",
|
|
60
|
+
"ChapkitJobScheduler",
|
|
61
|
+
# Artifact
|
|
62
|
+
"Artifact",
|
|
63
|
+
"ArtifactHierarchy",
|
|
64
|
+
"ArtifactIn",
|
|
65
|
+
"ArtifactManager",
|
|
66
|
+
"ArtifactOut",
|
|
67
|
+
"ArtifactRepository",
|
|
68
|
+
"ArtifactRouter",
|
|
69
|
+
"ArtifactTreeNode",
|
|
70
|
+
# Config
|
|
71
|
+
"BaseConfig",
|
|
72
|
+
"Config",
|
|
73
|
+
"ConfigIn",
|
|
74
|
+
"ConfigManager",
|
|
75
|
+
"ConfigOut",
|
|
76
|
+
"ConfigRepository",
|
|
77
|
+
# ML
|
|
78
|
+
"FunctionalModelRunner",
|
|
79
|
+
"MLManager",
|
|
80
|
+
"MLRouter",
|
|
81
|
+
"ModelRunnerProtocol",
|
|
82
|
+
"PredictionArtifactData",
|
|
83
|
+
"PredictRequest",
|
|
84
|
+
"PredictResponse",
|
|
85
|
+
"TrainedModelArtifactData",
|
|
86
|
+
"TrainRequest",
|
|
87
|
+
"TrainResponse",
|
|
88
|
+
# Task
|
|
89
|
+
"Task",
|
|
90
|
+
"TaskIn",
|
|
91
|
+
"TaskManager",
|
|
92
|
+
"TaskOut",
|
|
93
|
+
"TaskRegistry",
|
|
94
|
+
"TaskRepository",
|
|
95
|
+
"TaskRouter",
|
|
96
|
+
"validate_and_disable_orphaned_tasks",
|
|
97
|
+
]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Reusable Alembic migration helpers for chapkit tables.
|
|
2
|
+
|
|
3
|
+
This module provides helper functions for creating and dropping chapkit's database tables
|
|
4
|
+
in Alembic migrations. Using helpers instead of raw Alembic operations provides:
|
|
5
|
+
|
|
6
|
+
- Reusability across migrations
|
|
7
|
+
- Consistent table definitions
|
|
8
|
+
- Clear documentation
|
|
9
|
+
- Easier maintenance
|
|
10
|
+
|
|
11
|
+
Users can create their own helper modules following this pattern for custom tables.
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
# In your migration file
|
|
15
|
+
from chapkit.alembic_helpers import create_configs_table, drop_configs_table
|
|
16
|
+
|
|
17
|
+
def upgrade() -> None:
|
|
18
|
+
create_configs_table(op)
|
|
19
|
+
|
|
20
|
+
def downgrade() -> None:
|
|
21
|
+
drop_configs_table(op)
|
|
22
|
+
|
|
23
|
+
Creating Your Own Helpers:
|
|
24
|
+
Follow the same pattern for your custom tables:
|
|
25
|
+
|
|
26
|
+
# myapp/alembic_helpers.py
|
|
27
|
+
def create_users_table(op: Any) -> None:
|
|
28
|
+
'''Create users table.'''
|
|
29
|
+
op.create_table(
|
|
30
|
+
'users',
|
|
31
|
+
sa.Column('email', sa.String(), nullable=False),
|
|
32
|
+
sa.Column('name', sa.String(), nullable=False),
|
|
33
|
+
sa.Column('id', servicekit.types.ULIDType(length=26), nullable=False),
|
|
34
|
+
sa.Column('created_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
|
35
|
+
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
|
36
|
+
sa.Column('tags', sa.JSON(), nullable=False, server_default='[]'),
|
|
37
|
+
sa.PrimaryKeyConstraint('id'),
|
|
38
|
+
)
|
|
39
|
+
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=False)
|
|
40
|
+
|
|
41
|
+
def drop_users_table(op: Any) -> None:
|
|
42
|
+
'''Drop users table.'''
|
|
43
|
+
op.drop_index(op.f('ix_users_email'), table_name='users')
|
|
44
|
+
op.drop_table('users')
|
|
45
|
+
|
|
46
|
+
See examples/custom_migrations/ for a complete working example.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from typing import Any
|
|
50
|
+
|
|
51
|
+
import servicekit.types
|
|
52
|
+
import sqlalchemy as sa
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_artifacts_table(op: Any) -> None:
|
|
56
|
+
"""Create artifacts table for hierarchical artifact storage."""
|
|
57
|
+
op.create_table(
|
|
58
|
+
"artifacts",
|
|
59
|
+
sa.Column("parent_id", servicekit.types.ULIDType(length=26), nullable=True),
|
|
60
|
+
sa.Column("data", sa.PickleType(), nullable=False),
|
|
61
|
+
sa.Column("level", sa.Integer(), nullable=False),
|
|
62
|
+
sa.Column("id", servicekit.types.ULIDType(length=26), nullable=False),
|
|
63
|
+
sa.Column("created_at", sa.DateTime(), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
|
64
|
+
sa.Column("updated_at", sa.DateTime(), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
|
65
|
+
sa.Column("tags", sa.JSON(), nullable=False, server_default="[]"),
|
|
66
|
+
sa.ForeignKeyConstraint(["parent_id"], ["artifacts.id"], ondelete="SET NULL"),
|
|
67
|
+
sa.PrimaryKeyConstraint("id"),
|
|
68
|
+
)
|
|
69
|
+
op.create_index(op.f("ix_artifacts_level"), "artifacts", ["level"], unique=False)
|
|
70
|
+
op.create_index(op.f("ix_artifacts_parent_id"), "artifacts", ["parent_id"], unique=False)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def drop_artifacts_table(op: Any) -> None:
|
|
74
|
+
"""Drop artifacts table."""
|
|
75
|
+
op.drop_index(op.f("ix_artifacts_parent_id"), table_name="artifacts")
|
|
76
|
+
op.drop_index(op.f("ix_artifacts_level"), table_name="artifacts")
|
|
77
|
+
op.drop_table("artifacts")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def create_configs_table(op: Any) -> None:
|
|
81
|
+
"""Create configs table for configuration storage."""
|
|
82
|
+
op.create_table(
|
|
83
|
+
"configs",
|
|
84
|
+
sa.Column("name", sa.String(), nullable=False),
|
|
85
|
+
sa.Column("data", sa.JSON(), nullable=False),
|
|
86
|
+
sa.Column("id", servicekit.types.ULIDType(length=26), nullable=False),
|
|
87
|
+
sa.Column("created_at", sa.DateTime(), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
|
88
|
+
sa.Column("updated_at", sa.DateTime(), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
|
89
|
+
sa.Column("tags", sa.JSON(), nullable=False, server_default="[]"),
|
|
90
|
+
sa.PrimaryKeyConstraint("id"),
|
|
91
|
+
)
|
|
92
|
+
op.create_index(op.f("ix_configs_name"), "configs", ["name"], unique=False)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def drop_configs_table(op: Any) -> None:
|
|
96
|
+
"""Drop configs table."""
|
|
97
|
+
op.drop_index(op.f("ix_configs_name"), table_name="configs")
|
|
98
|
+
op.drop_table("configs")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def create_config_artifacts_table(op: Any) -> None:
|
|
102
|
+
"""Create config_artifacts junction table linking configs to artifacts."""
|
|
103
|
+
op.create_table(
|
|
104
|
+
"config_artifacts",
|
|
105
|
+
sa.Column("config_id", servicekit.types.ULIDType(length=26), nullable=False),
|
|
106
|
+
sa.Column("artifact_id", servicekit.types.ULIDType(length=26), nullable=False),
|
|
107
|
+
sa.ForeignKeyConstraint(["artifact_id"], ["artifacts.id"], ondelete="CASCADE"),
|
|
108
|
+
sa.ForeignKeyConstraint(["config_id"], ["configs.id"], ondelete="CASCADE"),
|
|
109
|
+
sa.PrimaryKeyConstraint("config_id", "artifact_id"),
|
|
110
|
+
sa.UniqueConstraint("artifact_id"),
|
|
111
|
+
sa.UniqueConstraint("artifact_id", name="uq_artifact_id"),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def drop_config_artifacts_table(op: Any) -> None:
|
|
116
|
+
"""Drop config_artifacts junction table."""
|
|
117
|
+
op.drop_table("config_artifacts")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def create_tasks_table(op: Any) -> None:
|
|
121
|
+
"""Create tasks table for task execution infrastructure."""
|
|
122
|
+
op.create_table(
|
|
123
|
+
"tasks",
|
|
124
|
+
sa.Column("command", sa.Text(), nullable=False),
|
|
125
|
+
sa.Column("task_type", sa.Text(), nullable=False, server_default="shell"),
|
|
126
|
+
sa.Column("parameters", sa.JSON(), nullable=True),
|
|
127
|
+
sa.Column("enabled", sa.Boolean(), nullable=False, server_default="1"),
|
|
128
|
+
sa.Column("id", servicekit.types.ULIDType(length=26), nullable=False),
|
|
129
|
+
sa.Column("created_at", sa.DateTime(), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
|
130
|
+
sa.Column("updated_at", sa.DateTime(), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=False),
|
|
131
|
+
sa.Column("tags", sa.JSON(), nullable=False, server_default="[]"),
|
|
132
|
+
sa.PrimaryKeyConstraint("id"),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def drop_tasks_table(op: Any) -> None:
|
|
137
|
+
"""Drop tasks table."""
|
|
138
|
+
op.drop_table("tasks")
|
chapkit/api/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""FastAPI routers and related presentation logic."""
|
|
2
|
+
|
|
3
|
+
from servicekit.api import CrudPermissions, CrudRouter, Router
|
|
4
|
+
from servicekit.api.middleware import (
|
|
5
|
+
add_error_handlers,
|
|
6
|
+
add_logging_middleware,
|
|
7
|
+
database_error_handler,
|
|
8
|
+
validation_error_handler,
|
|
9
|
+
)
|
|
10
|
+
from servicekit.api.routers import HealthRouter, HealthState, HealthStatus, JobRouter, SystemInfo, SystemRouter
|
|
11
|
+
from servicekit.api.service_builder import ServiceInfo
|
|
12
|
+
from servicekit.api.utilities import build_location_url, run_app
|
|
13
|
+
from servicekit.logging import (
|
|
14
|
+
add_request_context,
|
|
15
|
+
clear_request_context,
|
|
16
|
+
configure_logging,
|
|
17
|
+
get_logger,
|
|
18
|
+
reset_request_context,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from chapkit.artifact import ArtifactRouter
|
|
22
|
+
from chapkit.config import ConfigRouter
|
|
23
|
+
|
|
24
|
+
from .dependencies import get_artifact_manager, get_config_manager
|
|
25
|
+
from .service_builder import AssessedStatus, MLServiceBuilder, MLServiceInfo, ServiceBuilder
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
# Base classes
|
|
29
|
+
"Router",
|
|
30
|
+
"CrudRouter",
|
|
31
|
+
"CrudPermissions",
|
|
32
|
+
# Routers
|
|
33
|
+
"HealthRouter",
|
|
34
|
+
"HealthStatus",
|
|
35
|
+
"HealthState",
|
|
36
|
+
"JobRouter",
|
|
37
|
+
"SystemRouter",
|
|
38
|
+
"SystemInfo",
|
|
39
|
+
"ConfigRouter",
|
|
40
|
+
"ArtifactRouter",
|
|
41
|
+
# Dependencies
|
|
42
|
+
"get_config_manager",
|
|
43
|
+
"get_artifact_manager",
|
|
44
|
+
# Middleware
|
|
45
|
+
"add_error_handlers",
|
|
46
|
+
"add_logging_middleware",
|
|
47
|
+
"database_error_handler",
|
|
48
|
+
"validation_error_handler",
|
|
49
|
+
# Logging
|
|
50
|
+
"configure_logging",
|
|
51
|
+
"get_logger",
|
|
52
|
+
"add_request_context",
|
|
53
|
+
"clear_request_context",
|
|
54
|
+
"reset_request_context",
|
|
55
|
+
# Builders
|
|
56
|
+
"ServiceBuilder",
|
|
57
|
+
"MLServiceBuilder",
|
|
58
|
+
"ServiceInfo",
|
|
59
|
+
"MLServiceInfo",
|
|
60
|
+
"AssessedStatus",
|
|
61
|
+
# Utilities
|
|
62
|
+
"build_location_url",
|
|
63
|
+
"run_app",
|
|
64
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Feature-specific FastAPI dependency injection for managers."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from fastapi import Depends
|
|
6
|
+
from servicekit.api.dependencies import get_session
|
|
7
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
|
+
|
|
9
|
+
from chapkit.artifact import ArtifactManager, ArtifactRepository
|
|
10
|
+
from chapkit.config import BaseConfig, ConfigManager, ConfigRepository
|
|
11
|
+
from chapkit.ml import MLManager
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def get_config_manager(session: Annotated[AsyncSession, Depends(get_session)]) -> ConfigManager[BaseConfig]:
|
|
15
|
+
"""Get a config manager instance for dependency injection."""
|
|
16
|
+
repo = ConfigRepository(session)
|
|
17
|
+
return ConfigManager[BaseConfig](repo, BaseConfig)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def get_artifact_manager(session: Annotated[AsyncSession, Depends(get_session)]) -> ArtifactManager:
|
|
21
|
+
"""Get an artifact manager instance for dependency injection."""
|
|
22
|
+
artifact_repo = ArtifactRepository(session)
|
|
23
|
+
return ArtifactManager(artifact_repo)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def get_ml_manager() -> MLManager:
|
|
27
|
+
"""Get an ML manager instance for dependency injection.
|
|
28
|
+
|
|
29
|
+
Note: This is a placeholder. The actual dependency is built by ServiceBuilder
|
|
30
|
+
with the runner in closure, then overridden via app.dependency_overrides.
|
|
31
|
+
"""
|
|
32
|
+
raise RuntimeError("ML manager dependency not configured. Use ServiceBuilder.with_ml() to enable ML operations.")
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Service builder with module integration (config, artifact, ml)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from typing import Any, Callable, Coroutine, List, Self
|
|
10
|
+
|
|
11
|
+
from fastapi import Depends, FastAPI
|
|
12
|
+
from pydantic import EmailStr, HttpUrl
|
|
13
|
+
from servicekit.api.crud import CrudPermissions
|
|
14
|
+
from servicekit.api.dependencies import get_database, get_scheduler, get_session, set_scheduler
|
|
15
|
+
from servicekit.api.service_builder import BaseServiceBuilder, LifespanFactory, ServiceInfo
|
|
16
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
17
|
+
|
|
18
|
+
from chapkit.artifact import (
|
|
19
|
+
ArtifactHierarchy,
|
|
20
|
+
ArtifactIn,
|
|
21
|
+
ArtifactManager,
|
|
22
|
+
ArtifactOut,
|
|
23
|
+
ArtifactRepository,
|
|
24
|
+
ArtifactRouter,
|
|
25
|
+
)
|
|
26
|
+
from chapkit.config import BaseConfig, ConfigIn, ConfigManager, ConfigOut, ConfigRepository, ConfigRouter
|
|
27
|
+
from chapkit.ml import MLManager, MLRouter, ModelRunnerProtocol
|
|
28
|
+
from chapkit.scheduler import ChapkitJobScheduler
|
|
29
|
+
|
|
30
|
+
from .dependencies import get_artifact_manager as default_get_artifact_manager
|
|
31
|
+
from .dependencies import get_config_manager as default_get_config_manager
|
|
32
|
+
from .dependencies import get_ml_manager as default_get_ml_manager
|
|
33
|
+
|
|
34
|
+
# Type alias for dependency factory functions
|
|
35
|
+
type DependencyFactory = Callable[..., Coroutine[Any, Any, Any]]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AssessedStatus(StrEnum):
|
|
39
|
+
"""Status indicating the maturity and validation level of an ML service."""
|
|
40
|
+
|
|
41
|
+
gray = "gray" # Not intended for use, deprecated, or meant for legacy use only
|
|
42
|
+
red = "red" # Highly experimental prototype - not validated, only for early experimentation
|
|
43
|
+
orange = "orange" # Shows promise on limited data, needs manual configuration and careful evaluation
|
|
44
|
+
yellow = "yellow" # Ready for more rigorous testing
|
|
45
|
+
green = "green" # Validated and ready for production use
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class MLServiceInfo(ServiceInfo):
|
|
49
|
+
"""Extended service metadata for ML services with author, organization, and assessment info."""
|
|
50
|
+
|
|
51
|
+
author: str | None = None
|
|
52
|
+
author_note: str | None = None
|
|
53
|
+
author_assessed_status: AssessedStatus | None = None
|
|
54
|
+
contact_email: EmailStr | None = None
|
|
55
|
+
organization: str | None = None
|
|
56
|
+
organization_logo_url: str | HttpUrl | None = None
|
|
57
|
+
citation_info: str | None = None
|
|
58
|
+
allow_free_additional_continuous_covariates: bool = False
|
|
59
|
+
required_covariates: List[str] = field(default_factory=list)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(slots=True)
|
|
63
|
+
class _ConfigOptions:
|
|
64
|
+
"""Internal config options for ServiceBuilder."""
|
|
65
|
+
|
|
66
|
+
schema: type[BaseConfig]
|
|
67
|
+
prefix: str = "/api/v1/configs"
|
|
68
|
+
tags: List[str] = field(default_factory=lambda: ["Config"])
|
|
69
|
+
permissions: CrudPermissions = field(default_factory=CrudPermissions)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(slots=True)
|
|
73
|
+
class _ArtifactOptions:
|
|
74
|
+
"""Internal artifact options for ServiceBuilder."""
|
|
75
|
+
|
|
76
|
+
hierarchy: ArtifactHierarchy
|
|
77
|
+
prefix: str = "/api/v1/artifacts"
|
|
78
|
+
tags: List[str] = field(default_factory=lambda: ["Artifacts"])
|
|
79
|
+
enable_config_linking: bool = False
|
|
80
|
+
permissions: CrudPermissions = field(default_factory=CrudPermissions)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(slots=True)
|
|
84
|
+
class _MLOptions:
|
|
85
|
+
"""Internal ML options for ServiceBuilder."""
|
|
86
|
+
|
|
87
|
+
runner: ModelRunnerProtocol
|
|
88
|
+
prefix: str = "/api/v1/ml"
|
|
89
|
+
tags: List[str] = field(default_factory=lambda: ["ML"])
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ServiceBuilder(BaseServiceBuilder):
|
|
93
|
+
"""Service builder with integrated module support (config, artifact, ml)."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
96
|
+
"""Initialize service builder with module-specific state."""
|
|
97
|
+
super().__init__(**kwargs)
|
|
98
|
+
self._config_options: _ConfigOptions | None = None
|
|
99
|
+
self._artifact_options: _ArtifactOptions | None = None
|
|
100
|
+
self._ml_options: _MLOptions | None = None
|
|
101
|
+
|
|
102
|
+
# --------------------------------------------------------------------- Module-specific fluent methods
|
|
103
|
+
|
|
104
|
+
def with_config(
|
|
105
|
+
self,
|
|
106
|
+
schema: type[BaseConfig],
|
|
107
|
+
*,
|
|
108
|
+
prefix: str = "/api/v1/configs",
|
|
109
|
+
tags: List[str] | None = None,
|
|
110
|
+
permissions: CrudPermissions | None = None,
|
|
111
|
+
allow_create: bool | None = None,
|
|
112
|
+
allow_read: bool | None = None,
|
|
113
|
+
allow_update: bool | None = None,
|
|
114
|
+
allow_delete: bool | None = None,
|
|
115
|
+
) -> Self:
|
|
116
|
+
base = permissions or CrudPermissions()
|
|
117
|
+
perms = CrudPermissions(
|
|
118
|
+
create=allow_create if allow_create is not None else base.create,
|
|
119
|
+
read=allow_read if allow_read is not None else base.read,
|
|
120
|
+
update=allow_update if allow_update is not None else base.update,
|
|
121
|
+
delete=allow_delete if allow_delete is not None else base.delete,
|
|
122
|
+
)
|
|
123
|
+
self._config_options = _ConfigOptions(
|
|
124
|
+
schema=schema,
|
|
125
|
+
prefix=prefix,
|
|
126
|
+
tags=list(tags) if tags else ["Config"],
|
|
127
|
+
permissions=perms,
|
|
128
|
+
)
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
def with_artifacts(
|
|
132
|
+
self,
|
|
133
|
+
*,
|
|
134
|
+
hierarchy: ArtifactHierarchy,
|
|
135
|
+
prefix: str = "/api/v1/artifacts",
|
|
136
|
+
tags: List[str] | None = None,
|
|
137
|
+
enable_config_linking: bool = False,
|
|
138
|
+
permissions: CrudPermissions | None = None,
|
|
139
|
+
allow_create: bool | None = None,
|
|
140
|
+
allow_read: bool | None = None,
|
|
141
|
+
allow_update: bool | None = None,
|
|
142
|
+
allow_delete: bool | None = None,
|
|
143
|
+
) -> Self:
|
|
144
|
+
base = permissions or CrudPermissions()
|
|
145
|
+
perms = CrudPermissions(
|
|
146
|
+
create=allow_create if allow_create is not None else base.create,
|
|
147
|
+
read=allow_read if allow_read is not None else base.read,
|
|
148
|
+
update=allow_update if allow_update is not None else base.update,
|
|
149
|
+
delete=allow_delete if allow_delete is not None else base.delete,
|
|
150
|
+
)
|
|
151
|
+
self._artifact_options = _ArtifactOptions(
|
|
152
|
+
hierarchy=hierarchy,
|
|
153
|
+
prefix=prefix,
|
|
154
|
+
tags=list(tags) if tags else ["Artifacts"],
|
|
155
|
+
enable_config_linking=enable_config_linking,
|
|
156
|
+
permissions=perms,
|
|
157
|
+
)
|
|
158
|
+
return self
|
|
159
|
+
|
|
160
|
+
def with_ml(
|
|
161
|
+
self,
|
|
162
|
+
runner: ModelRunnerProtocol,
|
|
163
|
+
*,
|
|
164
|
+
prefix: str = "/api/v1/ml",
|
|
165
|
+
tags: List[str] | None = None,
|
|
166
|
+
) -> Self:
|
|
167
|
+
"""Enable ML train/predict endpoints with model runner."""
|
|
168
|
+
self._ml_options = _MLOptions(
|
|
169
|
+
runner=runner,
|
|
170
|
+
prefix=prefix,
|
|
171
|
+
tags=list(tags) if tags else ["ML"],
|
|
172
|
+
)
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
# --------------------------------------------------------------------- Extension point implementations
|
|
176
|
+
|
|
177
|
+
def _validate_module_configuration(self) -> None:
|
|
178
|
+
"""Validate module-specific configuration."""
|
|
179
|
+
if self._artifact_options and self._artifact_options.enable_config_linking and not self._config_options:
|
|
180
|
+
raise ValueError(
|
|
181
|
+
"Artifact config-linking requires a config schema. "
|
|
182
|
+
"Call `with_config(...)` before enabling config linking in artifacts."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if self._ml_options:
|
|
186
|
+
if not self._config_options:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
"ML operations require config for model configuration. "
|
|
189
|
+
"Call `with_config(...)` before `with_ml(...)`."
|
|
190
|
+
)
|
|
191
|
+
if not self._artifact_options:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
"ML operations require artifacts for model storage. "
|
|
194
|
+
"Call `with_artifacts(...)` before `with_ml(...)`."
|
|
195
|
+
)
|
|
196
|
+
if not self._job_options:
|
|
197
|
+
raise ValueError(
|
|
198
|
+
"ML operations require job scheduler for async execution. "
|
|
199
|
+
"Call `with_jobs(...)` before `with_ml(...)`."
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def _register_module_routers(self, app: FastAPI) -> None:
|
|
203
|
+
"""Register module-specific routers (config, artifact, task)."""
|
|
204
|
+
if self._config_options:
|
|
205
|
+
config_options = self._config_options
|
|
206
|
+
config_schema = config_options.schema
|
|
207
|
+
config_dep = self._build_config_dependency(config_schema)
|
|
208
|
+
entity_in_type: type[ConfigIn[BaseConfig]] = ConfigIn[config_schema] # type: ignore[valid-type]
|
|
209
|
+
entity_out_type: type[ConfigOut[BaseConfig]] = ConfigOut[config_schema] # type: ignore[valid-type]
|
|
210
|
+
config_router = ConfigRouter.create(
|
|
211
|
+
prefix=config_options.prefix,
|
|
212
|
+
tags=config_options.tags,
|
|
213
|
+
manager_factory=config_dep,
|
|
214
|
+
entity_in_type=entity_in_type,
|
|
215
|
+
entity_out_type=entity_out_type,
|
|
216
|
+
permissions=config_options.permissions,
|
|
217
|
+
enable_artifact_operations=(
|
|
218
|
+
self._artifact_options is not None and self._artifact_options.enable_config_linking
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
app.include_router(config_router)
|
|
222
|
+
app.dependency_overrides[default_get_config_manager] = config_dep
|
|
223
|
+
|
|
224
|
+
if self._artifact_options:
|
|
225
|
+
artifact_options = self._artifact_options
|
|
226
|
+
artifact_dep = self._build_artifact_dependency(
|
|
227
|
+
hierarchy=artifact_options.hierarchy,
|
|
228
|
+
include_config=artifact_options.enable_config_linking,
|
|
229
|
+
)
|
|
230
|
+
artifact_router = ArtifactRouter.create(
|
|
231
|
+
prefix=artifact_options.prefix,
|
|
232
|
+
tags=artifact_options.tags,
|
|
233
|
+
manager_factory=artifact_dep,
|
|
234
|
+
entity_in_type=ArtifactIn,
|
|
235
|
+
entity_out_type=ArtifactOut,
|
|
236
|
+
permissions=artifact_options.permissions,
|
|
237
|
+
enable_config_access=self._config_options is not None and artifact_options.enable_config_linking,
|
|
238
|
+
)
|
|
239
|
+
app.include_router(artifact_router)
|
|
240
|
+
app.dependency_overrides[default_get_artifact_manager] = artifact_dep
|
|
241
|
+
|
|
242
|
+
if self._ml_options:
|
|
243
|
+
ml_options = self._ml_options
|
|
244
|
+
ml_dep = self._build_ml_dependency()
|
|
245
|
+
ml_router = MLRouter.create(
|
|
246
|
+
prefix=ml_options.prefix,
|
|
247
|
+
tags=ml_options.tags,
|
|
248
|
+
manager_factory=ml_dep,
|
|
249
|
+
)
|
|
250
|
+
app.include_router(ml_router)
|
|
251
|
+
app.dependency_overrides[default_get_ml_manager] = ml_dep
|
|
252
|
+
|
|
253
|
+
# --------------------------------------------------------------------- Module dependency builders
|
|
254
|
+
|
|
255
|
+
@staticmethod
|
|
256
|
+
def _build_config_dependency(
|
|
257
|
+
schema: type[BaseConfig],
|
|
258
|
+
) -> DependencyFactory:
|
|
259
|
+
async def _dependency(session: AsyncSession = Depends(get_session)) -> ConfigManager[BaseConfig]:
|
|
260
|
+
repo = ConfigRepository(session)
|
|
261
|
+
return ConfigManager[BaseConfig](repo, schema)
|
|
262
|
+
|
|
263
|
+
return _dependency
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
def _build_artifact_dependency(
|
|
267
|
+
*,
|
|
268
|
+
hierarchy: ArtifactHierarchy,
|
|
269
|
+
include_config: bool,
|
|
270
|
+
) -> DependencyFactory:
|
|
271
|
+
async def _dependency(session: AsyncSession = Depends(get_session)) -> ArtifactManager:
|
|
272
|
+
artifact_repo = ArtifactRepository(session)
|
|
273
|
+
return ArtifactManager(artifact_repo, hierarchy=hierarchy)
|
|
274
|
+
|
|
275
|
+
return _dependency
|
|
276
|
+
|
|
277
|
+
def _build_ml_dependency(self) -> DependencyFactory:
|
|
278
|
+
ml_runner = self._ml_options.runner if self._ml_options else None
|
|
279
|
+
config_schema = self._config_options.schema if self._config_options else None
|
|
280
|
+
|
|
281
|
+
async def _dependency() -> MLManager:
|
|
282
|
+
if ml_runner is None:
|
|
283
|
+
raise RuntimeError("ML runner not configured")
|
|
284
|
+
if config_schema is None:
|
|
285
|
+
raise RuntimeError("Config schema not configured")
|
|
286
|
+
|
|
287
|
+
runner: ModelRunnerProtocol = ml_runner
|
|
288
|
+
scheduler_base = get_scheduler()
|
|
289
|
+
# ChapkitJobScheduler extends AIOJobScheduler which extends JobScheduler
|
|
290
|
+
if not isinstance(scheduler_base, ChapkitJobScheduler):
|
|
291
|
+
raise RuntimeError("Scheduler must be ChapkitJobScheduler for ML operations")
|
|
292
|
+
scheduler: ChapkitJobScheduler = scheduler_base
|
|
293
|
+
database = get_database()
|
|
294
|
+
return MLManager(runner, scheduler, database, config_schema)
|
|
295
|
+
|
|
296
|
+
return _dependency
|
|
297
|
+
|
|
298
|
+
def _build_lifespan(self) -> LifespanFactory:
|
|
299
|
+
"""Build lifespan context manager with ChapkitJobScheduler instead of AIOJobScheduler."""
|
|
300
|
+
# Get parent lifespan factory
|
|
301
|
+
parent_lifespan = super()._build_lifespan()
|
|
302
|
+
|
|
303
|
+
@asynccontextmanager
|
|
304
|
+
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
305
|
+
"""Override scheduler creation to use ChapkitJobScheduler."""
|
|
306
|
+
# Call parent lifespan which handles database and most setup
|
|
307
|
+
async with parent_lifespan(app):
|
|
308
|
+
# Replace AIOJobScheduler with ChapkitJobScheduler if jobs are enabled
|
|
309
|
+
if self._job_options is not None:
|
|
310
|
+
scheduler = ChapkitJobScheduler(max_concurrency=self._job_options.max_concurrency)
|
|
311
|
+
set_scheduler(scheduler)
|
|
312
|
+
app.state.scheduler = scheduler
|
|
313
|
+
|
|
314
|
+
yield
|
|
315
|
+
|
|
316
|
+
return lifespan
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class MLServiceBuilder(ServiceBuilder):
|
|
320
|
+
"""Specialized service builder for ML services with all required components pre-configured."""
|
|
321
|
+
|
|
322
|
+
def __init__(
|
|
323
|
+
self,
|
|
324
|
+
*,
|
|
325
|
+
info: ServiceInfo | MLServiceInfo,
|
|
326
|
+
config_schema: type[BaseConfig],
|
|
327
|
+
hierarchy: ArtifactHierarchy,
|
|
328
|
+
runner: ModelRunnerProtocol,
|
|
329
|
+
database_url: str = "sqlite+aiosqlite:///:memory:",
|
|
330
|
+
include_error_handlers: bool = True,
|
|
331
|
+
include_logging: bool = True,
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Initialize ML service builder with required ML components."""
|
|
334
|
+
super().__init__(
|
|
335
|
+
info=info,
|
|
336
|
+
database_url=database_url,
|
|
337
|
+
include_error_handlers=include_error_handlers,
|
|
338
|
+
include_logging=include_logging,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Automatically configure required ML components
|
|
342
|
+
self.with_health()
|
|
343
|
+
self.with_system()
|
|
344
|
+
self.with_config(config_schema)
|
|
345
|
+
self.with_artifacts(hierarchy=hierarchy, enable_config_linking=True)
|
|
346
|
+
self.with_jobs()
|
|
347
|
+
self.with_landing_page()
|
|
348
|
+
self.with_ml(runner=runner)
|