sima-cli 0.0.1__py3-none-any.whl → 0.0.11__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/__version__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  # sima_cli/__version__.py
2
- __version__ = "0.0.1"
2
+ __version__ = "0.0.11"
sima_cli/auth/login.py CHANGED
@@ -0,0 +1,103 @@
1
+ import click
2
+ import getpass
3
+ import requests
4
+ from sima_cli.utils.config import set_auth_token, get_auth_token
5
+ from sima_cli.utils.config_loader import load_resource_config
6
+ from sima_cli.utils.artifactory import exchange_identity_token, validate_token
7
+
8
+ def login(method: str = "external"):
9
+ """
10
+ Dispatch login based on the specified method.
11
+
12
+ Args:
13
+ method (str): 'external' (public developer portal) or 'internal' (Artifactory).
14
+ """
15
+ if method == "internal":
16
+ return login_internal()
17
+ else:
18
+ return login_external()
19
+
20
+ def login_internal():
21
+ """
22
+ Internal login using a manually provided identity token.
23
+
24
+ Flow:
25
+ 1. Prompt for identity token.
26
+ 2. Validate the token using the configured validation URL.
27
+ 3. If valid, exchange it for a short-lived access token.
28
+ 4. Save the short-lived token to local config.
29
+ """
30
+
31
+ cfg = load_resource_config()
32
+ auth_cfg = cfg.get("internal", {}).get("auth", {})
33
+ base_url = cfg.get("internal", {}).get("artifactory", {}).get("url", {})
34
+ validate_url = f"{base_url}/{auth_cfg.get("validate_url")}"
35
+ exchange_url = f"{base_url}/{auth_cfg.get("internal_url")}"
36
+
37
+ # Check for required config values
38
+ if not validate_url or not exchange_url:
39
+ click.echo("❌ Missing 'validate_url' or 'internal_url' in internal auth config.")
40
+ click.echo("👉 Please check ~/.sima-cli/resources_internal.yaml")
41
+ return
42
+
43
+ # Prompt for identity token
44
+ click.echo("🔐 Paste your Artifactory identity token below.")
45
+ identity_token = click.prompt("Identity Token", hide_input=True)
46
+
47
+ if not identity_token or len(identity_token.strip()) < 10:
48
+ return click.echo("❌ Invalid or empty token.")
49
+
50
+ # Step 1: Validate the identity token
51
+ is_valid, username = validate_token(identity_token, validate_url)
52
+ if not is_valid:
53
+ return click.echo("❌ Token validation failed. Please check your identity token.")
54
+
55
+ click.echo(f"✅ Identity token is valid")
56
+
57
+ # Step 2: Exchange for a short-lived access token (default: 7 days)
58
+ access_token, user_name = exchange_identity_token(identity_token, exchange_url, expires_in=604800)
59
+
60
+ if not access_token:
61
+ return click.echo("❌ Failed to acquire short-lived access token.")
62
+
63
+ # Step 3: Save token to internal auth config
64
+ set_auth_token(access_token, internal=True)
65
+ click.echo(f"💾 Short-lived access token saved successfully for {user_name} (valid for 7 days).")
66
+
67
+
68
+ def login_external():
69
+ """
70
+ External login using Developer Portal endpoint defined in the 'public' section of YAML config.
71
+ Prompts for username/password and retrieves access token.
72
+ """
73
+ cfg = load_resource_config()
74
+ auth_url = cfg.get("public", {}).get("auth", {}).get("external_url")
75
+
76
+ if not auth_url:
77
+ click.echo("❌ External auth URL not configured in YAML.")
78
+ return
79
+
80
+ click.echo("🌐 Logging in using external Developer Portal...")
81
+
82
+ # Prompt for credentials
83
+ username = click.prompt("Email or Username")
84
+ password = getpass.getpass("Password: ")
85
+
86
+ data = {
87
+ "username": username,
88
+ "password": password
89
+ }
90
+
91
+ try:
92
+ response = requests.post(auth_url, json=data)
93
+ response.raise_for_status()
94
+
95
+ token = response.json().get("access_token")
96
+ if not token:
97
+ return click.echo("❌ Failed to retrieve access token.")
98
+
99
+ set_auth_token(token)
100
+ click.echo("✅ External login successful.")
101
+
102
+ except requests.RequestException as e:
103
+ click.echo(f"❌ External login failed: {e}")
sima_cli/cli.py CHANGED
@@ -1,22 +1,52 @@
1
+ import os
1
2
  import click
2
3
  from sima_cli.utils.env import get_environment_type
3
4
  from sima_cli.update.updater import perform_update
5
+ from sima_cli.model_zoo.model import list_models, download_model
4
6
 
5
7
  # Entry point for the CLI tool using Click's command group decorator
6
8
  @click.group()
7
- def main():
8
- """sima-cli – SiMa Developer Portal CLI Tool"""
9
+ @click.option('-i', '--internal', is_flag=True, help="Use internal Artifactory resources.")
10
+ @click.pass_context
11
+ def main(ctx, internal):
12
+ """
13
+ sima-cli – SiMa Developer Portal CLI Tool
14
+
15
+ Global Options:
16
+ --internal Use internal Artifactory resources (can also be set via env variable SIMA_CLI_INTERNAL=1)
17
+ """
18
+ ctx.ensure_object(dict)
19
+
20
+ # Allow env override if --internal not explicitly passed
21
+ if not internal:
22
+ internal = os.getenv("SIMA_CLI_INTERNAL", "0") in ("1", "true", "yes")
23
+
24
+ ctx.obj["internal"] = internal
25
+
26
+ from sima_cli.utils.env import get_environment_type
9
27
  env_type, env_subtype = get_environment_type()
10
- click.echo(f"🔧 Environment: {env_type} ({env_subtype})")
28
+
29
+ if internal:
30
+ click.echo(f"🔧 Environment: {env_type} ({env_subtype}) | Internal: {internal}")
31
+ else:
32
+ click.echo(f"🔧 Environment: {env_type} ({env_subtype})")
33
+
34
+ if not internal:
35
+ click.echo(f"external environment is not supported yet..")
36
+ exit(0)
11
37
 
12
38
  # ----------------------
13
39
  # Authentication Command
14
40
  # ----------------------
15
41
  @main.command()
16
- def login():
42
+ @click.pass_context
43
+ def login(ctx):
17
44
  """Authenticate with the SiMa Developer Portal."""
18
- # Placeholder: Implement browser or manual token-based login
19
- click.echo("Login flow not yet implemented.")
45
+
46
+ from sima_cli.auth import login as perform_login
47
+
48
+ internal = ctx.obj.get("internal", False)
49
+ perform_login.login("internal" if internal else "external")
20
50
 
21
51
  # ----------------------
22
52
  # Download Command
@@ -24,14 +54,17 @@ def login():
24
54
  @main.command(name="download")
25
55
  @click.argument('url') # Accept both file and folder URLs
26
56
  @click.option('-d', '--dest', type=click.Path(), default='.', help="Target download directory")
27
- def download(url, dest):
57
+ @click.pass_context
58
+ def download(ctx, url, dest):
28
59
  """Download a file or a whole folder from a given URL."""
29
60
  from sima_cli.download.downloader import download_file_from_url, download_folder_from_url
30
61
 
62
+ internal = ctx.obj.get("internal", False)
63
+
31
64
  # First, try to download as a file
32
65
  try:
33
66
  click.echo("🔍 Checking if URL is a direct file...")
34
- path = download_file_from_url(url, dest)
67
+ path = download_file_from_url(url, dest, internal)
35
68
  click.echo(f"\n✅ File downloaded successfully to: {path}")
36
69
  return
37
70
  except Exception as e:
@@ -40,7 +73,7 @@ def download(url, dest):
40
73
  # If that fails, try to treat as a folder and download all files
41
74
  try:
42
75
  click.echo("🔍 Attempting folder download...")
43
- paths = download_folder_from_url(url, dest)
76
+ paths = download_folder_from_url(url, dest, internal)
44
77
  if not paths:
45
78
  raise RuntimeError("No files were downloaded.")
46
79
  click.echo(f"\n✅ Folder download completed. {len(paths)} files saved to: {dest}")
@@ -53,55 +86,85 @@ def download(url, dest):
53
86
  @main.command(name="update")
54
87
  @click.argument('version_or_url')
55
88
  @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)
89
+ @click.option(
90
+ '--board',
91
+ default='davinci',
92
+ type=click.Choice(['davinci', 'modalix'], case_sensitive=False),
93
+ show_default=True,
94
+ help="Target board type (davinci or modalix)."
95
+ )
96
+ @click.option(
97
+ '--passwd',
98
+ default='edgeai',
99
+ help="Optional SSH password for remote board (default is 'edgeai')."
100
+ )
101
+ @click.pass_context
102
+ def update(ctx, version_or_url, ip, board, passwd):
103
+ """
104
+ Run system update across different environments.
105
+ Downloads and applies firmware updates for PCIe host or SiMa board.
106
+
107
+ version_or_url: The version string (e.g. '1.5.0') or a direct URL to the firmware package.
108
+ """
109
+ internal = ctx.obj.get("internal", False)
110
+ perform_update(version_or_url, ip, board.lower(), internal, passwd=passwd)
59
111
 
60
112
  # ----------------------
61
113
  # Model Zoo Subcommands
62
114
  # ----------------------
63
115
  @main.group()
64
- def model_zoo():
116
+ @click.option('--ver', default="1.6.0", show_default=True, help="SDK version, minimum and default is 1.6.0")
117
+ @click.pass_context
118
+ def modelzoo(ctx, ver):
65
119
  """Access models from the Model Zoo."""
120
+ ctx.ensure_object(dict)
121
+ ctx.obj['ver'] = ver
66
122
  pass
67
123
 
68
- @model_zoo.command("list")
69
- @click.option('--ver', help="SDK version")
70
- def list_models(ver):
124
+ @modelzoo.command("list")
125
+ @click.pass_context
126
+ def list_models_cmd(ctx):
71
127
  """List available models."""
72
- # Placeholder: Call API to list models
73
- click.echo(f"Listing models for version: {ver or 'latest'}")
128
+ internal = ctx.obj.get("internal", False)
129
+ version = ctx.obj.get("ver")
130
+ click.echo(f"Listing models for version: {version}")
131
+ list_models(internal, version)
74
132
 
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):
133
+ @modelzoo.command("get")
134
+ @click.argument('model_name')
135
+ @click.pass_context
136
+ def get_model(ctx, model_name):
79
137
  """Download a specific model."""
80
- # Placeholder: Download and validate model
81
- click.echo(f"Getting model '{model_name}' for version: {ver or 'latest'}")
138
+ ver = ctx.obj.get("ver")
139
+ internal = ctx.obj.get("internal", False)
140
+ click.echo(f"Getting model '{model_name}' for version: {ver}")
141
+ download_model(internal, ver, model_name)
82
142
 
83
143
  # ----------------------
84
144
  # App Zoo Subcommands
85
145
  # ----------------------
86
- @main.group()
87
- def app_zoo():
88
- """Access apps from the App Zoo."""
89
- pass
146
+ # @main.group()
147
+ # @click.pass_context
148
+ # def app_zoo(ctx):
149
+ # """Access apps from the App Zoo."""
150
+ # pass
151
+
152
+ # @app_zoo.command("list")
153
+ # @click.option('--ver', help="SDK version")
154
+ # @click.pass_context
155
+ # def list_apps(ctx, ver):
156
+ # """List available apps."""
157
+ # # Placeholder: Call API to list apps
158
+ # click.echo(f"Listing apps for version: {ver or 'latest'}")
90
159
 
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'}")
160
+ # @app_zoo.command("get")
161
+ # @click.argument('app_name') # Required: app name
162
+ # @click.option('--ver', help="SDK version")
163
+ # @click.pass_context
164
+ # def get_app(ctx, app_name, ver):
165
+ # """Download a specific app."""
166
+ # # Placeholder: Download and validate app
167
+ # click.echo(f"Getting app '{app_name}' for version: {ver or 'latest'}")
105
168
 
106
169
  # ----------------------
107
170
  # Entry point for direct execution
@@ -3,13 +3,15 @@ import requests
3
3
  from urllib.parse import urlparse
4
4
  from tqdm import tqdm
5
5
  from typing import List
6
+ from sima_cli.utils.config import get_auth_token
6
7
 
7
- def _list_directory_files(url: str) -> List[str]:
8
+ def _list_directory_files(url: str, internal: bool = False) -> List[str]:
8
9
  """
9
10
  Attempt to list files in a server-hosted directory with index browsing enabled.
10
11
 
11
12
  Args:
12
13
  url (str): Base URL to the folder.
14
+ internal (bool): Whether the resource is internal (requires token).
13
15
 
14
16
  Returns:
15
17
  List[str]: List of full file URLs.
@@ -18,14 +20,18 @@ def _list_directory_files(url: str) -> List[str]:
18
20
  RuntimeError: If listing fails or HTML cannot be parsed.
19
21
  """
20
22
  try:
21
- response = requests.get(url, timeout=10)
23
+ headers = {}
24
+ token = get_auth_token(internal)
25
+ if token:
26
+ headers["Authorization"] = f"Bearer {token}"
27
+
28
+ response = requests.get(url, headers=headers, timeout=10)
22
29
  response.raise_for_status()
23
30
 
24
31
  if "text/html" not in response.headers.get("Content-Type", ""):
25
32
  raise RuntimeError("Directory listing not supported (non-HTML response).")
26
33
 
27
34
  import re
28
-
29
35
  hrefs = re.findall(r'href="([^"?/][^"?]*)"', response.text)
30
36
  files = [href for href in hrefs if href not in ("../", "") and not href.endswith("/")]
31
37
 
@@ -38,21 +44,21 @@ def _list_directory_files(url: str) -> List[str]:
38
44
  raise RuntimeError(f"Failed to list folder '{url}': {e}")
39
45
 
40
46
 
41
- def download_file_from_url(url: str, dest_folder: str = ".") -> str:
47
+ def download_file_from_url(url: str, dest_folder: str = ".", internal: bool = False) -> str:
42
48
  """
43
49
  Download a file from a direct URL with resume and skip support.
44
-
50
+
45
51
  Args:
46
52
  url (str): The full URL to download.
47
53
  dest_folder (str): The folder to save the downloaded file.
48
-
54
+ internal (bool): Whether this is internal resource on Artifactory
55
+
49
56
  Returns:
50
57
  str: Path to the downloaded file.
51
-
58
+
52
59
  Raises:
53
60
  Exception: if download fails.
54
61
  """
55
- # Extract file name
56
62
  parsed_url = urlparse(url)
57
63
  file_name = os.path.basename(parsed_url.path)
58
64
  if not file_name:
@@ -62,12 +68,17 @@ def download_file_from_url(url: str, dest_folder: str = ".") -> str:
62
68
  dest_path = os.path.join(dest_folder, file_name)
63
69
 
64
70
  resume_header = {}
71
+ headers = {}
65
72
  mode = 'wb'
66
73
  existing_size = 0
67
74
 
68
75
  try:
69
- # Get total size from HEAD request
70
- head = requests.head(url, timeout=10)
76
+ auth_token = get_auth_token(internal)
77
+ if auth_token:
78
+ headers["Authorization"] = f"Bearer {auth_token}"
79
+
80
+ # HEAD request to get total file size
81
+ head = requests.head(url, headers=headers, timeout=10)
71
82
  head.raise_for_status()
72
83
  total_size = int(head.headers.get('content-length', 0))
73
84
 
@@ -79,19 +90,17 @@ def download_file_from_url(url: str, dest_folder: str = ".") -> str:
79
90
  print(f"✔ File already exists and is complete: {file_name}")
80
91
  return dest_path
81
92
  elif existing_size < total_size:
82
- # Set up resume
83
93
  resume_header['Range'] = f'bytes={existing_size}-'
84
94
  mode = 'ab'
95
+ headers['Range'] = resume_header['Range']
85
96
  else:
86
- # Local file is bigger than expected — reset
87
97
  existing_size = 0
88
98
  mode = 'wb'
89
99
 
90
- # Start download (with optional Range header)
91
- with requests.get(url, stream=True, headers=resume_header, timeout=30) as r:
100
+ # Begin download with headers
101
+ with requests.get(url, stream=True, headers=headers, timeout=30) as r:
92
102
  r.raise_for_status()
93
103
 
94
- # If resuming, adjust total to show correct progress bar
95
104
  content_length = int(r.headers.get('content-length', 0))
96
105
  final_total = existing_size + content_length
97
106
 
@@ -113,25 +122,27 @@ def download_file_from_url(url: str, dest_folder: str = ".") -> str:
113
122
 
114
123
  return dest_path
115
124
 
116
- def download_folder_from_url(url: str, dest_folder: str = ".") -> List[str]:
125
+
126
+ def download_folder_from_url(url: str, dest_folder: str = ".", internal: bool = False) -> List[str]:
117
127
  """
118
128
  Download all files listed in a remote folder (server must support listing).
119
129
 
120
130
  Args:
121
131
  url (str): Folder URL.
122
132
  dest_folder (str): Local folder to save downloads.
133
+ internal (bool): Whether this is internal resource on Artifactory
123
134
 
124
135
  Returns:
125
136
  List[str]: Paths to all downloaded files.
126
137
  """
127
- file_urls = _list_directory_files(url)
138
+ file_urls = _list_directory_files(url, internal=internal)
128
139
  downloaded_paths = []
129
140
 
130
141
  for file_url in file_urls:
131
142
  try:
132
- downloaded_path = download_file_from_url(file_url, dest_folder)
143
+ downloaded_path = download_file_from_url(file_url, dest_folder, internal=internal)
133
144
  downloaded_paths.append(downloaded_path)
134
145
  except Exception as e:
135
146
  print(f"⚠ Skipped {file_url}: {e}")
136
147
 
137
- return downloaded_paths
148
+ return downloaded_paths
@@ -0,0 +1,148 @@
1
+ # model_zoo/models.py
2
+
3
+ import requests
4
+ import click
5
+ import os
6
+ from urllib.parse import urlparse
7
+
8
+ from sima_cli.utils.config import get_auth_token
9
+ from sima_cli.download import download_file_from_url
10
+
11
+ ARTIFACTORY_BASE_URL = "https://artifacts.eng.sima.ai:443/artifactory"
12
+
13
+ def _is_valid_url(url: str) -> bool:
14
+ try:
15
+ result = urlparse(url)
16
+ return all([result.scheme, result.netloc])
17
+ except:
18
+ return False
19
+
20
+ def _download_model_internal(ver: str, model_name: str):
21
+ repo = "sima-qa-releases"
22
+ base_path = f"SiMaCLI-SDK-Releases/{ver}-Release/modelzoo_edgematic/{model_name}"
23
+ aql_query = f"""
24
+ items.find({{
25
+ "repo": "{repo}",
26
+ "path": {{
27
+ "$match": "{base_path}*"
28
+ }},
29
+ "type": "file"
30
+ }}).include("repo", "path", "name")
31
+ """.strip()
32
+
33
+ aql_url = f"{ARTIFACTORY_BASE_URL}/api/search/aql"
34
+ headers = {
35
+ "Content-Type": "text/plain",
36
+ "Authorization": f"Bearer {get_auth_token(internal=True)}"
37
+ }
38
+
39
+ response = requests.post(aql_url, data=aql_query, headers=headers)
40
+ if response.status_code != 200:
41
+ click.echo(f"Failed to list model files. Status: {response.status_code}, path: {aql_url}")
42
+ click.echo(response.text)
43
+ return
44
+
45
+ results = response.json().get("results", [])
46
+ if not results:
47
+ click.echo(f"No files found for model: {model_name}")
48
+ return
49
+
50
+ dest_dir = os.path.join(os.getcwd(), model_name)
51
+ os.makedirs(dest_dir, exist_ok=True)
52
+
53
+ click.echo(f"Downloading files for model '{model_name}' to '{dest_dir}'...")
54
+
55
+ for item in results:
56
+ file_path = item["path"]
57
+ file_name = item["name"]
58
+ download_url = f"{ARTIFACTORY_BASE_URL}/{repo}/{file_path}/{file_name}"
59
+
60
+ try:
61
+ local_path = download_file_from_url(download_url, dest_folder=dest_dir, internal=True)
62
+ click.echo(f"✅ {file_name} -> {local_path}")
63
+ except Exception as e:
64
+ click.echo(f"❌ Failed to download {file_name}: {e}")
65
+
66
+ # Check for model_path.txt and optionally download external ONNX model
67
+ model_path_file = os.path.join(dest_dir, "model_path.txt")
68
+ if os.path.exists(model_path_file):
69
+ with open(model_path_file, "r") as f:
70
+ first_line = f.readline().strip()
71
+ if _is_valid_url(first_line):
72
+ click.echo(f"\n🔍 model_path.txt contains external model link:\n{first_line}")
73
+ if click.confirm("Do you want to download the FP32 ONNX model from this link?", default=True):
74
+ try:
75
+ external_model_path = download_file_from_url(first_line, dest_folder=dest_dir, internal=True)
76
+ click.echo(f"✅ External model downloaded to: {external_model_path}")
77
+ except Exception as e:
78
+ click.echo(f"❌ Failed to download external model: {e}")
79
+ else:
80
+ click.echo("⚠️ model_path.txt exists but does not contain a valid URL.")
81
+
82
+ def _list_available_models_internal(version: str):
83
+ repo_path = f"SiMaCLI-SDK-Releases/{version}-Release/modelzoo_edgematic"
84
+ aql_query = f"""
85
+ items.find({{
86
+ "repo": "sima-qa-releases",
87
+ "path": {{
88
+ "$match": "{repo_path}/*"
89
+ }},
90
+ "type": "folder"
91
+ }}).include("repo", "path", "name")
92
+ """.strip()
93
+
94
+ aql_url = f"{ARTIFACTORY_BASE_URL}/api/search/aql"
95
+ headers = {
96
+ "Content-Type": "text/plain",
97
+ "Authorization": f"Bearer {get_auth_token(internal=True)}"
98
+ }
99
+
100
+ response = requests.post(aql_url, data=aql_query, headers=headers)
101
+
102
+ if response.status_code != 200:
103
+ click.echo(f"Failed to retrieve model list. Status: {response.status_code}")
104
+ click.echo(response.text)
105
+ return
106
+
107
+ results = response.json().get("results", [])
108
+
109
+ base_prefix = f"SiMaCLI-SDK-Releases/{version}-Release/modelzoo_edgematic/"
110
+ model_paths = sorted({
111
+ item["path"].replace(base_prefix, "").rstrip("/") + "/" + item["name"]
112
+ for item in results
113
+ })
114
+
115
+ if not model_paths:
116
+ click.echo("No models found.")
117
+ return
118
+
119
+ # Pretty print table
120
+ max_len = max(len(name) for name in model_paths)
121
+ click.echo(f"{'-' * max_len}")
122
+ for path in model_paths:
123
+ click.echo(path.ljust(max_len))
124
+
125
+ return model_paths
126
+
127
+ def list_models(internal, ver):
128
+ if internal:
129
+ click.echo("Model Zoo Source : SiMa Artifactory...")
130
+ return _list_available_models_internal(ver)
131
+ else:
132
+ print('External model zoo not supported yet')
133
+
134
+ def download_model(internal, ver, model_name):
135
+ if internal:
136
+ click.echo("Model Zoo Source : SiMa Artifactory...")
137
+ return _download_model_internal(ver, model_name)
138
+ else:
139
+ print('External model zoo not supported yet')
140
+
141
+ # Module CLI tests
142
+ if __name__ == "__main__":
143
+ import sys
144
+ if len(sys.argv) < 2:
145
+ print("Usage: python models.py <version>")
146
+ else:
147
+ version_arg = sys.argv[1]
148
+ _list_available_models_internal(version_arg)