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 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
@@ -0,0 +1,4 @@
1
+ from sima_cli.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,2 @@
1
+ # sima_cli/__version__.py
2
+ __version__ = "0.0.1"
File without changes
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,11 @@
1
+ from .downloader import (
2
+ download_file_from_url,
3
+ download_folder_from_url,
4
+ _list_directory_files,
5
+ )
6
+
7
+ __all__ = [
8
+ "download_file_from_url",
9
+ "download_folder_from_url",
10
+ "_list_directory_files"
11
+ ]
@@ -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,3 @@
1
+ from .updater import perform_update
2
+
3
+ __all__ = ["perform_update"]
@@ -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
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (78.1.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sima-cli = sima_cli.cli:main
@@ -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.
@@ -0,0 +1,2 @@
1
+ sima_cli
2
+ tests
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
File without changes
tests/test_utils.py ADDED
File without changes