anor 0.1.0__tar.gz

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.
anor-0.1.0/.gitignore ADDED
@@ -0,0 +1,52 @@
1
+ # Dependencies
2
+ node_modules/
3
+ .pnpm-store/
4
+
5
+ # Build outputs
6
+ dist/
7
+ build/
8
+ .next/
9
+ out/
10
+
11
+ # Environment files
12
+ # .env
13
+ # .env.local
14
+ # .env.*.local
15
+ # .env.production
16
+ # .env.staging
17
+
18
+ # IDE
19
+ .idea/
20
+ .vscode/
21
+ *.swp
22
+ *.swo
23
+
24
+ # OS
25
+ .DS_Store
26
+ Thumbs.db
27
+
28
+ # Logs
29
+ *.log
30
+ npm-debug.log*
31
+ pnpm-debug.log*
32
+
33
+ # Testing
34
+ coverage/
35
+ .nyc_output/
36
+
37
+ # Prisma
38
+ apps/api/prisma/*.db
39
+ apps/api/prisma/*.db-journal
40
+
41
+ # Docker
42
+ .docker/
43
+
44
+ # Vercel
45
+ .vercel/
46
+
47
+ # GCP
48
+ .gcloud/
49
+ .vercel
50
+
51
+ # Orchestrator
52
+ apps/orchestrator/node/target/
anor-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: anor
3
+ Version: 0.1.0
4
+ Summary: Anor Cloud GPU CLI - Managed GPU compute made simple
5
+ Project-URL: Homepage, https://anor.cloud
6
+ Project-URL: Documentation, https://docs.anor.cloud
7
+ Project-URL: Repository, https://github.com/Ornn-AI/ornn-gpu-marketplace
8
+ Author-email: Anor Cloud <support@anor.cloud>
9
+ License-Expression: MIT
10
+ Keywords: ai,cli,cloud,compute,gpu,machine-learning
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
+ Requires-Python: >=3.8
25
+ Requires-Dist: click>=8.0.0
26
+ Requires-Dist: httpx>=0.24.0
27
+ Requires-Dist: rich>=13.0.0
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Anor CLI
31
+
32
+ Managed GPU compute made simple.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install anor
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```bash
43
+ # Login to your account
44
+ anor login
45
+
46
+ # Run a training job
47
+ anor run --grade standard --gpus 8 --max-rate 2.20 "python train.py"
48
+
49
+ # List your jobs
50
+ anor jobs list
51
+
52
+ # Get job logs
53
+ anor jobs logs <job-id>
54
+
55
+ # View pricing
56
+ anor pricing
57
+ ```
58
+
59
+ ## Commands
60
+
61
+ | Command | Description |
62
+ |---------|-------------|
63
+ | `anor login` | Login and store credentials |
64
+ | `anor logout` | Clear stored credentials |
65
+ | `anor whoami` | Show current user |
66
+ | `anor run` | Submit a compute job |
67
+ | `anor jobs list` | List your jobs |
68
+ | `anor jobs get <id>` | Get job details |
69
+ | `anor jobs logs <id>` | Get job logs |
70
+ | `anor jobs cancel <id>` | Cancel a job |
71
+ | `anor pricing` | Show current pricing |
72
+
73
+ ## Compute Grades
74
+
75
+ | Grade | Price | GPU Types |
76
+ |-------|-------|-----------|
77
+ | Standard | $1.80/GPU/hr | T4, V100, L4 |
78
+ | Performance | $2.50/GPU/hr | A100, A100-80GB |
79
+ | Premium | $4.00/GPU/hr | H100, H200, B200 |
80
+
81
+ ## Documentation
82
+
83
+ https://docs.anor.cloud
anor-0.1.0/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Anor CLI
2
+
3
+ Managed GPU compute made simple.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install anor
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # Login to your account
15
+ anor login
16
+
17
+ # Run a training job
18
+ anor run --grade standard --gpus 8 --max-rate 2.20 "python train.py"
19
+
20
+ # List your jobs
21
+ anor jobs list
22
+
23
+ # Get job logs
24
+ anor jobs logs <job-id>
25
+
26
+ # View pricing
27
+ anor pricing
28
+ ```
29
+
30
+ ## Commands
31
+
32
+ | Command | Description |
33
+ |---------|-------------|
34
+ | `anor login` | Login and store credentials |
35
+ | `anor logout` | Clear stored credentials |
36
+ | `anor whoami` | Show current user |
37
+ | `anor run` | Submit a compute job |
38
+ | `anor jobs list` | List your jobs |
39
+ | `anor jobs get <id>` | Get job details |
40
+ | `anor jobs logs <id>` | Get job logs |
41
+ | `anor jobs cancel <id>` | Cancel a job |
42
+ | `anor pricing` | Show current pricing |
43
+
44
+ ## Compute Grades
45
+
46
+ | Grade | Price | GPU Types |
47
+ |-------|-------|-----------|
48
+ | Standard | $1.80/GPU/hr | T4, V100, L4 |
49
+ | Performance | $2.50/GPU/hr | A100, A100-80GB |
50
+ | Premium | $4.00/GPU/hr | H100, H200, B200 |
51
+
52
+ ## Documentation
53
+
54
+ https://docs.anor.cloud
@@ -0,0 +1,2 @@
1
+ """Anor Cloud GPU CLI"""
2
+ __version__ = "0.1.0"
anor-0.1.0/anor/api.py ADDED
@@ -0,0 +1,124 @@
1
+ """API client for Anor Cloud"""
2
+ import httpx
3
+ from typing import Optional, Dict, Any, List
4
+
5
+ from .config import get_api_url, get_api_key
6
+
7
+
8
+ class APIError(Exception):
9
+ def __init__(self, message: str, status_code: int = 0):
10
+ self.message = message
11
+ self.status_code = status_code
12
+ super().__init__(message)
13
+
14
+
15
+ class AnorClient:
16
+ def __init__(self, api_url: Optional[str] = None, api_key: Optional[str] = None):
17
+ self.api_url = api_url or get_api_url()
18
+ self.api_key = api_key or get_api_key()
19
+ self.client = httpx.Client(
20
+ base_url=self.api_url,
21
+ timeout=30.0,
22
+ headers=self._headers(),
23
+ )
24
+
25
+ def _headers(self) -> Dict[str, str]:
26
+ headers = {"Content-Type": "application/json"}
27
+ if self.api_key:
28
+ headers["Authorization"] = f"Bearer {self.api_key}"
29
+ return headers
30
+
31
+ def _handle_response(self, response: httpx.Response) -> Any:
32
+ if response.status_code >= 400:
33
+ try:
34
+ error = response.json()
35
+ message = error.get("message", response.text)
36
+ except Exception:
37
+ message = response.text
38
+ raise APIError(message, response.status_code)
39
+
40
+ if response.headers.get("content-type", "").startswith("application/json"):
41
+ return response.json()
42
+ return response.text
43
+
44
+ # Auth
45
+ def login(self, email: str, password: str) -> Dict[str, Any]:
46
+ response = self.client.post("/api/v1/auth/login", json={
47
+ "email": email,
48
+ "password": password,
49
+ })
50
+ return self._handle_response(response)
51
+
52
+ def register(self, email: str, password: str, name: str) -> Dict[str, Any]:
53
+ response = self.client.post("/api/v1/auth/register", json={
54
+ "email": email,
55
+ "password": password,
56
+ "name": name,
57
+ })
58
+ return self._handle_response(response)
59
+
60
+ def get_me(self) -> Dict[str, Any]:
61
+ response = self.client.get("/api/v1/auth/me")
62
+ return self._handle_response(response)
63
+
64
+ def create_api_key(self, name: str) -> Dict[str, Any]:
65
+ response = self.client.post("/api/v1/auth/api-keys", json={"name": name})
66
+ return self._handle_response(response)
67
+
68
+ # Jobs
69
+ def create_job(
70
+ self,
71
+ command: str,
72
+ grade: str,
73
+ gpu_count: int,
74
+ max_hourly_rate: float,
75
+ name: Optional[str] = None,
76
+ image: Optional[str] = None,
77
+ workdir: Optional[str] = None,
78
+ env_vars: Optional[Dict[str, str]] = None,
79
+ ) -> Dict[str, Any]:
80
+ payload = {
81
+ "command": command,
82
+ "grade": grade.upper(),
83
+ "gpuCount": gpu_count,
84
+ "maxHourlyRate": max_hourly_rate,
85
+ }
86
+ if name:
87
+ payload["name"] = name
88
+ if image:
89
+ payload["image"] = image
90
+ if workdir:
91
+ payload["workdir"] = workdir
92
+ if env_vars:
93
+ payload["envVars"] = env_vars
94
+
95
+ response = self.client.post("/api/v1/jobs", json=payload)
96
+ return self._handle_response(response)
97
+
98
+ def list_jobs(self, state: Optional[str] = None, page: int = 1, limit: int = 20) -> Dict[str, Any]:
99
+ params = {"page": page, "limit": limit}
100
+ if state:
101
+ params["state"] = state
102
+ response = self.client.get("/api/v1/jobs", params=params)
103
+ return self._handle_response(response)
104
+
105
+ def get_job(self, job_id: str) -> Dict[str, Any]:
106
+ response = self.client.get(f"/api/v1/jobs/{job_id}")
107
+ return self._handle_response(response)
108
+
109
+ def get_job_logs(self, job_id: str) -> str:
110
+ response = self.client.get(f"/api/v1/jobs/{job_id}/logs")
111
+ return self._handle_response(response)
112
+
113
+ def cancel_job(self, job_id: str) -> Dict[str, Any]:
114
+ response = self.client.delete(f"/api/v1/jobs/{job_id}")
115
+ return self._handle_response(response)
116
+
117
+ def get_pricing(self) -> List[Dict[str, Any]]:
118
+ response = self.client.get("/api/v1/jobs/pricing")
119
+ return self._handle_response(response)
120
+
121
+ # Account
122
+ def get_balance(self) -> Dict[str, Any]:
123
+ response = self.client.get("/api/v1/account/balance")
124
+ return self._handle_response(response)
anor-0.1.0/anor/cli.py ADDED
@@ -0,0 +1,386 @@
1
+ """Anor Cloud CLI"""
2
+ import click
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+ from rich.progress import Progress, SpinnerColumn, TextColumn
6
+ from typing import Optional, Tuple
7
+ import getpass
8
+
9
+ from . import __version__
10
+ from .config import is_logged_in, set_api_key, clear_credentials, get_api_key
11
+ from .api import AnorClient, APIError
12
+
13
+ console = Console()
14
+
15
+
16
+ import functools
17
+
18
+ def require_auth(func):
19
+ """Decorator to require authentication"""
20
+ @functools.wraps(func)
21
+ def wrapper(*args, **kwargs):
22
+ if not is_logged_in():
23
+ console.print("[red]Not logged in. Run `anor login` first.[/red]")
24
+ raise SystemExit(1)
25
+ return func(*args, **kwargs)
26
+ return wrapper
27
+
28
+
29
+ @click.group()
30
+ @click.version_option(version=__version__, prog_name="anor")
31
+ def main():
32
+ """Anor Cloud GPU CLI - Managed GPU compute made simple."""
33
+ pass
34
+
35
+
36
+ # ============ Auth Commands ============
37
+
38
+ @main.command()
39
+ @click.option("--email", "-e", help="Email address")
40
+ @click.option("--password", "-p", help="Password")
41
+ @click.option("--api-key", "-k", help="Use existing API key")
42
+ def login(email: Optional[str], password: Optional[str], api_key: Optional[str]):
43
+ """Login to Anor Cloud"""
44
+ if api_key:
45
+ set_api_key(api_key)
46
+ console.print("[green]API key saved successfully![/green]")
47
+ return
48
+
49
+ if not email:
50
+ email = click.prompt("Email")
51
+ if not password:
52
+ password = getpass.getpass("Password: ")
53
+
54
+ try:
55
+ with Progress(
56
+ SpinnerColumn(),
57
+ TextColumn("[progress.description]{task.description}"),
58
+ console=console,
59
+ transient=True,
60
+ ) as progress:
61
+ progress.add_task("Logging in...", total=None)
62
+ client = AnorClient()
63
+ result = client.login(email, password)
64
+
65
+ if "accessToken" in result:
66
+ # Create an API key for CLI use
67
+ client = AnorClient(api_key=result["accessToken"])
68
+ key_result = client.create_api_key("anor-cli")
69
+ set_api_key(key_result["key"])
70
+ console.print(f"[green]Logged in as {email}[/green]")
71
+ else:
72
+ console.print("[red]Login failed: No access token received[/red]")
73
+ except APIError as e:
74
+ console.print(f"[red]Login failed: {e.message}[/red]")
75
+ raise SystemExit(1)
76
+
77
+
78
+ @main.command()
79
+ def logout():
80
+ """Logout and clear credentials"""
81
+ clear_credentials()
82
+ console.print("[green]Logged out successfully[/green]")
83
+
84
+
85
+ @main.command()
86
+ def whoami():
87
+ """Show current user"""
88
+ if not is_logged_in():
89
+ console.print("[yellow]Not logged in[/yellow]")
90
+ return
91
+
92
+ try:
93
+ client = AnorClient()
94
+ user = client.get_me()
95
+ console.print(f"[bold]Email:[/bold] {user.get('email', 'N/A')}")
96
+ console.print(f"[bold]Name:[/bold] {user.get('name', 'N/A')}")
97
+ console.print(f"[bold]ID:[/bold] {user.get('id', 'N/A')}")
98
+ except APIError as e:
99
+ console.print(f"[red]Error: {e.message}[/red]")
100
+
101
+
102
+ # ============ Run Command ============
103
+
104
+ @main.command()
105
+ @click.argument("command", nargs=-1, required=True)
106
+ @click.option("--grade", "-g", required=True, type=click.Choice(["standard", "performance", "premium"], case_sensitive=False), help="Compute grade")
107
+ @click.option("--gpus", required=True, type=int, help="Number of GPUs")
108
+ @click.option("--max-rate", required=True, type=float, help="Max hourly rate ($/GPU/hr)")
109
+ @click.option("--name", "-n", help="Job name")
110
+ @click.option("--image", "-i", help="Docker image")
111
+ @click.option("--workdir", "-w", help="Working directory")
112
+ @click.option("--env", "-e", multiple=True, help="Environment variables (KEY=VALUE)")
113
+ @require_auth
114
+ def run(command: Tuple[str, ...], grade: str, gpus: int, max_rate: float,
115
+ name: Optional[str], image: Optional[str], workdir: Optional[str], env: Tuple[str, ...]):
116
+ """Submit a compute job"""
117
+ cmd_str = " ".join(command)
118
+
119
+ env_vars = {}
120
+ for e in env:
121
+ if "=" in e:
122
+ key, value = e.split("=", 1)
123
+ env_vars[key] = value
124
+
125
+ try:
126
+ with Progress(
127
+ SpinnerColumn(),
128
+ TextColumn("[progress.description]{task.description}"),
129
+ console=console,
130
+ transient=True,
131
+ ) as progress:
132
+ progress.add_task("Submitting job...", total=None)
133
+ client = AnorClient()
134
+ job = client.create_job(
135
+ command=cmd_str,
136
+ grade=grade,
137
+ gpu_count=gpus,
138
+ max_hourly_rate=max_rate,
139
+ name=name,
140
+ image=image,
141
+ workdir=workdir,
142
+ env_vars=env_vars if env_vars else None,
143
+ )
144
+
145
+ console.print("[green]Job submitted successfully![/green]\n")
146
+ console.print(f"[bold]Job ID:[/bold] {job['id']}")
147
+ console.print(f"[bold]Grade:[/bold] {job['grade']}")
148
+ console.print(f"[bold]GPUs:[/bold] {job['gpuCount']}")
149
+ console.print(f"[bold]Max Rate:[/bold] ${job['maxHourlyRate']}/GPU/hr")
150
+ console.print(f"[bold]State:[/bold] {job['state']}")
151
+ console.print()
152
+ console.print(f"[dim]Track with: anor jobs get {job['id']}[/dim]")
153
+ console.print(f"[dim]View logs: anor jobs logs {job['id']}[/dim]")
154
+
155
+ except APIError as e:
156
+ console.print(f"[red]Failed to submit job: {e.message}[/red]")
157
+ raise SystemExit(1)
158
+
159
+
160
+ # ============ Jobs Commands ============
161
+
162
+ @main.group()
163
+ def jobs():
164
+ """Manage compute jobs"""
165
+ pass
166
+
167
+
168
+ @jobs.command("list")
169
+ @click.option("--state", "-s", help="Filter by state")
170
+ @click.option("--limit", "-l", default=20, help="Number of jobs to show")
171
+ @require_auth
172
+ def jobs_list(state: Optional[str], limit: int):
173
+ """List your jobs"""
174
+ try:
175
+ with Progress(
176
+ SpinnerColumn(),
177
+ TextColumn("[progress.description]{task.description}"),
178
+ console=console,
179
+ transient=True,
180
+ ) as progress:
181
+ progress.add_task("Fetching jobs...", total=None)
182
+ client = AnorClient()
183
+ result = client.list_jobs(state=state, limit=limit)
184
+
185
+ jobs_data = result.get("jobs", [])
186
+
187
+ if not jobs_data:
188
+ console.print("[yellow]No jobs found[/yellow]")
189
+ return
190
+
191
+ table = Table(title="Jobs")
192
+ table.add_column("ID", style="cyan")
193
+ table.add_column("Name")
194
+ table.add_column("Grade")
195
+ table.add_column("GPUs", justify="right")
196
+ table.add_column("State")
197
+ table.add_column("Created")
198
+
199
+ for job in jobs_data:
200
+ state_color = {
201
+ "QUEUED": "yellow",
202
+ "ROUTING": "yellow",
203
+ "STARTING": "blue",
204
+ "RUNNING": "green",
205
+ "SUCCEEDED": "green",
206
+ "FAILED": "red",
207
+ "CANCELLED": "dim",
208
+ }.get(job["state"], "white")
209
+
210
+ table.add_row(
211
+ job["id"][:8],
212
+ job.get("name") or "-",
213
+ job["grade"],
214
+ str(job["gpuCount"]),
215
+ f"[{state_color}]{job['state']}[/{state_color}]",
216
+ job.get("queuedAt", "")[:19] if job.get("queuedAt") else "-",
217
+ )
218
+
219
+ console.print(table)
220
+ console.print(f"\nShowing {len(jobs_data)} of {result.get('total', len(jobs_data))} jobs")
221
+
222
+ except APIError as e:
223
+ console.print(f"[red]Error: {e.message}[/red]")
224
+ raise SystemExit(1)
225
+
226
+
227
+ @jobs.command("get")
228
+ @click.argument("job_id")
229
+ @require_auth
230
+ def jobs_get(job_id: str):
231
+ """Get job details"""
232
+ try:
233
+ with Progress(
234
+ SpinnerColumn(),
235
+ TextColumn("[progress.description]{task.description}"),
236
+ console=console,
237
+ transient=True,
238
+ ) as progress:
239
+ progress.add_task("Fetching job...", total=None)
240
+ client = AnorClient()
241
+ job = client.get_job(job_id)
242
+
243
+ console.print("\n[bold]Job Details[/bold]")
244
+ console.print("─" * 40)
245
+ console.print(f"[bold]ID:[/bold] {job['id']}")
246
+ console.print(f"[bold]Grade:[/bold] {job['grade']}")
247
+ console.print(f"[bold]GPUs:[/bold] {job['gpuCount']}")
248
+ console.print(f"[bold]Command:[/bold] {job['command']}")
249
+ if job.get("image"):
250
+ console.print(f"[bold]Image:[/bold] {job['image']}")
251
+ console.print(f"[bold]Max Rate:[/bold] ${job['maxHourlyRate']}/GPU/hr")
252
+ if job.get("softLockPrice"):
253
+ console.print(f"[bold]Locked Price:[/bold] ${job['softLockPrice']}/GPU/hr")
254
+ console.print()
255
+ console.print(f"[bold]State:[/bold] {job['state']}")
256
+ if job.get("stateMessage"):
257
+ console.print(f"[bold]Message:[/bold] {job['stateMessage']}")
258
+ console.print()
259
+ if job.get("queuedAt"):
260
+ console.print(f"[bold]Queued:[/bold] {job['queuedAt']}")
261
+ if job.get("startedAt"):
262
+ console.print(f"[bold]Started:[/bold] {job['startedAt']}")
263
+ if job.get("endedAt"):
264
+ console.print(f"[bold]Ended:[/bold] {job['endedAt']}")
265
+
266
+ except APIError as e:
267
+ console.print(f"[red]Error: {e.message}[/red]")
268
+ raise SystemExit(1)
269
+
270
+
271
+ @jobs.command("logs")
272
+ @click.argument("job_id")
273
+ @click.option("--follow", "-f", is_flag=True, help="Follow log output (not yet implemented)")
274
+ @require_auth
275
+ def jobs_logs(job_id: str, follow: bool):
276
+ """Get job logs"""
277
+ try:
278
+ with Progress(
279
+ SpinnerColumn(),
280
+ TextColumn("[progress.description]{task.description}"),
281
+ console=console,
282
+ transient=True,
283
+ ) as progress:
284
+ progress.add_task("Fetching logs...", total=None)
285
+ client = AnorClient()
286
+ logs = client.get_job_logs(job_id)
287
+
288
+ if logs:
289
+ console.print(logs)
290
+ else:
291
+ console.print("[yellow]No logs available yet[/yellow]")
292
+
293
+ except APIError as e:
294
+ console.print(f"[red]Error: {e.message}[/red]")
295
+ raise SystemExit(1)
296
+
297
+
298
+ @jobs.command("cancel")
299
+ @click.argument("job_id")
300
+ @click.confirmation_option(prompt="Are you sure you want to cancel this job?")
301
+ @require_auth
302
+ def jobs_cancel(job_id: str):
303
+ """Cancel a job"""
304
+ try:
305
+ with Progress(
306
+ SpinnerColumn(),
307
+ TextColumn("[progress.description]{task.description}"),
308
+ console=console,
309
+ transient=True,
310
+ ) as progress:
311
+ progress.add_task("Cancelling job...", total=None)
312
+ client = AnorClient()
313
+ job = client.cancel_job(job_id)
314
+
315
+ console.print(f"[green]Job {job_id} cancelled[/green]")
316
+
317
+ except APIError as e:
318
+ console.print(f"[red]Error: {e.message}[/red]")
319
+ raise SystemExit(1)
320
+
321
+
322
+ # ============ Pricing Command ============
323
+
324
+ @main.command()
325
+ def pricing():
326
+ """Show current compute pricing"""
327
+ try:
328
+ with Progress(
329
+ SpinnerColumn(),
330
+ TextColumn("[progress.description]{task.description}"),
331
+ console=console,
332
+ transient=True,
333
+ ) as progress:
334
+ progress.add_task("Fetching pricing...", total=None)
335
+ client = AnorClient()
336
+ prices = client.get_pricing()
337
+
338
+ table = Table(title="Anor Compute Pricing")
339
+ table.add_column("Grade", style="bold")
340
+ table.add_column("Price ($/GPU/hr)", justify="right")
341
+ table.add_column("Available GPUs", justify="right")
342
+
343
+ for tier in prices:
344
+ table.add_row(
345
+ tier["grade"].title(),
346
+ f"${tier['pricePerGpuHour']:.2f}",
347
+ str(tier.get("availableGpus", 0)),
348
+ )
349
+
350
+ console.print(table)
351
+ console.print("\n[dim]Prices subject to real-time market conditions.[/dim]")
352
+ console.print("[dim]Use --max-rate to cap your spend.[/dim]")
353
+
354
+ except APIError as e:
355
+ console.print(f"[red]Error: {e.message}[/red]")
356
+ raise SystemExit(1)
357
+
358
+
359
+ # ============ Balance Command ============
360
+
361
+ @main.command()
362
+ @require_auth
363
+ def balance():
364
+ """Show account balance"""
365
+ try:
366
+ with Progress(
367
+ SpinnerColumn(),
368
+ TextColumn("[progress.description]{task.description}"),
369
+ console=console,
370
+ transient=True,
371
+ ) as progress:
372
+ progress.add_task("Fetching balance...", total=None)
373
+ client = AnorClient()
374
+ data = client.get_balance()
375
+
376
+ console.print(f"\n[bold]Available Balance:[/bold] ${data.get('available', 0):.2f}")
377
+ console.print(f"[bold]Pending:[/bold] ${data.get('pending', 0):.2f}")
378
+ console.print(f"[bold]Total:[/bold] ${data.get('total', 0):.2f}")
379
+
380
+ except APIError as e:
381
+ console.print(f"[red]Error: {e.message}[/red]")
382
+ raise SystemExit(1)
383
+
384
+
385
+ if __name__ == "__main__":
386
+ main()
@@ -0,0 +1,60 @@
1
+ """Configuration management for Anor CLI"""
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ CONFIG_DIR = Path.home() / ".anor"
8
+ CONFIG_FILE = CONFIG_DIR / "config.json"
9
+
10
+ DEFAULT_API_URL = "https://anorcloud.com"
11
+
12
+
13
+ def ensure_config_dir():
14
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
15
+
16
+
17
+ def load_config() -> dict:
18
+ if not CONFIG_FILE.exists():
19
+ return {}
20
+ try:
21
+ return json.loads(CONFIG_FILE.read_text())
22
+ except Exception:
23
+ return {}
24
+
25
+
26
+ def save_config(config: dict):
27
+ ensure_config_dir()
28
+ CONFIG_FILE.write_text(json.dumps(config, indent=2))
29
+
30
+
31
+ def get_api_url() -> str:
32
+ config = load_config()
33
+ return config.get("api_url") or os.environ.get("ANOR_API_URL") or DEFAULT_API_URL
34
+
35
+
36
+ def get_api_key() -> Optional[str]:
37
+ config = load_config()
38
+ return config.get("api_key") or os.environ.get("ANOR_API_KEY")
39
+
40
+
41
+ def set_api_key(api_key: str):
42
+ config = load_config()
43
+ config["api_key"] = api_key
44
+ save_config(config)
45
+
46
+
47
+ def set_api_url(api_url: str):
48
+ config = load_config()
49
+ config["api_url"] = api_url
50
+ save_config(config)
51
+
52
+
53
+ def clear_credentials():
54
+ config = load_config()
55
+ config.pop("api_key", None)
56
+ save_config(config)
57
+
58
+
59
+ def is_logged_in() -> bool:
60
+ return get_api_key() is not None
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "anor"
7
+ version = "0.1.0"
8
+ description = "Anor Cloud GPU CLI - Managed GPU compute made simple"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [
12
+ { name = "Anor Cloud", email = "support@anor.cloud" }
13
+ ]
14
+ keywords = ["gpu", "cloud", "compute", "cli", "machine-learning", "ai"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: Science/Research",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.8",
24
+ "Programming Language :: Python :: 3.9",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
29
+ ]
30
+ requires-python = ">=3.8"
31
+ dependencies = [
32
+ "click>=8.0.0",
33
+ "httpx>=0.24.0",
34
+ "rich>=13.0.0",
35
+ ]
36
+
37
+ [project.scripts]
38
+ anor = "anor.cli:main"
39
+
40
+ [project.urls]
41
+ Homepage = "https://anor.cloud"
42
+ Documentation = "https://docs.anor.cloud"
43
+ Repository = "https://github.com/Ornn-AI/ornn-gpu-marketplace"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["anor"]