sima-cli 0.0.1__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.
- sima_cli/__init__.py +12 -0
- sima_cli/__main__.py +4 -0
- sima_cli/__version__.py +2 -0
- sima_cli/app_zoo/__init__.py +0 -0
- sima_cli/app_zoo/app.py +0 -0
- sima_cli/auth/__init__.py +0 -0
- sima_cli/auth/login.py +0 -0
- sima_cli/cli.py +110 -0
- sima_cli/download/__init__.py +11 -0
- sima_cli/download/downloader.py +137 -0
- sima_cli/model_zoo/__init__.py +0 -0
- sima_cli/model_zoo/model.py +0 -0
- sima_cli/update/__init__.py +3 -0
- sima_cli/update/updater.py +55 -0
- sima_cli/utils/__init__.py +0 -0
- sima_cli/utils/config.py +52 -0
- sima_cli/utils/env.py +172 -0
- sima_cli/utils/network.py +42 -0
- sima_cli-0.0.1.dist-info/METADATA +112 -0
- sima_cli-0.0.1.dist-info/RECORD +32 -0
- sima_cli-0.0.1.dist-info/WHEEL +5 -0
- sima_cli-0.0.1.dist-info/entry_points.txt +2 -0
- sima_cli-0.0.1.dist-info/licenses/LICENSE +21 -0
- sima_cli-0.0.1.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_app_zoo.py +0 -0
- tests/test_auth.py +0 -0
- tests/test_cli.py +0 -0
- tests/test_download.py +115 -0
- tests/test_firmware.py +0 -0
- tests/test_model_zoo.py +0 -0
- tests/test_utils.py +0 -0
sima_cli/__init__.py
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
"""
|
2
|
+
sima_cli - SiMa Developer CLI Tool
|
3
|
+
|
4
|
+
This package provides the command-line interface for interacting with
|
5
|
+
the SiMa Developer Portal. Functionality includes authentication, model
|
6
|
+
and firmware downloads, and device updates.
|
7
|
+
|
8
|
+
To get started, run: `sima-cli help`
|
9
|
+
"""
|
10
|
+
from .__version__ import __version__
|
11
|
+
|
12
|
+
__all__ = ["__version__"]
|
sima_cli/__main__.py
ADDED
sima_cli/__version__.py
ADDED
File without changes
|
sima_cli/app_zoo/app.py
ADDED
File without changes
|
File without changes
|
sima_cli/auth/login.py
ADDED
File without changes
|
sima_cli/cli.py
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
import click
|
2
|
+
from sima_cli.utils.env import get_environment_type
|
3
|
+
from sima_cli.update.updater import perform_update
|
4
|
+
|
5
|
+
# Entry point for the CLI tool using Click's command group decorator
|
6
|
+
@click.group()
|
7
|
+
def main():
|
8
|
+
"""sima-cli β SiMa Developer Portal CLI Tool"""
|
9
|
+
env_type, env_subtype = get_environment_type()
|
10
|
+
click.echo(f"π§ Environment: {env_type} ({env_subtype})")
|
11
|
+
|
12
|
+
# ----------------------
|
13
|
+
# Authentication Command
|
14
|
+
# ----------------------
|
15
|
+
@main.command()
|
16
|
+
def login():
|
17
|
+
"""Authenticate with the SiMa Developer Portal."""
|
18
|
+
# Placeholder: Implement browser or manual token-based login
|
19
|
+
click.echo("Login flow not yet implemented.")
|
20
|
+
|
21
|
+
# ----------------------
|
22
|
+
# Download Command
|
23
|
+
# ----------------------
|
24
|
+
@main.command(name="download")
|
25
|
+
@click.argument('url') # Accept both file and folder URLs
|
26
|
+
@click.option('-d', '--dest', type=click.Path(), default='.', help="Target download directory")
|
27
|
+
def download(url, dest):
|
28
|
+
"""Download a file or a whole folder from a given URL."""
|
29
|
+
from sima_cli.download.downloader import download_file_from_url, download_folder_from_url
|
30
|
+
|
31
|
+
# First, try to download as a file
|
32
|
+
try:
|
33
|
+
click.echo("π Checking if URL is a direct file...")
|
34
|
+
path = download_file_from_url(url, dest)
|
35
|
+
click.echo(f"\nβ
File downloaded successfully to: {path}")
|
36
|
+
return
|
37
|
+
except Exception as e:
|
38
|
+
pass
|
39
|
+
|
40
|
+
# If that fails, try to treat as a folder and download all files
|
41
|
+
try:
|
42
|
+
click.echo("π Attempting folder download...")
|
43
|
+
paths = download_folder_from_url(url, dest)
|
44
|
+
if not paths:
|
45
|
+
raise RuntimeError("No files were downloaded.")
|
46
|
+
click.echo(f"\nβ
Folder download completed. {len(paths)} files saved to: {dest}")
|
47
|
+
except Exception as e:
|
48
|
+
click.echo(f"\nβ Failed to download as folder: {e}", err=True)
|
49
|
+
|
50
|
+
# ----------------------
|
51
|
+
# Update Command
|
52
|
+
# ----------------------
|
53
|
+
@main.command(name="update")
|
54
|
+
@click.argument('version_or_url')
|
55
|
+
@click.option('--ip', help="Target device IP address for remote firmware update.")
|
56
|
+
def update(version_or_url, ip):
|
57
|
+
"""Run system update across different environments."""
|
58
|
+
perform_update(version_or_url, ip)
|
59
|
+
|
60
|
+
# ----------------------
|
61
|
+
# Model Zoo Subcommands
|
62
|
+
# ----------------------
|
63
|
+
@main.group()
|
64
|
+
def model_zoo():
|
65
|
+
"""Access models from the Model Zoo."""
|
66
|
+
pass
|
67
|
+
|
68
|
+
@model_zoo.command("list")
|
69
|
+
@click.option('--ver', help="SDK version")
|
70
|
+
def list_models(ver):
|
71
|
+
"""List available models."""
|
72
|
+
# Placeholder: Call API to list models
|
73
|
+
click.echo(f"Listing models for version: {ver or 'latest'}")
|
74
|
+
|
75
|
+
@model_zoo.command("get")
|
76
|
+
@click.argument('model_name') # Required: model name
|
77
|
+
@click.option('--ver', help="SDK version")
|
78
|
+
def get_model(model_name, ver):
|
79
|
+
"""Download a specific model."""
|
80
|
+
# Placeholder: Download and validate model
|
81
|
+
click.echo(f"Getting model '{model_name}' for version: {ver or 'latest'}")
|
82
|
+
|
83
|
+
# ----------------------
|
84
|
+
# App Zoo Subcommands
|
85
|
+
# ----------------------
|
86
|
+
@main.group()
|
87
|
+
def app_zoo():
|
88
|
+
"""Access apps from the App Zoo."""
|
89
|
+
pass
|
90
|
+
|
91
|
+
@app_zoo.command("list")
|
92
|
+
@click.option('--ver', help="SDK version")
|
93
|
+
def list_apps(ver):
|
94
|
+
"""List available apps."""
|
95
|
+
# Placeholder: Call API to list apps
|
96
|
+
click.echo(f"Listing apps for version: {ver or 'latest'}")
|
97
|
+
|
98
|
+
@app_zoo.command("get")
|
99
|
+
@click.argument('app_name') # Required: app name
|
100
|
+
@click.option('--ver', help="SDK version")
|
101
|
+
def get_app(app_name, ver):
|
102
|
+
"""Download a specific app."""
|
103
|
+
# Placeholder: Download and validate app
|
104
|
+
click.echo(f"Getting app '{app_name}' for version: {ver or 'latest'}")
|
105
|
+
|
106
|
+
# ----------------------
|
107
|
+
# Entry point for direct execution
|
108
|
+
# ----------------------
|
109
|
+
if __name__ == "__main__":
|
110
|
+
main()
|
@@ -0,0 +1,137 @@
|
|
1
|
+
import os
|
2
|
+
import requests
|
3
|
+
from urllib.parse import urlparse
|
4
|
+
from tqdm import tqdm
|
5
|
+
from typing import List
|
6
|
+
|
7
|
+
def _list_directory_files(url: str) -> List[str]:
|
8
|
+
"""
|
9
|
+
Attempt to list files in a server-hosted directory with index browsing enabled.
|
10
|
+
|
11
|
+
Args:
|
12
|
+
url (str): Base URL to the folder.
|
13
|
+
|
14
|
+
Returns:
|
15
|
+
List[str]: List of full file URLs.
|
16
|
+
|
17
|
+
Raises:
|
18
|
+
RuntimeError: If listing fails or HTML cannot be parsed.
|
19
|
+
"""
|
20
|
+
try:
|
21
|
+
response = requests.get(url, timeout=10)
|
22
|
+
response.raise_for_status()
|
23
|
+
|
24
|
+
if "text/html" not in response.headers.get("Content-Type", ""):
|
25
|
+
raise RuntimeError("Directory listing not supported (non-HTML response).")
|
26
|
+
|
27
|
+
import re
|
28
|
+
|
29
|
+
hrefs = re.findall(r'href="([^"?/][^"?]*)"', response.text)
|
30
|
+
files = [href for href in hrefs if href not in ("../", "") and not href.endswith("/")]
|
31
|
+
|
32
|
+
if not files:
|
33
|
+
raise RuntimeError("No files found or listing is not permitted.")
|
34
|
+
|
35
|
+
return [url.rstrip("/") + "/" + fname for fname in files]
|
36
|
+
|
37
|
+
except Exception as e:
|
38
|
+
raise RuntimeError(f"Failed to list folder '{url}': {e}")
|
39
|
+
|
40
|
+
|
41
|
+
def download_file_from_url(url: str, dest_folder: str = ".") -> str:
|
42
|
+
"""
|
43
|
+
Download a file from a direct URL with resume and skip support.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
url (str): The full URL to download.
|
47
|
+
dest_folder (str): The folder to save the downloaded file.
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
str: Path to the downloaded file.
|
51
|
+
|
52
|
+
Raises:
|
53
|
+
Exception: if download fails.
|
54
|
+
"""
|
55
|
+
# Extract file name
|
56
|
+
parsed_url = urlparse(url)
|
57
|
+
file_name = os.path.basename(parsed_url.path)
|
58
|
+
if not file_name:
|
59
|
+
raise ValueError("Cannot determine file name from URL.")
|
60
|
+
|
61
|
+
os.makedirs(dest_folder, exist_ok=True)
|
62
|
+
dest_path = os.path.join(dest_folder, file_name)
|
63
|
+
|
64
|
+
resume_header = {}
|
65
|
+
mode = 'wb'
|
66
|
+
existing_size = 0
|
67
|
+
|
68
|
+
try:
|
69
|
+
# Get total size from HEAD request
|
70
|
+
head = requests.head(url, timeout=10)
|
71
|
+
head.raise_for_status()
|
72
|
+
total_size = int(head.headers.get('content-length', 0))
|
73
|
+
|
74
|
+
# Check for existing file
|
75
|
+
if os.path.exists(dest_path):
|
76
|
+
existing_size = os.path.getsize(dest_path)
|
77
|
+
|
78
|
+
if existing_size == total_size:
|
79
|
+
print(f"β File already exists and is complete: {file_name}")
|
80
|
+
return dest_path
|
81
|
+
elif existing_size < total_size:
|
82
|
+
# Set up resume
|
83
|
+
resume_header['Range'] = f'bytes={existing_size}-'
|
84
|
+
mode = 'ab'
|
85
|
+
else:
|
86
|
+
# Local file is bigger than expected β reset
|
87
|
+
existing_size = 0
|
88
|
+
mode = 'wb'
|
89
|
+
|
90
|
+
# Start download (with optional Range header)
|
91
|
+
with requests.get(url, stream=True, headers=resume_header, timeout=30) as r:
|
92
|
+
r.raise_for_status()
|
93
|
+
|
94
|
+
# If resuming, adjust total to show correct progress bar
|
95
|
+
content_length = int(r.headers.get('content-length', 0))
|
96
|
+
final_total = existing_size + content_length
|
97
|
+
|
98
|
+
with open(dest_path, mode) as f, tqdm(
|
99
|
+
desc=f"Downloading {file_name}",
|
100
|
+
total=final_total,
|
101
|
+
initial=existing_size,
|
102
|
+
unit='B',
|
103
|
+
unit_scale=True,
|
104
|
+
unit_divisor=1024
|
105
|
+
) as bar:
|
106
|
+
for chunk in r.iter_content(chunk_size=8192):
|
107
|
+
if chunk:
|
108
|
+
f.write(chunk)
|
109
|
+
bar.update(len(chunk))
|
110
|
+
|
111
|
+
except Exception as e:
|
112
|
+
raise RuntimeError(f"Download failed: {e}")
|
113
|
+
|
114
|
+
return dest_path
|
115
|
+
|
116
|
+
def download_folder_from_url(url: str, dest_folder: str = ".") -> List[str]:
|
117
|
+
"""
|
118
|
+
Download all files listed in a remote folder (server must support listing).
|
119
|
+
|
120
|
+
Args:
|
121
|
+
url (str): Folder URL.
|
122
|
+
dest_folder (str): Local folder to save downloads.
|
123
|
+
|
124
|
+
Returns:
|
125
|
+
List[str]: Paths to all downloaded files.
|
126
|
+
"""
|
127
|
+
file_urls = _list_directory_files(url)
|
128
|
+
downloaded_paths = []
|
129
|
+
|
130
|
+
for file_url in file_urls:
|
131
|
+
try:
|
132
|
+
downloaded_path = download_file_from_url(file_url, dest_folder)
|
133
|
+
downloaded_paths.append(downloaded_path)
|
134
|
+
except Exception as e:
|
135
|
+
print(f"β Skipped {file_url}: {e}")
|
136
|
+
|
137
|
+
return downloaded_paths
|
File without changes
|
File without changes
|
@@ -0,0 +1,55 @@
|
|
1
|
+
import click
|
2
|
+
import os
|
3
|
+
from sima_cli.utils.env import get_environment_type
|
4
|
+
from sima_cli.download import download_file_from_url
|
5
|
+
|
6
|
+
def perform_update(version_or_url: str, ip: str = None):
|
7
|
+
r"""
|
8
|
+
Update the system based on environment and input.
|
9
|
+
|
10
|
+
- On PCIe host: updates host driver and/or downloads firmware.
|
11
|
+
- On SiMa board: applies firmware update.
|
12
|
+
- In SDK: allows simulated or direct board update.
|
13
|
+
- Unknown env: requires --ip to specify remote device.
|
14
|
+
"""
|
15
|
+
env_type, env_subtype = get_environment_type()
|
16
|
+
click.echo(f"π Running update for environment: {env_type} ({env_subtype})")
|
17
|
+
click.echo(f"Requested version or URL: {version_or_url}")
|
18
|
+
|
19
|
+
if env_type == "host" and env_subtype == 'linux':
|
20
|
+
click.echo("π Updating PCIe host driver and downloading firmware...")
|
21
|
+
_update_host(version_or_url)
|
22
|
+
elif env_type == "board":
|
23
|
+
click.echo("π Updating firmware on SiMa board...")
|
24
|
+
_update_board(version_or_url)
|
25
|
+
elif env_type == "sdk":
|
26
|
+
click.echo("π Updating firmware from within the Palette SDK...")
|
27
|
+
_update_sdk(version_or_url)
|
28
|
+
elif ip:
|
29
|
+
click.echo(f"π Updating firmware on remote board at {ip}...")
|
30
|
+
_update_remote(ip, version_or_url)
|
31
|
+
else:
|
32
|
+
click.echo("β Unknown environment. Use --ip to specify target device.")
|
33
|
+
|
34
|
+
def _update_host(version_or_url: str):
|
35
|
+
# Simulate firmware download
|
36
|
+
try:
|
37
|
+
dest_dir = "/tmp/sima-firmware"
|
38
|
+
os.makedirs(dest_dir, exist_ok=True)
|
39
|
+
firmware_path = download_file_from_url(version_or_url, dest_dir)
|
40
|
+
click.echo(f"π¦ Firmware downloaded to: {firmware_path}")
|
41
|
+
click.echo("βοΈ Simulated PCIe host driver reload (not implemented).")
|
42
|
+
except Exception as e:
|
43
|
+
click.echo(f"β Host update failed: {e}")
|
44
|
+
|
45
|
+
def _update_board(version_or_url: str):
|
46
|
+
click.echo("βοΈ Simulated board firmware update (not implemented).")
|
47
|
+
# TODO: Validate path, unpack firmware, flash/update on device
|
48
|
+
|
49
|
+
def _update_sdk(version_or_url: str):
|
50
|
+
click.echo("βοΈ Simulated SDK firmware update logic (not implemented).")
|
51
|
+
# TODO: Implement update via SDK-based communication or tools
|
52
|
+
|
53
|
+
def _update_remote(ip: str, version_or_url: str):
|
54
|
+
click.echo("βοΈ Simulated remote update via SSH/SCP (not implemented).")
|
55
|
+
# TODO: Validate network reachability, copy and trigger update on remote
|
File without changes
|
sima_cli/utils/config.py
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
import os
|
2
|
+
import json
|
3
|
+
|
4
|
+
# Construct a cross-platform compatible config path
|
5
|
+
CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".sima-cli", "config.json")
|
6
|
+
|
7
|
+
def load_config():
|
8
|
+
"""
|
9
|
+
Load the CLI configuration from disk.
|
10
|
+
|
11
|
+
Returns:
|
12
|
+
dict: Parsed configuration data, or an empty dict if no config exists.
|
13
|
+
"""
|
14
|
+
if not os.path.exists(CONFIG_PATH):
|
15
|
+
return {}
|
16
|
+
try:
|
17
|
+
with open(CONFIG_PATH, "r") as f:
|
18
|
+
return json.load(f)
|
19
|
+
except Exception:
|
20
|
+
return {}
|
21
|
+
|
22
|
+
def save_config(data):
|
23
|
+
"""
|
24
|
+
Save a dictionary of configuration data to disk.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
data (dict): The configuration data to store.
|
28
|
+
"""
|
29
|
+
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
30
|
+
with open(CONFIG_PATH, "w") as f:
|
31
|
+
json.dump(data, f, indent=2)
|
32
|
+
|
33
|
+
def get_auth_token():
|
34
|
+
"""
|
35
|
+
Retrieve the current authentication token from the config.
|
36
|
+
|
37
|
+
Returns:
|
38
|
+
str or None: The stored auth token, or None if not set.
|
39
|
+
"""
|
40
|
+
config = load_config()
|
41
|
+
return config.get("auth_token")
|
42
|
+
|
43
|
+
def set_auth_token(token):
|
44
|
+
"""
|
45
|
+
Store a new authentication token in the config.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
token (str): The auth token to save.
|
49
|
+
"""
|
50
|
+
config = load_config()
|
51
|
+
config["auth_token"] = token
|
52
|
+
save_config(config)
|
sima_cli/utils/env.py
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
import os
|
2
|
+
import subprocess
|
3
|
+
import platform
|
4
|
+
|
5
|
+
# Utility functions to determine the environment:
|
6
|
+
# - Whether we are running on a SiMa board
|
7
|
+
# - Or from a PCIe host (e.g., a developer workstation)
|
8
|
+
|
9
|
+
def is_sima_board() -> bool:
|
10
|
+
"""
|
11
|
+
Detect if running on a SiMa board.
|
12
|
+
|
13
|
+
This is done by checking for the existence of a known build info file
|
14
|
+
located at /etc/build and looking for specific identifiers like
|
15
|
+
SIMA_BUILD_VERSION and MACHINE.
|
16
|
+
|
17
|
+
Returns:
|
18
|
+
bool: True if running on a SiMa board, False otherwise.
|
19
|
+
"""
|
20
|
+
build_file_path = "/etc/build"
|
21
|
+
if not os.path.exists(build_file_path):
|
22
|
+
return False
|
23
|
+
|
24
|
+
try:
|
25
|
+
with open(build_file_path, "r") as f:
|
26
|
+
content = f.read()
|
27
|
+
return "SIMA_BUILD_VERSION" in content and "MACHINE" in content
|
28
|
+
except Exception:
|
29
|
+
return False
|
30
|
+
|
31
|
+
def is_pcie_host() -> bool:
|
32
|
+
"""
|
33
|
+
Detect if running from a PCIe host (typically a Linux or macOS dev machine).
|
34
|
+
|
35
|
+
This assumes a PCIe host is not a SiMa board and is running on a standard
|
36
|
+
Unix-based OS like Linux or macOS (Darwin).
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
bool: True if likely a PCIe host, False otherwise.
|
40
|
+
"""
|
41
|
+
import platform
|
42
|
+
return not is_sima_board() and platform.system() in ["Linux", "Darwin"]
|
43
|
+
|
44
|
+
def get_sima_board_type() -> str:
|
45
|
+
"""
|
46
|
+
If running on a SiMa board, extract the board type from the MACHINE field in /etc/build.
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
str: The board type (e.g., "modalix", "davinci"), or an empty string if not found or not a SiMa board.
|
50
|
+
"""
|
51
|
+
build_file_path = "/etc/build"
|
52
|
+
if not os.path.exists(build_file_path):
|
53
|
+
return ""
|
54
|
+
|
55
|
+
try:
|
56
|
+
with open(build_file_path, "r") as f:
|
57
|
+
for line in f:
|
58
|
+
if line.startswith("MACHINE"):
|
59
|
+
# Format: MACHINE = modalix
|
60
|
+
parts = line.split("=")
|
61
|
+
if len(parts) == 2:
|
62
|
+
return parts[1].strip()
|
63
|
+
except Exception:
|
64
|
+
pass
|
65
|
+
|
66
|
+
return ""
|
67
|
+
|
68
|
+
def is_palette_sdk() -> bool:
|
69
|
+
"""
|
70
|
+
Check if the environment is running inside the Palette SDK container.
|
71
|
+
|
72
|
+
This is detected by checking for the /etc/sdk-release file and verifying
|
73
|
+
it contains the string 'Palette_SDK'.
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
bool: True if running in Palette SDK, False otherwise.
|
77
|
+
"""
|
78
|
+
sdk_release_path = "/etc/sdk-release"
|
79
|
+
if not os.path.exists(sdk_release_path):
|
80
|
+
return False
|
81
|
+
|
82
|
+
try:
|
83
|
+
with open(sdk_release_path, "r") as f:
|
84
|
+
content = f.read()
|
85
|
+
return "Palette_SDK" in content
|
86
|
+
except Exception:
|
87
|
+
return False
|
88
|
+
|
89
|
+
def get_environment_type() -> tuple[str, str]:
|
90
|
+
"""
|
91
|
+
Return the environment type and subtype as a tuple.
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
tuple:
|
95
|
+
- env_type (str): "board", "sdk", or "host"
|
96
|
+
- env_subtype (str): board type (e.g., "modalix"), "palette", or host OS (e.g., "mac", "linux", "windows")
|
97
|
+
"""
|
98
|
+
if is_palette_sdk():
|
99
|
+
return "sdk", "palette"
|
100
|
+
|
101
|
+
if is_sima_board():
|
102
|
+
board_type = get_sima_board_type()
|
103
|
+
return "board", board_type or "unknown"
|
104
|
+
|
105
|
+
import platform
|
106
|
+
system = platform.system().lower()
|
107
|
+
if system == "darwin":
|
108
|
+
return "host", "mac"
|
109
|
+
elif system == "linux":
|
110
|
+
return "host", "linux"
|
111
|
+
elif system == "windows":
|
112
|
+
return "host", "windows"
|
113
|
+
|
114
|
+
return "host", "unknown"
|
115
|
+
|
116
|
+
def check_pcie_card_installation() -> tuple[bool, str]:
|
117
|
+
"""
|
118
|
+
Check whether the PCIe SiMa card is properly installed on a supported Linux host.
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
tuple:
|
122
|
+
- success (bool): True if all checks pass, False otherwise.
|
123
|
+
- message (str): Summary of results or error message.
|
124
|
+
"""
|
125
|
+
# Platform check
|
126
|
+
if platform.system().lower() != "linux":
|
127
|
+
return False, "β This check is only supported on Linux hosts."
|
128
|
+
|
129
|
+
if is_sima_board():
|
130
|
+
return False, "β This check is not applicable when running on a SiMa board."
|
131
|
+
|
132
|
+
if is_palette_sdk():
|
133
|
+
return False, "β This check is not applicable inside the Palette SDK container."
|
134
|
+
|
135
|
+
try:
|
136
|
+
# Check GStreamer plugin
|
137
|
+
gst_result = subprocess.run(
|
138
|
+
["gst-inspect-1.0", "pciehost"],
|
139
|
+
capture_output=True, text=True
|
140
|
+
)
|
141
|
+
if gst_result.returncode != 0 or "pciehost" not in gst_result.stdout:
|
142
|
+
return False, "β GStreamer plugin 'pciehost' not found."
|
143
|
+
|
144
|
+
# Check kernel module
|
145
|
+
modinfo_result = subprocess.run(
|
146
|
+
["modinfo", "sima_mla_drv"],
|
147
|
+
capture_output=True, text=True
|
148
|
+
)
|
149
|
+
if modinfo_result.returncode != 0 or "sima_mla_drv" not in modinfo_result.stdout:
|
150
|
+
return False, "β sima_mla_drv kernel module not found or not loaded."
|
151
|
+
|
152
|
+
# Check PCI device presence
|
153
|
+
lspci_result = subprocess.run(
|
154
|
+
["lspci", "-vd", "1f06:abcd"],
|
155
|
+
capture_output=True, text=True
|
156
|
+
)
|
157
|
+
if lspci_result.returncode != 0 or "Device 1f06:abcd" not in lspci_result.stdout:
|
158
|
+
return False, "β PCIe SiMa card not detected."
|
159
|
+
|
160
|
+
return True, "β
PCIe SiMa card is properly installed and recognized."
|
161
|
+
|
162
|
+
except FileNotFoundError as e:
|
163
|
+
return False, f"β Required system tool not found: {e.filename}"
|
164
|
+
|
165
|
+
except Exception as e:
|
166
|
+
return False, f"β Unexpected error: {str(e)}"
|
167
|
+
|
168
|
+
|
169
|
+
if __name__ == "__main__":
|
170
|
+
env_type, env_subtype = get_environment_type()
|
171
|
+
print(f"Environment Type: {env_type}")
|
172
|
+
print(f"Environment Subtype: {env_subtype}")
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import requests
|
2
|
+
|
3
|
+
# This module wraps common network calls like GET and HEAD
|
4
|
+
# to centralize error handling, auth injection, and logging if needed.
|
5
|
+
|
6
|
+
def get(url, headers=None, timeout=10):
|
7
|
+
"""
|
8
|
+
Wrapper around requests.get() with built-in error checking.
|
9
|
+
|
10
|
+
Args:
|
11
|
+
url (str): The target URL.
|
12
|
+
headers (dict, optional): HTTP headers to include.
|
13
|
+
timeout (int): Request timeout in seconds.
|
14
|
+
|
15
|
+
Returns:
|
16
|
+
requests.Response: The successful response object.
|
17
|
+
|
18
|
+
Raises:
|
19
|
+
HTTPError: If the response contains an HTTP error status.
|
20
|
+
"""
|
21
|
+
response = requests.get(url, headers=headers, timeout=timeout)
|
22
|
+
response.raise_for_status()
|
23
|
+
return response
|
24
|
+
|
25
|
+
def head(url, headers=None, timeout=10):
|
26
|
+
"""
|
27
|
+
Wrapper around requests.head() with built-in error checking.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
url (str): The target URL.
|
31
|
+
headers (dict, optional): HTTP headers to include.
|
32
|
+
timeout (int): Request timeout in seconds.
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
requests.Response: The successful HEAD response.
|
36
|
+
|
37
|
+
Raises:
|
38
|
+
HTTPError: If the response contains an HTTP error status.
|
39
|
+
"""
|
40
|
+
response = requests.head(url, headers=headers, timeout=timeout)
|
41
|
+
response.raise_for_status()
|
42
|
+
return response
|
@@ -0,0 +1,112 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: sima-cli
|
3
|
+
Version: 0.0.1
|
4
|
+
Summary: CLI tool for SiMa Developer Portal to download models, firmware, and apps.
|
5
|
+
Home-page: https://developer.sima.ai/
|
6
|
+
Author: SiMa.ai
|
7
|
+
Author-email: "Sima.ai" <support@sima.ai>
|
8
|
+
License: MIT
|
9
|
+
Project-URL: Homepage, https://developer.sima.ai/
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
12
|
+
Classifier: Operating System :: OS Independent
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
14
|
+
Classifier: Intended Audience :: Developers
|
15
|
+
Classifier: Topic :: Software Development :: Build Tools
|
16
|
+
Classifier: Environment :: Console
|
17
|
+
Requires-Python: >=3.10
|
18
|
+
Description-Content-Type: text/markdown
|
19
|
+
License-File: LICENSE
|
20
|
+
Requires-Dist: requests
|
21
|
+
Requires-Dist: click
|
22
|
+
Requires-Dist: tqdm
|
23
|
+
Dynamic: author
|
24
|
+
Dynamic: license-file
|
25
|
+
Dynamic: requires-python
|
26
|
+
|
27
|
+
# sima-cli
|
28
|
+
|
29
|
+
`sima-cli` is a lightweight command-line tool to interface with the SiMa Developer Portal. It allows users to authenticate, download models and firmware, and manage updates for SiMa devices.
|
30
|
+
|
31
|
+
## π§ Features
|
32
|
+
|
33
|
+
- Login with browser-based or manual authentication.
|
34
|
+
- Download resources via full URL or URI.
|
35
|
+
- List and download models and apps.
|
36
|
+
- Update firmware from version or URL.
|
37
|
+
- Automatically detects board vs PCIe host environment.
|
38
|
+
|
39
|
+
---
|
40
|
+
|
41
|
+
## π» For Developers
|
42
|
+
|
43
|
+
### π Project Structure
|
44
|
+
|
45
|
+
```
|
46
|
+
sima-cli/
|
47
|
+
βββ sima_cli/ # CLI source code
|
48
|
+
β βββ cli.py # Main CLI entry point
|
49
|
+
β βββ auth/ # Authentication logic
|
50
|
+
β βββ download/ # Downloading logic
|
51
|
+
β βββ firmware/ # Firmware update logic
|
52
|
+
β βββ model_zoo/ # Model zoo interactions
|
53
|
+
β βββ app_zoo/ # App zoo interactions
|
54
|
+
β βββ utils/ # Environment and config helpers
|
55
|
+
βββ tests/ # Unit tests
|
56
|
+
βββ pyproject.toml # Build config (PEP 517/518)
|
57
|
+
βββ setup.cfg # Package config
|
58
|
+
βββ requirements.txt # Dependencies
|
59
|
+
βββ README.md # This file
|
60
|
+
```
|
61
|
+
|
62
|
+
---
|
63
|
+
|
64
|
+
### π Build Instructions
|
65
|
+
|
66
|
+
#### 1. Install dev dependencies
|
67
|
+
|
68
|
+
```bash
|
69
|
+
python -m venv venv
|
70
|
+
source venv/bin/activate
|
71
|
+
pip install -r requirements.txt
|
72
|
+
```
|
73
|
+
|
74
|
+
#### 2. Install package locally
|
75
|
+
|
76
|
+
```bash
|
77
|
+
pip install -e .
|
78
|
+
```
|
79
|
+
|
80
|
+
#### 3. Run CLI tool
|
81
|
+
|
82
|
+
```bash
|
83
|
+
sima-cli help
|
84
|
+
```
|
85
|
+
|
86
|
+
---
|
87
|
+
|
88
|
+
### π§ͺ Run Tests
|
89
|
+
|
90
|
+
```bash
|
91
|
+
python -m unittest discover tests
|
92
|
+
```
|
93
|
+
|
94
|
+
---
|
95
|
+
|
96
|
+
### π¦ Build and Publish to PyPI
|
97
|
+
|
98
|
+
```bash
|
99
|
+
# Build wheel and sdist
|
100
|
+
python -m build
|
101
|
+
|
102
|
+
# Upload using Twine
|
103
|
+
twine upload dist/*
|
104
|
+
```
|
105
|
+
|
106
|
+
> β Make sure your version number is updated in `setup.cfg` or `pyproject.toml` before release.
|
107
|
+
|
108
|
+
---
|
109
|
+
|
110
|
+
### π Related Links
|
111
|
+
|
112
|
+
- [SiMa Developer Portal](https://developer.sima.ai/)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
sima_cli/__init__.py,sha256=Nb2jSg9-CX1XvSc1c21U9qQ3atINxphuNkNfmR-9P3o,332
|
2
|
+
sima_cli/__main__.py,sha256=ehzD6AZ7zGytC2gLSvaJatxeD0jJdaEvNJvwYeGsWOg,69
|
3
|
+
sima_cli/__version__.py,sha256=1-g_0OvKg85G2JJP9evxAWF6QTX3q-G2AcL5e7Anr18,48
|
4
|
+
sima_cli/cli.py,sha256=X4h6Z5esJ7qAFd7dEzD52MR3p-9SqrDHtV_njmatdsI,3697
|
5
|
+
sima_cli/app_zoo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
+
sima_cli/app_zoo/app.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
+
sima_cli/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
sima_cli/auth/login.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
+
sima_cli/download/__init__.py,sha256=6y4O2FOCYFR2jdnQoVi3hRtEoZ0Gw6rydlTy1SGJ5FE,218
|
10
|
+
sima_cli/download/downloader.py,sha256=lVL3Vk3ltCktZ86oS3B6_vX8sCJIUfTSRiJVsjuCAYA,4319
|
11
|
+
sima_cli/model_zoo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
|
+
sima_cli/model_zoo/model.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
|
+
sima_cli/update/__init__.py,sha256=0P-z-rSaev40IhfJXytK3AFWv2_sdQU4Ry6ei2sEus0,66
|
14
|
+
sima_cli/update/updater.py,sha256=KUv1fIpyLOCUU_GNVBRBbM3kRaajTLtzI9_P5CtSPOw,2394
|
15
|
+
sima_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
+
sima_cli/utils/config.py,sha256=0FQ7TnDJBYdQEK_kMstvjKcQVke60PQrT-w7uk0SYwo,1280
|
17
|
+
sima_cli/utils/env.py,sha256=MjIHTioSvx8rQfHoLCAn5VzhaJwvXceTAcrmATXtn2Y,5487
|
18
|
+
sima_cli/utils/network.py,sha256=UvqxbqbWUczGFyO-t1SybG7Q-x9kjUVRNIn_D6APzy8,1252
|
19
|
+
sima_cli-0.0.1.dist-info/licenses/LICENSE,sha256=a260OFuV4SsMZ6sQCkoYbtws_4o2deFtbnT9kg7Rfd4,1082
|
20
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
21
|
+
tests/test_app_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
|
+
tests/test_auth.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
23
|
+
tests/test_cli.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
24
|
+
tests/test_download.py,sha256=t87DwxlHs26_ws9rpcHGwr_OrcRPd3hz6Zmm0vRee2U,4465
|
25
|
+
tests/test_firmware.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
26
|
+
tests/test_model_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
27
|
+
tests/test_utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
28
|
+
sima_cli-0.0.1.dist-info/METADATA,sha256=Ad-hG-Uxlmi_Mby158G2pAGuUCcOe4ZJu1FgglQiuho,2688
|
29
|
+
sima_cli-0.0.1.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
|
30
|
+
sima_cli-0.0.1.dist-info/entry_points.txt,sha256=xRYrDq1nCs6R8wEdB3c1kKuimxEjWJkHuCzArQPT0Xk,47
|
31
|
+
sima_cli-0.0.1.dist-info/top_level.txt,sha256=FtrbAUdHNohtEPteOblArxQNwoX9_t8qJQd59fagDlc,15
|
32
|
+
sima_cli-0.0.1.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 SiMa.ai
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the βSoftwareβ), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED βAS ISβ, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
tests/__init__.py
ADDED
File without changes
|
tests/test_app_zoo.py
ADDED
File without changes
|
tests/test_auth.py
ADDED
File without changes
|
tests/test_cli.py
ADDED
File without changes
|
tests/test_download.py
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
import unittest
|
2
|
+
from unittest.mock import patch, MagicMock, mock_open
|
3
|
+
import os
|
4
|
+
|
5
|
+
from sima_cli.download import (
|
6
|
+
download_file_from_url,
|
7
|
+
download_folder_from_url,
|
8
|
+
)
|
9
|
+
|
10
|
+
class TestDownloader(unittest.TestCase):
|
11
|
+
|
12
|
+
@patch("sima_cli.download.downloader.requests.get")
|
13
|
+
@patch("sima_cli.download.downloader.requests.head")
|
14
|
+
def test_download_file_success(self, mock_head, mock_get):
|
15
|
+
# Simulate HEAD response
|
16
|
+
mock_head_response = MagicMock()
|
17
|
+
mock_head_response.headers = {'content-length': '9'}
|
18
|
+
mock_head_response.raise_for_status = lambda: None
|
19
|
+
mock_head.return_value = mock_head_response
|
20
|
+
|
21
|
+
# Simulate GET response
|
22
|
+
mock_get_response = MagicMock()
|
23
|
+
mock_get_response.iter_content = lambda chunk_size: [b"test data"]
|
24
|
+
mock_get_response.headers = {'content-length': '9'}
|
25
|
+
mock_get_response.raise_for_status = lambda: None
|
26
|
+
mock_get_response.__enter__.return_value = mock_get_response
|
27
|
+
mock_get.return_value = mock_get_response
|
28
|
+
|
29
|
+
dest_folder = "test_output"
|
30
|
+
url = "https://127.0.0.1/sima/file.tar"
|
31
|
+
downloaded_path = download_file_from_url(url, dest_folder)
|
32
|
+
|
33
|
+
self.assertTrue(os.path.exists(downloaded_path))
|
34
|
+
with open(downloaded_path, "rb") as f:
|
35
|
+
self.assertEqual(f.read(), b"test data")
|
36
|
+
|
37
|
+
os.remove(downloaded_path)
|
38
|
+
os.rmdir(dest_folder)
|
39
|
+
|
40
|
+
@patch("sima_cli.download.downloader.requests.head")
|
41
|
+
def test_invalid_url_raises(self, mock_head):
|
42
|
+
# HEAD response without a file name
|
43
|
+
mock_head.return_value = MagicMock(headers={'content-length': '10'}, raise_for_status=lambda: None)
|
44
|
+
with self.assertRaises(ValueError):
|
45
|
+
download_file_from_url("https://example.com/", "somewhere")
|
46
|
+
|
47
|
+
@patch("sima_cli.download.downloader.requests.get")
|
48
|
+
@patch("sima_cli.download.downloader.requests.head")
|
49
|
+
def test_skip_already_downloaded_file(self, mock_head, mock_get):
|
50
|
+
dest_folder = "test_output"
|
51
|
+
os.makedirs(dest_folder, exist_ok=True)
|
52
|
+
test_file_path = os.path.join(dest_folder, "file.txt")
|
53
|
+
|
54
|
+
# Create a complete file manually
|
55
|
+
with open(test_file_path, "wb") as f:
|
56
|
+
f.write(b"123456789")
|
57
|
+
|
58
|
+
mock_head_response = MagicMock()
|
59
|
+
mock_head_response.headers = {'content-length': '9'}
|
60
|
+
mock_head_response.raise_for_status = lambda: None
|
61
|
+
mock_head.return_value = mock_head_response
|
62
|
+
|
63
|
+
url = "https://127.0.0.1/sima/file.txt"
|
64
|
+
returned_path = download_file_from_url(url, dest_folder)
|
65
|
+
|
66
|
+
self.assertEqual(returned_path, test_file_path)
|
67
|
+
mock_get.assert_not_called()
|
68
|
+
|
69
|
+
os.remove(test_file_path)
|
70
|
+
os.rmdir(dest_folder)
|
71
|
+
|
72
|
+
@patch("sima_cli.download.downloader.requests.get")
|
73
|
+
def test_list_directory_files_parses_links(self, mock_get):
|
74
|
+
from sima_cli.download.downloader import _list_directory_files
|
75
|
+
|
76
|
+
# Simulated directory listing HTML
|
77
|
+
mock_html = '''
|
78
|
+
<a href="../">Parent</a>
|
79
|
+
<a href="model1.onnx">model1.onnx</a>
|
80
|
+
<a href="readme.txt">readme.txt</a>
|
81
|
+
<a href="subfolder/">subfolder/</a>
|
82
|
+
'''
|
83
|
+
mock_get_response = MagicMock()
|
84
|
+
mock_get_response.text = mock_html
|
85
|
+
mock_get_response.headers = {"Content-Type": "text/html"}
|
86
|
+
mock_get_response.raise_for_status = lambda: None
|
87
|
+
mock_get_response.__enter__.return_value = mock_get_response
|
88
|
+
mock_get.return_value = mock_get_response
|
89
|
+
|
90
|
+
result = _list_directory_files("http://host/folder/")
|
91
|
+
self.assertEqual(result, [
|
92
|
+
"http://host/folder/model1.onnx",
|
93
|
+
"http://host/folder/readme.txt"
|
94
|
+
])
|
95
|
+
|
96
|
+
@patch("sima_cli.download.downloader.download_file_from_url")
|
97
|
+
@patch("sima_cli.download.downloader._list_directory_files")
|
98
|
+
def test_download_folder_from_url(self, mock_list_files, mock_download_file):
|
99
|
+
mock_list_files.return_value = [
|
100
|
+
"http://server/file1.txt",
|
101
|
+
"http://server/file2.txt"
|
102
|
+
]
|
103
|
+
mock_download_file.side_effect = lambda url, dest: os.path.join(dest, os.path.basename(url))
|
104
|
+
|
105
|
+
dest = "test_output"
|
106
|
+
os.makedirs(dest, exist_ok=True)
|
107
|
+
downloaded = download_folder_from_url("http://server/", dest)
|
108
|
+
|
109
|
+
self.assertEqual(len(downloaded), 2)
|
110
|
+
self.assertIn(os.path.join(dest, "file1.txt"), downloaded)
|
111
|
+
|
112
|
+
os.rmdir(dest)
|
113
|
+
|
114
|
+
if __name__ == "__main__":
|
115
|
+
unittest.main()
|
tests/test_firmware.py
ADDED
File without changes
|
tests/test_model_zoo.py
ADDED
File without changes
|
tests/test_utils.py
ADDED
File without changes
|