sima-cli 0.0.34__py3-none-any.whl → 0.0.36__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 +1 -1
- sima_cli/auth/basic_auth.py +0 -1
- sima_cli/auth/login.py +3 -3
- sima_cli/cli.py +6 -4
- sima_cli/download/downloader.py +32 -0
- sima_cli/install/metadata_installer.py +243 -8
- sima_cli/network/network.py +7 -1
- sima_cli/update/local.py +3 -3
- sima_cli/update/remote.py +20 -15
- sima_cli/update/updater.py +16 -10
- sima_cli/utils/env.py +29 -22
- {sima_cli-0.0.34.dist-info → sima_cli-0.0.36.dist-info}/METADATA +1 -1
- {sima_cli-0.0.34.dist-info → sima_cli-0.0.36.dist-info}/RECORD +17 -17
- {sima_cli-0.0.34.dist-info → sima_cli-0.0.36.dist-info}/WHEEL +0 -0
- {sima_cli-0.0.34.dist-info → sima_cli-0.0.36.dist-info}/entry_points.txt +0 -0
- {sima_cli-0.0.34.dist-info → sima_cli-0.0.36.dist-info}/licenses/LICENSE +0 -0
- {sima_cli-0.0.34.dist-info → sima_cli-0.0.36.dist-info}/top_level.txt +0 -0
sima_cli/__version__.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
# sima_cli/__version__.py
|
2
|
-
__version__ = "0.0.
|
2
|
+
__version__ = "0.0.36"
|
sima_cli/auth/basic_auth.py
CHANGED
sima_cli/auth/login.py
CHANGED
@@ -60,15 +60,15 @@ def login_internal():
|
|
60
60
|
|
61
61
|
click.echo(f"✅ Identity token is valid")
|
62
62
|
|
63
|
-
# Step 2: Exchange for a short-lived access token (default:
|
64
|
-
access_token, user_name = exchange_identity_token(identity_token, exchange_url, expires_in=
|
63
|
+
# Step 2: Exchange for a short-lived access token (default: 30 days)
|
64
|
+
access_token, user_name = exchange_identity_token(identity_token, exchange_url, expires_in=2592000)
|
65
65
|
|
66
66
|
if not access_token:
|
67
67
|
return click.echo("❌ Failed to acquire short-lived access token.")
|
68
68
|
|
69
69
|
# Step 3: Save token to internal auth config
|
70
70
|
set_auth_token(access_token, internal=True)
|
71
|
-
click.echo(f"💾 Short-lived access token saved successfully for {user_name} (valid for
|
71
|
+
click.echo(f"💾 Short-lived access token saved successfully for {user_name} (valid for 30 days).")
|
72
72
|
|
73
73
|
|
74
74
|
def _login_external():
|
sima_cli/cli.py
CHANGED
@@ -151,7 +151,7 @@ def download(ctx, url, dest):
|
|
151
151
|
default='edgeai',
|
152
152
|
help="Optional SSH password for remote board (default is 'edgeai')."
|
153
153
|
)
|
154
|
-
@click.option("-f", "--flavor", type=click.Choice(["headless", "full"], case_sensitive=False), default="
|
154
|
+
@click.option("-f", "--flavor", type=click.Choice(["headless", "full", "auto"], case_sensitive=False), default="auto", show_default=True, help="firmware flavor, full image supports NVMe, GUI on Modalix DevKit.")
|
155
155
|
@click.pass_context
|
156
156
|
def update(ctx, version_or_url, ip, yes, passwd, flavor):
|
157
157
|
"""
|
@@ -313,9 +313,7 @@ def install_cmd(ctx, component, version, mirror, tag):
|
|
313
313
|
if component:
|
314
314
|
click.echo(f"⚠️ Component '{component}' is ignored when using --metadata. Proceeding with metadata-based installation.")
|
315
315
|
click.echo(f"🔧 Installing generic component from metadata URL: {mirror}")
|
316
|
-
|
317
|
-
click.echo("✅ Installation complete.")
|
318
|
-
return
|
316
|
+
return install_from_metadata(metadata_url=mirror, internal=internal)
|
319
317
|
|
320
318
|
# No component and no metadata: error
|
321
319
|
if not component:
|
@@ -324,6 +322,10 @@ def install_cmd(ctx, component, version, mirror, tag):
|
|
324
322
|
|
325
323
|
component = component.lower()
|
326
324
|
|
325
|
+
# if user specified gh: as component, treat it the same as -m
|
326
|
+
if component.startswith("gh:"):
|
327
|
+
return install_from_metadata(metadata_url=component, internal=False)
|
328
|
+
|
327
329
|
# Validate version requirement
|
328
330
|
if component in SDK_DEPENDENT_COMPONENTS and not version:
|
329
331
|
click.echo(f"❌ The component '{component}' requires a specific SDK version. Please provide one using -v.")
|
sima_cli/download/downloader.py
CHANGED
@@ -135,6 +135,38 @@ def download_file_from_url(url: str, dest_folder: str = ".", internal: bool = Fa
|
|
135
135
|
|
136
136
|
return dest_path
|
137
137
|
|
138
|
+
def check_url_available(url: str, internal: bool = False) -> bool:
|
139
|
+
"""
|
140
|
+
Perform a HEAD request to check if a resource is available.
|
141
|
+
|
142
|
+
Args:
|
143
|
+
url (str): The full URL to check.
|
144
|
+
internal (bool): Whether this is an internal resource on Artifactory.
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
bool: True if the resource is available (status 200–399), False otherwise.
|
148
|
+
"""
|
149
|
+
headers = {}
|
150
|
+
try:
|
151
|
+
if internal:
|
152
|
+
auth_token = get_auth_token(internal=True)
|
153
|
+
if auth_token:
|
154
|
+
headers["Authorization"] = f"Bearer {auth_token}"
|
155
|
+
head_fn = requests.head
|
156
|
+
elif 'https://docs.sima.ai' in url:
|
157
|
+
session = login_external()
|
158
|
+
head_fn = session.head
|
159
|
+
else:
|
160
|
+
session = requests.Session()
|
161
|
+
head_fn = session.head
|
162
|
+
|
163
|
+
resp = head_fn(url, headers=headers, timeout=10, allow_redirects=True)
|
164
|
+
# Consider any 2xx or 3xx as "available"
|
165
|
+
return 200 <= resp.status_code < 400
|
166
|
+
|
167
|
+
except Exception as e:
|
168
|
+
print(f"⚠️ HEAD check failed for {url}: {e}")
|
169
|
+
return False
|
138
170
|
|
139
171
|
def download_folder_from_url(url: str, dest_folder: str = ".", internal: bool = False) -> List[str]:
|
140
172
|
"""
|
@@ -9,13 +9,13 @@ import tarfile
|
|
9
9
|
import zipfile
|
10
10
|
import stat
|
11
11
|
from urllib.parse import urlparse
|
12
|
-
from InquirerPy import inquirer
|
13
12
|
|
14
13
|
from typing import Dict
|
15
14
|
from tqdm import tqdm
|
16
15
|
from urllib.parse import urljoin
|
17
16
|
from pathlib import Path
|
18
17
|
import subprocess
|
18
|
+
import requests
|
19
19
|
|
20
20
|
from rich.console import Console
|
21
21
|
from rich.panel import Panel
|
@@ -30,6 +30,153 @@ from sima_cli.install.metadata_info import print_metadata_summary, parse_size_st
|
|
30
30
|
|
31
31
|
console = Console()
|
32
32
|
|
33
|
+
def _copy_dir(src: Path, dest: Path, label: str):
|
34
|
+
"""
|
35
|
+
Copy files from src → dest, merging with existing files (no deletion).
|
36
|
+
Ensures that all parent directories for dest are created.
|
37
|
+
"""
|
38
|
+
if not src.exists():
|
39
|
+
raise FileNotFoundError(f"SDK {label} not found: {src}")
|
40
|
+
|
41
|
+
# Ensure full path exists
|
42
|
+
dest.mkdir(parents=True, exist_ok=True)
|
43
|
+
|
44
|
+
# Copy tree correctly
|
45
|
+
for item in src.iterdir():
|
46
|
+
target = dest / item.name
|
47
|
+
if item.is_dir():
|
48
|
+
shutil.copytree(item, target, dirs_exist_ok=True)
|
49
|
+
else:
|
50
|
+
shutil.copy2(item, target)
|
51
|
+
|
52
|
+
click.echo(f"✅ Copied {label} into {dest}")
|
53
|
+
|
54
|
+
def _prepare_pipeline_project(repo_dir: Path):
|
55
|
+
"""
|
56
|
+
Prepare a pipeline project by copying required SDK sources into the repo.
|
57
|
+
|
58
|
+
Steps:
|
59
|
+
1. Copy core sources into the project folder
|
60
|
+
2. Parse .project/pluginsInfo
|
61
|
+
3. Copy required plugin sources from the SDK plugin zoo
|
62
|
+
"""
|
63
|
+
plugins_info_file = repo_dir / ".project" / "pluginsInfo.json"
|
64
|
+
if not plugins_info_file.exists():
|
65
|
+
return
|
66
|
+
|
67
|
+
click.echo("📦 Preparing pipeline project...")
|
68
|
+
|
69
|
+
try:
|
70
|
+
data = json.loads(plugins_info_file.read_text())
|
71
|
+
plugins = data.get("pluginsInfo", [])
|
72
|
+
except Exception as e:
|
73
|
+
raise RuntimeError(f"Failed to read {plugins_info_file}: {e}")
|
74
|
+
|
75
|
+
# Step a: copy core
|
76
|
+
# Define what to copy
|
77
|
+
copy_map = [
|
78
|
+
(
|
79
|
+
Path("/usr/local/simaai/plugin_zoo/gst-simaai-plugins-base/core"),
|
80
|
+
repo_dir / "core",
|
81
|
+
"core"
|
82
|
+
),
|
83
|
+
(
|
84
|
+
Path("/usr/local/simaai/utils/gst_app"),
|
85
|
+
repo_dir / "dependencies" / "gst_app",
|
86
|
+
"dependencies/gst_app"
|
87
|
+
),
|
88
|
+
(
|
89
|
+
Path("/usr/local/simaai/plugin_zoo/gst-simaai-plugins-base/gst/templates"),
|
90
|
+
repo_dir / "plugins" / "templates",
|
91
|
+
"plugins/templates"
|
92
|
+
),
|
93
|
+
]
|
94
|
+
|
95
|
+
# Execute
|
96
|
+
for src, dest, label in copy_map:
|
97
|
+
_copy_dir(src, dest, label)
|
98
|
+
|
99
|
+
# Step b/c: scan plugin paths and copy SDK plugins
|
100
|
+
sdk_plugins_base = Path("/usr/local/simaai/plugin_zoo/gst-simaai-plugins-base/gst")
|
101
|
+
dest_plugins_dir = repo_dir / "plugins"
|
102
|
+
dest_plugins_dir.mkdir(exist_ok=True)
|
103
|
+
|
104
|
+
for plugin in plugins:
|
105
|
+
try:
|
106
|
+
path = plugin.get("path", "")
|
107
|
+
if not path:
|
108
|
+
continue
|
109
|
+
parts = path.split("/")
|
110
|
+
if len(parts) < 2:
|
111
|
+
continue
|
112
|
+
|
113
|
+
plugin_name = parts[1]
|
114
|
+
sdk_plugin_path = sdk_plugins_base / plugin_name
|
115
|
+
if not sdk_plugin_path.exists():
|
116
|
+
click.echo(
|
117
|
+
f"⚠️ Missing plugin source: {plugin_name} in the SDK, skipping. "
|
118
|
+
"It's possible that this is a custom plugin already exists in the repo"
|
119
|
+
)
|
120
|
+
continue
|
121
|
+
|
122
|
+
dest_plugin_path = dest_plugins_dir / plugin_name
|
123
|
+
dest_plugin_path.mkdir(parents=True, exist_ok=True)
|
124
|
+
|
125
|
+
# Merge instead of deleting
|
126
|
+
shutil.copytree(sdk_plugin_path, dest_plugin_path, dirs_exist_ok=True)
|
127
|
+
|
128
|
+
click.echo(f"✅ Copied plugin {plugin_name} into {dest_plugin_path}")
|
129
|
+
|
130
|
+
except Exception as e:
|
131
|
+
click.echo(f"❌ Error copying plugin {plugin}: {e}")
|
132
|
+
|
133
|
+
click.echo("🎉 Pipeline project prepared.")
|
134
|
+
|
135
|
+
def _download_github_repo(owner: str, repo: str, ref: str, dest_folder: str, token: str = None) -> str:
|
136
|
+
"""
|
137
|
+
Download and extract a GitHub repo tarball via the REST API (no git required).
|
138
|
+
|
139
|
+
Args:
|
140
|
+
owner (str): GitHub org/user
|
141
|
+
repo (str): Repo name
|
142
|
+
ref (str): Branch, tag, or commit (default = default branch)
|
143
|
+
dest_folder (str): Where to extract
|
144
|
+
token (str): Optional GitHub token for private repos
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
str: Path to the extracted repo
|
148
|
+
"""
|
149
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/tarball/{ref}" if ref else f"https://api.github.com/repos/{owner}/{repo}/tarball"
|
150
|
+
headers = {}
|
151
|
+
if token:
|
152
|
+
headers["Authorization"] = f"Bearer {token}"
|
153
|
+
|
154
|
+
click.echo(f"🐙 Downloading GitHub repo: {owner}/{repo}" + (f"@{ref}" if ref else ""))
|
155
|
+
|
156
|
+
with requests.get(url, headers=headers, stream=True) as r:
|
157
|
+
if r.status_code in (401, 403):
|
158
|
+
raise PermissionError("Authentication required for GitHub repo")
|
159
|
+
r.raise_for_status()
|
160
|
+
|
161
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".tar.gz") as tmp_file:
|
162
|
+
for chunk in r.iter_content(chunk_size=8192):
|
163
|
+
tmp_file.write(chunk)
|
164
|
+
tmp_path = Path(tmp_file.name)
|
165
|
+
|
166
|
+
repo_dir = Path(dest_folder) / repo
|
167
|
+
repo_dir.mkdir(parents=True, exist_ok=True)
|
168
|
+
|
169
|
+
_extract_tar_strip_top_level(tmp_path, repo_dir)
|
170
|
+
tmp_path.unlink(missing_ok=True)
|
171
|
+
click.echo(f"✅ Downloaded GitHub repo to folder: {repo_dir}")
|
172
|
+
|
173
|
+
try:
|
174
|
+
_prepare_pipeline_project(repo_dir)
|
175
|
+
except Exception as e:
|
176
|
+
click.echo(f"⚠️ Pipeline preparation skipped: {e}")
|
177
|
+
|
178
|
+
return str(repo_dir)
|
179
|
+
|
33
180
|
def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal: bool = False, skip_models: bool = False) -> list:
|
34
181
|
"""
|
35
182
|
Downloads resources defined in metadata to a local destination folder.
|
@@ -85,11 +232,34 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
|
|
85
232
|
model_path = snapshot_download(
|
86
233
|
repo_id=repo_id,
|
87
234
|
local_dir=target_dir,
|
88
|
-
|
235
|
+
local_dir_use_symlinks=False
|
89
236
|
)
|
90
237
|
local_paths.append(model_path)
|
91
238
|
continue
|
92
239
|
|
240
|
+
if resource.startswith("gh:"):
|
241
|
+
resource_spec = resource[3:]
|
242
|
+
if "@" in resource_spec:
|
243
|
+
repo_id, ref = resource_spec.split("@", 1)
|
244
|
+
else:
|
245
|
+
repo_id, ref = resource_spec, None
|
246
|
+
|
247
|
+
if "/" not in repo_id:
|
248
|
+
raise click.ClickException(f"❌ Invalid GitHub repo spec: {resource}")
|
249
|
+
|
250
|
+
owner, name = repo_id.split("/", 1)
|
251
|
+
|
252
|
+
try:
|
253
|
+
token = os.getenv("GITHUB_TOKEN", None)
|
254
|
+
repo_path = _download_github_repo(owner, name, ref, dest_folder, token)
|
255
|
+
except Exception as e:
|
256
|
+
raise click.ClickException(
|
257
|
+
f"❌ Failed to download GitHub repo {owner}/{name}@{ref or 'default'}: {e}"
|
258
|
+
)
|
259
|
+
|
260
|
+
local_paths.append(repo_path)
|
261
|
+
continue
|
262
|
+
|
93
263
|
# Handle normal relative or absolute URLs
|
94
264
|
resource_url = urljoin(base_url, resource)
|
95
265
|
local_path = download_file_from_url(
|
@@ -113,6 +283,8 @@ def selectable_resource_handler(metadata):
|
|
113
283
|
choices = [(f"{i.get('name','Unnamed')} ({i.get('url','')})" if i.get('url') else i.get('name','Unnamed')) for i in selectable]
|
114
284
|
choices.append("Skip")
|
115
285
|
|
286
|
+
from InquirerPy import inquirer
|
287
|
+
|
116
288
|
sel = inquirer.select(message="Select an opt-in resource to download:", choices=choices).execute()
|
117
289
|
if sel == "Skip":
|
118
290
|
print("✅ No selectable resource chosen.")
|
@@ -295,6 +467,25 @@ def _extract_zip_streaming(zip_path: Path, extract_dir: Path, overwrite: bool =
|
|
295
467
|
|
296
468
|
print(f"✅ Extracted {len(members)} entries to {extract_dir}/")
|
297
469
|
|
470
|
+
def _extract_tar_strip_top_level(tar_path: Path, extract_dir: Path):
|
471
|
+
"""Extract a GitHub tarball, stripping the top-level folder."""
|
472
|
+
with tarfile.open(tar_path, "r:*") as tar:
|
473
|
+
members = tar.getmembers()
|
474
|
+
|
475
|
+
# Detect top-level prefix (first part before '/')
|
476
|
+
top_level = None
|
477
|
+
if members:
|
478
|
+
first_name = members[0].name
|
479
|
+
top_level = first_name.split("/", 1)[0]
|
480
|
+
|
481
|
+
for member in members:
|
482
|
+
# Strip top-level folder
|
483
|
+
if top_level and member.name.startswith(top_level + "/"):
|
484
|
+
member.name = member.name[len(top_level) + 1 :]
|
485
|
+
if not member.name:
|
486
|
+
continue
|
487
|
+
tar.extract(member, path=extract_dir)
|
488
|
+
|
298
489
|
def _combine_multipart_files(folder: str):
|
299
490
|
"""
|
300
491
|
Scan a folder for multipart files like name-split-aa, -ab, etc.,
|
@@ -352,20 +543,27 @@ def _combine_multipart_files(folder: str):
|
|
352
543
|
|
353
544
|
print(f"✅ Extracted to: {extract_dir}/")
|
354
545
|
|
355
|
-
def _extract_archives_in_folder(folder: str):
|
546
|
+
def _extract_archives_in_folder(folder: str, local_paths):
|
356
547
|
"""
|
357
|
-
Extract
|
548
|
+
Extract .tar, .gz, .tar.gz, and .zip files in the given folder,
|
549
|
+
but only if they are listed in local_paths.
|
358
550
|
Uses streaming to avoid NFS performance issues.
|
359
551
|
"""
|
360
552
|
folder = Path(folder)
|
553
|
+
local_paths = {str(Path(p).resolve()) for p in local_paths}
|
554
|
+
|
361
555
|
for file in folder.iterdir():
|
362
556
|
if not file.is_file():
|
363
557
|
continue
|
364
558
|
|
365
|
-
|
366
|
-
if
|
559
|
+
file_resolved = str(file.resolve())
|
560
|
+
if file_resolved not in local_paths:
|
561
|
+
continue
|
562
|
+
|
563
|
+
# TAR, GZ, TAR.GZ → all handled by _extract_tar_streaming
|
564
|
+
if file.suffix in [".tar", ".gz"] or file.name.endswith(".tar.gz"):
|
367
565
|
extract_dir = folder / file.stem.replace(".tar", "")
|
368
|
-
print(f"📦 Extracting TAR
|
566
|
+
print(f"📦 Extracting TAR/GZ: {file.name} to {extract_dir}/")
|
369
567
|
_extract_tar_streaming(file, extract_dir)
|
370
568
|
|
371
569
|
# ZIP
|
@@ -470,8 +668,45 @@ def _run_installation_script(metadata: Dict, extract_path: str = "."):
|
|
470
668
|
|
471
669
|
print("✅ Installation completed successfully.")
|
472
670
|
|
671
|
+
def _resolve_github_metadata_url(gh_ref: str) -> str:
|
672
|
+
"""
|
673
|
+
Resolve a GitHub shorthand like gh:org/repo@tag into a raw URL for metadata.json.
|
674
|
+
If tag is omitted, defaults to 'main'.
|
675
|
+
"""
|
676
|
+
try:
|
677
|
+
_, repo_ref = gh_ref.split(":", 1) # remove 'gh:'
|
678
|
+
if "@" in repo_ref:
|
679
|
+
org_repo, tag = repo_ref.split("@", 1)
|
680
|
+
else:
|
681
|
+
org_repo, tag = repo_ref, "main"
|
682
|
+
|
683
|
+
owner, repo = org_repo.split("/", 1)
|
684
|
+
token = os.getenv("GITHUB_TOKEN")
|
685
|
+
|
686
|
+
# Use GitHub API to fetch the metadata.json file
|
687
|
+
api_url = f"https://api.github.com/repos/{owner}/{repo}/contents/metadata.json?ref={tag}"
|
688
|
+
headers = {"Accept": "application/vnd.github.v3.raw"}
|
689
|
+
if token:
|
690
|
+
headers["Authorization"] = f"token {token}"
|
691
|
+
|
692
|
+
r = requests.get(api_url, headers=headers)
|
693
|
+
r.raise_for_status()
|
694
|
+
|
695
|
+
# Write metadata.json locally so downstream logic can use a file/URL
|
696
|
+
local_path = os.path.join("/tmp", f"{repo}-{tag}-metadata.json")
|
697
|
+
with open(local_path, "wb") as f:
|
698
|
+
f.write(r.content)
|
699
|
+
|
700
|
+
return local_path
|
701
|
+
except Exception as e:
|
702
|
+
raise RuntimeError(f"Failed to resolve GitHub metadata URL {gh_ref}: {e}")
|
703
|
+
|
473
704
|
def install_from_metadata(metadata_url: str, internal: bool, install_dir: str = '.'):
|
474
705
|
try:
|
706
|
+
if metadata_url.startswith("gh:"):
|
707
|
+
metadata_url = _resolve_github_metadata_url(metadata_url)
|
708
|
+
internal = False
|
709
|
+
|
475
710
|
metadata, _ = _download_and_validate_metadata(metadata_url, internal)
|
476
711
|
print_metadata_summary(metadata=metadata)
|
477
712
|
|
@@ -480,7 +715,7 @@ def install_from_metadata(metadata_url: str, internal: bool, install_dir: str =
|
|
480
715
|
local_paths = _download_assets(metadata, metadata_url, install_dir, internal)
|
481
716
|
if len(local_paths) > 0:
|
482
717
|
_combine_multipart_files(install_dir)
|
483
|
-
_extract_archives_in_folder(install_dir)
|
718
|
+
_extract_archives_in_folder(install_dir, local_paths)
|
484
719
|
_run_installation_script(metadata=metadata, extract_path=install_dir)
|
485
720
|
|
486
721
|
except Exception as e:
|
sima_cli/network/network.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
from InquirerPy import inquirer
|
2
1
|
import subprocess
|
3
2
|
import os
|
4
3
|
import re
|
@@ -144,6 +143,7 @@ def set_default_route(iface, ip):
|
|
144
143
|
check=True
|
145
144
|
)
|
146
145
|
print(f"✅ Default route set via {iface} ({gateway})")
|
146
|
+
|
147
147
|
except subprocess.CalledProcessError:
|
148
148
|
print(f"❌ Failed to set default route via {iface}")
|
149
149
|
|
@@ -152,6 +152,10 @@ def network_menu():
|
|
152
152
|
print("❌ This command only runs on the DevKit")
|
153
153
|
return
|
154
154
|
|
155
|
+
from InquirerPy import inquirer
|
156
|
+
|
157
|
+
print("✅ Scanning network configuration, please wait...")
|
158
|
+
|
155
159
|
while True:
|
156
160
|
interfaces = get_interfaces()
|
157
161
|
choices = ["🚪 Quit Menu"]
|
@@ -165,6 +169,8 @@ def network_menu():
|
|
165
169
|
iface_map[label] = iface
|
166
170
|
|
167
171
|
try:
|
172
|
+
|
173
|
+
|
168
174
|
iface_choice = inquirer.fuzzy(
|
169
175
|
message="Select Ethernet Interface:",
|
170
176
|
choices=choices,
|
sima_cli/update/local.py
CHANGED
@@ -86,7 +86,7 @@ def get_local_board_info() -> Tuple[str, str, bool]:
|
|
86
86
|
Retrieve the local board type and build version by reading /etc/build or /etc/buildinfo.
|
87
87
|
|
88
88
|
Returns:
|
89
|
-
(board_type, build_version,
|
89
|
+
(board_type, build_version, devkit_name, full_image): Tuple of strings, or ('', '') on failure.
|
90
90
|
"""
|
91
91
|
board_type = ""
|
92
92
|
build_version = ""
|
@@ -107,9 +107,9 @@ def get_local_board_info() -> Tuple[str, str, bool]:
|
|
107
107
|
except Exception:
|
108
108
|
continue
|
109
109
|
|
110
|
-
|
110
|
+
devkit_name = get_exact_devkit_type()
|
111
111
|
|
112
|
-
return board_type, build_version,
|
112
|
+
return board_type, build_version, devkit_name, is_board_running_full_image()
|
113
113
|
|
114
114
|
|
115
115
|
def get_boot_mmc(mounts_path="/proc/mounts", cmdline_path="/proc/cmdline"):
|
sima_cli/update/remote.py
CHANGED
@@ -53,19 +53,22 @@ def _wait_for_ssh(ip: str, timeout: int = 120):
|
|
53
53
|
else:
|
54
54
|
print("\r✅ Board is online! \n")
|
55
55
|
|
56
|
-
|
56
|
+
|
57
|
+
def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str, str, str, bool]:
|
57
58
|
"""
|
58
|
-
Connect to the remote board and retrieve board type, build version, and
|
59
|
+
Connect to the remote board and retrieve board type, build version, and devkit model.
|
59
60
|
|
60
61
|
Args:
|
61
62
|
ip (str): IP address of the board.
|
63
|
+
passwd (str): SSH password.
|
62
64
|
|
63
65
|
Returns:
|
64
|
-
(board_type, build_version,
|
66
|
+
(board_type, build_version, devkit_model, full_image):
|
67
|
+
Tuple of strings + bool, or ('', '', '', False) on failure.
|
65
68
|
"""
|
66
69
|
board_type = ""
|
67
70
|
build_version = ""
|
68
|
-
|
71
|
+
devkit_model = ""
|
69
72
|
full_image = False
|
70
73
|
|
71
74
|
try:
|
@@ -77,12 +80,17 @@ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str,
|
|
77
80
|
_, stdout, _ = ssh.exec_command("cat /etc/build 2>/dev/null || cat /etc/buildinfo 2>/dev/null")
|
78
81
|
build_output = stdout.read().decode()
|
79
82
|
|
80
|
-
# Retrieve
|
81
|
-
_, stdout, _ = ssh.exec_command("
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
83
|
+
# Retrieve model from /proc/device-tree/model
|
84
|
+
_, stdout, _ = ssh.exec_command("cat /proc/device-tree/model 2>/dev/null || echo ''")
|
85
|
+
model_output = stdout.read().decode().strip()
|
86
|
+
if model_output:
|
87
|
+
# Normalize string: remove "SiMa.ai " prefix and trailing "Board"
|
88
|
+
if model_output.startswith("SiMa.ai "):
|
89
|
+
model_output = model_output[len("SiMa.ai "):]
|
90
|
+
model_output = model_output.replace(" Board", "")
|
91
|
+
devkit_model = model_output.lower().replace(" ", "-")
|
92
|
+
|
93
|
+
# Check for NVMe tool presence (determine if full image is flashed)
|
86
94
|
nvme_check_cmd = r'PATH="$PATH:/usr/sbin:/sbin"; command -v nvme >/dev/null 2>&1 || which nvme >/dev/null 2>&1; echo $?'
|
87
95
|
_, stdout, _ = ssh.exec_command(nvme_check_cmd)
|
88
96
|
nvme_rc = stdout.read().decode().strip()
|
@@ -90,6 +98,7 @@ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str,
|
|
90
98
|
|
91
99
|
ssh.close()
|
92
100
|
|
101
|
+
# Parse build info
|
93
102
|
for line in build_output.splitlines():
|
94
103
|
line = line.strip()
|
95
104
|
if line.startswith("MACHINE"):
|
@@ -97,11 +106,7 @@ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str,
|
|
97
106
|
elif line.startswith("SIMA_BUILD_VERSION"):
|
98
107
|
build_version = line.split("=", 1)[-1].strip()
|
99
108
|
|
100
|
-
|
101
|
-
if line.startswith("fdt_name"):
|
102
|
-
fdt_name = line.split("=", 1)[-1].strip().replace('.dtb', '')
|
103
|
-
|
104
|
-
return board_type, build_version, fdt_name, full_image
|
109
|
+
return board_type, build_version, devkit_model, full_image
|
105
110
|
|
106
111
|
except Exception as e:
|
107
112
|
click.echo(f"Unable to retrieve board info with error: {e}, board may be still booting.")
|
sima_cli/update/updater.py
CHANGED
@@ -7,7 +7,6 @@ import tarfile
|
|
7
7
|
import gzip
|
8
8
|
import subprocess
|
9
9
|
import shutil
|
10
|
-
from InquirerPy import inquirer
|
11
10
|
from urllib.parse import urlparse
|
12
11
|
from typing import List
|
13
12
|
from sima_cli.utils.env import get_environment_type
|
@@ -82,6 +81,11 @@ def _confirm_flavor_switching(full_image: bool, flavor: str) -> str:
|
|
82
81
|
Returns:
|
83
82
|
str: The flavor to use ('full' or 'headless')
|
84
83
|
"""
|
84
|
+
# when the flavor is set to auto use the detected flavor instead
|
85
|
+
if flavor == 'auto':
|
86
|
+
flavor = 'full' if full_image else 'headless'
|
87
|
+
click.echo(f"✅ Automatically detected the flavor of the running image: [{flavor}], Proceeding to update")
|
88
|
+
|
85
89
|
if (full_image and flavor != 'full') or (not full_image and flavor == 'full'):
|
86
90
|
click.echo(f"🔄 The current image running on the board has a different flavor from what you specified ({flavor}).")
|
87
91
|
click.echo("Please choose an option:")
|
@@ -113,7 +117,9 @@ def _pick_from_available_versions(board: str, version_or_url: str, internal: boo
|
|
113
117
|
try:
|
114
118
|
if len(available_versions) > 1:
|
115
119
|
click.echo("Multiple firmware versions found matching your input:")
|
116
|
-
|
120
|
+
|
121
|
+
from InquirerPy import inquirer
|
122
|
+
|
117
123
|
selected_version = inquirer.fuzzy(
|
118
124
|
message="Select a version:",
|
119
125
|
choices=available_versions,
|
@@ -440,7 +446,7 @@ def _update_remote(extracted_paths: List[str], ip: str, board: str, passwd: str,
|
|
440
446
|
|
441
447
|
# Get remote board info
|
442
448
|
click.echo("🔍 Checking remote board type and version...")
|
443
|
-
remote_board, remote_version,
|
449
|
+
remote_board, remote_version, _, _ = get_remote_board_info(ip, passwd)
|
444
450
|
|
445
451
|
if not remote_board:
|
446
452
|
click.echo("❌ Could not determine remote board type.")
|
@@ -480,7 +486,7 @@ def download_image(version_or_url: str, board: str, swtype: str, internal: bool
|
|
480
486
|
extracted_paths = _download_image(version_or_url, board, internal, update_type, flavor=flavor, swtype=swtype)
|
481
487
|
return extracted_paths
|
482
488
|
|
483
|
-
def perform_update(version_or_url: str, ip: str = None, internal: bool = False, passwd: str = "edgeai", auto_confirm: bool = False, flavor: str = '
|
489
|
+
def perform_update(version_or_url: str, ip: str = None, internal: bool = False, passwd: str = "edgeai", auto_confirm: bool = False, flavor: str = 'auto'):
|
484
490
|
r"""
|
485
491
|
Update the system based on environment and input.
|
486
492
|
|
@@ -495,7 +501,7 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
|
|
495
501
|
internal (bool): If True, enable internal-only behaviors (e.g., Artifactory access).
|
496
502
|
passwd (str): Password for the board user (default: "edgeai").
|
497
503
|
auto_confirm (bool): If True, auto-confirm firmware update without prompting.
|
498
|
-
flavor (str): headless or full
|
504
|
+
flavor (str): headless or full, or auto (detect the running image flavor and use it)
|
499
505
|
"""
|
500
506
|
try:
|
501
507
|
board = ''
|
@@ -504,17 +510,17 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
|
|
504
510
|
click.echo(f"🔧 Requested version or URL: {version_or_url}, with flavor {flavor}")
|
505
511
|
|
506
512
|
if env_type == 'board':
|
507
|
-
board, version,
|
513
|
+
board, version, devkit_name, full_image = get_local_board_info()
|
508
514
|
else:
|
509
|
-
board, version,
|
515
|
+
board, version, devkit_name, full_image = get_remote_board_info(ip, passwd)
|
510
516
|
|
511
517
|
flavor = _confirm_flavor_switching(full_image=full_image, flavor=flavor)
|
512
518
|
|
513
519
|
if board in ['davinci', 'modalix']:
|
514
|
-
click.echo(f"🔧 Target board: {board} : {
|
520
|
+
click.echo(f"🔧 Target board: {board}, specific type: [{devkit_name}], board currently running: {version}, full_image: {full_image}")
|
515
521
|
|
516
|
-
if flavor == 'full' and 'modalix' not in
|
517
|
-
click.echo(f"❌ You've requested updating {
|
522
|
+
if flavor == 'full' and 'modalix' not in devkit_name:
|
523
|
+
click.echo(f"❌ You've requested updating {devkit_name} to full image, this is only supported for the Modalix DevKit")
|
518
524
|
return
|
519
525
|
|
520
526
|
# Davinci only supports headless build, so ignore the full flavor
|
sima_cli/utils/env.py
CHANGED
@@ -73,44 +73,51 @@ def get_sima_board_type() -> str:
|
|
73
73
|
|
74
74
|
def is_modalix_devkit() -> bool:
|
75
75
|
"""
|
76
|
-
Determines whether the current system is a Modalix DevKit (
|
77
|
-
by checking if
|
76
|
+
Determines whether the current system is a Modalix DevKit (SoM)
|
77
|
+
by checking if "Modalix SoM" is present in /proc/device-tree/model.
|
78
78
|
|
79
79
|
Returns:
|
80
|
-
bool: True if running on a Modalix DevKit, False otherwise.
|
80
|
+
bool: True if running on a Modalix DevKit (SoM), False otherwise.
|
81
81
|
"""
|
82
|
-
|
82
|
+
model_path = "/proc/device-tree/model"
|
83
|
+
if not os.path.exists(model_path):
|
83
84
|
return False
|
84
85
|
|
85
86
|
try:
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
except subprocess.CalledProcessError:
|
87
|
+
with open(model_path, "r") as f:
|
88
|
+
line = f.readline().strip()
|
89
|
+
return "Modalix SoM" in line
|
90
|
+
except Exception:
|
91
91
|
return False
|
92
|
-
|
93
|
-
return False
|
92
|
+
|
94
93
|
|
95
94
|
def get_exact_devkit_type() -> str:
|
96
95
|
"""
|
97
|
-
Extracts the exact devkit type from
|
96
|
+
Extracts the exact devkit type from /proc/device-tree/model.
|
97
|
+
|
98
|
+
Example mappings:
|
99
|
+
"SiMa.ai Modalix SoM Board" -> "modalix-som"
|
100
|
+
"SiMa.ai Modalix DVT Board" -> "modalix-dvt"
|
101
|
+
"SiMa.ai DaVinci Half-Height..." -> "davinci-half-height-half-length"
|
98
102
|
|
99
103
|
Returns:
|
100
|
-
str:
|
104
|
+
str: Normalized devkit type (lowercase, spaces -> "-"),
|
105
|
+
or an empty string if not found or unavailable.
|
101
106
|
"""
|
102
|
-
|
107
|
+
model_path = "/proc/device-tree/model"
|
108
|
+
if not os.path.exists(model_path):
|
103
109
|
return ""
|
104
110
|
|
105
111
|
try:
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
if line.startswith("
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
112
|
+
with open(model_path, "r") as f:
|
113
|
+
line = f.readline().strip()
|
114
|
+
# Remove "SiMa.ai " prefix if present
|
115
|
+
if line.startswith("SiMa.ai "):
|
116
|
+
line = line[len("SiMa.ai "):]
|
117
|
+
# Remove trailing "Board"
|
118
|
+
line = line.replace(" Board", "")
|
119
|
+
# Normalize
|
120
|
+
return line.lower().replace(" ", "-")
|
114
121
|
except Exception:
|
115
122
|
return ""
|
116
123
|
|
@@ -1,27 +1,27 @@
|
|
1
1
|
sima_cli/__init__.py,sha256=Nb2jSg9-CX1XvSc1c21U9qQ3atINxphuNkNfmR-9P3o,332
|
2
2
|
sima_cli/__main__.py,sha256=ehzD6AZ7zGytC2gLSvaJatxeD0jJdaEvNJvwYeGsWOg,69
|
3
|
-
sima_cli/__version__.py,sha256=
|
4
|
-
sima_cli/cli.py,sha256=
|
3
|
+
sima_cli/__version__.py,sha256=Bbhf5gpY7WnVH4M-cc4ByQEC0_SMh5CebOd37T5sqCA,49
|
4
|
+
sima_cli/cli.py,sha256=qm8rtydcugVdd37Rd79weMdX78Y7SKWaUe4LGfX-1XY,17339
|
5
5
|
sima_cli/app_zoo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
6
|
sima_cli/app_zoo/app.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
7
|
sima_cli/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
sima_cli/auth/basic_auth.py,sha256=
|
9
|
-
sima_cli/auth/login.py,sha256=
|
8
|
+
sima_cli/auth/basic_auth.py,sha256=UMEXCCnNQpjpp6RZxREs6iiKtYyaeqZBnTSR0wA8s6Q,8767
|
9
|
+
sima_cli/auth/login.py,sha256=nE-dSHK_husXw1XJaEcOe3I1_bnwHkLgO_BqKuQODDM,3781
|
10
10
|
sima_cli/data/resources_internal.yaml,sha256=zlQD4cSnZK86bLtTWuvEudZTARKiuIKmB--Jv4ajL8o,200
|
11
11
|
sima_cli/data/resources_public.yaml,sha256=U7hmUomGeQ2ULdo1BU2OQHr0PyKBamIdK9qrutDlX8o,201
|
12
12
|
sima_cli/download/__init__.py,sha256=6y4O2FOCYFR2jdnQoVi3hRtEoZ0Gw6rydlTy1SGJ5FE,218
|
13
|
-
sima_cli/download/downloader.py,sha256=
|
13
|
+
sima_cli/download/downloader.py,sha256=UQdrBWLQsPQygaoVaufOjbzWmRoNnk6pgLdnbnVi04U,6436
|
14
14
|
sima_cli/install/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
15
|
sima_cli/install/hostdriver.py,sha256=kAWDLebs60mbWIyTbUxmNrChcKW1uD5r7FtWNSUVUE4,5852
|
16
16
|
sima_cli/install/metadata_info.py,sha256=wmMqwzGfXbuilkqaxRVrFOzOtTOiONkmPCyA2oDAQpA,2168
|
17
|
-
sima_cli/install/metadata_installer.py,sha256=
|
17
|
+
sima_cli/install/metadata_installer.py,sha256=zIxs9nSX7EC1d6qxL2woZiMSQfFfIOzB2natyTvIPDI,27428
|
18
18
|
sima_cli/install/metadata_validator.py,sha256=7954rp9vFRNnqmIMvCVTjq40kUIEbGXzfc8HmQmChe0,5221
|
19
19
|
sima_cli/install/optiview.py,sha256=r4DYdQDTUbZVCR87hl5T21gsjZrhqpU8hWnYxKmUJ_k,4790
|
20
20
|
sima_cli/install/palette.py,sha256=uRznoHa4Mv9ZXHp6AoqknfC3RxpYNKi9Ins756Cyifk,3930
|
21
21
|
sima_cli/mla/meminfo.py,sha256=ndc8kQJmWGEIdvNh6iIhATGdrkqM2pbddr_eHxaPNfg,1466
|
22
22
|
sima_cli/model_zoo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
23
23
|
sima_cli/model_zoo/model.py,sha256=q91Nrg62j1TqwPO8HiX4nlEFCCmzNEFcyFTBVMbJm8w,9836
|
24
|
-
sima_cli/network/network.py,sha256=
|
24
|
+
sima_cli/network/network.py,sha256=kXYI2oxgeIg7LoGt2VKF9JlK8DIfQT87A9-x-D6uHCA,7714
|
25
25
|
sima_cli/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
26
26
|
sima_cli/sdk/syscheck.py,sha256=h9zCULW67y4i2hqiGc-hc1ucBDShA5FAe9NxwBGq-fM,4575
|
27
27
|
sima_cli/serial/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -32,21 +32,21 @@ sima_cli/update/__init__.py,sha256=0P-z-rSaev40IhfJXytK3AFWv2_sdQU4Ry6ei2sEus0,6
|
|
32
32
|
sima_cli/update/bmaptool.py,sha256=KrhUGShBwY4Wzz50QiuMYAxxPgEy1nz5C68G-0a4qF4,4988
|
33
33
|
sima_cli/update/bootimg.py,sha256=OA_GyZwI8dbU3kaucKmoxAZbnnSnjXeOkU6yuDPji1k,13632
|
34
34
|
sima_cli/update/cleanlog.py,sha256=-V6eDl3MdsvDmCfkKUJTqkXJ_WnLJE01uxS7z96b15g,909
|
35
|
-
sima_cli/update/local.py,sha256=
|
35
|
+
sima_cli/update/local.py,sha256=z3oRk6JH-zbCdoS3JpgeW_ZB4kolG7nPhLC55A2yssk,5597
|
36
36
|
sima_cli/update/netboot.py,sha256=hsJQLq4HVwFFkaWjA54VZdkMGDhO0RmylciS78qAfrM,19663
|
37
37
|
sima_cli/update/query.py,sha256=6RgvQfQT1_EtBGcibvVcz003dRKOq17NaGgL2mhaBbY,4891
|
38
|
-
sima_cli/update/remote.py,sha256=
|
39
|
-
sima_cli/update/updater.py,sha256=
|
38
|
+
sima_cli/update/remote.py,sha256=wC4MSBQVxmibxtPBchAzFMhZYcRjxTiLlPSzVI0en4o,14690
|
39
|
+
sima_cli/update/updater.py,sha256=NPd32OzM_356Nm1bEDDNh7faubW_VSOk12J4I6Yc2SQ,24580
|
40
40
|
sima_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
41
41
|
sima_cli/utils/artifactory.py,sha256=6YyVpzVm8ATy7NEwT9nkWx-wptkXrvG7Wl_zDT6jmLs,2390
|
42
42
|
sima_cli/utils/config.py,sha256=wE-cPQqY_gOqaP8t01xsRHD9tBUGk9MgBUm2GYYxI3E,1616
|
43
43
|
sima_cli/utils/config_loader.py,sha256=7I5we1yiCai18j9R9jvhfUzAmT3OjAqVK35XSLuUw8c,2005
|
44
44
|
sima_cli/utils/disk.py,sha256=66Kr631yhc_ny19up2aijfycWfD35AeLQOJgUsuH2hY,446
|
45
|
-
sima_cli/utils/env.py,sha256=
|
45
|
+
sima_cli/utils/env.py,sha256=LtV8S1kCkOpi-7Gj4rhidQRN13x_NDKy4W_LxujheeI,8400
|
46
46
|
sima_cli/utils/net.py,sha256=WVntA4CqipkNrrkA4tBVRadJft_pMcGYh4Re5xk3rqo,971
|
47
47
|
sima_cli/utils/network.py,sha256=UvqxbqbWUczGFyO-t1SybG7Q-x9kjUVRNIn_D6APzy8,1252
|
48
48
|
sima_cli/utils/pkg_update_check.py,sha256=IAV_NAOsBDL_lYNYMRYfdZWuVq-rJ_zzHjJJZ7UQaoc,3274
|
49
|
-
sima_cli-0.0.
|
49
|
+
sima_cli-0.0.36.dist-info/licenses/LICENSE,sha256=a260OFuV4SsMZ6sQCkoYbtws_4o2deFtbnT9kg7Rfd4,1082
|
50
50
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
51
51
|
tests/test_app_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
52
52
|
tests/test_auth.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -55,8 +55,8 @@ tests/test_download.py,sha256=t87DwxlHs26_ws9rpcHGwr_OrcRPd3hz6Zmm0vRee2U,4465
|
|
55
55
|
tests/test_firmware.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
56
56
|
tests/test_model_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
57
57
|
tests/test_utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
58
|
-
sima_cli-0.0.
|
59
|
-
sima_cli-0.0.
|
60
|
-
sima_cli-0.0.
|
61
|
-
sima_cli-0.0.
|
62
|
-
sima_cli-0.0.
|
58
|
+
sima_cli-0.0.36.dist-info/METADATA,sha256=nRB0ysTR2pmyDNlBC6Tx7gk1VPJ6o83ZulKr99RLJTo,3705
|
59
|
+
sima_cli-0.0.36.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
60
|
+
sima_cli-0.0.36.dist-info/entry_points.txt,sha256=xRYrDq1nCs6R8wEdB3c1kKuimxEjWJkHuCzArQPT0Xk,47
|
61
|
+
sima_cli-0.0.36.dist-info/top_level.txt,sha256=FtrbAUdHNohtEPteOblArxQNwoX9_t8qJQd59fagDlc,15
|
62
|
+
sima_cli-0.0.36.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|