leap-bundle 0.0.1a1__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.
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
File without changes
@@ -0,0 +1,112 @@
1
+ """Authentication commands for LEAP CLI."""
2
+
3
+ import requests
4
+ import typer
5
+ from rich.console import Console
6
+
7
+ from leap_bundle.utils.config import (
8
+ clear_api_token,
9
+ get_api_token,
10
+ get_server_url,
11
+ is_logged_in,
12
+ set_api_token,
13
+ )
14
+
15
+ console = Console()
16
+ app = typer.Typer()
17
+
18
+
19
+ def validate_api_token(token: str) -> bool:
20
+ """Validate API token with the LEAP platform."""
21
+ try:
22
+ server_url = get_server_url()
23
+ api_url = f"{server_url.rstrip('/')}/api/cli/login"
24
+ response = requests.post(api_url, json={"api_token": token}, timeout=10)
25
+ return response.status_code == 200
26
+ except requests.RequestException as e:
27
+ console.print(f"[red]✗[/red] Failed to validate API token: {e}")
28
+ return False
29
+
30
+
31
+ @app.command("login")
32
+ def login(
33
+ api_token: str = typer.Argument(..., help="API token for LEAP platform"),
34
+ ) -> None:
35
+ """Login to LEAP platform."""
36
+ if is_logged_in():
37
+ console.print(
38
+ "[yellow]⚠[/yellow] You are already logged in. "
39
+ "Run 'leap-bundle logout' first if you want to change your token."
40
+ )
41
+ return
42
+
43
+ console.print("[blue]ℹ[/blue] Validating API token...")
44
+ if not validate_api_token(api_token):
45
+ console.print(
46
+ "[red]✗[/red] Invalid API token. Please check your token and try again."
47
+ )
48
+ raise typer.Exit(1)
49
+
50
+ try:
51
+ set_api_token(api_token)
52
+ console.print("[green]✓[/green] Successfully logged in to LEAP platform!")
53
+ except Exception as e:
54
+ console.print(f"[red]✗[/red] Failed to save login credentials: {e}")
55
+ raise typer.Exit(1) from None
56
+
57
+
58
+ @app.command("logout")
59
+ def logout() -> None:
60
+ """Logout from LEAP platform."""
61
+ if not is_logged_in():
62
+ console.print("[blue]ℹ[/blue] You are not currently logged in.")
63
+ return
64
+
65
+ try:
66
+ clear_api_token()
67
+ console.print("[green]✓[/green] Successfully logged out from LEAP platform!")
68
+ except Exception as e:
69
+ console.print(f"[red]✗[/red] Failed to clear login credentials: {e}")
70
+ raise typer.Exit(1) from None
71
+
72
+
73
+ @app.command("whoami")
74
+ def whoami() -> None:
75
+ """Show current user information."""
76
+ if not is_logged_in():
77
+ console.print(
78
+ "[red]✗[/red] You are not logged in. Run 'leap-bundle login' first."
79
+ )
80
+ raise typer.Exit(1)
81
+
82
+ try:
83
+ api_token = get_api_token()
84
+ server_url = get_server_url()
85
+ api_url = f"{server_url.rstrip('/')}/api/cli/whoami"
86
+
87
+ response = requests.get(
88
+ api_url, headers={"Authorization": f"Bearer {api_token}"}, timeout=10
89
+ )
90
+
91
+ if response.status_code == 200:
92
+ data = response.json()
93
+ console.print(f"[green]✓[/green] Logged in as: {data['email']}")
94
+ else:
95
+ error_data = (
96
+ response.json()
97
+ if response.headers.get("content-type", "").startswith(
98
+ "application/json"
99
+ )
100
+ else {}
101
+ )
102
+ error_message = error_data.get("error", "Failed to get user information")
103
+ console.print(f"[red]✗[/red] {error_message}")
104
+ raise typer.Exit(1)
105
+ except requests.RequestException as e:
106
+ console.print(f"[red]✗[/red] Failed to connect to server: {e}")
107
+ raise typer.Exit(1) from None
108
+ except typer.Exit:
109
+ raise # Re-raise typer.Exit without handling it
110
+ except Exception as e:
111
+ console.print(f"[red]✗[/red] Unexpected error: {e}")
112
+ raise typer.Exit(1) from None
@@ -0,0 +1,35 @@
1
+ """Cancel command for bundle requests."""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+
6
+ from leap_bundle.utils.api_client import APIClient
7
+ from leap_bundle.utils.config import is_logged_in
8
+
9
+ console = Console()
10
+
11
+
12
+ def cancel(
13
+ request_id: str = typer.Argument(..., help="Bundle request ID to cancel"),
14
+ ) -> None:
15
+ """Cancel a bundle request."""
16
+
17
+ if not is_logged_in():
18
+ console.print(
19
+ "[red]✗[/red] You must be logged in. Run 'leap-bundle login' first."
20
+ )
21
+ raise typer.Exit(1)
22
+
23
+ try:
24
+ client = APIClient()
25
+ console.print(f"[blue]ℹ[/blue] Cancelling bundle request {request_id}...")
26
+
27
+ result = client.cancel_bundle_request(request_id)
28
+ message = result.get("message", "Request cancelled successfully.")
29
+
30
+ console.print(f"[green]✓[/green] {message}")
31
+
32
+ except Exception as e:
33
+ from leap_bundle.utils.api_client import handle_cli_exception
34
+
35
+ handle_cli_exception(e)
@@ -0,0 +1,31 @@
1
+ """Configuration commands for LEAP CLI."""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+
6
+ from leap_bundle.utils.config import get_config_file_path, load_config, set_server_url
7
+
8
+ console = Console()
9
+ app = typer.Typer()
10
+
11
+
12
+ @app.command("config")
13
+ def config(
14
+ server: str = typer.Option(None, "--server", help="Set the server URL"),
15
+ ) -> None:
16
+ """Configure LEAP CLI settings."""
17
+ if server:
18
+ set_server_url(server)
19
+ console.print(f"[green]✓[/green] Server URL set to: {server}")
20
+ else:
21
+ config_path = get_config_file_path()
22
+ console.print(f"[blue]ℹ[/blue] Config file location: {config_path}")
23
+
24
+ config_data = load_config()
25
+ if config_data:
26
+ console.print("\n[blue]Current configuration:[/blue]")
27
+ for key, value in config_data.items():
28
+ if key != "api_token":
29
+ console.print(f" {key}: {value}")
30
+ else:
31
+ console.print("\n[yellow]No configuration found.[/yellow]")
@@ -0,0 +1,87 @@
1
+ """Create command for bundle requests."""
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.progress import Progress, SpinnerColumn, TextColumn
8
+
9
+ from leap_bundle.utils.api_client import APIClient, upload_directory_to_s3
10
+ from leap_bundle.utils.config import is_logged_in
11
+ from leap_bundle.utils.hash import calculate_directory_hash
12
+
13
+ console = Console()
14
+
15
+
16
+ # TODO: allow force recreate in the future
17
+ def create(
18
+ input_path: str = typer.Argument(..., help="Directory path to upload"),
19
+ # force_recreate: bool = typer.Option(
20
+ # False, "--force", help="Force recreate even if request exists"
21
+ # ),
22
+ ) -> None:
23
+ """Create a new bundle request and upload directory."""
24
+
25
+ if not is_logged_in():
26
+ console.print(
27
+ "[red]✗[/red] You must be logged in. Run 'leap-bundle login' first."
28
+ )
29
+ raise typer.Exit(1)
30
+
31
+ path = Path(input_path)
32
+ if not path.exists():
33
+ console.print(f"[red]✗[/red] Directory does not exist: {input_path}")
34
+ raise typer.Exit(1)
35
+
36
+ if not path.is_dir():
37
+ console.print(f"[red]✗[/red] Path is not a directory: {input_path}")
38
+ raise typer.Exit(1)
39
+
40
+ try:
41
+ console.print("[blue]ℹ[/blue] Calculating directory hash...")
42
+ input_hash = calculate_directory_hash(str(path.absolute()))
43
+
44
+ client = APIClient()
45
+ console.print("[blue]ℹ[/blue] Submitting bundle request...")
46
+
47
+ result = client.create_bundle_request(str(path.absolute()), input_hash, False)
48
+
49
+ if result["exists"]:
50
+ console.print(f"[yellow]⚠[/yellow] {result['message']}")
51
+ return
52
+
53
+ request_id = result["new_request_id"]
54
+ signed_url = result["signed_url"]
55
+
56
+ console.print(f"[green]✓[/green] Bundle request created with ID: {request_id}")
57
+
58
+ console.print("[blue]ℹ[/blue] Starting upload...")
59
+ client.update_bundle_request_status(request_id, "uploading_started")
60
+
61
+ with Progress(
62
+ SpinnerColumn(),
63
+ TextColumn("[progress.description]{task.description}"),
64
+ console=console,
65
+ ) as progress:
66
+ task = progress.add_task("Uploading directory...", total=None)
67
+
68
+ try:
69
+ upload_directory_to_s3(signed_url, str(path.absolute()))
70
+ progress.update(task, description="Upload completed!")
71
+
72
+ client.update_bundle_request_status(request_id, "uploading_completed")
73
+ console.print(
74
+ f"[green]✓[/green] Upload completed successfully! Request ID: {request_id}"
75
+ )
76
+
77
+ except Exception as upload_error:
78
+ client.update_bundle_request_status(
79
+ request_id, "uploading_failed", str(upload_error)
80
+ )
81
+ console.print(f"[red]✗[/red] Upload failed: {upload_error}")
82
+ raise typer.Exit(1) from upload_error
83
+
84
+ except Exception as e:
85
+ from leap_bundle.utils.api_client import handle_cli_exception
86
+
87
+ handle_cli_exception(e)
@@ -0,0 +1,78 @@
1
+ """Download command for bundle requests."""
2
+
3
+ from pathlib import Path
4
+ from urllib.parse import unquote, urlparse
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.progress import Progress, SpinnerColumn, TextColumn
9
+
10
+ from leap_bundle.utils.api_client import APIClient, download_from_s3
11
+ from leap_bundle.utils.config import is_logged_in
12
+
13
+ console = Console()
14
+
15
+
16
+ def download(
17
+ request_id: str = typer.Argument(..., help="Bundle request ID to download"),
18
+ output_path: str = typer.Option(
19
+ ".", "--output-path", help="Directory path to download files to"
20
+ ),
21
+ ) -> None:
22
+ """Download completed bundle request output."""
23
+
24
+ if not is_logged_in():
25
+ console.print(
26
+ "[red]✗[/red] You must be logged in. Run 'leap-bundle login' first."
27
+ )
28
+ raise typer.Exit(1)
29
+
30
+ output_dir = Path(output_path)
31
+ if not output_dir.exists():
32
+ console.print(f"[red]✗[/red] Output directory does not exist: {output_path}")
33
+ raise typer.Exit(1)
34
+
35
+ if not output_dir.is_dir():
36
+ console.print(f"[red]✗[/red] Output path is not a directory: {output_path}")
37
+ raise typer.Exit(1)
38
+
39
+ try:
40
+ client = APIClient()
41
+ console.print(
42
+ f"[blue]ℹ[/blue] Requesting download for bundle request {request_id}..."
43
+ )
44
+
45
+ result = client.download_bundle_request(request_id)
46
+ signed_url = result["signed_url"]
47
+ parsed_url = urlparse(signed_url)
48
+ output_file = Path(unquote(parsed_url.path)).name
49
+ if not output_file or output_file == "/":
50
+ output_file = f"bundle-{request_id}.bundle"
51
+
52
+ console.print(
53
+ f"[green]✓[/green] Download URL obtained for request {request_id}"
54
+ )
55
+
56
+ with Progress(
57
+ SpinnerColumn(),
58
+ TextColumn("[progress.description]{task.description}"),
59
+ console=console,
60
+ ) as progress:
61
+ task = progress.add_task("Downloading bundle output...", total=None)
62
+
63
+ try:
64
+ download_from_s3(signed_url, str(output_file))
65
+ progress.update(task, description="Download completed!")
66
+
67
+ console.print(
68
+ f"[green]✓[/green] Download completed successfully! File saved to: {output_file}"
69
+ )
70
+
71
+ except Exception as download_error:
72
+ console.print(f"[red]✗[/red] Download failed: {download_error}")
73
+ raise typer.Exit(1) from download_error
74
+
75
+ except Exception as e:
76
+ from leap_bundle.utils.api_client import handle_cli_exception
77
+
78
+ handle_cli_exception(e)
@@ -0,0 +1,73 @@
1
+ """List command for bundle requests."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from leap_bundle.utils.api_client import APIClient
10
+ from leap_bundle.utils.config import is_logged_in
11
+
12
+ console = Console()
13
+
14
+
15
+ def list_requests(
16
+ request_id: Optional[str] = typer.Argument(
17
+ None, help="Optional request ID to get details for a specific request"
18
+ ),
19
+ ) -> None:
20
+ """List bundle requests or get details for a specific request."""
21
+
22
+ if not is_logged_in():
23
+ console.print(
24
+ "[red]✗[/red] You must be logged in. Run 'leap-bundle login' first."
25
+ )
26
+ raise typer.Exit(1)
27
+
28
+ try:
29
+ client = APIClient()
30
+
31
+ if request_id:
32
+ console.print(
33
+ f"[blue]ℹ[/blue] Fetching details for request {request_id}..."
34
+ )
35
+ result = client.get_bundle_request(request_id)
36
+ request = result["request"]
37
+
38
+ console.print("[green]✓[/green] Request Details:")
39
+ console.print(f" ID: {request['external_id']}")
40
+ console.print(f" Input Path: {request['input_path']}")
41
+ console.print(f" Status: {request['status']}")
42
+ console.print(f" Creation: {request['created_at']}")
43
+ console.print(f" Update: {request['updated_at']}")
44
+ else:
45
+ console.print("[blue]ℹ[/blue] Fetching bundle requests...")
46
+ result = client.list_bundle_requests()
47
+ requests = result["requests"]
48
+
49
+ if not requests:
50
+ console.print("[yellow]⚠[/yellow] No bundle requests found.")
51
+ return
52
+
53
+ table = Table(title="Bundle Requests (50 most recent)")
54
+ table.add_column("ID", style="cyan")
55
+ table.add_column("Input Path", style="green")
56
+ table.add_column("Status", style="yellow")
57
+ table.add_column("Creation", style="blue")
58
+
59
+ for request in requests:
60
+ table.add_row(
61
+ str(request["external_id"]),
62
+ request["input_path"],
63
+ request["status"],
64
+ request["created_at"],
65
+ )
66
+
67
+ console.print(table)
68
+ console.print(f"[green]✓[/green] Found {len(requests)} bundle requests.")
69
+
70
+ except Exception as e:
71
+ from leap_bundle.utils.api_client import handle_cli_exception
72
+
73
+ handle_cli_exception(e)
leap_bundle/main.py ADDED
@@ -0,0 +1,68 @@
1
+ """Main entry point for the LEAP CLI."""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+
6
+ from leap_bundle.commands.auth import login, logout, whoami
7
+ from leap_bundle.commands.cancel import cancel
8
+ from leap_bundle.commands.config import config
9
+ from leap_bundle.commands.create import create
10
+ from leap_bundle.commands.download import download
11
+ from leap_bundle.commands.list import list_requests
12
+
13
+ console = Console()
14
+
15
+ app = typer.Typer(
16
+ name="leap-bundle",
17
+ help="Command line interface for the LEAP (Liquid Edge AI Platform) platform.",
18
+ rich_markup_mode="rich",
19
+ )
20
+
21
+
22
+ def version_callback(value: bool) -> None:
23
+ """Show version information."""
24
+ if value:
25
+ from leap_bundle import __version__
26
+
27
+ console.print(f"leap-bundle version {__version__}")
28
+ raise typer.Exit()
29
+
30
+
31
+ def help_command(ctx: typer.Context) -> None:
32
+ """Show help information."""
33
+ if ctx.parent:
34
+ console.print(ctx.parent.get_help())
35
+ else:
36
+ console.print("Help information not available.")
37
+
38
+
39
+ app.command("login")(login)
40
+ app.command("logout")(logout)
41
+ app.command("whoami")(whoami)
42
+ app.command("cancel")(cancel)
43
+ app.command("config")(config)
44
+ app.command("create")(create)
45
+ app.command("download")(download)
46
+ app.command("list")(list_requests)
47
+ app.command("help")(help_command)
48
+
49
+
50
+ @app.callback(invoke_without_command=True)
51
+ def main(
52
+ ctx: typer.Context,
53
+ version: bool = typer.Option(
54
+ None,
55
+ "--version",
56
+ "-v",
57
+ callback=version_callback,
58
+ is_eager=True,
59
+ help="Show version and exit.",
60
+ ),
61
+ ) -> None:
62
+ """LEAP CLI - Command line interface for the LEAP platform."""
63
+ if ctx.invoked_subcommand is None:
64
+ console.print(ctx.get_help())
65
+
66
+
67
+ if __name__ == "__main__":
68
+ app()
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,211 @@
1
+ """API client utilities for LEAP CLI."""
2
+
3
+ from typing import Any, Dict, Literal, Optional, cast
4
+
5
+ import requests
6
+
7
+ from leap_bundle.utils.config import get_api_token, get_server_url
8
+
9
+
10
+ class APIClient:
11
+ """Client for LEAP API interactions."""
12
+
13
+ def __init__(self) -> None:
14
+ self.server_url = get_server_url()
15
+ self.api_token = get_api_token()
16
+
17
+ def _get_headers(self) -> Dict[str, str]:
18
+ """Get headers with authentication."""
19
+ if not self.api_token:
20
+ raise ValueError(
21
+ "No API token found. Please run 'leap-bundle login' first."
22
+ )
23
+
24
+ return {
25
+ "Authorization": f"Bearer {self.api_token}",
26
+ "Content-Type": "application/json",
27
+ }
28
+
29
+ def create_bundle_request(
30
+ self, input_path: str, input_hash: str, force_recreate: bool = False
31
+ ) -> Dict[str, Any]:
32
+ """Create a new bundle request."""
33
+ url = f"{self.server_url.rstrip('/')}/api/cli/bundle-requests"
34
+ payload = {
35
+ "input_path": input_path,
36
+ "input_hash": input_hash,
37
+ "force_recreate": force_recreate,
38
+ }
39
+
40
+ response = requests.post(
41
+ url, json=payload, headers=self._get_headers(), timeout=30
42
+ )
43
+
44
+ if response.status_code == 409:
45
+ return {"exists": True, "message": response.json().get("message", "")}
46
+ elif response.status_code == 200:
47
+ return {"exists": False, **response.json()}
48
+ else:
49
+ try:
50
+ error_data = response.json()
51
+ error_msg = error_data.get(
52
+ "error", f"HTTP {response.status_code} error"
53
+ )
54
+ except ValueError:
55
+ error_msg = f"HTTP {response.status_code} error"
56
+ raise requests.HTTPError(error_msg, response=response)
57
+
58
+ def update_bundle_request_status(
59
+ self,
60
+ request_id: str,
61
+ status: Literal["uploading_started", "uploading_completed", "uploading_failed"],
62
+ user_message: Optional[str] = None,
63
+ ) -> None:
64
+ """Update bundle request status via PATCH endpoint."""
65
+ url = f"{self.server_url.rstrip('/')}/api/cli/bundle-requests/{request_id}"
66
+ payload: Dict[str, Any] = {"status": status}
67
+ if user_message:
68
+ payload["user_message"] = user_message
69
+
70
+ response = requests.patch(
71
+ url, json=payload, headers=self._get_headers(), timeout=30
72
+ )
73
+ response.raise_for_status()
74
+
75
+ def list_bundle_requests(self) -> Dict[str, Any]:
76
+ """List bundle requests for the authenticated user."""
77
+ url = f"{self.server_url.rstrip('/')}/api/cli/bundle-requests"
78
+
79
+ response = requests.get(url, headers=self._get_headers(), timeout=30)
80
+
81
+ if response.status_code != 200:
82
+ try:
83
+ error_data = response.json()
84
+ error_msg = error_data.get(
85
+ "error", f"HTTP {response.status_code} error"
86
+ )
87
+ except ValueError:
88
+ error_msg = f"HTTP {response.status_code} error"
89
+ raise requests.HTTPError(error_msg, response=response)
90
+
91
+ return cast(Dict[str, Any], response.json())
92
+
93
+ def get_bundle_request(self, request_id: str) -> Dict[str, Any]:
94
+ """Get details for a specific bundle request."""
95
+ url = f"{self.server_url.rstrip('/')}/api/cli/bundle-requests/{request_id}"
96
+
97
+ response = requests.get(url, headers=self._get_headers(), timeout=30)
98
+
99
+ if response.status_code != 200:
100
+ try:
101
+ error_data = response.json()
102
+ error_msg = error_data.get(
103
+ "error", f"HTTP {response.status_code} error"
104
+ )
105
+ except ValueError:
106
+ error_msg = f"HTTP {response.status_code} error"
107
+ raise requests.HTTPError(error_msg, response=response)
108
+
109
+ return cast(Dict[str, Any], response.json())
110
+
111
+ def download_bundle_request(self, request_id: str) -> Dict[str, Any]:
112
+ """Get download URL for a completed bundle request."""
113
+ url = f"{self.server_url.rstrip('/')}/api/cli/bundle-requests/{request_id}/download"
114
+
115
+ response = requests.post(url, headers=self._get_headers(), timeout=30)
116
+
117
+ if response.status_code != 200:
118
+ try:
119
+ error_data = response.json()
120
+ error_msg = error_data.get(
121
+ "error", f"HTTP {response.status_code} error"
122
+ )
123
+ except ValueError:
124
+ error_msg = f"HTTP {response.status_code} error"
125
+ raise requests.HTTPError(error_msg, response=response)
126
+
127
+ return cast(Dict[str, Any], response.json())
128
+
129
+ def cancel_bundle_request(self, request_id: str) -> Dict[str, Any]:
130
+ """Cancel a bundle request."""
131
+ url = f"{self.server_url.rstrip('/')}/api/cli/bundle-requests/{request_id}"
132
+
133
+ response = requests.delete(url, headers=self._get_headers(), timeout=30)
134
+
135
+ if response.status_code not in [200, 404]:
136
+ try:
137
+ error_data = response.json()
138
+ error_msg = error_data.get(
139
+ "error", f"HTTP {response.status_code} error"
140
+ )
141
+ except ValueError:
142
+ error_msg = f"HTTP {response.status_code} error"
143
+ raise requests.HTTPError(error_msg, response=response)
144
+
145
+ result: Dict[str, Any] = response.json()
146
+ return result
147
+
148
+
149
+ def upload_directory_to_s3(
150
+ signed_url_data: Dict[str, Any], directory_path: str
151
+ ) -> None:
152
+ """Upload directory to S3 using signed URL."""
153
+ import os
154
+ from pathlib import Path
155
+
156
+ path = Path(directory_path)
157
+ url = signed_url_data["url"]
158
+ fields = signed_url_data["fields"]
159
+
160
+ for root, _, filenames in os.walk(path):
161
+ for filename in filenames:
162
+ file_path = Path(root) / filename
163
+ relative_path = file_path.relative_to(path)
164
+
165
+ form_data = fields.copy()
166
+ form_data["key"] = form_data["key"].replace(
167
+ "${filename}", str(relative_path)
168
+ )
169
+
170
+ with open(file_path, "rb") as f:
171
+ files = {"file": f}
172
+ response = requests.post(url, data=form_data, files=files, timeout=300)
173
+ response.raise_for_status()
174
+
175
+
176
+ def extract_error_message(response: requests.Response) -> str:
177
+ """Extract error message from HTTP response, preferring server-provided error field."""
178
+ try:
179
+ error_data = response.json()
180
+ return str(error_data.get("error", f"HTTP {response.status_code} error"))
181
+ except ValueError:
182
+ return f"HTTP {response.status_code} error"
183
+
184
+
185
+ def handle_cli_exception(e: Exception) -> None:
186
+ """Handle CLI exceptions with proper error message extraction and display."""
187
+ import typer
188
+ from rich.console import Console
189
+
190
+ console = Console()
191
+
192
+ if hasattr(e, "response") and e.response is not None:
193
+ error_message = extract_error_message(e.response)
194
+ console.print(f"[red]✗[/red] {error_message}")
195
+ else:
196
+ console.print(f"[red]✗[/red] Error: {e}")
197
+ raise typer.Exit(1) from e
198
+
199
+
200
+ def download_from_s3(signed_url: str, output_path: str) -> None:
201
+ """Download file from S3 using signed URL."""
202
+ from pathlib import Path
203
+
204
+ output_file = Path(output_path)
205
+ output_file.parent.mkdir(parents=True, exist_ok=True)
206
+
207
+ response = requests.get(signed_url, timeout=300)
208
+ response.raise_for_status()
209
+
210
+ with open(output_file, "wb") as f:
211
+ f.write(response.content)
@@ -0,0 +1,98 @@
1
+ """Configuration utilities for LEAP CLI."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import yaml
8
+
9
+
10
+ def get_config_file_path() -> Path:
11
+ """Get the path to the LEAP CLI config file."""
12
+ return Path.home() / ".liquid-leap"
13
+
14
+
15
+ def load_config() -> dict[str, str]:
16
+ """Load configuration from the config file."""
17
+ config_path = get_config_file_path()
18
+ if not config_path.exists():
19
+ return {}
20
+
21
+ try:
22
+ with open(config_path) as f:
23
+ return yaml.safe_load(f) or {}
24
+ except (yaml.YAMLError, OSError):
25
+ return {}
26
+
27
+
28
+ def save_config(config: dict[str, str]) -> None:
29
+ """Save configuration to the config file."""
30
+ config_path = get_config_file_path()
31
+
32
+ config_with_version = {"version": 1, **config}
33
+
34
+ try:
35
+ with open(config_path, "w") as f:
36
+ yaml.safe_dump(config_with_version, f, default_flow_style=False)
37
+
38
+ os.chmod(config_path, 0o600)
39
+ except OSError:
40
+ pass
41
+
42
+
43
+ def is_logged_in() -> bool:
44
+ """Check if the user is currently logged in."""
45
+ config = load_config()
46
+ return bool(config.get("api_token"))
47
+
48
+
49
+ def get_api_token() -> Optional[str]:
50
+ """Get the stored API token."""
51
+ config = load_config()
52
+ return config.get("api_token")
53
+
54
+
55
+ def get_server_url() -> str:
56
+ """Get the configured server URL."""
57
+ config = load_config()
58
+ return config.get("server_url", "https://leap.liquid.ai")
59
+
60
+
61
+ def set_server_url(url: str) -> None:
62
+ """Store the server URL in the config file."""
63
+ config_path = get_config_file_path()
64
+ config_exists = config_path.exists()
65
+
66
+ config = load_config()
67
+ config["server_url"] = url
68
+ save_config(config)
69
+
70
+ if not config_exists:
71
+ from rich.console import Console
72
+
73
+ console = Console()
74
+ console.print(f"[blue]ℹ[/blue] Config file created at: {config_path}")
75
+
76
+
77
+ def set_api_token(token: str) -> None:
78
+ """Store the API token in the config file."""
79
+ config_path = get_config_file_path()
80
+ config_exists = config_path.exists()
81
+
82
+ config = load_config()
83
+ config["api_token"] = token
84
+ save_config(config)
85
+
86
+ if not config_exists:
87
+ from rich.console import Console
88
+
89
+ console = Console()
90
+ console.print(f"[blue]ℹ[/blue] Config file created at: {config_path}")
91
+
92
+
93
+ def clear_api_token() -> None:
94
+ """Remove the API token from the config file."""
95
+ config = load_config()
96
+ if "api_token" in config:
97
+ del config["api_token"]
98
+ save_config(config)
@@ -0,0 +1,31 @@
1
+ """Hashing utilities for LEAP CLI."""
2
+
3
+ import hashlib
4
+ import os
5
+ from pathlib import Path
6
+ from typing import List
7
+
8
+
9
+ def calculate_directory_hash(directory_path: str) -> str:
10
+ """Calculate SHA256 hash of all files in a directory."""
11
+ path = Path(directory_path)
12
+ if not path.exists() or not path.is_dir():
13
+ raise ValueError(f"Directory does not exist: {directory_path}")
14
+
15
+ files: List[Path] = []
16
+ for root, _, filenames in os.walk(path):
17
+ for filename in filenames:
18
+ files.append(Path(root) / filename)
19
+
20
+ files.sort()
21
+
22
+ hasher = hashlib.sha256()
23
+ for file_path in files:
24
+ relative_path = file_path.relative_to(path)
25
+ hasher.update(str(relative_path).encode("utf-8"))
26
+
27
+ with open(file_path, "rb") as f:
28
+ for chunk in iter(lambda: f.read(4096), b""):
29
+ hasher.update(chunk)
30
+
31
+ return hasher.hexdigest()
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: leap-bundle
3
+ Version: 0.0.1a1
4
+ Summary: Command line interface for Liquid Edge AI Platform (LEAP)
5
+ Project-URL: Homepage, https://leap.liquid.ai
6
+ Author-email: Liquid AI <leap@liquid.ai>
7
+ License: LFM Open License v1.0
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: Other/Proprietary License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.8
18
+ Requires-Dist: pyyaml>=6.0.0
19
+ Requires-Dist: requests>=2.31.0
20
+ Requires-Dist: rich>=13.0.0
21
+ Requires-Dist: typer>=0.9.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: mypy>=1.0.0; extra == 'dev'
24
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
25
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
26
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # leap-bundle
30
+
31
+ Command line interface for Liquid Edge AI Platform (LEAP).
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install leap-bundle
37
+ ```
38
+
39
+ ## Commands
40
+
41
+ | Command | Description |
42
+ | --- | --- |
43
+ | `leap-bundle login <api-token>` | Authenticate with LEAP using API token |
44
+ | `leap-bundle whoami` | Show current authenticated user |
45
+ | `leap-bundle logout` | Logout from LEAP |
46
+ | `leap-bundle config` | Show current configuration |
47
+ | `leap-bundle config --server <url>` | Set server URL (default: https://leap.liquid.ai) |
48
+ | `leap-bundle create` | Submit new bundle request |
49
+ | `leap-bundle list` | List all bundle requests |
50
+ | `leap-bundle list <request-id>` | Show details of a specific request |
51
+ | `leap-bundle cancel <request-id>` | Cancel a bundle request |
52
+ | `leap-bundle download <request-id>` | Download the bundle file for a specific request |
53
+
54
+ ## Development
55
+
56
+ This package uses `uv` for dependency management.
57
+
58
+ ### Setup
59
+
60
+ ```bash
61
+ # Install uv if you haven't already
62
+ curl -LsSf https://astral.sh/uv/install.sh | sh
63
+
64
+ # Install dependencies
65
+ uv sync --dev
66
+ ```
67
+
68
+ ### Development Commands
69
+
70
+ | `uv` command | `npm` command | Description |
71
+ | --- | --- | --- |
72
+ | `uv run ruff check .` | `npm run lint` | Run code linting |
73
+ | `uv run ruff format .` | `npm run format` | Format code using ruff |
74
+ | `uv run mypy .` | `npm run typecheck` | Run type checking with mypy |
75
+ | `uv run pytest` | `npm run test` | Run tests using pytest |
76
+ | | `npm run check` | Run all above checks |
77
+
78
+ ### Local Development
79
+
80
+ ```bash
81
+ # Install the package in virtual environment
82
+ uv pip install -e .
83
+
84
+ # Run the CLI
85
+ uv run leap-bundle --help
86
+ # Or activate the virtual environment and run directly
87
+ source .venv/bin/activate
88
+ leap-bundle --help
89
+ ```
@@ -0,0 +1,17 @@
1
+ leap_bundle/__init__.py,sha256=sXLh7g3KC4QCFxcZGBTpG2scR7hmmBsMjq6LqRptkRg,22
2
+ leap_bundle/main.py,sha256=GZmsSoFxRWoiNjWahiFJ-DAUqQjlvMlUuRTfyy3jsW4,1755
3
+ leap_bundle/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ leap_bundle/commands/auth.py,sha256=ZILBhXoAsVRFdjLc7uvo9zIoKTnAjhNoA8KWXE3ERoA,3589
5
+ leap_bundle/commands/cancel.py,sha256=qZcQGxot6Oqto9etBuoegAafHfYt8kunEEyHfiM_bOg,968
6
+ leap_bundle/commands/config.py,sha256=n9mFW2c9FrZRUJ7fC_kGZ0JE1pOGLJbURi3lAsZVLe4,995
7
+ leap_bundle/commands/create.py,sha256=TI2Xmx8fsklqKUOkUipkH-h_7ETAqnfza-Wwp6kdAhg,3057
8
+ leap_bundle/commands/download.py,sha256=513Qx2iRzrGjcFEA225zqV99rNYbUUvCl386i6QF5zo,2608
9
+ leap_bundle/commands/list.py,sha256=R4fe9f_g5baoyU3rd8C1FgY2wbnL_qATXVTk-2nj3Rs,2477
10
+ leap_bundle/utils/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
+ leap_bundle/utils/api_client.py,sha256=vdE0vEN0K8RThpsAqGX1bL-PdTB1z3DO-9BCeucuOmU,7560
12
+ leap_bundle/utils/config.py,sha256=YuvlUDaSX-Tl3TWi7JtOb5m3fsys6K4tjrny2WrXu6k,2493
13
+ leap_bundle/utils/hash.py,sha256=Bv-WV34-PuZXSvtfMf6Cqc-_9_I68dBmlNcsjwXsGbI,896
14
+ leap_bundle-0.0.1a1.dist-info/METADATA,sha256=rsCZNAKYkewh1ppriLzFuJcii-FwVA4l2qaHBzFSriQ,2759
15
+ leap_bundle-0.0.1a1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ leap_bundle-0.0.1a1.dist-info/entry_points.txt,sha256=n0Yyt_fEhDZJsY7R2i9EGuhFEud11Qlm_lZZmDXo0k0,53
17
+ leap_bundle-0.0.1a1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ leap-bundle = leap_bundle.main:app