flowyml 1.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 (159) hide show
  1. flowyml/__init__.py +207 -0
  2. flowyml/assets/__init__.py +22 -0
  3. flowyml/assets/artifact.py +40 -0
  4. flowyml/assets/base.py +209 -0
  5. flowyml/assets/dataset.py +100 -0
  6. flowyml/assets/featureset.py +301 -0
  7. flowyml/assets/metrics.py +104 -0
  8. flowyml/assets/model.py +82 -0
  9. flowyml/assets/registry.py +157 -0
  10. flowyml/assets/report.py +315 -0
  11. flowyml/cli/__init__.py +5 -0
  12. flowyml/cli/experiment.py +232 -0
  13. flowyml/cli/init.py +256 -0
  14. flowyml/cli/main.py +327 -0
  15. flowyml/cli/run.py +75 -0
  16. flowyml/cli/stack_cli.py +532 -0
  17. flowyml/cli/ui.py +33 -0
  18. flowyml/core/__init__.py +68 -0
  19. flowyml/core/advanced_cache.py +274 -0
  20. flowyml/core/approval.py +64 -0
  21. flowyml/core/cache.py +203 -0
  22. flowyml/core/checkpoint.py +148 -0
  23. flowyml/core/conditional.py +373 -0
  24. flowyml/core/context.py +155 -0
  25. flowyml/core/error_handling.py +419 -0
  26. flowyml/core/executor.py +354 -0
  27. flowyml/core/graph.py +185 -0
  28. flowyml/core/parallel.py +452 -0
  29. flowyml/core/pipeline.py +764 -0
  30. flowyml/core/project.py +253 -0
  31. flowyml/core/resources.py +424 -0
  32. flowyml/core/scheduler.py +630 -0
  33. flowyml/core/scheduler_config.py +32 -0
  34. flowyml/core/step.py +201 -0
  35. flowyml/core/step_grouping.py +292 -0
  36. flowyml/core/templates.py +226 -0
  37. flowyml/core/versioning.py +217 -0
  38. flowyml/integrations/__init__.py +1 -0
  39. flowyml/integrations/keras.py +134 -0
  40. flowyml/monitoring/__init__.py +1 -0
  41. flowyml/monitoring/alerts.py +57 -0
  42. flowyml/monitoring/data.py +102 -0
  43. flowyml/monitoring/llm.py +160 -0
  44. flowyml/monitoring/monitor.py +57 -0
  45. flowyml/monitoring/notifications.py +246 -0
  46. flowyml/registry/__init__.py +5 -0
  47. flowyml/registry/model_registry.py +491 -0
  48. flowyml/registry/pipeline_registry.py +55 -0
  49. flowyml/stacks/__init__.py +27 -0
  50. flowyml/stacks/base.py +77 -0
  51. flowyml/stacks/bridge.py +288 -0
  52. flowyml/stacks/components.py +155 -0
  53. flowyml/stacks/gcp.py +499 -0
  54. flowyml/stacks/local.py +112 -0
  55. flowyml/stacks/migration.py +97 -0
  56. flowyml/stacks/plugin_config.py +78 -0
  57. flowyml/stacks/plugins.py +401 -0
  58. flowyml/stacks/registry.py +226 -0
  59. flowyml/storage/__init__.py +26 -0
  60. flowyml/storage/artifacts.py +246 -0
  61. flowyml/storage/materializers/__init__.py +20 -0
  62. flowyml/storage/materializers/base.py +133 -0
  63. flowyml/storage/materializers/keras.py +185 -0
  64. flowyml/storage/materializers/numpy.py +94 -0
  65. flowyml/storage/materializers/pandas.py +142 -0
  66. flowyml/storage/materializers/pytorch.py +135 -0
  67. flowyml/storage/materializers/sklearn.py +110 -0
  68. flowyml/storage/materializers/tensorflow.py +152 -0
  69. flowyml/storage/metadata.py +931 -0
  70. flowyml/tracking/__init__.py +1 -0
  71. flowyml/tracking/experiment.py +211 -0
  72. flowyml/tracking/leaderboard.py +191 -0
  73. flowyml/tracking/runs.py +145 -0
  74. flowyml/ui/__init__.py +15 -0
  75. flowyml/ui/backend/Dockerfile +31 -0
  76. flowyml/ui/backend/__init__.py +0 -0
  77. flowyml/ui/backend/auth.py +163 -0
  78. flowyml/ui/backend/main.py +187 -0
  79. flowyml/ui/backend/routers/__init__.py +0 -0
  80. flowyml/ui/backend/routers/assets.py +45 -0
  81. flowyml/ui/backend/routers/execution.py +179 -0
  82. flowyml/ui/backend/routers/experiments.py +49 -0
  83. flowyml/ui/backend/routers/leaderboard.py +118 -0
  84. flowyml/ui/backend/routers/notifications.py +72 -0
  85. flowyml/ui/backend/routers/pipelines.py +110 -0
  86. flowyml/ui/backend/routers/plugins.py +192 -0
  87. flowyml/ui/backend/routers/projects.py +85 -0
  88. flowyml/ui/backend/routers/runs.py +66 -0
  89. flowyml/ui/backend/routers/schedules.py +222 -0
  90. flowyml/ui/backend/routers/traces.py +84 -0
  91. flowyml/ui/frontend/Dockerfile +20 -0
  92. flowyml/ui/frontend/README.md +315 -0
  93. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
  94. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
  95. flowyml/ui/frontend/dist/index.html +16 -0
  96. flowyml/ui/frontend/index.html +15 -0
  97. flowyml/ui/frontend/nginx.conf +26 -0
  98. flowyml/ui/frontend/package-lock.json +3545 -0
  99. flowyml/ui/frontend/package.json +33 -0
  100. flowyml/ui/frontend/postcss.config.js +6 -0
  101. flowyml/ui/frontend/src/App.jsx +21 -0
  102. flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
  103. flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
  104. flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
  105. flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
  106. flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
  107. flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
  108. flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
  109. flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
  110. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
  111. flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
  112. flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
  113. flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
  114. flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
  115. flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
  116. flowyml/ui/frontend/src/components/Layout.jsx +108 -0
  117. flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
  118. flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
  119. flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
  120. flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
  121. flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
  122. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
  123. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
  124. flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
  125. flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
  126. flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
  127. flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
  128. flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
  129. flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
  130. flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
  131. flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
  132. flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
  133. flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
  134. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
  135. flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
  136. flowyml/ui/frontend/src/index.css +11 -0
  137. flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
  138. flowyml/ui/frontend/src/main.jsx +10 -0
  139. flowyml/ui/frontend/src/router/index.jsx +39 -0
  140. flowyml/ui/frontend/src/services/pluginService.js +90 -0
  141. flowyml/ui/frontend/src/utils/api.js +47 -0
  142. flowyml/ui/frontend/src/utils/cn.js +6 -0
  143. flowyml/ui/frontend/tailwind.config.js +31 -0
  144. flowyml/ui/frontend/vite.config.js +21 -0
  145. flowyml/ui/utils.py +77 -0
  146. flowyml/utils/__init__.py +67 -0
  147. flowyml/utils/config.py +308 -0
  148. flowyml/utils/debug.py +240 -0
  149. flowyml/utils/environment.py +346 -0
  150. flowyml/utils/git.py +319 -0
  151. flowyml/utils/logging.py +61 -0
  152. flowyml/utils/performance.py +314 -0
  153. flowyml/utils/stack_config.py +296 -0
  154. flowyml/utils/validation.py +270 -0
  155. flowyml-1.1.0.dist-info/METADATA +372 -0
  156. flowyml-1.1.0.dist-info/RECORD +159 -0
  157. flowyml-1.1.0.dist-info/WHEEL +4 -0
  158. flowyml-1.1.0.dist-info/entry_points.txt +3 -0
  159. flowyml-1.1.0.dist-info/licenses/LICENSE +17 -0
@@ -0,0 +1,296 @@
1
+ """Configuration loader for flowyml stacks.
2
+
3
+ Loads stack configurations from YAML files, environment variables,
4
+ and provides defaults.
5
+ """
6
+
7
+ import os
8
+ import yaml
9
+ from pathlib import Path
10
+ from typing import Any
11
+ from dataclasses import dataclass, field
12
+
13
+
14
+ @dataclass
15
+ class StackConfigFile:
16
+ """Stack configuration loaded from flowyml.yaml.
17
+
18
+ Example flowyml.yaml:
19
+ ```yaml
20
+ stacks:
21
+ local:
22
+ type: local
23
+ artifact_store:
24
+ path: .flowyml/artifacts
25
+ metadata_store:
26
+ path: .flowyml/metadata.db
27
+
28
+ production:
29
+ type: gcp
30
+ project_id: ${GCP_PROJECT_ID}
31
+ region: us-central1
32
+ artifact_store:
33
+ type: gcs
34
+ bucket: ${GCP_BUCKET}
35
+ container_registry:
36
+ type: gcr
37
+ uri: gcr.io/${GCP_PROJECT_ID}
38
+ orchestrator:
39
+ type: vertex_ai
40
+
41
+ resources:
42
+ default:
43
+ cpu: "2"
44
+ memory: "8Gi"
45
+
46
+ gpu_training:
47
+ cpu: "8"
48
+ memory: "32Gi"
49
+ gpu: nvidia-tesla-v100
50
+ gpu_count: 2
51
+
52
+ docker:
53
+ dockerfile: ./Dockerfile
54
+ build_context: .
55
+ use_poetry: true
56
+ requirements_file: requirements.txt
57
+ ```
58
+ """
59
+
60
+ stacks: dict[str, dict[str, Any]] = field(default_factory=dict)
61
+ resources: dict[str, dict[str, Any]] = field(default_factory=dict)
62
+ docker: dict[str, Any] = field(default_factory=dict)
63
+ default_stack: str | None = None
64
+
65
+
66
+ class ConfigLoader:
67
+ """Load and manage flowyml configurations."""
68
+
69
+ DEFAULT_CONFIG_FILES = [
70
+ "flowyml.yaml",
71
+ "flowyml.yml",
72
+ ".flowyml/config.yaml",
73
+ ".flowyml/config.yml",
74
+ ]
75
+
76
+ def __init__(self, config_path: str | None = None):
77
+ """Initialize configuration loader.
78
+
79
+ Args:
80
+ config_path: Path to configuration file. If None, searches default locations.
81
+ """
82
+ self.config_path = self._find_config_file(config_path)
83
+ self.config: StackConfigFile | None = None
84
+
85
+ if self.config_path:
86
+ self.load()
87
+
88
+ def _find_config_file(self, config_path: str | None = None) -> Path | None:
89
+ """Find configuration file."""
90
+ if config_path:
91
+ path = Path(config_path)
92
+ if path.exists():
93
+ return path
94
+ raise FileNotFoundError(f"Config file not found: {config_path}")
95
+
96
+ # Search default locations
97
+ for default_path in self.DEFAULT_CONFIG_FILES:
98
+ path = Path(default_path)
99
+ if path.exists():
100
+ return path
101
+
102
+ return None
103
+
104
+ def load(self) -> StackConfigFile:
105
+ """Load configuration from file."""
106
+ if not self.config_path:
107
+ # Return empty config
108
+ self.config = StackConfigFile()
109
+ return self.config
110
+
111
+ with open(self.config_path) as f:
112
+ data = yaml.safe_load(f) or {}
113
+
114
+ # Expand environment variables
115
+ data = self._expand_env_vars(data)
116
+
117
+ self.config = StackConfigFile(
118
+ stacks=data.get("stacks", {}),
119
+ resources=data.get("resources", {}),
120
+ docker=data.get("docker", {}),
121
+ default_stack=data.get("default_stack"),
122
+ )
123
+
124
+ return self.config
125
+
126
+ def _expand_env_vars(self, data: Any) -> Any:
127
+ """Recursively expand environment variables in config."""
128
+ if isinstance(data, dict):
129
+ return {k: self._expand_env_vars(v) for k, v in data.items()}
130
+ elif isinstance(data, list):
131
+ return [self._expand_env_vars(item) for item in data]
132
+ elif isinstance(data, str):
133
+ # Expand ${VAR} or $VAR
134
+ import re
135
+
136
+ def replace_var(match):
137
+ var_name = match.group(1) or match.group(2)
138
+ return os.getenv(var_name, match.group(0))
139
+
140
+ pattern = r"\$\{([^}]+)\}|\$([A-Z_][A-Z0-9_]*)"
141
+ return re.sub(pattern, replace_var, data)
142
+ else:
143
+ return data
144
+
145
+ def get_stack_config(self, stack_name: str) -> dict[str, Any] | None:
146
+ """Get configuration for a specific stack."""
147
+ if not self.config:
148
+ return None
149
+ return self.config.stacks.get(stack_name)
150
+
151
+ def get_resource_config(self, resource_name: str = "default") -> dict[str, Any] | None:
152
+ """Get resource configuration."""
153
+ if not self.config:
154
+ return None
155
+ return self.config.resources.get(resource_name)
156
+
157
+ def get_docker_config(self) -> dict[str, Any]:
158
+ """Get Docker configuration."""
159
+ if not self.config:
160
+ return self._get_default_docker_config()
161
+ return self.config.docker or self._get_default_docker_config()
162
+
163
+ def _get_default_docker_config(self) -> dict[str, Any]:
164
+ """Get default Docker configuration."""
165
+ # Auto-detect Dockerfile
166
+ dockerfile = None
167
+ for possible_dockerfile in ["Dockerfile", "docker/Dockerfile", ".docker/Dockerfile"]:
168
+ if Path(possible_dockerfile).exists():
169
+ dockerfile = possible_dockerfile
170
+ break
171
+
172
+ # Check for poetry
173
+ use_poetry = Path("pyproject.toml").exists()
174
+
175
+ # Check for requirements.txt
176
+ requirements_file = "requirements.txt" if Path("requirements.txt").exists() else None
177
+
178
+ return {
179
+ "dockerfile": dockerfile,
180
+ "build_context": ".",
181
+ "use_poetry": use_poetry,
182
+ "requirements_file": requirements_file,
183
+ "base_image": "python:3.11-slim",
184
+ }
185
+
186
+ def list_stacks(self) -> list[str]:
187
+ """List all configured stacks."""
188
+ if not self.config:
189
+ return []
190
+ return list(self.config.stacks.keys())
191
+
192
+ def get_default_stack(self) -> str | None:
193
+ """Get the default stack name."""
194
+ if not self.config:
195
+ return None
196
+ return self.config.default_stack or (list(self.config.stacks.keys())[0] if self.config.stacks else None)
197
+
198
+
199
+ def load_config(config_path: str | None = None) -> ConfigLoader:
200
+ """Load flowyml configuration.
201
+
202
+ Args:
203
+ config_path: Path to config file. If None, searches default locations.
204
+
205
+ Returns:
206
+ ConfigLoader instance
207
+ """
208
+ return ConfigLoader(config_path)
209
+
210
+
211
+ def create_stack_from_config(config: dict[str, Any], name: str):
212
+ """Create a stack instance from configuration dictionary.
213
+
214
+ Args:
215
+ config: Stack configuration dictionary
216
+ name: Stack name
217
+
218
+ Returns:
219
+ Stack instance
220
+ """
221
+ stack_type = config.get("type", "local")
222
+
223
+ if stack_type == "local":
224
+ from flowyml.stacks.local import LocalStack
225
+
226
+ return LocalStack(
227
+ name=name,
228
+ artifact_path=config.get("artifact_store", {}).get("path", ".flowyml/artifacts"),
229
+ metadata_path=config.get("metadata_store", {}).get("path", ".flowyml/metadata.db"),
230
+ )
231
+
232
+ elif stack_type == "gcp":
233
+ from flowyml.stacks.gcp import GCPStack
234
+
235
+ return GCPStack(
236
+ name=name,
237
+ project_id=config.get("project_id"),
238
+ region=config.get("region", "us-central1"),
239
+ bucket_name=config.get("artifact_store", {}).get("bucket"),
240
+ registry_uri=config.get("container_registry", {}).get("uri"),
241
+ service_account=config.get("orchestrator", {}).get("service_account"),
242
+ )
243
+
244
+ else:
245
+ raise ValueError(f"Unknown stack type: {stack_type}")
246
+
247
+
248
+ def create_resource_config_from_dict(config: dict[str, Any]):
249
+ """Create ResourceConfig from dictionary."""
250
+ from flowyml.stacks.components import ResourceConfig
251
+
252
+ return ResourceConfig(**config)
253
+
254
+
255
+ def create_docker_config_from_dict(config: dict[str, Any]):
256
+ """Create DockerConfig from dictionary."""
257
+ from flowyml.stacks.components import DockerConfig
258
+
259
+ # Handle poetry configuration
260
+ if config.get("use_poetry"):
261
+ # Read dependencies from pyproject.toml
262
+ import toml
263
+
264
+ try:
265
+ with open("pyproject.toml") as f:
266
+ pyproject = toml.load(f)
267
+
268
+ # Extract dependencies
269
+ deps = pyproject.get("tool", {}).get("poetry", {}).get("dependencies", {})
270
+ requirements = [
271
+ f"{name}>={version.replace('^', '')}"
272
+ for name, version in deps.items()
273
+ if name != "python" and isinstance(version, str)
274
+ ]
275
+ config["requirements"] = requirements
276
+ except Exception:
277
+ pass
278
+
279
+ # Handle requirements file
280
+ elif config.get("requirements_file"):
281
+ try:
282
+ with open(config["requirements_file"]) as f:
283
+ requirements = [line.strip() for line in f if line.strip() and not line.startswith("#")]
284
+ config["requirements"] = requirements
285
+ except Exception:
286
+ pass
287
+
288
+ return DockerConfig(
289
+ image=config.get("image"),
290
+ dockerfile=config.get("dockerfile"),
291
+ build_context=config.get("build_context", "."),
292
+ requirements=config.get("requirements"),
293
+ base_image=config.get("base_image", "python:3.11-slim"),
294
+ env_vars=config.get("env_vars", {}),
295
+ build_args=config.get("build_args", {}),
296
+ )
@@ -0,0 +1,270 @@
1
+ """Pydantic schemas and validation utilities for flowyml."""
2
+
3
+ from enum import Enum
4
+ from typing import Any, Literal
5
+ from pydantic import BaseModel, Field, field_validator, ConfigDict
6
+
7
+
8
+ class CacheStrategy(str, Enum):
9
+ """Cache strategy options."""
10
+
11
+ CODE_HASH = "code_hash"
12
+ INPUT_HASH = "input_hash"
13
+ SEMANTIC = "semantic"
14
+ CUSTOM = "custom"
15
+ NONE = "none"
16
+
17
+
18
+ class ResourceRequirements(BaseModel):
19
+ """Resource requirements for step execution."""
20
+
21
+ model_config = ConfigDict(extra="forbid")
22
+
23
+ cpus: int | None = Field(None, ge=1, description="Number of CPUs required")
24
+ memory: str | None = Field(None, description="Memory required (e.g., '4GB', '512MB')")
25
+ gpus: int | None = Field(None, ge=0, description="Number of GPUs required")
26
+ disk: str | None = Field(None, description="Disk space required")
27
+
28
+ @field_validator("memory", "disk")
29
+ @classmethod
30
+ def validate_size_format(cls, v: str | None) -> str | None:
31
+ """Validate memory/disk size format."""
32
+ if v is None:
33
+ return v
34
+
35
+ valid_units = ["B", "KB", "MB", "GB", "TB"]
36
+ v_upper = v.upper()
37
+
38
+ for unit in valid_units:
39
+ if v_upper.endswith(unit):
40
+ try:
41
+ size_val = v_upper[: -len(unit)]
42
+ float(size_val)
43
+ return v
44
+ except ValueError:
45
+ raise ValueError(f"Invalid size format: {v}. Expected format: <number><unit> (e.g., '4GB')")
46
+
47
+ raise ValueError(f"Invalid size unit. Must be one of: {', '.join(valid_units)}")
48
+
49
+
50
+ class RetryConfig(BaseModel):
51
+ """Retry configuration for step execution."""
52
+
53
+ model_config = ConfigDict(extra="forbid")
54
+
55
+ max_attempts: int = Field(3, ge=1, le=10, description="Maximum number of retry attempts")
56
+ initial_delay: float = Field(1.0, ge=0, description="Initial delay in seconds")
57
+ max_delay: float = Field(60.0, ge=0, description="Maximum delay in seconds")
58
+ exponential_base: float = Field(2.0, ge=1.0, description="Exponential backoff base")
59
+ retry_on: list[str] | None = Field(None, description="Exception types to retry on")
60
+ not_retry_on: list[str] | None = Field(None, description="Exception types not to retry on")
61
+
62
+ @field_validator("max_delay")
63
+ @classmethod
64
+ def max_delay_greater_than_initial(cls, v: float, info) -> float:
65
+ """Validate max_delay is greater than initial_delay."""
66
+ if "initial_delay" in info.data and v < info.data["initial_delay"]:
67
+ raise ValueError("max_delay must be greater than or equal to initial_delay")
68
+ return v
69
+
70
+
71
+ class StepConfig(BaseModel):
72
+ """Configuration for pipeline steps."""
73
+
74
+ model_config = ConfigDict(extra="forbid")
75
+
76
+ name: str = Field(..., min_length=1, description="Step name")
77
+ inputs: list[str] = Field(default_factory=list, description="Input asset names")
78
+ outputs: list[str] = Field(default_factory=list, description="Output asset names")
79
+ cache: CacheStrategy | bool = Field(True, description="Caching strategy")
80
+ retry: RetryConfig | None = Field(None, description="Retry configuration")
81
+ timeout: int | None = Field(None, ge=1, description="Timeout in seconds")
82
+ resources: ResourceRequirements | None = Field(None, description="Resource requirements")
83
+ tags: dict[str, str] = Field(default_factory=dict, description="Step tags")
84
+ enable_cache: bool = Field(True, description="Enable caching for this step")
85
+
86
+
87
+ class PipelineConfig(BaseModel):
88
+ """Configuration for pipelines."""
89
+
90
+ model_config = ConfigDict(extra="allow") # Allow extra fields for flexibility
91
+
92
+ name: str = Field(..., min_length=1, description="Pipeline name")
93
+ description: str | None = Field(None, description="Pipeline description")
94
+ version: str | None = Field(None, description="Pipeline version")
95
+ tags: dict[str, str] = Field(default_factory=dict, description="Pipeline tags")
96
+ enable_cache: bool = Field(True, description="Enable caching globally")
97
+ fail_fast: bool = Field(True, description="Stop on first error")
98
+ max_parallel: int | None = Field(None, ge=1, description="Maximum parallel steps")
99
+
100
+
101
+ class ContextConfig(BaseModel):
102
+ """Configuration for pipeline context."""
103
+
104
+ model_config = ConfigDict(extra="allow") # Allow arbitrary parameters
105
+
106
+ # Common ML parameters
107
+ learning_rate: float | None = Field(None, gt=0, lt=1, description="Learning rate")
108
+ batch_size: int | None = Field(None, ge=1, description="Batch size")
109
+ epochs: int | None = Field(None, ge=1, description="Number of epochs")
110
+ seed: int | None = Field(None, description="Random seed")
111
+
112
+ # Infrastructure parameters
113
+ device: str | None = Field(None, description="Device (cpu/cuda/mps)")
114
+ num_workers: int | None = Field(None, ge=0, description="Number of workers")
115
+
116
+ # Experiment metadata
117
+ experiment_name: str | None = Field(None, description="Experiment name")
118
+ run_name: str | None = Field(None, description="Run name")
119
+ tags: dict[str, Any] = Field(default_factory=dict, description="Context tags")
120
+
121
+ @field_validator("device")
122
+ @classmethod
123
+ def validate_device(cls, v: str | None) -> str | None:
124
+ """Validate device string."""
125
+ if v is None:
126
+ return v
127
+
128
+ valid_devices = ["cpu", "cuda", "mps", "tpu"]
129
+ if v.lower() not in valid_devices and not v.startswith("cuda:"):
130
+ raise ValueError(f"Invalid device. Must be one of: {', '.join(valid_devices)} or 'cuda:N'")
131
+
132
+ return v.lower()
133
+
134
+
135
+ class StackConfig(BaseModel):
136
+ """Configuration for execution stacks."""
137
+
138
+ model_config = ConfigDict(extra="forbid")
139
+
140
+ name: str = Field(..., min_length=1, description="Stack name")
141
+ executor_type: Literal["local", "aws", "gcp", "azure", "kubernetes"] = Field(
142
+ "local",
143
+ description="Executor type",
144
+ )
145
+ artifact_store_path: str = Field(".flowyml/artifacts", description="Artifact storage path")
146
+ metadata_store_path: str = Field(".flowyml/metadata.db", description="Metadata storage path")
147
+ container_registry: str | None = Field(None, description="Container registry URL")
148
+ enable_caching: bool = Field(True, description="Enable caching")
149
+
150
+
151
+ class DatasetSchema(BaseModel):
152
+ """Schema for dataset validation."""
153
+
154
+ model_config = ConfigDict(extra="allow")
155
+
156
+ name: str = Field(..., description="Dataset name")
157
+ version: str | None = Field(None, description="Dataset version")
158
+ num_samples: int | None = Field(None, ge=0, description="Number of samples")
159
+ num_features: int | None = Field(None, ge=0, description="Number of features")
160
+ feature_names: list[str] | None = Field(None, description="Feature names")
161
+ target_column: str | None = Field(None, description="Target column name")
162
+ task_type: Literal["classification", "regression", "clustering", "other"] | None = Field(
163
+ None,
164
+ description="ML task type",
165
+ )
166
+
167
+
168
+ class ModelSchema(BaseModel):
169
+ """Schema for model validation."""
170
+
171
+ model_config = ConfigDict(extra="allow")
172
+
173
+ name: str = Field(..., description="Model name")
174
+ framework: Literal["pytorch", "tensorflow", "sklearn", "xgboost", "lightgbm", "other"] = Field(
175
+ ...,
176
+ description="ML framework",
177
+ )
178
+ architecture: str | None = Field(None, description="Model architecture")
179
+ input_shape: list[int] | None = Field(None, description="Input shape")
180
+ output_shape: list[int] | None = Field(None, description="Output shape")
181
+ num_parameters: int | None = Field(None, ge=0, description="Number of parameters")
182
+
183
+
184
+ class MetricsSchema(BaseModel):
185
+ """Schema for metrics validation."""
186
+
187
+ model_config = ConfigDict(extra="allow")
188
+
189
+ accuracy: float | None = Field(None, ge=0, le=1, description="Accuracy")
190
+ precision: float | None = Field(None, ge=0, le=1, description="Precision")
191
+ recall: float | None = Field(None, ge=0, le=1, description="Recall")
192
+ f1_score: float | None = Field(None, ge=0, le=1, description="F1 score")
193
+ loss: float | None = Field(None, ge=0, description="Loss")
194
+ mse: float | None = Field(None, ge=0, description="Mean squared error")
195
+ mae: float | None = Field(None, ge=0, description="Mean absolute error")
196
+ r2_score: float | None = Field(None, description="R2 score")
197
+
198
+
199
+ class ExperimentConfig(BaseModel):
200
+ """Configuration for experiments."""
201
+
202
+ model_config = ConfigDict(extra="allow")
203
+
204
+ name: str = Field(..., description="Experiment name")
205
+ description: str | None = Field(None, description="Experiment description")
206
+ tags: dict[str, str] = Field(default_factory=dict, description="Experiment tags")
207
+ parameters: dict[str, Any] = Field(default_factory=dict, description="Experiment parameters")
208
+ track_code: bool = Field(True, description="Track code changes")
209
+ track_environment: bool = Field(True, description="Track environment")
210
+ track_system: bool = Field(True, description="Track system info")
211
+
212
+
213
+ def validate_step_config(config: dict[str, Any]) -> StepConfig:
214
+ """Validate step configuration.
215
+
216
+ Args:
217
+ config: Step configuration dictionary
218
+
219
+ Returns:
220
+ Validated StepConfig instance
221
+
222
+ Raises:
223
+ ValidationError: If validation fails
224
+ """
225
+ return StepConfig(**config)
226
+
227
+
228
+ def validate_pipeline_config(config: dict[str, Any]) -> PipelineConfig:
229
+ """Validate pipeline configuration.
230
+
231
+ Args:
232
+ config: Pipeline configuration dictionary
233
+
234
+ Returns:
235
+ Validated PipelineConfig instance
236
+
237
+ Raises:
238
+ ValidationError: If validation fails
239
+ """
240
+ return PipelineConfig(**config)
241
+
242
+
243
+ def validate_context_config(config: dict[str, Any]) -> ContextConfig:
244
+ """Validate context configuration.
245
+
246
+ Args:
247
+ config: Context configuration dictionary
248
+
249
+ Returns:
250
+ Validated ContextConfig instance
251
+
252
+ Raises:
253
+ ValidationError: If validation fails
254
+ """
255
+ return ContextConfig(**config)
256
+
257
+
258
+ def validate_metrics(metrics: dict[str, Any]) -> MetricsSchema:
259
+ """Validate metrics.
260
+
261
+ Args:
262
+ metrics: Metrics dictionary
263
+
264
+ Returns:
265
+ Validated MetricsSchema instance
266
+
267
+ Raises:
268
+ ValidationError: If validation fails
269
+ """
270
+ return MetricsSchema(**metrics)