chapkit 0.6.4__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 (65) hide show
  1. chapkit/__init__.py +125 -0
  2. chapkit/alembic/__init__.py +1 -0
  3. chapkit/alembic/env.py +76 -0
  4. chapkit/alembic/script.py.mako +26 -0
  5. chapkit/alembic/versions/20251010_0927_4d869b5fb06e_initial_schema.py +38 -0
  6. chapkit/alembic/versions/__init__.py +1 -0
  7. chapkit/alembic_helpers.py +138 -0
  8. chapkit/api/__init__.py +64 -0
  9. chapkit/api/dependencies.py +32 -0
  10. chapkit/api/service_builder.py +369 -0
  11. chapkit/artifact/__init__.py +18 -0
  12. chapkit/artifact/manager.py +132 -0
  13. chapkit/artifact/models.py +36 -0
  14. chapkit/artifact/repository.py +48 -0
  15. chapkit/artifact/router.py +126 -0
  16. chapkit/artifact/schemas.py +67 -0
  17. chapkit/cli/__init__.py +5 -0
  18. chapkit/cli/__main__.py +6 -0
  19. chapkit/cli/cli.py +47 -0
  20. chapkit/cli/init.py +209 -0
  21. chapkit/cli/templates/.gitignore +152 -0
  22. chapkit/cli/templates/Dockerfile.jinja2 +86 -0
  23. chapkit/cli/templates/README.md.jinja2 +176 -0
  24. chapkit/cli/templates/compose.monitoring.yml.jinja2 +90 -0
  25. chapkit/cli/templates/compose.yml.jinja2 +32 -0
  26. chapkit/cli/templates/main.py.jinja2 +146 -0
  27. chapkit/cli/templates/main_shell.py.jinja2 +105 -0
  28. chapkit/cli/templates/main_task.py.jinja2 +169 -0
  29. chapkit/cli/templates/monitoring/grafana/dashboards/chapkit-service-metrics.json +1232 -0
  30. chapkit/cli/templates/monitoring/grafana/provisioning/dashboards/dashboard.yml +13 -0
  31. chapkit/cli/templates/monitoring/grafana/provisioning/datasources/prometheus.yml +25 -0
  32. chapkit/cli/templates/monitoring/prometheus/prometheus.yml.jinja2 +13 -0
  33. chapkit/cli/templates/postman_collection_ml.json.jinja2 +351 -0
  34. chapkit/cli/templates/postman_collection_ml_shell.json.jinja2 +374 -0
  35. chapkit/cli/templates/postman_collection_task.json.jinja2 +381 -0
  36. chapkit/cli/templates/pyproject.toml.jinja2 +15 -0
  37. chapkit/cli/templates/scripts/predict_model.py.jinja2 +74 -0
  38. chapkit/cli/templates/scripts/train_model.py.jinja2 +66 -0
  39. chapkit/config/__init__.py +20 -0
  40. chapkit/config/manager.py +63 -0
  41. chapkit/config/models.py +60 -0
  42. chapkit/config/repository.py +76 -0
  43. chapkit/config/router.py +139 -0
  44. chapkit/config/schemas.py +63 -0
  45. chapkit/data/__init__.py +22 -0
  46. chapkit/data/dataframe.py +1104 -0
  47. chapkit/ml/__init__.py +29 -0
  48. chapkit/ml/manager.py +231 -0
  49. chapkit/ml/router.py +114 -0
  50. chapkit/ml/runner.py +271 -0
  51. chapkit/ml/schemas.py +98 -0
  52. chapkit/py.typed +0 -0
  53. chapkit/scheduler.py +154 -0
  54. chapkit/task/__init__.py +20 -0
  55. chapkit/task/manager.py +300 -0
  56. chapkit/task/models.py +20 -0
  57. chapkit/task/registry.py +46 -0
  58. chapkit/task/repository.py +31 -0
  59. chapkit/task/router.py +115 -0
  60. chapkit/task/schemas.py +28 -0
  61. chapkit/task/validation.py +76 -0
  62. chapkit-0.6.4.dist-info/METADATA +231 -0
  63. chapkit-0.6.4.dist-info/RECORD +65 -0
  64. chapkit-0.6.4.dist-info/WHEEL +4 -0
  65. chapkit-0.6.4.dist-info/entry_points.txt +3 -0
chapkit/__init__.py ADDED
@@ -0,0 +1,125 @@
1
+ """Chapkit - ML/data service modules built on servicekit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ # Read version from package metadata - must be before internal imports
8
+ try:
9
+ from importlib.metadata import version as _get_version
10
+
11
+ __version__ = _get_version("chapkit")
12
+ except Exception:
13
+ __version__ = "unknown"
14
+
15
+ # CLI feature
16
+ # Scheduler feature
17
+ # Artifact feature
18
+ from .artifact import (
19
+ Artifact,
20
+ ArtifactHierarchy,
21
+ ArtifactIn,
22
+ ArtifactManager,
23
+ ArtifactOut,
24
+ ArtifactRepository,
25
+ ArtifactRouter,
26
+ ArtifactTreeNode,
27
+ )
28
+ from .cli import app as cli_app
29
+
30
+ # Config feature
31
+ from .config import (
32
+ BaseConfig,
33
+ Config,
34
+ ConfigIn,
35
+ ConfigManager,
36
+ ConfigOut,
37
+ ConfigRepository,
38
+ )
39
+
40
+ # Data feature
41
+ from .data import DataFrame, GroupBy
42
+
43
+ # ML feature
44
+ from .ml import (
45
+ FunctionalModelRunner,
46
+ MLManager,
47
+ MLRouter,
48
+ ModelRunnerProtocol,
49
+ PredictionArtifactData,
50
+ PredictRequest,
51
+ PredictResponse,
52
+ TrainedModelArtifactData,
53
+ TrainRequest,
54
+ TrainResponse,
55
+ )
56
+ from .scheduler import ChapkitJobRecord, ChapkitJobScheduler
57
+
58
+ # Task feature
59
+ from .task import (
60
+ Task,
61
+ TaskIn,
62
+ TaskManager,
63
+ TaskOut,
64
+ TaskRegistry,
65
+ TaskRepository,
66
+ TaskRouter,
67
+ validate_and_disable_orphaned_tasks,
68
+ )
69
+
70
+
71
+ def get_alembic_dir() -> Path:
72
+ """Get the path to chapkit's bundled alembic migrations directory."""
73
+ return Path(__file__).parent / "alembic"
74
+
75
+
76
+ __all__ = [
77
+ # Version
78
+ "__version__",
79
+ # Utils
80
+ "get_alembic_dir",
81
+ # CLI
82
+ "cli_app",
83
+ # Scheduler
84
+ "ChapkitJobRecord",
85
+ "ChapkitJobScheduler",
86
+ # Artifact
87
+ "Artifact",
88
+ "ArtifactHierarchy",
89
+ "ArtifactIn",
90
+ "ArtifactManager",
91
+ "ArtifactOut",
92
+ "ArtifactRepository",
93
+ "ArtifactRouter",
94
+ "ArtifactTreeNode",
95
+ # Config
96
+ "BaseConfig",
97
+ "Config",
98
+ "ConfigIn",
99
+ "ConfigManager",
100
+ "ConfigOut",
101
+ "ConfigRepository",
102
+ # Data
103
+ "DataFrame",
104
+ "GroupBy",
105
+ # ML
106
+ "FunctionalModelRunner",
107
+ "MLManager",
108
+ "MLRouter",
109
+ "ModelRunnerProtocol",
110
+ "PredictionArtifactData",
111
+ "PredictRequest",
112
+ "PredictResponse",
113
+ "TrainedModelArtifactData",
114
+ "TrainRequest",
115
+ "TrainResponse",
116
+ # Task
117
+ "Task",
118
+ "TaskIn",
119
+ "TaskManager",
120
+ "TaskOut",
121
+ "TaskRegistry",
122
+ "TaskRepository",
123
+ "TaskRouter",
124
+ "validate_and_disable_orphaned_tasks",
125
+ ]
@@ -0,0 +1 @@
1
+ """Chapkit database migrations."""
chapkit/alembic/env.py ADDED
@@ -0,0 +1,76 @@
1
+ """Alembic environment configuration for async SQLAlchemy migrations."""
2
+
3
+ import asyncio
4
+ from logging.config import fileConfig
5
+
6
+ from alembic import context
7
+
8
+ # Import the Base metadata from servicekit
9
+ from servicekit import Base
10
+ from sqlalchemy import pool
11
+ from sqlalchemy.engine import Connection
12
+ from sqlalchemy.ext.asyncio import async_engine_from_config
13
+
14
+ # This is the Alembic Config object
15
+ config = context.config
16
+
17
+ # Interpret the config file for Python logging (if present)
18
+ if config.config_file_name is not None:
19
+ fileConfig(config.config_file_name)
20
+
21
+ # Set target metadata for 'autogenerate' support
22
+ target_metadata = Base.metadata
23
+
24
+
25
+ def run_migrations_offline() -> None:
26
+ """Run migrations in 'offline' mode (generates SQL scripts)."""
27
+ url = config.get_main_option("sqlalchemy.url")
28
+ context.configure(
29
+ url=url,
30
+ target_metadata=target_metadata,
31
+ literal_binds=True,
32
+ dialect_opts={"paramstyle": "named"},
33
+ )
34
+
35
+ with context.begin_transaction():
36
+ context.run_migrations()
37
+
38
+
39
+ def do_run_migrations(connection: Connection) -> None:
40
+ """Run migrations with a connection."""
41
+ context.configure(connection=connection, target_metadata=target_metadata)
42
+
43
+ with context.begin_transaction():
44
+ context.run_migrations()
45
+
46
+
47
+ async def run_async_migrations() -> None:
48
+ """Run migrations in async mode using async engine."""
49
+ connectable = async_engine_from_config(
50
+ config.get_section(config.config_ini_section, {}),
51
+ prefix="sqlalchemy.",
52
+ poolclass=pool.NullPool,
53
+ )
54
+
55
+ async with connectable.connect() as connection:
56
+ await connection.run_sync(do_run_migrations)
57
+
58
+ await connectable.dispose()
59
+
60
+
61
+ def run_migrations_online() -> None:
62
+ """Run migrations in 'online' mode (connects to database)."""
63
+ # Since we're being called from Database.init() via run_in_executor,
64
+ # we're in a separate thread and can safely create a new event loop
65
+ loop = asyncio.new_event_loop()
66
+ asyncio.set_event_loop(loop)
67
+ try:
68
+ loop.run_until_complete(run_async_migrations())
69
+ finally:
70
+ loop.close()
71
+
72
+
73
+ if context.is_offline_mode():
74
+ run_migrations_offline()
75
+ else:
76
+ run_migrations_online()
@@ -0,0 +1,26 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ ${imports if imports else ""}
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = ${repr(up_revision)}
14
+ down_revision = ${repr(down_revision)}
15
+ branch_labels = ${repr(branch_labels)}
16
+ depends_on = ${repr(depends_on)}
17
+
18
+
19
+ def upgrade() -> None:
20
+ """Apply database schema changes."""
21
+ ${upgrades if upgrades else "pass"}
22
+
23
+
24
+ def downgrade() -> None:
25
+ """Revert database schema changes."""
26
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,38 @@
1
+ """Initial database schema migration."""
2
+
3
+ from alembic import op
4
+
5
+ from chapkit.alembic_helpers import (
6
+ create_artifacts_table,
7
+ create_config_artifacts_table,
8
+ create_configs_table,
9
+ create_tasks_table,
10
+ drop_artifacts_table,
11
+ drop_config_artifacts_table,
12
+ drop_configs_table,
13
+ drop_tasks_table,
14
+ )
15
+
16
+ # revision identifiers, used by Alembic.
17
+ revision = "4d869b5fb06e"
18
+ down_revision = None
19
+ branch_labels = None
20
+ depends_on = None
21
+
22
+
23
+ def upgrade() -> None:
24
+ """Apply database schema changes."""
25
+ # Chapkit domain tables
26
+ create_artifacts_table(op)
27
+ create_configs_table(op)
28
+ create_config_artifacts_table(op)
29
+ create_tasks_table(op)
30
+
31
+
32
+ def downgrade() -> None:
33
+ """Revert database schema changes."""
34
+ # Drop in reverse order
35
+ drop_tasks_table(op)
36
+ drop_config_artifacts_table(op)
37
+ drop_configs_table(op)
38
+ drop_artifacts_table(op)
@@ -0,0 +1 @@
1
+ """Alembic migration versions."""
@@ -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")
@@ -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.")