sima-cli 0.0.28__py3-none-any.whl → 0.0.30__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.28"
2
+ __version__ = "0.0.30"
@@ -78,55 +78,116 @@ def _fetch_and_store_csrf_token(session: requests.Session) -> str:
78
78
 
79
79
 
80
80
  def login_external():
81
- """Interactive login workflow with CSRF token, cookie caching, and dummy session validation."""
81
+ """Interactive login workflow with CSRF token, cookie caching, and TOTP handling."""
82
82
  for attempt in range(1, 4):
83
83
  session = requests.Session()
84
84
  session.headers.update(HEADERS)
85
85
 
86
86
  _load_cookie_jar(session)
87
- csrf_token = _load_csrf_token()
88
-
89
- if not csrf_token:
90
- csrf_token = _fetch_and_store_csrf_token(session)
91
-
87
+ csrf_token = _load_csrf_token() or _fetch_and_store_csrf_token(session)
92
88
  if not csrf_token:
93
89
  click.echo("❌ CSRF token is missing or invalid.")
94
90
  continue
95
-
96
91
  session.headers["X-CSRF-Token"] = csrf_token
97
92
 
98
93
  if _is_session_valid(session):
99
94
  click.echo("🚀 You are already logged in.")
100
95
  return session
101
96
 
102
- # Prompt user login
97
+ # Fresh login prompt
103
98
  _delete_auth_files()
104
99
  click.echo(f"🔐 Sima.ai Developer Portal Login Attempt {attempt}/3")
105
100
  username = click.prompt("Email or Username")
106
101
  password = getpass.getpass("Password: ")
107
102
 
108
- login_data = {
103
+ # Base payload (no TOTP yet)
104
+ base_data = {
109
105
  "login": username,
110
106
  "password": password,
111
- "second_factor_method": "1"
107
+ "second_factor_method": "1", # TOTP
112
108
  }
113
109
 
114
- try:
115
- resp = session.post(LOGIN_URL, data=login_data)
116
- name = resp.json().get('users')[0]['name'] if 'users' in resp.json() else ''
117
- if resp.status_code != 200:
118
- click.echo(f"⚠️ Login request returned status {resp.status_code}")
119
- continue
120
- except Exception as e:
121
- click.echo(f"❌ Login request failed: {e}")
122
- continue
123
-
124
- if _is_session_valid(session):
110
+ def _post_login(payload: dict):
111
+ """POST and return (status_code, json or None, text) with robust error handling."""
112
+ try:
113
+ resp = session.post(LOGIN_URL, data=payload, timeout=30)
114
+ except Exception as e:
115
+ return None, None, f"request failed: {e}"
116
+ j = None
117
+ try:
118
+ j = resp.json()
119
+ except Exception:
120
+ pass
121
+ return resp.status_code, j, (j or resp.text)
122
+
123
+ # First try without TOTP (server may ask for it)
124
+ status, j, raw = _post_login(base_data)
125
+
126
+ # Helper: decide if success
127
+ def _success():
128
+ # Prefer server 'ok': True, but also double-check the session cookie validity
129
+ if j and j.get("ok") is True:
130
+ return True
131
+ return _is_session_valid(session)
132
+
133
+ # If immediate success
134
+ if status == 200 and _success():
125
135
  _save_cookie_jar(session)
126
- click.echo(f" Login successful. Welcome to Sima Developer Portal, {name}!")
136
+ welcome = (j.get("users", [{}])[0].get("name") if isinstance(j, dict) else "") or ""
137
+ click.echo(f"✅ Login successful. Welcome to Sima Developer Portal{', ' + welcome if welcome else ''}!")
127
138
  return session
139
+
140
+ # See if TOTP is required/invalid; then prompt and retry up to 3 times
141
+ def _needs_totp(payload_json):
142
+ if not isinstance(payload_json, dict):
143
+ return False
144
+ if payload_json.get("totp_enabled") is True:
145
+ return True
146
+ reason = payload_json.get("reason") or payload_json.get("error")
147
+ return str(reason) in {"invalid_second_factor", "second_factor_required"}
148
+
149
+ if _needs_totp(j):
150
+ # Try up to 3 TOTP attempts within this login attempt
151
+ for totp_try in range(1, 4):
152
+ totp = click.prompt(f"🔢 Enter TOTP code (attempt {totp_try}/3)", hide_input=True)
153
+ data = dict(base_data)
154
+ data["second_factor_token"] = totp
155
+
156
+ status, j2, raw2 = _post_login(data)
157
+ if status == 200 and (j2 and j2.get("ok") is True or _is_session_valid(session)):
158
+ _save_cookie_jar(session)
159
+ welcome = (j2.get("users", [{}])[0].get("name") if isinstance(j2, dict) else "") or ""
160
+ click.echo(f"✅ Login successful. Welcome to Sima Developer Portal{', ' + welcome if welcome else ''}!")
161
+ return session
162
+
163
+ # If still invalid 2FA, let user try again; otherwise break to outer loop
164
+ msg = ""
165
+ if isinstance(j2, dict):
166
+ reason = j2.get("reason") or j2.get("error") or ""
167
+ msg = f" ({reason})" if reason else ""
168
+ if isinstance(j2, dict) and str(j2.get("reason")) in {"invalid_second_factor"}:
169
+ click.echo(f"❌ Invalid authentication code. Please try again.{msg}")
170
+ continue
171
+ else:
172
+ click.echo(f"❌ Login failed with TOTP{msg}.")
173
+ break # go to next overall attempt
174
+
175
+ # exhausted TOTP tries
176
+ click.echo("❌ TOTP verification failed after 3 attempts.")
177
+ continue # next overall attempt
178
+
179
+ # Not a TOTP case; report error and continue
180
+ err_detail = ""
181
+ if isinstance(j, dict):
182
+ err_detail = j.get("error") or j.get("message") or ""
183
+ reason = j.get("reason")
184
+ if reason and reason != err_detail:
185
+ err_detail = f"{err_detail} ({reason})" if err_detail else reason
128
186
  else:
129
- click.echo("❌ Login failed.")
187
+ err_detail = str(raw)[:200]
188
+
189
+ click.echo(f"❌ Login failed. {err_detail or 'Please check your credentials and try again.'}")
130
190
 
131
191
  click.echo("❌ Login failed after 3 attempts.")
132
192
  raise SystemExit(1)
193
+
sima_cli/serial/serial.py CHANGED
@@ -3,6 +3,8 @@ import subprocess
3
3
  import shutil
4
4
  import click
5
5
  import os
6
+ import errno
7
+ import glob
6
8
  from sima_cli.utils.env import is_sima_board
7
9
 
8
10
  def connect_serial(ctx, baud):
@@ -69,6 +71,37 @@ def _connect_mac(baud):
69
71
  except KeyboardInterrupt:
70
72
  click.echo("\n❎ Serial connection interrupted by user.")
71
73
 
74
+ def _is_busy(port: str) -> bool:
75
+ # Try fuser
76
+ try:
77
+ r = subprocess.run(["fuser", "-s", port], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
78
+ if r.returncode == 0:
79
+ return True
80
+ if r.returncode == 1:
81
+ return False
82
+ except FileNotFoundError:
83
+ pass
84
+
85
+ try:
86
+ r = subprocess.run(["lsof", "-Fn", port], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
87
+ return r.returncode == 0
88
+ except FileNotFoundError:
89
+ pass
90
+ return False
91
+
92
+ def _has_rw_permission(port: str) -> bool:
93
+ # Quick check
94
+ if not os.access(port, os.R_OK | os.W_OK):
95
+ # Double-check by attempting to open; some perms lie with access()
96
+ try:
97
+ fd = os.open(port, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
98
+ os.close(fd)
99
+ return True
100
+ except OSError as e:
101
+ if e.errno in (errno.EACCES, errno.EPERM):
102
+ return False
103
+ # Other errors (e.g., EBUSY) are handled elsewhere
104
+ return True
72
105
 
73
106
  def _connect_linux(baud):
74
107
  terminal = "picocom"
@@ -118,20 +151,62 @@ def _connect_linux(baud):
118
151
  click.echo("❌ No USB serial device found.")
119
152
  raise SystemExit(1)
120
153
 
121
- port = ports[0]
154
+ # Discover ports
155
+ ports = sorted(glob.glob("/dev/ttyUSB*"))
156
+ if not ports:
157
+ click.echo("❌ No USB serial device found.")
158
+ raise SystemExit(1)
159
+
160
+ # Classify ports
161
+ busy, no_perm, free_ok = [], [], []
162
+ for p in ports:
163
+ if _is_busy(p):
164
+ busy.append(p)
165
+ elif not _has_rw_permission(p):
166
+ no_perm.append(p)
167
+ else:
168
+ free_ok.append(p)
169
+
170
+ if busy:
171
+ click.echo("⚠ Busy (in use): " + ", ".join(busy))
172
+ if no_perm:
173
+ click.echo("⛔ No permission: " + ", ".join(no_perm))
174
+ click.echo(
175
+ "\nTo fix permissions on Ubuntu/Debian:\n"
176
+ " sudo usermod -aG dialout $USER\n"
177
+ " # then log out and log back in (or reboot)\n"
178
+ "Temporary (until reboot):\n"
179
+ " sudo chmod a+rw /dev/ttyUSBX # not recommended long-term\n"
180
+ )
181
+
182
+ if not free_ok:
183
+ click.echo("❌ No accessible, free USB serial ports available.")
184
+ raise SystemExit(1)
185
+
186
+ # Choose port
187
+ if len(free_ok) == 1:
188
+ port = free_ok[0]
189
+ click.echo(f"✅ Using the only free port: {port}")
190
+ else:
191
+ click.echo("🔍 Multiple free ports found:")
192
+ for i, p in enumerate(free_ok, 1):
193
+ click.echo(f" {i}. {p}")
194
+ idx = click.prompt(f"Select a port [1-{len(free_ok)}]", type=int, default=1)
195
+ if not (1 <= idx <= len(free_ok)):
196
+ click.echo("❌ Invalid selection.")
197
+ raise SystemExit(1)
198
+ port = free_ok[idx - 1]
199
+
200
+ # Connect
122
201
  click.echo(f"🔌 Connecting to {port} with {terminal} ({baud} 8N1)...")
123
202
  try:
124
203
  if terminal == "picocom":
125
- click.echo("🧷 To exit: Press Ctrl + A, then Ctrl + X")
126
- subprocess.run(
127
- ["sudo", terminal, "-b", f"{baud}", "--databits", "8", "--parity", "n", "--stopbits", "1", port]
128
- )
204
+ click.echo("🧷 To exit: Ctrl+A, then Ctrl+X")
205
+ subprocess.run([terminal, "-b", f"{baud}", "--databits", "8", "--parity", "n", "--stopbits", "1", port])
129
206
  else: # minicom
130
- config_file = os.path.expanduser("~/.minirc.custom")
131
- click.echo("🧷 To exit: Press Ctrl + A, then Q")
132
- subprocess.run(
133
- ["sudo", terminal, "-C", config_file, "-D", port]
134
- )
207
+ cfg = os.path.expanduser("~/.minirc.custom")
208
+ click.echo("🧷 To exit: Ctrl+A, then Q")
209
+ subprocess.run([terminal, "-C", cfg, "-D", port])
135
210
  except KeyboardInterrupt:
136
211
  click.echo("\n❎ Serial connection interrupted by user.")
137
212
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sima-cli
3
- Version: 0.0.28
3
+ Version: 0.0.30
4
4
  Summary: CLI tool for SiMa Developer Portal to download models, firmware, and apps.
5
5
  Home-page: https://developer.sima.ai/
6
6
  Author: SiMa.ai
@@ -1,11 +1,11 @@
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=wdQWaXW7_2lzCGpGJA9Ok41JbEuK-vA6q78SmligY_w,49
3
+ sima_cli/__version__.py,sha256=fcUZK1xhI360Deo68dmn4Df_ffnBdYiwwdvLTu09P58,49
4
4
  sima_cli/cli.py,sha256=GYmQ7_XObl9VgFwuWWkWDo-_Y_Vn6lM53F7mKiYGubI,17126
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=pcocI6v496vC7v5MLYPZq3AEgD-2DdkzNFZiKsbx3eQ,4290
8
+ sima_cli/auth/basic_auth.py,sha256=mEmPrj32TVu1s34xR_UrJlIKHA3xBh98i_FzIZvAWag,7364
9
9
  sima_cli/auth/login.py,sha256=yCYXWgrfbP4jSTZ3hITfxlgHkdVQVzsd8hQKpqaqCKs,3780
10
10
  sima_cli/data/resources_internal.yaml,sha256=zlQD4cSnZK86bLtTWuvEudZTARKiuIKmB--Jv4ajL8o,200
11
11
  sima_cli/data/resources_public.yaml,sha256=U7hmUomGeQ2ULdo1BU2OQHr0PyKBamIdK9qrutDlX8o,201
@@ -25,7 +25,7 @@ sima_cli/network/network.py,sha256=ToDCQBfX0bUFEWWtfS8srImK5T11MX6R4MBQFM80faY,7
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
28
- sima_cli/serial/serial.py,sha256=1We85F9-l1FZcsLFxRzxbfPxAHeSCVlBOUyOUpqNf_s,6202
28
+ sima_cli/serial/serial.py,sha256=c5k0H2BTJGGCfysmBX1Ijp2pUT9serwdmE6_WyLUiQs,8664
29
29
  sima_cli/storage/nvme.py,sha256=cCzYWcyPwcFu5pSMBkovsS4EwovaIMGolhEFStogXMA,4739
30
30
  sima_cli/storage/sdcard.py,sha256=-WULjdV31-n8v5OOqfxR77qBbIK4hJnrD3xWxUVMoGI,6324
31
31
  sima_cli/update/__init__.py,sha256=0P-z-rSaev40IhfJXytK3AFWv2_sdQU4Ry6ei2sEus0,66
@@ -44,7 +44,7 @@ sima_cli/utils/disk.py,sha256=66Kr631yhc_ny19up2aijfycWfD35AeLQOJgUsuH2hY,446
44
44
  sima_cli/utils/env.py,sha256=IP5HrH0lE7RMSiBeXcEt5GCLMT5p-QQroG-uGzl5XFU,8181
45
45
  sima_cli/utils/net.py,sha256=WVntA4CqipkNrrkA4tBVRadJft_pMcGYh4Re5xk3rqo,971
46
46
  sima_cli/utils/network.py,sha256=UvqxbqbWUczGFyO-t1SybG7Q-x9kjUVRNIn_D6APzy8,1252
47
- sima_cli-0.0.28.dist-info/licenses/LICENSE,sha256=a260OFuV4SsMZ6sQCkoYbtws_4o2deFtbnT9kg7Rfd4,1082
47
+ sima_cli-0.0.30.dist-info/licenses/LICENSE,sha256=a260OFuV4SsMZ6sQCkoYbtws_4o2deFtbnT9kg7Rfd4,1082
48
48
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
49
  tests/test_app_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
50
  tests/test_auth.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -53,8 +53,8 @@ tests/test_download.py,sha256=t87DwxlHs26_ws9rpcHGwr_OrcRPd3hz6Zmm0vRee2U,4465
53
53
  tests/test_firmware.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
54
  tests/test_model_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
55
  tests/test_utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
- sima_cli-0.0.28.dist-info/METADATA,sha256=ETEoEF5hAtX6d8yNLF6dzARvDLO3AyuKfoiSn5wKRPg,3705
57
- sima_cli-0.0.28.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
- sima_cli-0.0.28.dist-info/entry_points.txt,sha256=xRYrDq1nCs6R8wEdB3c1kKuimxEjWJkHuCzArQPT0Xk,47
59
- sima_cli-0.0.28.dist-info/top_level.txt,sha256=FtrbAUdHNohtEPteOblArxQNwoX9_t8qJQd59fagDlc,15
60
- sima_cli-0.0.28.dist-info/RECORD,,
56
+ sima_cli-0.0.30.dist-info/METADATA,sha256=CywF-g-sv44c9qK1UIAu3AROiPOwnPISqiSSQMnOEiY,3705
57
+ sima_cli-0.0.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
+ sima_cli-0.0.30.dist-info/entry_points.txt,sha256=xRYrDq1nCs6R8wEdB3c1kKuimxEjWJkHuCzArQPT0Xk,47
59
+ sima_cli-0.0.30.dist-info/top_level.txt,sha256=FtrbAUdHNohtEPteOblArxQNwoX9_t8qJQd59fagDlc,15
60
+ sima_cli-0.0.30.dist-info/RECORD,,