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.
- leap_bundle/__init__.py +1 -0
- leap_bundle/commands/__init__.py +0 -0
- leap_bundle/commands/auth.py +112 -0
- leap_bundle/commands/cancel.py +35 -0
- leap_bundle/commands/config.py +31 -0
- leap_bundle/commands/create.py +87 -0
- leap_bundle/commands/download.py +78 -0
- leap_bundle/commands/list.py +73 -0
- leap_bundle/main.py +68 -0
- leap_bundle/utils/__init__.py +1 -0
- leap_bundle/utils/api_client.py +211 -0
- leap_bundle/utils/config.py +98 -0
- leap_bundle/utils/hash.py +31 -0
- leap_bundle-0.0.1a1.dist-info/METADATA +89 -0
- leap_bundle-0.0.1a1.dist-info/RECORD +17 -0
- leap_bundle-0.0.1a1.dist-info/WHEEL +4 -0
- leap_bundle-0.0.1a1.dist-info/entry_points.txt +2 -0
leap_bundle/__init__.py
ADDED
|
@@ -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,,
|