QuickLab-MCP 0.1.0__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 (92) hide show
  1. ml_mcp/__init__.py +3 -0
  2. ml_mcp/application/__init__.py +1 -0
  3. ml_mcp/application/analysis/__init__.py +15 -0
  4. ml_mcp/application/analysis/diagnostics.py +122 -0
  5. ml_mcp/application/analysis/service.py +155 -0
  6. ml_mcp/application/artifacts/__init__.py +5 -0
  7. ml_mcp/application/artifacts/service.py +149 -0
  8. ml_mcp/application/datasets/__init__.py +6 -0
  9. ml_mcp/application/datasets/service.py +178 -0
  10. ml_mcp/application/datasets/validator.py +156 -0
  11. ml_mcp/application/experiments/__init__.py +5 -0
  12. ml_mcp/application/experiments/service.py +254 -0
  13. ml_mcp/application/models/__init__.py +6 -0
  14. ml_mcp/application/models/catalog_seed.py +131 -0
  15. ml_mcp/application/models/service.py +177 -0
  16. ml_mcp/application/projects/__init__.py +5 -0
  17. ml_mcp/application/projects/service.py +93 -0
  18. ml_mcp/config/__init__.py +5 -0
  19. ml_mcp/config/settings.py +176 -0
  20. ml_mcp/domain/__init__.py +1 -0
  21. ml_mcp/domain/errors/__init__.py +39 -0
  22. ml_mcp/domain/errors/base.py +155 -0
  23. ml_mcp/domain/errors/codes.py +23 -0
  24. ml_mcp/domain/policies/__init__.py +16 -0
  25. ml_mcp/domain/policies/rbac.py +126 -0
  26. ml_mcp/domain/policies/tenant_isolation.py +34 -0
  27. ml_mcp/domain/policies/tool_policy.py +185 -0
  28. ml_mcp/domain/value_objects/__init__.py +41 -0
  29. ml_mcp/domain/value_objects/dataset.py +60 -0
  30. ml_mcp/domain/value_objects/experiment.py +131 -0
  31. ml_mcp/domain/value_objects/model.py +48 -0
  32. ml_mcp/infrastructure/__init__.py +1 -0
  33. ml_mcp/infrastructure/object_storage/__init__.py +5 -0
  34. ml_mcp/infrastructure/object_storage/s3.py +272 -0
  35. ml_mcp/infrastructure/postgres/__init__.py +14 -0
  36. ml_mcp/infrastructure/postgres/base.py +40 -0
  37. ml_mcp/infrastructure/postgres/models.py +356 -0
  38. ml_mcp/infrastructure/postgres/repositories/__init__.py +21 -0
  39. ml_mcp/infrastructure/postgres/repositories/artifacts.py +34 -0
  40. ml_mcp/infrastructure/postgres/repositories/audit.py +34 -0
  41. ml_mcp/infrastructure/postgres/repositories/datasets.py +85 -0
  42. ml_mcp/infrastructure/postgres/repositories/experiments.py +134 -0
  43. ml_mcp/infrastructure/postgres/repositories/metrics.py +61 -0
  44. ml_mcp/infrastructure/postgres/repositories/models.py +61 -0
  45. ml_mcp/infrastructure/postgres/repositories/outbox.py +47 -0
  46. ml_mcp/infrastructure/postgres/repositories/projects.py +83 -0
  47. ml_mcp/infrastructure/postgres/session.py +117 -0
  48. ml_mcp/infrastructure/queue/__init__.py +6 -0
  49. ml_mcp/infrastructure/queue/outbox_processor.py +67 -0
  50. ml_mcp/infrastructure/queue/task_queue.py +98 -0
  51. ml_mcp/infrastructure/redis/__init__.py +11 -0
  52. ml_mcp/infrastructure/redis/client.py +64 -0
  53. ml_mcp/infrastructure/redis/rate_limiter.py +129 -0
  54. ml_mcp/infrastructure/telemetry/__init__.py +23 -0
  55. ml_mcp/infrastructure/telemetry/logging.py +155 -0
  56. ml_mcp/infrastructure/telemetry/otel.py +141 -0
  57. ml_mcp/mcp/__init__.py +1 -0
  58. ml_mcp/mcp/prompts.py +38 -0
  59. ml_mcp/mcp/resources.py +35 -0
  60. ml_mcp/mcp/schemas/__init__.py +1 -0
  61. ml_mcp/mcp/schemas/tools.py +124 -0
  62. ml_mcp/mcp/tools/__init__.py +63 -0
  63. ml_mcp/mcp/tools/analysis.py +37 -0
  64. ml_mcp/mcp/tools/datasets.py +82 -0
  65. ml_mcp/mcp/tools/discovery.py +82 -0
  66. ml_mcp/mcp/tools/experiments.py +96 -0
  67. ml_mcp/mcp/tools/projects.py +39 -0
  68. ml_mcp/mcp/tools/results.py +59 -0
  69. ml_mcp/server/__init__.py +1 -0
  70. ml_mcp/server/app.py +488 -0
  71. ml_mcp/server/auth.py +146 -0
  72. ml_mcp/server/context.py +59 -0
  73. ml_mcp/server/http.py +183 -0
  74. ml_mcp/server/middleware.py +121 -0
  75. ml_mcp/server/stdio.py +42 -0
  76. ml_mcp/workers/__init__.py +1 -0
  77. ml_mcp/workers/daemon.py +130 -0
  78. ml_mcp/workers/metrics.py +110 -0
  79. ml_mcp/workers/preprocessing.py +170 -0
  80. ml_mcp/workers/runner.py +250 -0
  81. ml_mcp/workers/sandbox.py +35 -0
  82. ml_mcp/workers/trainers/__init__.py +52 -0
  83. ml_mcp/workers/trainers/base.py +52 -0
  84. ml_mcp/workers/trainers/catboost_model.py +85 -0
  85. ml_mcp/workers/trainers/lightgbm_model.py +81 -0
  86. ml_mcp/workers/trainers/sklearn_models.py +266 -0
  87. ml_mcp/workers/trainers/xgboost_model.py +82 -0
  88. quicklab_mcp-0.1.0.dist-info/METADATA +540 -0
  89. quicklab_mcp-0.1.0.dist-info/RECORD +92 -0
  90. quicklab_mcp-0.1.0.dist-info/WHEEL +5 -0
  91. quicklab_mcp-0.1.0.dist-info/entry_points.txt +4 -0
  92. quicklab_mcp-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,16 @@
1
+ """Domain policies package."""
2
+
3
+ from ml_mcp.domain.policies.rbac import ROLE_PERMISSIONS, Principal, Role, Scope
4
+ from ml_mcp.domain.policies.tenant_isolation import validate_tenant_access
5
+ from ml_mcp.domain.policies.tool_policy import TOOL_POLICIES, ToolClass, ToolPolicy
6
+
7
+ __all__ = [
8
+ "Role",
9
+ "Scope",
10
+ "ROLE_PERMISSIONS",
11
+ "Principal",
12
+ "ToolClass",
13
+ "ToolPolicy",
14
+ "TOOL_POLICIES",
15
+ "validate_tenant_access",
16
+ ]
@@ -0,0 +1,126 @@
1
+ """Role-Based Access Control (RBAC) and permission scope evaluation."""
2
+
3
+ from enum import StrEnum
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from ml_mcp.domain.errors import AuthorizationDeniedError
8
+
9
+
10
+ class Role(StrEnum):
11
+ """Supported principal roles in ModelLab."""
12
+
13
+ VIEWER = "viewer"
14
+ RESEARCHER = "researcher"
15
+ OPERATOR = "operator"
16
+ ADMIN = "admin"
17
+
18
+
19
+ class Scope(StrEnum):
20
+ """Granular permission scopes for ML control plane operations."""
21
+
22
+ PROJECTS_READ = "ml:projects:read"
23
+ PROJECTS_WRITE = "ml:projects:write"
24
+ MODELS_READ = "ml:models:read"
25
+ DATASETS_READ = "ml:datasets:read"
26
+ DATASETS_WRITE = "ml:datasets:write"
27
+ EXPERIMENTS_READ = "ml:experiments:read"
28
+ EXPERIMENTS_CREATE = "ml:experiments:create"
29
+ EXPERIMENTS_CANCEL = "ml:experiments:cancel"
30
+ ANALYSIS_CREATE = "ml:analysis:create"
31
+ ARTIFACTS_READ = "ml:artifacts:read"
32
+ WORKER_OPERATE = "ml:worker:operate"
33
+ EXPERIMENT_OVERRIDE = "ml:experiment:override"
34
+ ADMIN = "ml:admin"
35
+
36
+
37
+ # Standard role permissions matrix per Architecture.md section 7
38
+ ROLE_PERMISSIONS: dict[Role, set[Scope]] = {
39
+ Role.VIEWER: {
40
+ Scope.PROJECTS_READ,
41
+ Scope.MODELS_READ,
42
+ Scope.DATASETS_READ,
43
+ Scope.EXPERIMENTS_READ,
44
+ Scope.ARTIFACTS_READ,
45
+ },
46
+ Role.RESEARCHER: {
47
+ Scope.PROJECTS_READ,
48
+ Scope.PROJECTS_WRITE,
49
+ Scope.MODELS_READ,
50
+ Scope.DATASETS_READ,
51
+ Scope.DATASETS_WRITE,
52
+ Scope.EXPERIMENTS_READ,
53
+ Scope.EXPERIMENTS_CREATE,
54
+ Scope.EXPERIMENTS_CANCEL,
55
+ Scope.ANALYSIS_CREATE,
56
+ Scope.ARTIFACTS_READ,
57
+ },
58
+ Role.OPERATOR: {
59
+ Scope.PROJECTS_READ,
60
+ Scope.PROJECTS_WRITE,
61
+ Scope.MODELS_READ,
62
+ Scope.DATASETS_READ,
63
+ Scope.DATASETS_WRITE,
64
+ Scope.EXPERIMENTS_READ,
65
+ Scope.EXPERIMENTS_CREATE,
66
+ Scope.EXPERIMENTS_CANCEL,
67
+ Scope.ANALYSIS_CREATE,
68
+ Scope.ARTIFACTS_READ,
69
+ Scope.WORKER_OPERATE,
70
+ Scope.EXPERIMENT_OVERRIDE,
71
+ },
72
+ Role.ADMIN: set(Scope),
73
+ }
74
+
75
+
76
+ class Principal(BaseModel):
77
+ """Authenticated caller identity."""
78
+
79
+ principal_id: str = Field(..., min_length=1)
80
+ tenant_id: str = Field(..., min_length=1)
81
+ project_ids: list[str] = Field(
82
+ default_factory=list,
83
+ description="List of authorized project IDs, empty allows all tenant projects",
84
+ )
85
+ role: Role = Role.VIEWER
86
+ scopes: set[str] = Field(
87
+ default_factory=set, description="Explicit scopes granted via token claims"
88
+ )
89
+
90
+ def has_permission(self, required_scope: Scope) -> bool:
91
+ """Evaluate if the principal holds the required permission via role and scope intersection.
92
+
93
+ Enforces strict OAuth 2.1 scope attenuation:
94
+ 1. The requested scope MUST be allowed by the principal's role.
95
+ 2. If explicit token scopes are present, the requested scope MUST also be granted
96
+ by the token (or covered by a wildcard '*').
97
+ """
98
+ role_allowed = ROLE_PERMISSIONS.get(self.role, set())
99
+ if required_scope not in role_allowed:
100
+ return False
101
+
102
+ # If no explicit token scopes are set, the principal has full role permissions
103
+ if not self.scopes:
104
+ return True
105
+
106
+ # If explicit scopes are set, evaluate scope intersection
107
+ scope_vals = {s.value if isinstance(s, Scope) else s for s in self.scopes}
108
+ req_val = required_scope.value
109
+ req_short = req_val.removeprefix("ml:")
110
+
111
+ return (
112
+ "*" in self.scopes
113
+ or "admin" in scope_vals
114
+ or Scope.ADMIN.value in scope_vals
115
+ or Scope.ADMIN in self.scopes
116
+ or req_val in scope_vals
117
+ or req_short in scope_vals
118
+ )
119
+
120
+ def enforce_permission(self, required_scope: Scope) -> None:
121
+ """Enforce permission, raising AuthorizationDeniedError if missing."""
122
+ if not self.has_permission(required_scope):
123
+ raise AuthorizationDeniedError(
124
+ f"Principal '{self.principal_id}' lacks required permission scope '{required_scope.value}'",
125
+ details={"required_scope": required_scope.value, "role": self.role.value},
126
+ )
@@ -0,0 +1,34 @@
1
+ """Tenant isolation and project boundary enforcement."""
2
+
3
+ from ml_mcp.domain.errors import AuthorizationDeniedError, ResourceNotFoundError
4
+ from ml_mcp.domain.policies.rbac import Principal
5
+
6
+
7
+ def validate_tenant_access(
8
+ principal: Principal,
9
+ resource_tenant_id: str,
10
+ resource_project_id: str | None = None,
11
+ ) -> None:
12
+ """Validate that the authenticated principal is authorized to access resources belonging to a given tenant and project.
13
+
14
+ Raises:
15
+ ResourceNotFoundError: if cross-tenant access is detected (anti-enumeration: 404 with 0 foreign tenant leaks).
16
+ AuthorizationDeniedError: if unauthorized cross-project access within the same tenant is detected.
17
+ """
18
+ # Tenant boundary enforcement: cross-tenant access returns 404 with zero foreign tenant leakage
19
+ if principal.tenant_id != resource_tenant_id:
20
+ raise ResourceNotFoundError("Resource", resource_project_id or "requested")
21
+
22
+ # Project boundary enforcement: if principal has restricted project_ids, check membership
23
+ if (
24
+ resource_project_id is not None
25
+ and principal.project_ids
26
+ and resource_project_id not in principal.project_ids
27
+ ):
28
+ raise AuthorizationDeniedError(
29
+ f"Principal '{principal.principal_id}' does not have access to project '{resource_project_id}'",
30
+ details={
31
+ "allowed_projects": principal.project_ids,
32
+ "requested_project": resource_project_id,
33
+ },
34
+ )
@@ -0,0 +1,185 @@
1
+ """Tool classification and security requirements."""
2
+
3
+ from enum import StrEnum
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from ml_mcp.domain.policies.rbac import Scope
8
+
9
+
10
+ class ToolClass(StrEnum):
11
+ """Policy classification for rate limiting, auditing, and access control."""
12
+
13
+ READ = "READ"
14
+ MUTATION = "MUTATION"
15
+ EXPERIMENT = "EXPERIMENT"
16
+ CONTROL = "CONTROL"
17
+ ANALYSIS = "ANALYSIS"
18
+ ARTIFACT = "ARTIFACT"
19
+
20
+
21
+ class ToolPolicy(BaseModel):
22
+ """Security and rate-limiting policy governing an MCP tool."""
23
+
24
+ tool_name: str
25
+ tool_class: ToolClass
26
+ required_scope: Scope
27
+ requires_auth: bool = True
28
+ requires_audit: bool = True
29
+ rate_limit_category: str # "read", "metadata", "experiment", "analysis"
30
+
31
+
32
+ TOOL_POLICIES: dict[str, ToolPolicy] = {
33
+ # Discovery tools
34
+ "list_models": ToolPolicy(
35
+ tool_name="list_models",
36
+ tool_class=ToolClass.READ,
37
+ required_scope=Scope.MODELS_READ,
38
+ requires_audit=False,
39
+ rate_limit_category="read",
40
+ ),
41
+ "get_model": ToolPolicy(
42
+ tool_name="get_model",
43
+ tool_class=ToolClass.READ,
44
+ required_scope=Scope.MODELS_READ,
45
+ requires_audit=False,
46
+ rate_limit_category="read",
47
+ ),
48
+ "list_model_versions": ToolPolicy(
49
+ tool_name="list_model_versions",
50
+ tool_class=ToolClass.READ,
51
+ required_scope=Scope.MODELS_READ,
52
+ requires_audit=False,
53
+ rate_limit_category="read",
54
+ ),
55
+ "list_datasets": ToolPolicy(
56
+ tool_name="list_datasets",
57
+ tool_class=ToolClass.READ,
58
+ required_scope=Scope.DATASETS_READ,
59
+ requires_audit=False,
60
+ rate_limit_category="read",
61
+ ),
62
+ "get_dataset": ToolPolicy(
63
+ tool_name="get_dataset",
64
+ tool_class=ToolClass.READ,
65
+ required_scope=Scope.DATASETS_READ,
66
+ requires_audit=False,
67
+ rate_limit_category="read",
68
+ ),
69
+ "list_dataset_versions": ToolPolicy(
70
+ tool_name="list_dataset_versions",
71
+ tool_class=ToolClass.READ,
72
+ required_scope=Scope.DATASETS_READ,
73
+ requires_audit=False,
74
+ rate_limit_category="read",
75
+ ),
76
+ # Dataset management tools
77
+ "register_dataset": ToolPolicy(
78
+ tool_name="register_dataset",
79
+ tool_class=ToolClass.MUTATION,
80
+ required_scope=Scope.DATASETS_WRITE,
81
+ requires_audit=True,
82
+ rate_limit_category="experiment",
83
+ ),
84
+ "validate_dataset": ToolPolicy(
85
+ tool_name="validate_dataset",
86
+ tool_class=ToolClass.READ,
87
+ required_scope=Scope.DATASETS_READ,
88
+ requires_audit=True,
89
+ rate_limit_category="metadata",
90
+ ),
91
+ "inspect_dataset": ToolPolicy(
92
+ tool_name="inspect_dataset",
93
+ tool_class=ToolClass.READ,
94
+ required_scope=Scope.DATASETS_READ,
95
+ requires_audit=True,
96
+ rate_limit_category="metadata",
97
+ ),
98
+ # Experiment tools
99
+ "create_experiment": ToolPolicy(
100
+ tool_name="create_experiment",
101
+ tool_class=ToolClass.EXPERIMENT,
102
+ required_scope=Scope.EXPERIMENTS_CREATE,
103
+ requires_audit=True,
104
+ rate_limit_category="experiment",
105
+ ),
106
+ "get_experiment": ToolPolicy(
107
+ tool_name="get_experiment",
108
+ tool_class=ToolClass.READ,
109
+ required_scope=Scope.EXPERIMENTS_READ,
110
+ requires_audit=False,
111
+ rate_limit_category="read",
112
+ ),
113
+ "cancel_experiment": ToolPolicy(
114
+ tool_name="cancel_experiment",
115
+ tool_class=ToolClass.CONTROL,
116
+ required_scope=Scope.EXPERIMENTS_CANCEL,
117
+ requires_audit=True,
118
+ rate_limit_category="experiment",
119
+ ),
120
+ "list_experiments": ToolPolicy(
121
+ tool_name="list_experiments",
122
+ tool_class=ToolClass.READ,
123
+ required_scope=Scope.EXPERIMENTS_READ,
124
+ requires_audit=False,
125
+ rate_limit_category="read",
126
+ ),
127
+ # Result tools
128
+ "get_experiment_metrics": ToolPolicy(
129
+ tool_name="get_experiment_metrics",
130
+ tool_class=ToolClass.READ,
131
+ required_scope=Scope.EXPERIMENTS_READ,
132
+ requires_audit=False,
133
+ rate_limit_category="read",
134
+ ),
135
+ "get_experiment_predictions": ToolPolicy(
136
+ tool_name="get_experiment_predictions",
137
+ tool_class=ToolClass.ARTIFACT,
138
+ required_scope=Scope.ARTIFACTS_READ,
139
+ requires_audit=True,
140
+ rate_limit_category="metadata",
141
+ ),
142
+ "list_experiment_artifacts": ToolPolicy(
143
+ tool_name="list_experiment_artifacts",
144
+ tool_class=ToolClass.READ,
145
+ required_scope=Scope.ARTIFACTS_READ,
146
+ requires_audit=False,
147
+ rate_limit_category="read",
148
+ ),
149
+ "read_experiment_artifact": ToolPolicy(
150
+ tool_name="read_experiment_artifact",
151
+ tool_class=ToolClass.ARTIFACT,
152
+ required_scope=Scope.ARTIFACTS_READ,
153
+ requires_audit=True,
154
+ rate_limit_category="metadata",
155
+ ),
156
+ "compare_experiments": ToolPolicy(
157
+ tool_name="compare_experiments",
158
+ tool_class=ToolClass.READ,
159
+ required_scope=Scope.EXPERIMENTS_READ,
160
+ requires_audit=False,
161
+ rate_limit_category="read",
162
+ ),
163
+ # Analysis tools
164
+ "analyze_experiment": ToolPolicy(
165
+ tool_name="analyze_experiment",
166
+ tool_class=ToolClass.ANALYSIS,
167
+ required_scope=Scope.ANALYSIS_CREATE,
168
+ requires_audit=True,
169
+ rate_limit_category="analysis",
170
+ ),
171
+ "analyze_model_errors": ToolPolicy(
172
+ tool_name="analyze_model_errors",
173
+ tool_class=ToolClass.ANALYSIS,
174
+ required_scope=Scope.ANALYSIS_CREATE,
175
+ requires_audit=True,
176
+ rate_limit_category="analysis",
177
+ ),
178
+ "check_experiment_validity": ToolPolicy(
179
+ tool_name="check_experiment_validity",
180
+ tool_class=ToolClass.ANALYSIS,
181
+ required_scope=Scope.ANALYSIS_CREATE,
182
+ requires_audit=True,
183
+ rate_limit_category="analysis",
184
+ ),
185
+ }
@@ -0,0 +1,41 @@
1
+ """Domain value objects package."""
2
+
3
+ from ml_mcp.domain.value_objects.dataset import (
4
+ ColumnSchema,
5
+ DatasetFormat,
6
+ DatasetSchema,
7
+ FeatureType,
8
+ )
9
+ from ml_mcp.domain.value_objects.experiment import (
10
+ EvaluationConfig,
11
+ ExperimentSpec,
12
+ ExperimentStatus,
13
+ ModelFamily,
14
+ ResourcePolicy,
15
+ SplitStrategy,
16
+ SplitStrategyType,
17
+ TaskType,
18
+ )
19
+ from ml_mcp.domain.value_objects.model import (
20
+ ApprovalStatus,
21
+ HyperparameterConstraint,
22
+ ModelMetadata,
23
+ )
24
+
25
+ __all__ = [
26
+ "TaskType",
27
+ "ModelFamily",
28
+ "SplitStrategyType",
29
+ "SplitStrategy",
30
+ "EvaluationConfig",
31
+ "ResourcePolicy",
32
+ "ExperimentStatus",
33
+ "ExperimentSpec",
34
+ "DatasetFormat",
35
+ "FeatureType",
36
+ "ColumnSchema",
37
+ "DatasetSchema",
38
+ "ApprovalStatus",
39
+ "HyperparameterConstraint",
40
+ "ModelMetadata",
41
+ ]
@@ -0,0 +1,60 @@
1
+ """Dataset value objects and schema contracts."""
2
+
3
+ from enum import StrEnum
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, Field, field_validator
7
+
8
+
9
+ class DatasetFormat(StrEnum):
10
+ """Allowed tabular dataset formats."""
11
+
12
+ CSV = "csv"
13
+ PARQUET = "parquet"
14
+ JSONL = "jsonl"
15
+
16
+
17
+ class FeatureType(StrEnum):
18
+ """Canonical feature types inferred and used in ML preprocessing."""
19
+
20
+ NUMERIC = "numeric"
21
+ CATEGORICAL = "categorical"
22
+ BOOLEAN = "boolean"
23
+ DATETIME = "datetime"
24
+ TEXT = "text"
25
+
26
+
27
+ class ColumnSchema(BaseModel):
28
+ """Schema and statistical metadata for a single dataset column."""
29
+
30
+ name: str = Field(..., min_length=1)
31
+ feature_type: FeatureType
32
+ nullable: bool = True
33
+ null_count: int = Field(default=0, ge=0)
34
+ unique_count: int = Field(default=0, ge=0)
35
+ sample_values: list[Any] = Field(default_factory=list)
36
+
37
+
38
+ class DatasetSchema(BaseModel):
39
+ """Complete dataset schema structure with content digest and profile."""
40
+
41
+ columns: list[ColumnSchema]
42
+ row_count: int = Field(..., ge=0)
43
+ column_count: int = Field(..., ge=1)
44
+ size_bytes: int = Field(..., ge=0)
45
+ content_hash: str = Field(..., description="SHA-256 content hash of canonicalized dataset")
46
+
47
+ @field_validator("columns")
48
+ @classmethod
49
+ def validate_unique_column_names(cls, v: list[ColumnSchema]) -> list[ColumnSchema]:
50
+ names = [col.name.lower() for col in v]
51
+ if len(names) != len(set(names)):
52
+ raise ValueError("Dataset schema contains duplicate column names (case-insensitive)")
53
+ return v
54
+
55
+ def get_column(self, name: str) -> ColumnSchema | None:
56
+ name_lower = name.lower()
57
+ for col in self.columns:
58
+ if col.name.lower() == name_lower:
59
+ return col
60
+ return None
@@ -0,0 +1,131 @@
1
+ """Experiment value objects and specification contracts."""
2
+
3
+ from enum import StrEnum
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, Field, model_validator
7
+
8
+
9
+ class TaskType(StrEnum):
10
+ """Supported machine learning task types."""
11
+
12
+ BINARY_CLASSIFICATION = "binary_classification"
13
+ MULTICLASS_CLASSIFICATION = "multiclass_classification"
14
+ REGRESSION = "regression"
15
+
16
+
17
+ class ModelFamily(StrEnum):
18
+ """Initial approved tabular model families."""
19
+
20
+ LOGISTIC_REGRESSION = "logistic_regression"
21
+ RANDOM_FOREST = "random_forest"
22
+ XGBOOST = "xgboost"
23
+ LIGHTGBM = "lightgbm"
24
+ CATBOOST = "catboost"
25
+ LINEAR_SVM = "linear_svm"
26
+ MLP = "mlp"
27
+
28
+
29
+ class SplitStrategyType(StrEnum):
30
+ """Dataset splitting strategies."""
31
+
32
+ TRAIN_TEST_SPLIT = "train_test_split"
33
+ K_FOLD = "k_fold"
34
+ STRATIFIED_K_FOLD = "stratified_k_fold"
35
+ TIME_SERIES_SPLIT = "time_series_split"
36
+
37
+
38
+ class SplitStrategy(BaseModel):
39
+ """Configuration for splitting dataset into training and evaluation partitions."""
40
+
41
+ strategy: SplitStrategyType = SplitStrategyType.TRAIN_TEST_SPLIT
42
+ test_size: float = Field(
43
+ default=0.2, ge=0.05, le=0.5, description="Fraction of data reserved for testing"
44
+ )
45
+ n_splits: int = Field(
46
+ default=5, ge=2, le=20, description="Folds for cross-validation strategies"
47
+ )
48
+ shuffle: bool = True
49
+ random_seed: int = Field(default=42, ge=0)
50
+
51
+
52
+ class EvaluationConfig(BaseModel):
53
+ """Evaluation metrics configuration."""
54
+
55
+ primary_metric: str = Field(..., description="Target optimization and reporting metric")
56
+ additional_metrics: list[str] = Field(default_factory=list)
57
+
58
+
59
+ class ResourcePolicy(BaseModel):
60
+ """Hardware quotas and execution bounds for worker isolation."""
61
+
62
+ max_runtime_seconds: int = Field(default=1800, ge=10, le=7200)
63
+ max_memory_mb: int = Field(default=4096, ge=256, le=32768)
64
+ max_cpu_cores: int = Field(default=2, ge=1, le=16)
65
+
66
+
67
+ class ExperimentStatus(StrEnum):
68
+ """Lifecycle states of an ML experiment."""
69
+
70
+ CREATED = "CREATED"
71
+ VALIDATING = "VALIDATING"
72
+ REJECTED = "REJECTED"
73
+ QUEUED = "QUEUED"
74
+ RUNNING = "RUNNING"
75
+ SUCCEEDED = "SUCCEEDED"
76
+ FAILED = "FAILED"
77
+ CANCEL_REQUESTED = "CANCEL_REQUESTED"
78
+ CANCELLED = "CANCELLED"
79
+ ANALYZING = "ANALYZING"
80
+ ANALYSIS_READY = "ANALYSIS_READY"
81
+
82
+
83
+ class ExperimentSpec(BaseModel):
84
+ """Complete, immutable experiment specification defining reproducible execution."""
85
+
86
+ tenant_id: str = Field(..., min_length=1)
87
+ project_id: str = Field(..., min_length=1)
88
+ dataset_version_id: str = Field(..., min_length=1)
89
+ model_version_id: str = Field(..., min_length=1)
90
+ task_type: TaskType
91
+ target_column: str = Field(..., min_length=1)
92
+ feature_columns: list[str] | None = None
93
+ split_strategy: SplitStrategy = Field(default_factory=SplitStrategy)
94
+ evaluation_config: EvaluationConfig
95
+ hyperparameters: dict[str, Any] = Field(default_factory=dict)
96
+ resource_policy: ResourcePolicy = Field(default_factory=ResourcePolicy)
97
+ random_seed: int = Field(default=42, ge=0)
98
+ idempotency_key: str | None = None
99
+ created_by: str = Field(..., min_length=1)
100
+
101
+ @model_validator(mode="after")
102
+ def validate_metrics_compatible_with_task(self) -> "ExperimentSpec":
103
+ classification_metrics = {
104
+ "accuracy",
105
+ "f1",
106
+ "f1_macro",
107
+ "f1_weighted",
108
+ "precision",
109
+ "recall",
110
+ "roc_auc",
111
+ "pr_auc",
112
+ "log_loss",
113
+ "brier_score",
114
+ }
115
+ regression_metrics = {"mse", "rmse", "mae", "r2", "mape", "explained_variance"}
116
+
117
+ pm = self.evaluation_config.primary_metric.lower()
118
+ if (
119
+ self.task_type in (TaskType.BINARY_CLASSIFICATION, TaskType.MULTICLASS_CLASSIFICATION)
120
+ and pm in regression_metrics
121
+ and pm not in classification_metrics
122
+ ):
123
+ raise ValueError(f"Metric '{pm}' is not compatible with classification task")
124
+ if (
125
+ self.task_type == TaskType.REGRESSION
126
+ and pm in classification_metrics
127
+ and pm not in regression_metrics
128
+ ):
129
+ raise ValueError(f"Metric '{pm}' is not compatible with regression task")
130
+
131
+ return self
@@ -0,0 +1,48 @@
1
+ """Model registry value objects and constraints."""
2
+
3
+ from enum import StrEnum
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from ml_mcp.domain.value_objects.experiment import ModelFamily, TaskType
9
+
10
+
11
+ class ApprovalStatus(StrEnum):
12
+ """Approval lifecycle of registered models."""
13
+
14
+ PENDING = "PENDING"
15
+ APPROVED = "APPROVED"
16
+ REJECTED = "REJECTED"
17
+ REVOKED = "REVOKED"
18
+
19
+
20
+ class HyperparameterConstraint(BaseModel):
21
+ """Schema constraint definition for a model hyperparameter."""
22
+
23
+ name: str
24
+ type: str # "int", "float", "str", "bool"
25
+ default: Any
26
+ min_value: float | int | None = None
27
+ max_value: float | int | None = None
28
+ allowed_values: list[Any] | None = None
29
+ description: str = ""
30
+
31
+
32
+ class ModelMetadata(BaseModel):
33
+ """Immutable model architecture specification in the catalog."""
34
+
35
+ model_id: str = Field(..., min_length=1)
36
+ version: str = Field(..., min_length=1)
37
+ name: str = Field(..., min_length=1)
38
+ description: str = ""
39
+ family: ModelFamily
40
+ supported_task_types: list[TaskType]
41
+ approval_status: ApprovalStatus = ApprovalStatus.PENDING
42
+ container_image_digest: str = Field(
43
+ ..., description="SHA-256 digest of verified worker container image"
44
+ )
45
+ artifact_digest: str | None = None
46
+ supported_hyperparameters: list[HyperparameterConstraint] = Field(default_factory=list)
47
+ license: str = "Apache-2.0"
48
+ publisher: str = "ModelLab"
@@ -0,0 +1 @@
1
+ """Infrastructure layer package."""
@@ -0,0 +1,5 @@
1
+ """Object storage package."""
2
+
3
+ from ml_mcp.infrastructure.object_storage.s3 import S3StorageService, get_storage_service
4
+
5
+ __all__ = ["S3StorageService", "get_storage_service"]