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.
- flow/__init__.py +68 -0
- flow/_internal/auth.py +333 -0
- flow/_internal/config.py +217 -0
- flow/_internal/config_loader.py +224 -0
- flow/_internal/data/__init__.py +11 -0
- flow/_internal/data/loaders.py +303 -0
- flow/_internal/data/mount_processor.py +137 -0
- flow/_internal/data/resolver.py +106 -0
- flow/_internal/frontends/__init__.py +10 -0
- flow/_internal/frontends/base.py +72 -0
- flow/_internal/frontends/cli/__init__.py +9 -0
- flow/_internal/frontends/cli/adapter.py +173 -0
- flow/_internal/frontends/registry.py +58 -0
- flow/_internal/frontends/slurm/__init__.py +17 -0
- flow/_internal/frontends/slurm/adapter.py +168 -0
- flow/_internal/frontends/slurm/converter.py +238 -0
- flow/_internal/frontends/slurm/parser.py +373 -0
- flow/_internal/frontends/submitit/__init__.py +5 -0
- flow/_internal/frontends/submitit/adapter.py +312 -0
- flow/_internal/frontends/yaml/__init__.py +5 -0
- flow/_internal/frontends/yaml/adapter.py +260 -0
- flow/_internal/init/__init__.py +14 -0
- flow/_internal/init/resolver.py +77 -0
- flow/_internal/init/validator.py +147 -0
- flow/_internal/init/writer.py +154 -0
- flow/_internal/integrations/__init__.py +1 -0
- flow/_internal/integrations/google_colab.py +408 -0
- flow/_internal/integrations/jupyter.py +530 -0
- flow/_internal/integrations/jupyter_persistence.py +343 -0
- flow/_internal/integrations/jupyter_session.py +207 -0
- flow/_internal/io/__init__.py +22 -0
- flow/_internal/io/http.py +205 -0
- flow/_internal/managers/__init__.py +5 -0
- flow/_internal/managers/task_manager.py +275 -0
- flow/_internal/storage/__init__.py +17 -0
- flow/_internal/storage/base.py +107 -0
- flow/_internal/storage/resolvers.py +158 -0
- flow/api/__init__.py +8 -0
- flow/api/client.py +1329 -0
- flow/api/decorators.py +560 -0
- flow/api/invoke.py +633 -0
- flow/api/models.py +1146 -0
- flow/cli/__init__.py +11 -0
- flow/cli/__main__.py +8 -0
- flow/cli/app.py +51 -0
- flow/cli/commands/README.md +98 -0
- flow/cli/commands/__init__.py +34 -0
- flow/cli/commands/base.py +64 -0
- flow/cli/commands/cancel.py +70 -0
- flow/cli/commands/example.py +92 -0
- flow/cli/commands/init.py +1362 -0
- flow/cli/commands/logs.py +90 -0
- flow/cli/commands/run.py +158 -0
- flow/cli/commands/ssh.py +89 -0
- flow/cli/commands/status.py +119 -0
- flow/cli/commands/utils.py +122 -0
- flow/cli/commands/volumes.py +225 -0
- flow/cli/formatters/__init__.py +1 -0
- flow/cli/formatters/slurm.py +191 -0
- flow/cli/main.py +8 -0
- flow/cli/migrate.py +65 -0
- flow/core/__init__.py +25 -0
- flow/core/code_packager.py +127 -0
- flow/core/engine.py +243 -0
- flow/core/interfaces.py +993 -0
- flow/core/resources/__init__.py +6 -0
- flow/core/resources/matcher.py +115 -0
- flow/core/resources/parser.py +82 -0
- flow/core/task_engine.py +438 -0
- flow/errors.py +634 -0
- flow/providers/__init__.py +32 -0
- flow/providers/base.py +105 -0
- flow/providers/factory.py +34 -0
- flow/providers/fcp/README.md +98 -0
- flow/providers/fcp/__init__.py +15 -0
- flow/providers/fcp/adapters/__init__.py +20 -0
- flow/providers/fcp/adapters/models.py +273 -0
- flow/providers/fcp/adapters/mounts.py +116 -0
- flow/providers/fcp/adapters/storage.py +93 -0
- flow/providers/fcp/api/__init__.py +31 -0
- flow/providers/fcp/api/handlers.py +131 -0
- flow/providers/fcp/api/types.py +170 -0
- flow/providers/fcp/bidding/__init__.py +28 -0
- flow/providers/fcp/bidding/builder.py +189 -0
- flow/providers/fcp/bidding/finder.py +386 -0
- flow/providers/fcp/bidding/manager.py +251 -0
- flow/providers/fcp/core/__init__.py +81 -0
- flow/providers/fcp/core/constants.py +258 -0
- flow/providers/fcp/core/errors.py +70 -0
- flow/providers/fcp/core/models.py +134 -0
- flow/providers/fcp/provider.py +2041 -0
- flow/providers/fcp/provider.py,cover +1718 -0
- flow/providers/fcp/resources/__init__.py +30 -0
- flow/providers/fcp/resources/gpu.py +57 -0
- flow/providers/fcp/resources/instances.py +138 -0
- flow/providers/fcp/resources/projects.py +161 -0
- flow/providers/fcp/resources/ssh.py +476 -0
- flow/providers/fcp/runtime/__init__.py +13 -0
- flow/providers/fcp/runtime/quota.py +47 -0
- flow/providers/fcp/runtime/startup/__init__.py +36 -0
- flow/providers/fcp/runtime/startup/builder.py +324 -0
- flow/providers/fcp/runtime/startup/legacy.py +49 -0
- flow/providers/fcp/runtime/startup/sections.py +797 -0
- flow/providers/fcp/runtime/startup/templates.py +148 -0
- flow/providers/local/__init__.py +23 -0
- flow/providers/local/config.py +134 -0
- flow/providers/local/executor.py +537 -0
- flow/providers/local/logs.py +224 -0
- flow/providers/local/provider.py +484 -0
- flow/providers/local/storage.py +151 -0
- flow/providers/registry.py +158 -0
- flow/utils/__init__.py +0 -0
- flow/utils/cache.py +128 -0
- flow/utils/exceptions.py +15 -0
- flow/utils/instance_parser.py +213 -0
- flow/utils/instance_validator.py +272 -0
- flow/utils/region_validator.py +32 -0
- flow/utils/retry.py +82 -0
- flow/utils/retry_helper.py +189 -0
- flow/utils/security.py +109 -0
- flow/utils/validation.py +171 -0
- flow_compute-2.0.0.dist-info/METADATA +1115 -0
- flow_compute-2.0.0.dist-info/RECORD +126 -0
- flow_compute-2.0.0.dist-info/WHEEL +4 -0
- flow_compute-2.0.0.dist-info/entry_points.txt +2 -0
- 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)
|
flow/_internal/config.py
ADDED
|
@@ -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]
|