alphai 0.0.7__py3-none-any.whl → 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.
- alphai/__init__.py +8 -4
- alphai/auth.py +362 -0
- alphai/cli.py +1015 -0
- alphai/client.py +400 -0
- alphai/config.py +88 -0
- alphai/docker.py +764 -0
- alphai/utils.py +192 -0
- alphai-0.1.0.dist-info/METADATA +394 -0
- alphai-0.1.0.dist-info/RECORD +12 -0
- {alphai-0.0.7.dist-info → alphai-0.1.0.dist-info}/WHEEL +2 -1
- alphai-0.1.0.dist-info/entry_points.txt +2 -0
- alphai-0.1.0.dist-info/top_level.txt +1 -0
- alphai/alphai.py +0 -786
- alphai/api/client.py +0 -0
- alphai/benchmarking/benchmarker.py +0 -37
- alphai/client/__init__.py +0 -0
- alphai/client/client.py +0 -382
- alphai/profilers/__init__.py +0 -0
- alphai/profilers/configs_base.py +0 -7
- alphai/profilers/jax.py +0 -37
- alphai/profilers/pytorch.py +0 -83
- alphai/profilers/pytorch_utils.py +0 -419
- alphai/util.py +0 -19
- alphai-0.0.7.dist-info/LICENSE +0 -201
- alphai-0.0.7.dist-info/METADATA +0 -125
- alphai-0.0.7.dist-info/RECORD +0 -16
alphai/client.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""Client wrapper for alph-sdk."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional, List, Dict, Any
|
|
5
|
+
from alph_sdk import AlphSDK
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
|
|
11
|
+
from .config import Config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TunnelData:
|
|
15
|
+
"""Custom wrapper for tunnel data with additional fields."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, tunnel_data, cloudflared_token: str, jupyter_token: Optional[str] = None):
|
|
18
|
+
"""Initialize with tunnel data and tokens."""
|
|
19
|
+
self.original_data = tunnel_data
|
|
20
|
+
self.cloudflared_token = cloudflared_token
|
|
21
|
+
self.jupyter_token = jupyter_token
|
|
22
|
+
self.project_data = None
|
|
23
|
+
|
|
24
|
+
# Proxy all attributes from original data
|
|
25
|
+
for attr in ['id', 'name', 'app_url', 'jupyter_url', 'hostname', 'jupyter_hostname', 'created_at']:
|
|
26
|
+
if hasattr(tunnel_data, attr):
|
|
27
|
+
setattr(self, attr, getattr(tunnel_data, attr))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AlphAIClient:
|
|
31
|
+
"""High-level client for interacting with the Alph API."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, config: Optional[Config] = None):
|
|
34
|
+
"""Initialize the client with configuration."""
|
|
35
|
+
self.config = config or Config.load()
|
|
36
|
+
self.console = Console()
|
|
37
|
+
self._sdk = None
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def sdk(self) -> AlphSDK:
|
|
41
|
+
"""Get the SDK instance, creating it if necessary."""
|
|
42
|
+
if self._sdk is None:
|
|
43
|
+
if not self.config.bearer_token:
|
|
44
|
+
self.console.print("[red]Error: No authentication token found. Please run 'alphai login' first.[/red]")
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
self._sdk = AlphSDK(**self.config.to_sdk_config())
|
|
48
|
+
return self._sdk
|
|
49
|
+
|
|
50
|
+
def test_connection(self) -> bool:
|
|
51
|
+
"""Test the connection to the API."""
|
|
52
|
+
try:
|
|
53
|
+
# Try to get organizations as a connection test
|
|
54
|
+
response = self.sdk.orgs.get()
|
|
55
|
+
return response.result.status == "success" if response.result.status else True
|
|
56
|
+
except Exception as e:
|
|
57
|
+
self.console.print(f"[red]Connection test failed: {e}[/red]")
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
def get_organizations(self) -> List[Dict[str, Any]]:
|
|
61
|
+
"""Get all organizations."""
|
|
62
|
+
try:
|
|
63
|
+
response = self.sdk.orgs.get()
|
|
64
|
+
# Access organizations from response.result.organizations
|
|
65
|
+
return response.result.organizations or []
|
|
66
|
+
except Exception as e:
|
|
67
|
+
self.console.print(f"[red]Error getting organizations: {e}[/red]")
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
def create_organization(self, name: str, description: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
|
71
|
+
"""Create a new organization."""
|
|
72
|
+
try:
|
|
73
|
+
response = self.sdk.orgs.create({
|
|
74
|
+
"name": name,
|
|
75
|
+
"description": description or ""
|
|
76
|
+
})
|
|
77
|
+
if response.result.status == "success":
|
|
78
|
+
self.console.print(f"[green]✓ Organization '{name}' created successfully[/green]")
|
|
79
|
+
return response.result.organization
|
|
80
|
+
else:
|
|
81
|
+
self.console.print(f"[red]Failed to create organization: {response.result.status}[/red]")
|
|
82
|
+
return None
|
|
83
|
+
except Exception as e:
|
|
84
|
+
self.console.print(f"[red]Error creating organization: {e}[/red]")
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def get_projects(self, org_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
88
|
+
"""Get all projects, optionally filtered by organization."""
|
|
89
|
+
try:
|
|
90
|
+
params = {}
|
|
91
|
+
if org_id:
|
|
92
|
+
params["org_id"] = org_id
|
|
93
|
+
|
|
94
|
+
response = self.sdk.projects.get(**params)
|
|
95
|
+
# Access projects from response.result.projects
|
|
96
|
+
return response.result.projects or []
|
|
97
|
+
except Exception as e:
|
|
98
|
+
self.console.print(f"[red]Error getting projects: {e}[/red]")
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
def display_organizations(self, orgs: List[Dict[str, Any]]) -> None:
|
|
102
|
+
"""Display organizations in a nice table format."""
|
|
103
|
+
if not orgs:
|
|
104
|
+
self.console.print("[yellow]No organizations found.[/yellow]")
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
table = Table(title="Organizations")
|
|
108
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
109
|
+
table.add_column("Name", style="green")
|
|
110
|
+
table.add_column("Role", style="blue")
|
|
111
|
+
table.add_column("Slug", style="dim")
|
|
112
|
+
table.add_column("Subscription", style="yellow")
|
|
113
|
+
|
|
114
|
+
for org in orgs:
|
|
115
|
+
table.add_row(
|
|
116
|
+
org.id or "",
|
|
117
|
+
org.name or "",
|
|
118
|
+
org.role or "",
|
|
119
|
+
org.slug or "",
|
|
120
|
+
org.subscription_level or ""
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
self.console.print(table)
|
|
124
|
+
|
|
125
|
+
def display_projects(self, projects: List[Dict[str, Any]]) -> None:
|
|
126
|
+
"""Display projects in a nice table format."""
|
|
127
|
+
if not projects:
|
|
128
|
+
self.console.print("[yellow]No projects found.[/yellow]")
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
table = Table(title="Projects")
|
|
132
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
133
|
+
table.add_column("Name", style="green")
|
|
134
|
+
table.add_column("Organization", style="blue")
|
|
135
|
+
table.add_column("Status", style="yellow")
|
|
136
|
+
table.add_column("Created", style="dim")
|
|
137
|
+
|
|
138
|
+
for project in projects:
|
|
139
|
+
# Handle organization name safely
|
|
140
|
+
org_name = ""
|
|
141
|
+
if project.organization:
|
|
142
|
+
org_name = project.organization.name or ""
|
|
143
|
+
|
|
144
|
+
table.add_row(
|
|
145
|
+
project.id or "",
|
|
146
|
+
project.name or "",
|
|
147
|
+
org_name,
|
|
148
|
+
project.status or "",
|
|
149
|
+
project.created_at or ""
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
self.console.print(table)
|
|
153
|
+
|
|
154
|
+
def display_status(self) -> None:
|
|
155
|
+
"""Display current configuration status."""
|
|
156
|
+
status_info = []
|
|
157
|
+
|
|
158
|
+
# API URL
|
|
159
|
+
status_info.append(f"[bold]API URL:[/bold] {self.config.api_url}")
|
|
160
|
+
|
|
161
|
+
# Authentication status
|
|
162
|
+
if self.config.bearer_token:
|
|
163
|
+
status_info.append("[bold]Authentication:[/bold] [green]✓ Logged in[/green]")
|
|
164
|
+
else:
|
|
165
|
+
status_info.append("[bold]Authentication:[/bold] [red]✗ Not logged in[/red]")
|
|
166
|
+
|
|
167
|
+
# Current organization
|
|
168
|
+
if self.config.current_org:
|
|
169
|
+
status_info.append(f"[bold]Current Organization:[/bold] {self.config.current_org}")
|
|
170
|
+
else:
|
|
171
|
+
status_info.append("[bold]Current Organization:[/bold] [dim]None selected[/dim]")
|
|
172
|
+
|
|
173
|
+
# Current project
|
|
174
|
+
if self.config.current_project:
|
|
175
|
+
status_info.append(f"[bold]Current Project:[/bold] {self.config.current_project}")
|
|
176
|
+
else:
|
|
177
|
+
status_info.append("[bold]Current Project:[/bold] [dim]None selected[/dim]")
|
|
178
|
+
|
|
179
|
+
# Debug mode
|
|
180
|
+
if self.config.debug:
|
|
181
|
+
status_info.append("[bold]Debug Mode:[/bold] [yellow]Enabled[/yellow]")
|
|
182
|
+
|
|
183
|
+
panel = Panel(
|
|
184
|
+
"\n".join(status_info),
|
|
185
|
+
title="alphai Status",
|
|
186
|
+
title_align="left"
|
|
187
|
+
)
|
|
188
|
+
self.console.print(panel)
|
|
189
|
+
|
|
190
|
+
def create_tunnel(
|
|
191
|
+
self,
|
|
192
|
+
org_slug: str,
|
|
193
|
+
project_name: str,
|
|
194
|
+
app_port: int = 5000,
|
|
195
|
+
jupyter_port: int = 8888
|
|
196
|
+
) -> Optional[Dict[str, Any]]:
|
|
197
|
+
"""Create a new tunnel and return the tunnel data including token."""
|
|
198
|
+
try:
|
|
199
|
+
response = self.sdk.tunnels.create(request={
|
|
200
|
+
"org_slug": org_slug,
|
|
201
|
+
"project_name": project_name,
|
|
202
|
+
"app_port": app_port,
|
|
203
|
+
"jupyter_port": jupyter_port
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
if response.result.status == "success":
|
|
207
|
+
tunnel_data = response.result.data
|
|
208
|
+
self.console.print(f"[green]✓ Tunnel created successfully[/green]")
|
|
209
|
+
self.console.print(f"[blue]Tunnel ID: {tunnel_data.id}[/blue]")
|
|
210
|
+
self.console.print(f"[blue]App URL: {tunnel_data.app_url}[/blue]")
|
|
211
|
+
self.console.print(f"[blue]Jupyter URL: {tunnel_data.jupyter_url}[/blue]")
|
|
212
|
+
return tunnel_data
|
|
213
|
+
else:
|
|
214
|
+
self.console.print(f"[red]Failed to create tunnel: {response.result.status}[/red]")
|
|
215
|
+
return None
|
|
216
|
+
except Exception as e:
|
|
217
|
+
self.console.print(f"[red]Error creating tunnel: {e}[/red]")
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
def get_tunnel(self, tunnel_id: str) -> Optional[Dict[str, Any]]:
|
|
221
|
+
"""Get tunnel information by ID."""
|
|
222
|
+
try:
|
|
223
|
+
response = self.sdk.tunnels.get(tunnel_id=tunnel_id)
|
|
224
|
+
return response.result.data if response.result.status == "success" else None
|
|
225
|
+
except Exception as e:
|
|
226
|
+
self.console.print(f"[red]Error getting tunnel: {e}[/red]")
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
def delete_tunnel(self, tunnel_id: str) -> bool:
|
|
230
|
+
"""Delete a tunnel by ID."""
|
|
231
|
+
try:
|
|
232
|
+
response = self.sdk.tunnels.delete(tunnel_id=tunnel_id)
|
|
233
|
+
if response.result.status == "success":
|
|
234
|
+
self.console.print(f"[green]✓ Tunnel {tunnel_id} deleted successfully[/green]")
|
|
235
|
+
return True
|
|
236
|
+
else:
|
|
237
|
+
self.console.print(f"[red]Failed to delete tunnel: {response.result.status}[/red]")
|
|
238
|
+
return False
|
|
239
|
+
except Exception as e:
|
|
240
|
+
self.console.print(f"[red]Error deleting tunnel: {e}[/red]")
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
def create_project(
|
|
244
|
+
self,
|
|
245
|
+
name: str,
|
|
246
|
+
organization_id: str,
|
|
247
|
+
port: int = 5000,
|
|
248
|
+
url: Optional[str] = None,
|
|
249
|
+
port_forward_url: Optional[str] = None,
|
|
250
|
+
token: Optional[str] = None
|
|
251
|
+
) -> Optional[Dict[str, Any]]:
|
|
252
|
+
"""Create a new project."""
|
|
253
|
+
try:
|
|
254
|
+
response = self.sdk.projects.create(request={
|
|
255
|
+
"name": name,
|
|
256
|
+
"organization_id": organization_id,
|
|
257
|
+
"port": port,
|
|
258
|
+
"url": url,
|
|
259
|
+
"port_forward_url": port_forward_url,
|
|
260
|
+
"token": token,
|
|
261
|
+
"server_request": "external",
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
if response.result.status == "success":
|
|
265
|
+
project_data = response.result.project # Use 'project' instead of 'data'
|
|
266
|
+
self.console.print(f"[green]✓ Project '{name}' created successfully[/green]")
|
|
267
|
+
return project_data
|
|
268
|
+
else:
|
|
269
|
+
self.console.print(f"[red]Failed to create project: {response.result.status}[/red]")
|
|
270
|
+
return None
|
|
271
|
+
except Exception as e:
|
|
272
|
+
self.console.print(f"[red]Error creating project: {e}[/red]")
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
def get_organization_by_slug(self, slug: str) -> Optional[Dict[str, Any]]:
|
|
276
|
+
"""Get organization by slug."""
|
|
277
|
+
try:
|
|
278
|
+
orgs = self.get_organizations()
|
|
279
|
+
for org in orgs:
|
|
280
|
+
if hasattr(org, 'slug') and org.slug == slug:
|
|
281
|
+
return org
|
|
282
|
+
return None
|
|
283
|
+
except Exception as e:
|
|
284
|
+
self.console.print(f"[red]Error getting organization by slug: {e}[/red]")
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
def create_tunnel_with_project(
|
|
288
|
+
self,
|
|
289
|
+
org_slug: str,
|
|
290
|
+
project_name: str,
|
|
291
|
+
app_port: int = 5000,
|
|
292
|
+
jupyter_port: int = 8888,
|
|
293
|
+
jupyter_token: Optional[str] = None
|
|
294
|
+
) -> Optional[TunnelData]:
|
|
295
|
+
"""Create a tunnel and associated project."""
|
|
296
|
+
# First, get the organization
|
|
297
|
+
org = self.get_organization_by_slug(org_slug)
|
|
298
|
+
if not org:
|
|
299
|
+
self.console.print(f"[red]Organization with slug '{org_slug}' not found[/red]")
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
# Create the tunnel
|
|
303
|
+
tunnel_data = self.create_tunnel(org_slug, project_name, app_port, jupyter_port)
|
|
304
|
+
if not tunnel_data:
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
# Create custom wrapper with tokens
|
|
308
|
+
wrapped_tunnel = TunnelData(
|
|
309
|
+
tunnel_data=tunnel_data,
|
|
310
|
+
cloudflared_token=tunnel_data.token,
|
|
311
|
+
jupyter_token=jupyter_token
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Create the associated project (with Jupyter token if available)
|
|
315
|
+
self.console.print(f"[yellow]Creating associated project '{project_name}'...[/yellow]")
|
|
316
|
+
project_data = self.create_project(
|
|
317
|
+
name=project_name,
|
|
318
|
+
organization_id=org.id,
|
|
319
|
+
port=app_port,
|
|
320
|
+
url=tunnel_data.jupyter_url,
|
|
321
|
+
port_forward_url=tunnel_data.app_url,
|
|
322
|
+
token=jupyter_token # Use Jupyter token, not cloudflared token
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Store project data in wrapper
|
|
326
|
+
wrapped_tunnel.project_data = project_data
|
|
327
|
+
|
|
328
|
+
# Enhanced logging output
|
|
329
|
+
self.console.print("\n[bold green]🎉 Tunnel and Project Setup Complete![/bold green]")
|
|
330
|
+
self.console.print(f"[bold]Tunnel ID:[/bold] {wrapped_tunnel.id}")
|
|
331
|
+
if project_data:
|
|
332
|
+
self.console.print(f"[bold]Project ID:[/bold] {project_data.id if hasattr(project_data, 'id') else 'Created'}")
|
|
333
|
+
|
|
334
|
+
# Token information
|
|
335
|
+
self.console.print(f"\n[bold]Cloudflared Token:[/bold]")
|
|
336
|
+
self.console.print(f"[dim]For tunnel setup: cloudflared service install {wrapped_tunnel.cloudflared_token}[/dim]")
|
|
337
|
+
|
|
338
|
+
if jupyter_token:
|
|
339
|
+
self.console.print(f"\n[bold]Jupyter Token:[/bold]")
|
|
340
|
+
self.console.print(f"[dim]For Jupyter access: {jupyter_token}[/dim]")
|
|
341
|
+
|
|
342
|
+
# URLs
|
|
343
|
+
self.console.print(f"\n[bold]Access URLs:[/bold]")
|
|
344
|
+
self.console.print(f"[blue]• App URL: {wrapped_tunnel.app_url}[/blue]")
|
|
345
|
+
self.console.print(f"[blue]• Jupyter URL: {wrapped_tunnel.jupyter_url}[/blue]")
|
|
346
|
+
|
|
347
|
+
return wrapped_tunnel
|
|
348
|
+
|
|
349
|
+
def update_project_jupyter_token(self, project_data: Dict[str, Any], jupyter_token: str) -> bool:
|
|
350
|
+
"""Update project with Jupyter token after container starts."""
|
|
351
|
+
# Since there's no update method, we'll store this for the next version
|
|
352
|
+
# For now, just print that we have the token
|
|
353
|
+
self.console.print(f"[green]✓ Jupyter token extracted: {jupyter_token[:12]}...[/green]")
|
|
354
|
+
return True
|
|
355
|
+
|
|
356
|
+
def delete_project(self, project_id: str) -> bool:
|
|
357
|
+
"""Delete a project by ID."""
|
|
358
|
+
try:
|
|
359
|
+
response = self.sdk.projects.delete(project_id=project_id)
|
|
360
|
+
if response.result.status == "success":
|
|
361
|
+
self.console.print(f"[green]✓ Project {project_id} deleted successfully[/green]")
|
|
362
|
+
return True
|
|
363
|
+
else:
|
|
364
|
+
self.console.print(f"[red]Failed to delete project: {response.result.status}[/red]")
|
|
365
|
+
return False
|
|
366
|
+
except Exception as e:
|
|
367
|
+
self.console.print(f"[red]Error deleting project: {e}[/red]")
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
def cleanup_tunnel_and_project(
|
|
371
|
+
self,
|
|
372
|
+
tunnel_id: Optional[str] = None,
|
|
373
|
+
project_id: Optional[str] = None,
|
|
374
|
+
force: bool = False
|
|
375
|
+
) -> bool:
|
|
376
|
+
"""Comprehensive cleanup of tunnel and project resources."""
|
|
377
|
+
success = True
|
|
378
|
+
|
|
379
|
+
if not tunnel_id and not project_id:
|
|
380
|
+
self.console.print("[yellow]No tunnel or project ID provided for cleanup[/yellow]")
|
|
381
|
+
return True
|
|
382
|
+
|
|
383
|
+
# Delete tunnel first
|
|
384
|
+
if tunnel_id:
|
|
385
|
+
self.console.print(f"[yellow]Deleting tunnel {tunnel_id}...[/yellow]")
|
|
386
|
+
if not self.delete_tunnel(tunnel_id):
|
|
387
|
+
success = False
|
|
388
|
+
|
|
389
|
+
# Delete project second
|
|
390
|
+
if project_id:
|
|
391
|
+
self.console.print(f"[yellow]Deleting project {project_id}...[/yellow]")
|
|
392
|
+
if not self.delete_project(project_id):
|
|
393
|
+
success = False
|
|
394
|
+
|
|
395
|
+
if success:
|
|
396
|
+
self.console.print("[green]✓ Tunnel and project cleanup completed successfully[/green]")
|
|
397
|
+
else:
|
|
398
|
+
self.console.print("[yellow]⚠ Tunnel and project cleanup completed with errors[/yellow]")
|
|
399
|
+
|
|
400
|
+
return success
|
alphai/config.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Configuration management for alphai CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import stat
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Dict, Any
|
|
7
|
+
import json
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Config(BaseModel):
|
|
12
|
+
"""Configuration model for alphai CLI."""
|
|
13
|
+
|
|
14
|
+
api_url: str = Field(default="https://www.runalph.ai", description="API base URL")
|
|
15
|
+
bearer_token: Optional[str] = Field(default=None, description="Bearer token for API authentication")
|
|
16
|
+
current_org: Optional[str] = Field(default=None, description="Current organization ID")
|
|
17
|
+
current_project: Optional[str] = Field(default=None, description="Current project ID")
|
|
18
|
+
debug: bool = Field(default=False, description="Enable debug mode")
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def get_config_dir(cls) -> Path:
|
|
22
|
+
"""Get the configuration directory."""
|
|
23
|
+
config_dir = Path.home() / ".alphai"
|
|
24
|
+
config_dir.mkdir(exist_ok=True)
|
|
25
|
+
# Set directory permissions to be readable/writable only by owner
|
|
26
|
+
config_dir.chmod(0o700)
|
|
27
|
+
return config_dir
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get_config_file(cls) -> Path:
|
|
31
|
+
"""Get the configuration file path."""
|
|
32
|
+
return cls.get_config_dir() / "config.json"
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def load(cls) -> "Config":
|
|
36
|
+
"""Load configuration from file and environment."""
|
|
37
|
+
config_file = cls.get_config_file()
|
|
38
|
+
config_data = {}
|
|
39
|
+
|
|
40
|
+
# Load from file if it exists
|
|
41
|
+
if config_file.exists():
|
|
42
|
+
try:
|
|
43
|
+
with open(config_file, 'r') as f:
|
|
44
|
+
config_data = json.load(f)
|
|
45
|
+
except (json.JSONDecodeError, IOError):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
# Override with environment variables
|
|
49
|
+
if os.getenv("ALPHAI_API_URL"):
|
|
50
|
+
config_data["api_url"] = os.getenv("ALPHAI_API_URL")
|
|
51
|
+
|
|
52
|
+
if os.getenv("ALPHAI_DEBUG"):
|
|
53
|
+
config_data["debug"] = os.getenv("ALPHAI_DEBUG").lower() in ("true", "1", "yes")
|
|
54
|
+
|
|
55
|
+
# Load bearer token from environment if available (takes precedence)
|
|
56
|
+
if os.getenv("ALPHAI_BEARER_TOKEN"):
|
|
57
|
+
config_data["bearer_token"] = os.getenv("ALPHAI_BEARER_TOKEN")
|
|
58
|
+
|
|
59
|
+
return cls(**config_data)
|
|
60
|
+
|
|
61
|
+
def save(self) -> None:
|
|
62
|
+
"""Save configuration to file with secure permissions."""
|
|
63
|
+
config_file = self.get_config_file()
|
|
64
|
+
config_data = self.model_dump()
|
|
65
|
+
|
|
66
|
+
# Write the config file
|
|
67
|
+
with open(config_file, 'w') as f:
|
|
68
|
+
json.dump(config_data, f, indent=2)
|
|
69
|
+
|
|
70
|
+
# Set file permissions to be readable/writable only by owner for security
|
|
71
|
+
config_file.chmod(0o600)
|
|
72
|
+
|
|
73
|
+
def set_bearer_token(self, token: str) -> None:
|
|
74
|
+
"""Set and save bearer token securely."""
|
|
75
|
+
self.bearer_token = token
|
|
76
|
+
self.save()
|
|
77
|
+
|
|
78
|
+
def clear_bearer_token(self) -> None:
|
|
79
|
+
"""Clear the bearer token."""
|
|
80
|
+
self.bearer_token = None
|
|
81
|
+
self.save()
|
|
82
|
+
|
|
83
|
+
def to_sdk_config(self) -> Dict[str, Any]:
|
|
84
|
+
"""Convert to SDK configuration format."""
|
|
85
|
+
return {
|
|
86
|
+
"bearer_auth": self.bearer_token,
|
|
87
|
+
"server_url": self.api_url,
|
|
88
|
+
}
|