flow-compute 2.0.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 (126) hide show
  1. flow/__init__.py +68 -0
  2. flow/_internal/auth.py +333 -0
  3. flow/_internal/config.py +217 -0
  4. flow/_internal/config_loader.py +224 -0
  5. flow/_internal/data/__init__.py +11 -0
  6. flow/_internal/data/loaders.py +303 -0
  7. flow/_internal/data/mount_processor.py +137 -0
  8. flow/_internal/data/resolver.py +106 -0
  9. flow/_internal/frontends/__init__.py +10 -0
  10. flow/_internal/frontends/base.py +72 -0
  11. flow/_internal/frontends/cli/__init__.py +9 -0
  12. flow/_internal/frontends/cli/adapter.py +173 -0
  13. flow/_internal/frontends/registry.py +58 -0
  14. flow/_internal/frontends/slurm/__init__.py +17 -0
  15. flow/_internal/frontends/slurm/adapter.py +168 -0
  16. flow/_internal/frontends/slurm/converter.py +238 -0
  17. flow/_internal/frontends/slurm/parser.py +373 -0
  18. flow/_internal/frontends/submitit/__init__.py +5 -0
  19. flow/_internal/frontends/submitit/adapter.py +312 -0
  20. flow/_internal/frontends/yaml/__init__.py +5 -0
  21. flow/_internal/frontends/yaml/adapter.py +260 -0
  22. flow/_internal/init/__init__.py +14 -0
  23. flow/_internal/init/resolver.py +77 -0
  24. flow/_internal/init/validator.py +147 -0
  25. flow/_internal/init/writer.py +154 -0
  26. flow/_internal/integrations/__init__.py +1 -0
  27. flow/_internal/integrations/google_colab.py +408 -0
  28. flow/_internal/integrations/jupyter.py +530 -0
  29. flow/_internal/integrations/jupyter_persistence.py +343 -0
  30. flow/_internal/integrations/jupyter_session.py +207 -0
  31. flow/_internal/io/__init__.py +22 -0
  32. flow/_internal/io/http.py +205 -0
  33. flow/_internal/managers/__init__.py +5 -0
  34. flow/_internal/managers/task_manager.py +275 -0
  35. flow/_internal/storage/__init__.py +17 -0
  36. flow/_internal/storage/base.py +107 -0
  37. flow/_internal/storage/resolvers.py +158 -0
  38. flow/api/__init__.py +8 -0
  39. flow/api/client.py +1329 -0
  40. flow/api/decorators.py +560 -0
  41. flow/api/invoke.py +633 -0
  42. flow/api/models.py +1146 -0
  43. flow/cli/__init__.py +11 -0
  44. flow/cli/__main__.py +8 -0
  45. flow/cli/app.py +51 -0
  46. flow/cli/commands/README.md +98 -0
  47. flow/cli/commands/__init__.py +34 -0
  48. flow/cli/commands/base.py +64 -0
  49. flow/cli/commands/cancel.py +70 -0
  50. flow/cli/commands/example.py +92 -0
  51. flow/cli/commands/init.py +1362 -0
  52. flow/cli/commands/logs.py +90 -0
  53. flow/cli/commands/run.py +158 -0
  54. flow/cli/commands/ssh.py +89 -0
  55. flow/cli/commands/status.py +119 -0
  56. flow/cli/commands/utils.py +122 -0
  57. flow/cli/commands/volumes.py +225 -0
  58. flow/cli/formatters/__init__.py +1 -0
  59. flow/cli/formatters/slurm.py +191 -0
  60. flow/cli/main.py +8 -0
  61. flow/cli/migrate.py +65 -0
  62. flow/core/__init__.py +25 -0
  63. flow/core/code_packager.py +127 -0
  64. flow/core/engine.py +243 -0
  65. flow/core/interfaces.py +993 -0
  66. flow/core/resources/__init__.py +6 -0
  67. flow/core/resources/matcher.py +115 -0
  68. flow/core/resources/parser.py +82 -0
  69. flow/core/task_engine.py +438 -0
  70. flow/errors.py +634 -0
  71. flow/providers/__init__.py +32 -0
  72. flow/providers/base.py +105 -0
  73. flow/providers/factory.py +34 -0
  74. flow/providers/fcp/README.md +98 -0
  75. flow/providers/fcp/__init__.py +15 -0
  76. flow/providers/fcp/adapters/__init__.py +20 -0
  77. flow/providers/fcp/adapters/models.py +273 -0
  78. flow/providers/fcp/adapters/mounts.py +116 -0
  79. flow/providers/fcp/adapters/storage.py +93 -0
  80. flow/providers/fcp/api/__init__.py +31 -0
  81. flow/providers/fcp/api/handlers.py +131 -0
  82. flow/providers/fcp/api/types.py +170 -0
  83. flow/providers/fcp/bidding/__init__.py +28 -0
  84. flow/providers/fcp/bidding/builder.py +189 -0
  85. flow/providers/fcp/bidding/finder.py +386 -0
  86. flow/providers/fcp/bidding/manager.py +251 -0
  87. flow/providers/fcp/core/__init__.py +81 -0
  88. flow/providers/fcp/core/constants.py +258 -0
  89. flow/providers/fcp/core/errors.py +70 -0
  90. flow/providers/fcp/core/models.py +134 -0
  91. flow/providers/fcp/provider.py +2041 -0
  92. flow/providers/fcp/provider.py,cover +1718 -0
  93. flow/providers/fcp/resources/__init__.py +30 -0
  94. flow/providers/fcp/resources/gpu.py +57 -0
  95. flow/providers/fcp/resources/instances.py +138 -0
  96. flow/providers/fcp/resources/projects.py +161 -0
  97. flow/providers/fcp/resources/ssh.py +476 -0
  98. flow/providers/fcp/runtime/__init__.py +13 -0
  99. flow/providers/fcp/runtime/quota.py +47 -0
  100. flow/providers/fcp/runtime/startup/__init__.py +36 -0
  101. flow/providers/fcp/runtime/startup/builder.py +324 -0
  102. flow/providers/fcp/runtime/startup/legacy.py +49 -0
  103. flow/providers/fcp/runtime/startup/sections.py +797 -0
  104. flow/providers/fcp/runtime/startup/templates.py +148 -0
  105. flow/providers/local/__init__.py +23 -0
  106. flow/providers/local/config.py +134 -0
  107. flow/providers/local/executor.py +537 -0
  108. flow/providers/local/logs.py +224 -0
  109. flow/providers/local/provider.py +484 -0
  110. flow/providers/local/storage.py +151 -0
  111. flow/providers/registry.py +158 -0
  112. flow/utils/__init__.py +0 -0
  113. flow/utils/cache.py +128 -0
  114. flow/utils/exceptions.py +15 -0
  115. flow/utils/instance_parser.py +213 -0
  116. flow/utils/instance_validator.py +272 -0
  117. flow/utils/region_validator.py +32 -0
  118. flow/utils/retry.py +82 -0
  119. flow/utils/retry_helper.py +189 -0
  120. flow/utils/security.py +109 -0
  121. flow/utils/validation.py +171 -0
  122. flow_compute-2.0.0.dist-info/METADATA +1115 -0
  123. flow_compute-2.0.0.dist-info/RECORD +126 -0
  124. flow_compute-2.0.0.dist-info/WHEEL +4 -0
  125. flow_compute-2.0.0.dist-info/entry_points.txt +2 -0
  126. flow_compute-2.0.0.dist-info/licenses/LICENSE.txt +202 -0
flow/__init__.py ADDED
@@ -0,0 +1,68 @@
1
+ """Flow SDK - GPU compute made simple."""
2
+
3
+ # Public API imports
4
+ from flow.api.client import Flow
5
+ from flow.api.decorators import FlowApp, app
6
+ from flow.api.invoke import invoke
7
+ from flow.api.models import Task, TaskConfig, TaskStatus, Volume, VolumeSpec
8
+
9
+ # Public errors and constants
10
+ from flow.errors import (
11
+ APIError,
12
+ AuthenticationError,
13
+ ConfigParserError,
14
+ FlowError,
15
+ FlowOperationError,
16
+ NetworkError,
17
+ ProviderError,
18
+ QuotaExceededError,
19
+ ResourceNotAvailableError,
20
+ ResourceNotFoundError,
21
+ TaskExecutionError,
22
+ TaskNotFoundError,
23
+ TimeoutError,
24
+ ValidationAPIError,
25
+ ValidationError,
26
+ VolumeError,
27
+ )
28
+ from flow.providers.fcp.core.constants import DEFAULT_REGION
29
+
30
+ # Version
31
+ try:
32
+ from importlib.metadata import version
33
+ __version__ = version("flow-sdk")
34
+ except Exception:
35
+ __version__ = "0.0.0+unknown"
36
+
37
+ __all__ = [
38
+ # Main API
39
+ "Flow",
40
+ "FlowApp",
41
+ "invoke",
42
+ "app",
43
+ # Models
44
+ "TaskConfig",
45
+ "Task",
46
+ "Volume",
47
+ "VolumeSpec",
48
+ "TaskStatus",
49
+ # Errors
50
+ "FlowError",
51
+ "AuthenticationError",
52
+ "ResourceNotFoundError",
53
+ "TaskNotFoundError",
54
+ "ValidationError",
55
+ "APIError",
56
+ "ValidationAPIError",
57
+ "NetworkError",
58
+ "TimeoutError",
59
+ "ProviderError",
60
+ "ConfigParserError",
61
+ "ResourceNotAvailableError",
62
+ "QuotaExceededError",
63
+ "VolumeError",
64
+ "TaskExecutionError",
65
+ "FlowOperationError",
66
+ # Constants
67
+ "DEFAULT_REGION",
68
+ ]
flow/_internal/auth.py ADDED
@@ -0,0 +1,333 @@
1
+ """Authentication support for Flow SDK.
2
+
3
+ Supports multiple authentication methods:
4
+ - API key authentication (default)
5
+ - Email/password authentication with session management
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+ from datetime import datetime, timedelta
12
+ from pathlib import Path
13
+ from typing import Any, Dict, Optional
14
+
15
+ from flow.core.interfaces import IHttpClient
16
+ from flow.errors import AuthenticationError
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class AuthConfig:
22
+ """Authentication configuration."""
23
+
24
+ def __init__(
25
+ self,
26
+ api_key: Optional[str] = None,
27
+ email: Optional[str] = None,
28
+ password: Optional[str] = None,
29
+ session_file: Optional[Path] = None,
30
+ ):
31
+ """Initialize auth config.
32
+
33
+ Args:
34
+ api_key: API key for authentication
35
+ email: Email for email/password auth
36
+ password: Password for email/password auth
37
+ session_file: Path to store session data
38
+ """
39
+ self.api_key = api_key or os.getenv("FCP_API_KEY")
40
+ self.email = email
41
+ self.password = password
42
+ self.session_file = session_file or self._default_session_file()
43
+
44
+ def _default_session_file(self) -> Path:
45
+ """Get default session file path."""
46
+ home = Path.home()
47
+ flow_dir = home / ".flow"
48
+ flow_dir.mkdir(exist_ok=True)
49
+ return flow_dir / "session.json"
50
+
51
+ @property
52
+ def has_api_key(self) -> bool:
53
+ """Check if API key is available."""
54
+ return bool(self.api_key)
55
+
56
+ @property
57
+ def has_credentials(self) -> bool:
58
+ """Check if email/password credentials are available."""
59
+ return bool(self.email and self.password)
60
+
61
+
62
+ class Session:
63
+ """Authentication session data."""
64
+
65
+ def __init__(self, token: str, expires_at: datetime, user_id: str):
66
+ """Initialize session.
67
+
68
+ Args:
69
+ token: Session token
70
+ expires_at: When session expires
71
+ user_id: ID of authenticated user
72
+ """
73
+ self.token = token
74
+ self.expires_at = expires_at
75
+ self.user_id = user_id
76
+
77
+ @property
78
+ def is_valid(self) -> bool:
79
+ """Check if session is still valid."""
80
+ return datetime.now() < self.expires_at
81
+
82
+ def to_dict(self) -> Dict[str, Any]:
83
+ """Convert to dictionary for serialization."""
84
+ return {
85
+ "token": self.token,
86
+ "expires_at": self.expires_at.isoformat(),
87
+ "user_id": self.user_id,
88
+ }
89
+
90
+ @classmethod
91
+ def from_dict(cls, data: Dict[str, Any]) -> "Session":
92
+ """Create from dictionary."""
93
+ return cls(
94
+ token=data["token"],
95
+ expires_at=datetime.fromisoformat(data["expires_at"]),
96
+ user_id=data["user_id"],
97
+ )
98
+
99
+
100
+ class Authenticator:
101
+ """Handles authentication for Flow SDK."""
102
+
103
+ def __init__(self, config: AuthConfig, http_client: IHttpClient):
104
+ """Initialize authenticator.
105
+
106
+ Args:
107
+ config: Authentication configuration
108
+ http_client: HTTP client for API requests
109
+ """
110
+ self.config = config
111
+ self.http = http_client
112
+ self._session: Optional[Session] = None
113
+
114
+ def authenticate(self) -> str:
115
+ """Get authentication token.
116
+
117
+ Returns API key or session token based on configuration.
118
+
119
+ Returns:
120
+ Authentication token
121
+
122
+ Raises:
123
+ AuthenticationError: If authentication fails
124
+ """
125
+ # Try API key first
126
+ if self.config.has_api_key:
127
+ return self.config.api_key
128
+
129
+ # Try existing session
130
+ if self._session and self._session.is_valid:
131
+ return self._session.token
132
+
133
+ # Try loading saved session
134
+ saved_session = self._load_session()
135
+ if saved_session and saved_session.is_valid:
136
+ self._session = saved_session
137
+ return saved_session.token
138
+
139
+ # Try email/password authentication
140
+ if self.config.has_credentials:
141
+ session = self._authenticate_with_credentials()
142
+ self._session = session
143
+ self._save_session(session)
144
+ return session.token
145
+
146
+ raise AuthenticationError(
147
+ "No valid authentication method available. "
148
+ "Set FCP_API_KEY or provide email/password."
149
+ )
150
+
151
+ def get_access_token(self) -> str:
152
+ """Get access token for API requests.
153
+
154
+ This is a convenience method that wraps authenticate()
155
+ for compatibility with existing code.
156
+
157
+ Returns:
158
+ Authentication token
159
+
160
+ Raises:
161
+ AuthenticationError: If authentication fails
162
+ """
163
+ return self.authenticate()
164
+
165
+ def _authenticate_with_credentials(self) -> Session:
166
+ """Authenticate with email/password.
167
+
168
+ Returns:
169
+ Session object
170
+
171
+ Raises:
172
+ AuthenticationError: If authentication fails
173
+ """
174
+ try:
175
+ response = self.http.request(
176
+ method="POST",
177
+ url="/auth/login",
178
+ json={
179
+ "email": self.config.email,
180
+ "password": self.config.password,
181
+ },
182
+ retry_server_errors=False, # Don't retry auth failures
183
+ )
184
+
185
+ # Extract session data
186
+ token = response.get("token")
187
+ expires_in = response.get("expires_in", 3600) # Default 1 hour
188
+ user_id = response.get("user_id", "")
189
+
190
+ if not token:
191
+ raise AuthenticationError("No token in login response")
192
+
193
+ # Create session
194
+ expires_at = datetime.now() + timedelta(seconds=expires_in)
195
+ session = Session(token, expires_at, user_id)
196
+
197
+ logger.info(f"Successfully authenticated as user {user_id}")
198
+ return session
199
+
200
+ except Exception as e:
201
+ raise AuthenticationError(f"Login failed: {e}") from e
202
+
203
+ def logout(self):
204
+ """Log out and clear session."""
205
+ if self._session:
206
+ try:
207
+ # Notify server
208
+ self.http.request(
209
+ method="POST",
210
+ url="/auth/logout",
211
+ headers={"Authorization": f"Bearer {self._session.token}"},
212
+ )
213
+ except Exception as e:
214
+ logger.warning(f"Logout request failed: {e}")
215
+
216
+ # Clear local session
217
+ self._session = None
218
+ self._clear_saved_session()
219
+
220
+ def _load_session(self) -> Optional[Session]:
221
+ """Load saved session from file."""
222
+ if not self.config.session_file.exists():
223
+ return None
224
+
225
+ try:
226
+ with open(self.config.session_file) as f:
227
+ data = json.load(f)
228
+ return Session.from_dict(data)
229
+ except Exception as e:
230
+ logger.warning(f"Failed to load session: {e}")
231
+ return None
232
+
233
+ def _save_session(self, session: Session):
234
+ """Save session to file."""
235
+ try:
236
+ # Ensure directory exists
237
+ self.config.session_file.parent.mkdir(parents=True, exist_ok=True)
238
+
239
+ # Save with restricted permissions
240
+ with open(self.config.session_file, "w") as f:
241
+ json.dump(session.to_dict(), f)
242
+
243
+ # Set file permissions (Unix only)
244
+ try:
245
+ os.chmod(self.config.session_file, 0o600)
246
+ except AttributeError:
247
+ pass # Windows doesn't support chmod
248
+
249
+ except Exception as e:
250
+ logger.warning(f"Failed to save session: {e}")
251
+
252
+ def _clear_saved_session(self):
253
+ """Remove saved session file."""
254
+ try:
255
+ if self.config.session_file.exists():
256
+ self.config.session_file.unlink()
257
+ except Exception as e:
258
+ logger.warning(f"Failed to clear session: {e}")
259
+
260
+
261
+ def ensure_initialized() -> bool:
262
+ """Ensure Flow is properly configured, launching interactive setup if needed.
263
+
264
+ Returns:
265
+ True if configuration is valid, False if setup was cancelled
266
+ """
267
+ from flow._internal.config_loader import ConfigLoader
268
+
269
+ loader = ConfigLoader()
270
+ if loader.has_valid_config():
271
+ return True
272
+
273
+ # No valid configuration found - launch interactive setup
274
+ print("\nNo Flow configuration found. Run setup to configure.\n")
275
+ return False
276
+
277
+
278
+ def validate_config() -> bool:
279
+ """Validate current configuration and connectivity.
280
+
281
+ Returns:
282
+ True if configuration is valid and API is reachable
283
+ """
284
+ try:
285
+ from flow import Flow
286
+
287
+ # Try to create client - this will validate credentials
288
+ client = Flow()
289
+ # Try a simple API call to verify connectivity
290
+ try:
291
+ client.status("test-connection")
292
+ except Exception as e:
293
+ # If error is "not found", auth worked
294
+ if "not found" in str(e).lower():
295
+ return True
296
+ raise
297
+ return True
298
+ except AuthenticationError:
299
+ logger.error("Authentication failed - invalid API key")
300
+ return False
301
+ except Exception as e:
302
+ logger.error(f"Configuration validation failed: {e}")
303
+ return False
304
+
305
+
306
+ def create_authenticator(
307
+ api_key: Optional[str] = None,
308
+ email: Optional[str] = None,
309
+ password: Optional[str] = None,
310
+ http_client: Optional[IHttpClient] = None,
311
+ ) -> Authenticator:
312
+ """Create authenticator with config.
313
+
314
+ Args:
315
+ api_key: API key (or from FCP_API_KEY env)
316
+ email: Email for login
317
+ password: Password for login
318
+ http_client: HTTP client to use
319
+
320
+ Returns:
321
+ Configured authenticator
322
+ """
323
+ from flow._internal.io.http import HttpClient
324
+
325
+ config = AuthConfig(api_key=api_key, email=email, password=password)
326
+
327
+ if not http_client:
328
+ # Create basic HTTP client for auth requests
329
+ http_client = HttpClient(
330
+ base_url=os.getenv("FCP_API_URL", "https://api.mlfoundry.com"),
331
+ )
332
+
333
+ return Authenticator(config, http_client)
@@ -0,0 +1,217 @@
1
+ """Configuration for Flow SDK.
2
+
3
+ Clean, provider-agnostic configuration system that separates
4
+ core SDK configuration from provider-specific settings.
5
+ """
6
+
7
+ import os
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Dict, Optional, Type
10
+
11
+
12
+ @dataclass
13
+ class Config:
14
+ """Provider-agnostic Flow SDK configuration.
15
+
16
+ Core configuration that works across all providers. This class provides
17
+ a unified interface for managing authentication and provider settings
18
+ regardless of the underlying compute provider.
19
+
20
+ Attributes:
21
+ provider: The compute provider to use (e.g., 'fcp').
22
+ auth_token: Authentication token for API access.
23
+ provider_config: Dictionary of provider-specific settings.
24
+
25
+ Example:
26
+ >>> # Create config from environment
27
+ >>> config = Config.from_env()
28
+
29
+ >>> # Create config manually
30
+ >>> config = Config(
31
+ ... provider="fcp",
32
+ ... auth_token="your-api-key",
33
+ ... provider_config={
34
+ ... "project": "my-project",
35
+ ... "region": "us-east-1"
36
+ ... }
37
+ ... )
38
+ """
39
+
40
+ provider: str = "fcp"
41
+ auth_token: Optional[str] = None
42
+ provider_config: Dict[str, Any] = field(default_factory=dict)
43
+
44
+ @classmethod
45
+ def from_env(cls, require_auth: bool = True) -> "Config":
46
+ """Create config from environment variables and config files.
47
+
48
+ Loads configuration from multiple sources in precedence order:
49
+ 1. Environment variables (highest priority)
50
+ 2. flow.yaml in current directory
51
+ 3. ~/.flow/config.yaml (lowest priority)
52
+
53
+ Environment variables:
54
+ FLOW_PROVIDER: Provider to use (default: fcp)
55
+ FCP_API_KEY: Authentication token for FCP provider
56
+ FCP_DEFAULT_PROJECT: Default project for FCP
57
+ FCP_DEFAULT_REGION: Default region for FCP
58
+ FCP_SSH_KEYS: Comma-separated SSH key names
59
+
60
+ Args:
61
+ require_auth: Whether to require authentication token.
62
+ Set to False for operations that don't need auth.
63
+
64
+ Returns:
65
+ Config: Loaded configuration object.
66
+
67
+ Raises:
68
+ ValueError: If authentication is required but not configured.
69
+
70
+ Example:
71
+ >>> # Load config requiring authentication
72
+ >>> config = Config.from_env(require_auth=True)
73
+
74
+ >>> # Load config for local operations
75
+ >>> config = Config.from_env(require_auth=False)
76
+ """
77
+ from flow._internal.config_loader import ConfigLoader
78
+
79
+ # Load from all sources with proper precedence
80
+ loader = ConfigLoader()
81
+ sources = loader.load_all_sources()
82
+
83
+ provider = sources.provider
84
+ auth_token = sources.api_key
85
+
86
+ # Load provider-specific config
87
+ provider_config = {}
88
+ if provider == "fcp":
89
+ provider_config = sources.get_fcp_config()
90
+
91
+ # Validate auth if required
92
+ if require_auth and (not auth_token or auth_token.startswith("YOUR_")):
93
+ raise ValueError(
94
+ "Authentication not configured. Please either:\n"
95
+ "1. Set FCP_API_KEY environment variable\n"
96
+ "2. Run 'flow init' to set up authentication\n"
97
+ "3. For tests, ensure FLOW_DISABLE_KEYCHAIN=1 is set"
98
+ )
99
+
100
+ return cls(provider=provider, auth_token=auth_token, provider_config=provider_config)
101
+
102
+ def get_headers(self) -> Dict[str, str]:
103
+ """Get HTTP headers for API requests.
104
+
105
+ Returns:
106
+ Dict[str, str]: Headers including authorization and content type.
107
+
108
+ Example:
109
+ >>> config = Config(auth_token="abc123")
110
+ >>> headers = config.get_headers()
111
+ >>> headers
112
+ {'Authorization': 'Bearer abc123', 'Content-Type': 'application/json'}
113
+ """
114
+ return {
115
+ "Authorization": f"Bearer {self.auth_token}",
116
+ "Content-Type": "application/json",
117
+ }
118
+
119
+
120
+ # Provider-specific configuration classes
121
+ @dataclass
122
+ class FCPConfig:
123
+ """FCP (Foundry Cloud Platform) provider-specific configuration.
124
+
125
+ Attributes:
126
+ api_url: Base URL for FCP API endpoints.
127
+ project: FCP project identifier.
128
+ region: Default region for resource creation.
129
+ ssh_keys: List of SSH key names for instance access.
130
+
131
+ Example:
132
+ >>> fcp_config = FCPConfig(
133
+ ... project="my-project",
134
+ ... region="us-east-1",
135
+ ... ssh_keys=["my-key", "team-key"]
136
+ ... )
137
+ """
138
+
139
+ api_url: str = field(
140
+ default_factory=lambda: os.getenv("FCP_API_URL", "https://api.mlfoundry.com")
141
+ )
142
+ project: Optional[str] = None
143
+ region: Optional[str] = None
144
+ ssh_keys: Optional[list[str]] = None
145
+
146
+ @classmethod
147
+ def from_dict(cls, data: Dict[str, Any]) -> "FCPConfig":
148
+ """Create FCPConfig from dictionary.
149
+
150
+ Args:
151
+ data: Dictionary containing configuration values.
152
+ Unknown keys are ignored.
153
+
154
+ Returns:
155
+ FCPConfig: Configuration object with values from dictionary.
156
+
157
+ Example:
158
+ >>> config_dict = {
159
+ ... "project": "ml-training",
160
+ ... "region": "us-west-2",
161
+ ... "ssh_keys": ["dev-key"],
162
+ ... "unknown_key": "ignored"
163
+ ... }
164
+ >>> fcp_config = FCPConfig.from_dict(config_dict)
165
+ >>> fcp_config.project
166
+ 'ml-training'
167
+ """
168
+ # Get default api_url from environment if not in data
169
+ default_api_url = os.getenv("FCP_API_URL", "https://api.mlfoundry.com")
170
+
171
+ return cls(
172
+ api_url=data.get("api_url", default_api_url),
173
+ project=data.get("project"),
174
+ region=data.get("region"),
175
+ ssh_keys=data.get("ssh_keys"),
176
+ )
177
+
178
+ @property
179
+ def api_key(self) -> Optional[str]:
180
+ """Legacy property for compatibility during migration.
181
+
182
+ This will be removed once all provider code is updated.
183
+ """
184
+ # Temporary: Auth token retrieval during migration period
185
+ # TODO: Remove once provider code is updated to use main Config
186
+ return os.environ.get("FCP_API_KEY")
187
+
188
+
189
+ # Registry for provider configurations
190
+ PROVIDER_CONFIGS: Dict[str, Type] = {
191
+ "fcp": FCPConfig,
192
+ }
193
+
194
+
195
+ def get_provider_config_class(provider: str) -> Type:
196
+ """Get the configuration class for a provider.
197
+
198
+ Args:
199
+ provider: Provider name (e.g., 'fcp').
200
+
201
+ Returns:
202
+ Type: The configuration class for the specified provider.
203
+
204
+ Raises:
205
+ ValueError: If the provider is not recognized.
206
+
207
+ Example:
208
+ >>> config_class = get_provider_config_class("fcp")
209
+ >>> config_class.__name__
210
+ 'FCPConfig'
211
+ """
212
+ if provider not in PROVIDER_CONFIGS:
213
+ raise ValueError(
214
+ f"Unknown provider: {provider}. "
215
+ f"Available providers: {', '.join(PROVIDER_CONFIGS.keys())}"
216
+ )
217
+ return PROVIDER_CONFIGS[provider]