opensees-cli 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/PKG-INFO +2 -2
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/README.md +1 -1
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/pyproject.toml +1 -1
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli/__init__.py +1 -1
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli/admin.py +65 -4
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli/auth.py +6 -1
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli/run.py +26 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli/version.py +34 -17
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli.egg-info/PKG-INFO +2 -2
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli.egg-info/SOURCES.txt +2 -1
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/tests/test_admin_commands.py +46 -1
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/tests/test_run_commands.py +21 -0
- opensees_cli-0.2.2/tests/test_version_commands.py +72 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/setup.cfg +0 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli/__main__.py +0 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli/api.py +0 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli/config.py +0 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli/files.py +0 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli/main.py +0 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli/path_setup.py +0 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli.egg-info/dependency_links.txt +0 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli.egg-info/entry_points.txt +0 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli.egg-info/requires.txt +0 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/src/opensees_cli.egg-info/top_level.txt +0 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/tests/test_api.py +0 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/tests/test_auth_commands.py +0 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/tests/test_config.py +0 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/tests/test_files_commands.py +0 -0
- {opensees_cli-0.2.1 → opensees_cli-0.2.2}/tests/test_main_commands.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opensees_cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Run OpenSees simulations in the cloud from the command line.
|
|
5
5
|
Author: Minjie Zhu
|
|
6
6
|
License: Proprietary
|
|
@@ -121,7 +121,7 @@ ops run help
|
|
|
121
121
|
Validation enforced by CLI (local uploads):
|
|
122
122
|
- path must exist on disk and be a regular file
|
|
123
123
|
- file extension must be `.py`
|
|
124
|
-
- file
|
|
124
|
+
- upload uses the cloud file API (same as `ops files upload`; up to 100 MB per file)
|
|
125
125
|
|
|
126
126
|
## Common Workflows
|
|
127
127
|
|
|
@@ -98,7 +98,7 @@ ops run help
|
|
|
98
98
|
Validation enforced by CLI (local uploads):
|
|
99
99
|
- path must exist on disk and be a regular file
|
|
100
100
|
- file extension must be `.py`
|
|
101
|
-
- file
|
|
101
|
+
- upload uses the cloud file API (same as `ops files upload`; up to 100 MB per file)
|
|
102
102
|
|
|
103
103
|
## Common Workflows
|
|
104
104
|
|
|
@@ -11,7 +11,7 @@ from rich.table import Table
|
|
|
11
11
|
from typer.core import TyperGroup
|
|
12
12
|
|
|
13
13
|
from opensees_cli import api, config
|
|
14
|
-
from opensees_cli.auth import _to_local, print_quota
|
|
14
|
+
from opensees_cli.auth import _to_local, _fmt_bytes, print_quota
|
|
15
15
|
|
|
16
16
|
_RUNTIME_TOKEN_RE = re.compile(r"^((\d+(?:\.\d+)?)(h|m|s))+$")
|
|
17
17
|
|
|
@@ -113,7 +113,7 @@ def list_users():
|
|
|
113
113
|
table.add_column("Admin")
|
|
114
114
|
table.add_column("Last Login")
|
|
115
115
|
table.add_column("Last Activity")
|
|
116
|
-
table.add_column("
|
|
116
|
+
table.add_column("Pinned")
|
|
117
117
|
|
|
118
118
|
for u in users:
|
|
119
119
|
enabled = "[green]yes[/green]" if u.get("enabled") else "[red]no[/red]"
|
|
@@ -125,7 +125,7 @@ def list_users():
|
|
|
125
125
|
admin,
|
|
126
126
|
u.get("last_login_at", "") or "-",
|
|
127
127
|
u.get("last_activity_at", "") or "-",
|
|
128
|
-
u.get("
|
|
128
|
+
u.get("openseespy_version", "") or "-",
|
|
129
129
|
)
|
|
130
130
|
console.print(table)
|
|
131
131
|
|
|
@@ -177,7 +177,7 @@ def delete_user(email: Optional[str] = typer.Option(None, "--email", "-e")):
|
|
|
177
177
|
|
|
178
178
|
@app.command("user-info")
|
|
179
179
|
def user_info(email: Optional[str] = typer.Option(None, "--email", "-e")):
|
|
180
|
-
"""Show a user's account details and
|
|
180
|
+
"""Show a user's account details, quota, analyses, and files."""
|
|
181
181
|
if not email:
|
|
182
182
|
email = typer.prompt("Email")
|
|
183
183
|
try:
|
|
@@ -195,6 +195,11 @@ def user_info(email: Optional[str] = typer.Option(None, "--email", "-e")):
|
|
|
195
195
|
console.print(f" Last login: {_to_local(r['last_login_at'])}")
|
|
196
196
|
if r.get("last_activity_at"):
|
|
197
197
|
console.print(f" Last active: {_to_local(r['last_activity_at'])}")
|
|
198
|
+
pin = r.get("openseespy_version") or ""
|
|
199
|
+
if pin:
|
|
200
|
+
console.print(f" OpenSeesPy: {pin} (pinned)")
|
|
201
|
+
else:
|
|
202
|
+
console.print(" OpenSeesPy: [dim]-[/dim] (not pinned)")
|
|
198
203
|
if r.get("last_cli_version"):
|
|
199
204
|
console.print(f" CLI version: {r['last_cli_version']}")
|
|
200
205
|
if r.get("last_ip"):
|
|
@@ -205,6 +210,62 @@ def user_info(email: Optional[str] = typer.Option(None, "--email", "-e")):
|
|
|
205
210
|
console.print()
|
|
206
211
|
print_quota(q)
|
|
207
212
|
|
|
213
|
+
analyses = r.get("analyses") or []
|
|
214
|
+
analyses_total = int(r.get("analyses_total") or 0)
|
|
215
|
+
analyses_running = int(r.get("analyses_running") or 0)
|
|
216
|
+
console.print()
|
|
217
|
+
extra = f", {analyses_running} running" if analyses_running else ""
|
|
218
|
+
console.print(f"[bold]Analyses[/bold] [dim]{analyses_total} total{extra}[/dim]")
|
|
219
|
+
if not analyses:
|
|
220
|
+
console.print(" [dim]No analyses.[/dim]")
|
|
221
|
+
else:
|
|
222
|
+
table = Table(show_header=True)
|
|
223
|
+
table.add_column("Analysis", style="dim", max_width=8)
|
|
224
|
+
table.add_column("Status")
|
|
225
|
+
table.add_column("Type")
|
|
226
|
+
table.add_column("File")
|
|
227
|
+
table.add_column("Tasks")
|
|
228
|
+
table.add_column("Created")
|
|
229
|
+
for a in analyses:
|
|
230
|
+
aid = (a.get("analysis_id") or "")[:8]
|
|
231
|
+
total = int(a.get("total_tasks") or 1)
|
|
232
|
+
table.add_row(
|
|
233
|
+
aid,
|
|
234
|
+
str(a.get("status") or ""),
|
|
235
|
+
str(a.get("type") or ""),
|
|
236
|
+
str(a.get("filename") or ""),
|
|
237
|
+
str(total),
|
|
238
|
+
_to_local(a["created_at"]) if a.get("created_at") else "",
|
|
239
|
+
)
|
|
240
|
+
console.print(table)
|
|
241
|
+
if analyses_total > len(analyses):
|
|
242
|
+
console.print(f" [dim]Showing {len(analyses)} of {analyses_total}.[/dim]")
|
|
243
|
+
|
|
244
|
+
files = r.get("files") or []
|
|
245
|
+
files_total = int(r.get("files_total") or 0)
|
|
246
|
+
files_size = int(r.get("files_size") or 0)
|
|
247
|
+
console.print()
|
|
248
|
+
console.print(
|
|
249
|
+
f"[bold]Files[/bold] [dim]{files_total} file"
|
|
250
|
+
f"{'' if files_total == 1 else 's'}, {_fmt_bytes(files_size)}[/dim]"
|
|
251
|
+
)
|
|
252
|
+
if not files:
|
|
253
|
+
console.print(" [dim]No files.[/dim]")
|
|
254
|
+
else:
|
|
255
|
+
table = Table(show_header=True)
|
|
256
|
+
table.add_column("Path")
|
|
257
|
+
table.add_column("Size", justify="right")
|
|
258
|
+
table.add_column("Modified")
|
|
259
|
+
for f in files:
|
|
260
|
+
table.add_row(
|
|
261
|
+
str(f.get("path") or ""),
|
|
262
|
+
_fmt_bytes(f.get("size") or 0),
|
|
263
|
+
_to_local(f["last_modified"]) if f.get("last_modified") else "",
|
|
264
|
+
)
|
|
265
|
+
console.print(table)
|
|
266
|
+
if files_total > len(files):
|
|
267
|
+
console.print(f" [dim]Showing {len(files)} of {files_total}.[/dim]")
|
|
268
|
+
|
|
208
269
|
|
|
209
270
|
@app.command("set-quota")
|
|
210
271
|
def set_quota(
|
|
@@ -460,7 +460,12 @@ def login(email: Optional[str] = typer.Option(None, "--email", "-e")):
|
|
|
460
460
|
from opensees_cli import __version__
|
|
461
461
|
r = api.post(
|
|
462
462
|
"/auth/login",
|
|
463
|
-
{
|
|
463
|
+
{
|
|
464
|
+
"email": email,
|
|
465
|
+
"password": password,
|
|
466
|
+
"cli_version": __version__,
|
|
467
|
+
"openseespy_version": config.get_openseespy_version() or "",
|
|
468
|
+
},
|
|
464
469
|
auth=False,
|
|
465
470
|
)
|
|
466
471
|
config.save_credentials(
|
|
@@ -739,6 +739,7 @@ def status(
|
|
|
739
739
|
"analysis_id": aid,
|
|
740
740
|
"status": a.get("status"),
|
|
741
741
|
"filename": a.get("filename"),
|
|
742
|
+
"type": _run_type(a),
|
|
742
743
|
"total_tasks": total_tasks,
|
|
743
744
|
"completed_tasks": completed,
|
|
744
745
|
"failed_tasks": failed,
|
|
@@ -758,6 +759,7 @@ def status(
|
|
|
758
759
|
table = Table(show_header=True, title="Running analyses")
|
|
759
760
|
table.add_column("Analysis", style="dim", max_width=8)
|
|
760
761
|
table.add_column("Status")
|
|
762
|
+
table.add_column("Type")
|
|
761
763
|
table.add_column("File")
|
|
762
764
|
table.add_column("Tasks")
|
|
763
765
|
table.add_column("Created")
|
|
@@ -773,6 +775,7 @@ def status(
|
|
|
773
775
|
table.add_row(
|
|
774
776
|
aid,
|
|
775
777
|
_status_style(a.get("status", "")),
|
|
778
|
+
_run_type(a),
|
|
776
779
|
a.get("filename", ""),
|
|
777
780
|
tasks_str,
|
|
778
781
|
_to_local(a.get("created_at", "")) if a.get("created_at") else "",
|
|
@@ -1971,6 +1974,23 @@ def _status_style(s: str) -> str:
|
|
|
1971
1974
|
return styles.get(s, s)
|
|
1972
1975
|
|
|
1973
1976
|
|
|
1977
|
+
_LAMBDA_MAX_TIMEOUT = 900
|
|
1978
|
+
|
|
1979
|
+
|
|
1980
|
+
def _run_type(r: dict) -> str:
|
|
1981
|
+
"""Worker type shown to users: ``fast`` (Lambda) or ``long`` (Fargate)."""
|
|
1982
|
+
t = r.get("type")
|
|
1983
|
+
if t in ("fast", "long"):
|
|
1984
|
+
return t
|
|
1985
|
+
timeout = r.get("timeout")
|
|
1986
|
+
if timeout is not None:
|
|
1987
|
+
try:
|
|
1988
|
+
return "long" if int(timeout) > _LAMBDA_MAX_TIMEOUT else "fast"
|
|
1989
|
+
except (TypeError, ValueError):
|
|
1990
|
+
pass
|
|
1991
|
+
return ""
|
|
1992
|
+
|
|
1993
|
+
|
|
1974
1994
|
def _print_status(r: dict) -> None:
|
|
1975
1995
|
"""Print analysis summary, per-task list, or individual task detail."""
|
|
1976
1996
|
# Multi-task analysis
|
|
@@ -1978,6 +1998,9 @@ def _print_status(r: dict) -> None:
|
|
|
1978
1998
|
analysis_id = r.get("analysis_id", "")
|
|
1979
1999
|
console.print(f" Analysis: [dim]{analysis_id}[/dim]")
|
|
1980
2000
|
console.print(f" File: {r.get('filename', '')}")
|
|
2001
|
+
run_type = _run_type(r)
|
|
2002
|
+
if run_type:
|
|
2003
|
+
console.print(f" Type: {run_type}")
|
|
1981
2004
|
total = r["total_tasks"]
|
|
1982
2005
|
completed = r.get("completed", 0)
|
|
1983
2006
|
failed = r.get("failed", 0)
|
|
@@ -2031,6 +2054,9 @@ def _print_status(r: dict) -> None:
|
|
|
2031
2054
|
console.print(f" Analysis: [dim]{analysis_id}[/dim]")
|
|
2032
2055
|
|
|
2033
2056
|
console.print(f" Status: {_status_style(r.get('status', ''))}")
|
|
2057
|
+
run_type = _run_type(r)
|
|
2058
|
+
if run_type:
|
|
2059
|
+
console.print(f" Type: {run_type}")
|
|
2034
2060
|
if r.get("filename"):
|
|
2035
2061
|
console.print(f" File: {r['filename']}")
|
|
2036
2062
|
if r.get("created_at"):
|
|
@@ -9,6 +9,18 @@ from rich.table import Table
|
|
|
9
9
|
from opensees_cli import api, config
|
|
10
10
|
|
|
11
11
|
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _sync_pin_to_server(version: Optional[str]) -> None:
|
|
15
|
+
"""Best-effort: store the local pin on the user record for admin list-users."""
|
|
16
|
+
if not api.has_stored_credentials():
|
|
17
|
+
return
|
|
18
|
+
try:
|
|
19
|
+
api.post("/auth/me", {"openseespy_version": version or ""})
|
|
20
|
+
except api.ApiError:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
12
24
|
app = typer.Typer(
|
|
13
25
|
name="version",
|
|
14
26
|
help="Manage OpenSeesPy versions",
|
|
@@ -43,6 +55,7 @@ def set_version(version: str):
|
|
|
43
55
|
console.print(f"[yellow]Warning: could not verify version: {e.message}[/yellow]")
|
|
44
56
|
|
|
45
57
|
config.set_openseespy_version(version)
|
|
58
|
+
_sync_pin_to_server(version)
|
|
46
59
|
console.print(f"[green]OpenSeesPy version pinned to: {version}[/green]")
|
|
47
60
|
|
|
48
61
|
|
|
@@ -74,6 +87,24 @@ def get_version():
|
|
|
74
87
|
raise typer.Exit(1)
|
|
75
88
|
|
|
76
89
|
|
|
90
|
+
def _status_label(v: dict) -> str:
|
|
91
|
+
"""Human status for the list table. Partial rows are numbered, not named.
|
|
92
|
+
|
|
93
|
+
``partial 1`` — only the first worker is registered (Lambda).
|
|
94
|
+
``partial 2`` — only the second worker is registered (Fargate).
|
|
95
|
+
"""
|
|
96
|
+
status = (v.get("status") or "").strip() or "unknown"
|
|
97
|
+
if status != "partial":
|
|
98
|
+
return status
|
|
99
|
+
has_first = bool(v.get("lambda_arn"))
|
|
100
|
+
has_second = bool(v.get("fargate_task_def"))
|
|
101
|
+
if has_first and not has_second:
|
|
102
|
+
return "partial 1"
|
|
103
|
+
if has_second and not has_first:
|
|
104
|
+
return "partial 2"
|
|
105
|
+
return "partial"
|
|
106
|
+
|
|
107
|
+
|
|
77
108
|
@app.command("list")
|
|
78
109
|
def list_versions(
|
|
79
110
|
status: str = typer.Option(
|
|
@@ -82,12 +113,6 @@ def list_versions(
|
|
|
82
113
|
"-s",
|
|
83
114
|
help="Filter by status: active, inactive, partial",
|
|
84
115
|
),
|
|
85
|
-
show_arns: bool = typer.Option(
|
|
86
|
-
False,
|
|
87
|
-
"--arns",
|
|
88
|
-
"-a",
|
|
89
|
-
help="Show Lambda and Fargate ARNs",
|
|
90
|
-
),
|
|
91
116
|
):
|
|
92
117
|
"""
|
|
93
118
|
List available OpenSeesPy versions in the registry.
|
|
@@ -95,7 +120,6 @@ def list_versions(
|
|
|
95
120
|
Example:
|
|
96
121
|
ops version list
|
|
97
122
|
ops version list --status active
|
|
98
|
-
ops version list --arns
|
|
99
123
|
"""
|
|
100
124
|
try:
|
|
101
125
|
params = {}
|
|
@@ -118,9 +142,6 @@ def list_versions(
|
|
|
118
142
|
table.add_column("Version")
|
|
119
143
|
table.add_column("Status")
|
|
120
144
|
table.add_column("Updated")
|
|
121
|
-
if show_arns:
|
|
122
|
-
table.add_column("Lambda ARN", overflow="fold")
|
|
123
|
-
table.add_column("Fargate Task Def", overflow="fold")
|
|
124
145
|
|
|
125
146
|
for v in versions:
|
|
126
147
|
version_str = v["version"]
|
|
@@ -132,7 +153,7 @@ def list_versions(
|
|
|
132
153
|
if tags:
|
|
133
154
|
version_str = f"[bold green]{version_str} ({', '.join(tags)})[/bold green]"
|
|
134
155
|
|
|
135
|
-
status_str = v
|
|
156
|
+
status_str = _status_label(v)
|
|
136
157
|
if status_str == "active":
|
|
137
158
|
status_str = f"[green]{status_str}[/green]"
|
|
138
159
|
elif status_str == "inactive":
|
|
@@ -144,12 +165,7 @@ def list_versions(
|
|
|
144
165
|
if updated:
|
|
145
166
|
updated = updated.split("T")[0]
|
|
146
167
|
|
|
147
|
-
|
|
148
|
-
if show_arns:
|
|
149
|
-
row.append(v.get("lambda_arn", ""))
|
|
150
|
-
row.append(v.get("fargate_task_def", ""))
|
|
151
|
-
|
|
152
|
-
table.add_row(*row)
|
|
168
|
+
table.add_row(version_str, status_str, updated)
|
|
153
169
|
|
|
154
170
|
console.print(table)
|
|
155
171
|
|
|
@@ -165,6 +181,7 @@ def clear_version():
|
|
|
165
181
|
ops version clear
|
|
166
182
|
"""
|
|
167
183
|
config.clear_openseespy_version()
|
|
184
|
+
_sync_pin_to_server(None)
|
|
168
185
|
console.print("[green]Pin cleared — will use API default (latest active)[/green]")
|
|
169
186
|
|
|
170
187
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opensees_cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Run OpenSees simulations in the cloud from the command line.
|
|
5
5
|
Author: Minjie Zhu
|
|
6
6
|
License: Proprietary
|
|
@@ -121,7 +121,7 @@ ops run help
|
|
|
121
121
|
Validation enforced by CLI (local uploads):
|
|
122
122
|
- path must exist on disk and be a regular file
|
|
123
123
|
- file extension must be `.py`
|
|
124
|
-
- file
|
|
124
|
+
- upload uses the cloud file API (same as `ops files upload`; up to 100 MB per file)
|
|
125
125
|
|
|
126
126
|
## Common Workflows
|
|
127
127
|
|
|
@@ -16,13 +16,16 @@ class TestListUsers:
|
|
|
16
16
|
"is_admin": False,
|
|
17
17
|
"last_login_at": "2025-01-01T00:00:00Z",
|
|
18
18
|
"last_activity_at": "",
|
|
19
|
-
"
|
|
19
|
+
"openseespy_version": "3.7.1.0",
|
|
20
20
|
},
|
|
21
21
|
]
|
|
22
22
|
})
|
|
23
23
|
result = cli_runner.invoke(app, ["admin", "list-users"])
|
|
24
24
|
assert result.exit_code == 0
|
|
25
25
|
assert "alice@te" in result.output
|
|
26
|
+
assert "3.7.1.0" in result.output
|
|
27
|
+
assert "CLI Version" not in result.output
|
|
28
|
+
assert "Pinned" in result.output
|
|
26
29
|
|
|
27
30
|
def test_list_users_empty(self, cli_runner, admin_creds, patch_httpx, mock_router):
|
|
28
31
|
mock_router.add("GET", "/prod/admin/users", json={"users": []})
|
|
@@ -66,6 +69,7 @@ class TestUserInfo:
|
|
|
66
69
|
"email": "alice@test.com",
|
|
67
70
|
"is_admin": False,
|
|
68
71
|
"is_enabled": True,
|
|
72
|
+
"openseespy_version": "3.7.1.0",
|
|
69
73
|
"quota": {
|
|
70
74
|
"max_concurrent_runs": 5,
|
|
71
75
|
"max_tasks_per_analysis": 100,
|
|
@@ -75,10 +79,51 @@ class TestUserInfo:
|
|
|
75
79
|
"max_monthly_storage": 1073741824,
|
|
76
80
|
"storage_used": 0,
|
|
77
81
|
},
|
|
82
|
+
"analyses_total": 2,
|
|
83
|
+
"analyses_running": 1,
|
|
84
|
+
"analyses": [
|
|
85
|
+
{
|
|
86
|
+
"analysis_id": "aaa11111-0000-0000-0000-000000000000",
|
|
87
|
+
"status": "running",
|
|
88
|
+
"filename": "Truss.py",
|
|
89
|
+
"type": "fast",
|
|
90
|
+
"total_tasks": 1,
|
|
91
|
+
"created_at": "2026-09-01T00:00:00Z",
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
"files_total": 1,
|
|
95
|
+
"files_size": 128,
|
|
96
|
+
"files": [
|
|
97
|
+
{"path": "Truss.py", "size": 128, "last_modified": "2026-09-01T00:00:00Z"},
|
|
98
|
+
],
|
|
78
99
|
})
|
|
79
100
|
result = cli_runner.invoke(app, ["admin", "user-info", "-e", "alice@test.com"])
|
|
80
101
|
assert result.exit_code == 0
|
|
81
102
|
assert "alice@test.com" in result.output
|
|
103
|
+
assert "3.7.1.0" in result.output
|
|
104
|
+
assert "pinned" in result.output.lower()
|
|
105
|
+
assert "Truss.py" in result.output
|
|
106
|
+
assert "Analyses" in result.output
|
|
107
|
+
assert "Files" in result.output
|
|
108
|
+
|
|
109
|
+
def test_user_info_not_pinned(self, cli_runner, admin_creds, patch_httpx, mock_router):
|
|
110
|
+
mock_router.add("POST", "/prod/admin/user-info", json={
|
|
111
|
+
"email": "bob@test.com",
|
|
112
|
+
"is_admin": False,
|
|
113
|
+
"is_enabled": True,
|
|
114
|
+
"openseespy_version": None,
|
|
115
|
+
"analyses_total": 0,
|
|
116
|
+
"analyses_running": 0,
|
|
117
|
+
"analyses": [],
|
|
118
|
+
"files_total": 0,
|
|
119
|
+
"files_size": 0,
|
|
120
|
+
"files": [],
|
|
121
|
+
})
|
|
122
|
+
result = cli_runner.invoke(app, ["admin", "user-info", "-e", "bob@test.com"])
|
|
123
|
+
assert result.exit_code == 0
|
|
124
|
+
assert "not pinned" in result.output.lower()
|
|
125
|
+
assert "No analyses" in result.output
|
|
126
|
+
assert "No files" in result.output
|
|
82
127
|
|
|
83
128
|
|
|
84
129
|
class TestSetQuota:
|
|
@@ -183,6 +183,7 @@ class TestRunStatus:
|
|
|
183
183
|
"analysis_id": "aaa55555",
|
|
184
184
|
"status": "completed",
|
|
185
185
|
"filename": "model.py",
|
|
186
|
+
"type": "fast",
|
|
186
187
|
"total_tasks": 1,
|
|
187
188
|
"created_at": "2025-06-01T00:00:00Z",
|
|
188
189
|
"runtime_seconds": 12.5,
|
|
@@ -191,6 +192,26 @@ class TestRunStatus:
|
|
|
191
192
|
result = cli_runner.invoke(app, ["run", "status", "aaa55555"])
|
|
192
193
|
assert result.exit_code == 0
|
|
193
194
|
assert "completed" in result.output.lower()
|
|
195
|
+
assert "Type:" in result.output
|
|
196
|
+
assert "fast" in result.output
|
|
197
|
+
assert "lambda" not in result.output.lower()
|
|
198
|
+
assert "fargate" not in result.output.lower()
|
|
199
|
+
|
|
200
|
+
def test_status_long_type(self, cli_runner, logged_in_creds, patch_httpx, mock_router):
|
|
201
|
+
mock_router.add("GET", "/prod/run/status", json={
|
|
202
|
+
"task_id": "ttt55556",
|
|
203
|
+
"analysis_id": "aaa55556",
|
|
204
|
+
"status": "running",
|
|
205
|
+
"filename": "model.py",
|
|
206
|
+
"type": "long",
|
|
207
|
+
"timeout": 1800,
|
|
208
|
+
"total_tasks": 1,
|
|
209
|
+
})
|
|
210
|
+
result = cli_runner.invoke(app, ["run", "status", "aaa55556"])
|
|
211
|
+
assert result.exit_code == 0
|
|
212
|
+
assert "Type:" in result.output
|
|
213
|
+
assert "long" in result.output
|
|
214
|
+
assert "fargate" not in result.output.lower()
|
|
194
215
|
|
|
195
216
|
def test_status_no_running(self, cli_runner, logged_in_creds, patch_httpx, mock_router):
|
|
196
217
|
mock_router.add("GET", "/prod/run/list", json={"analyses": []})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Tests for ops version list status labels."""
|
|
2
|
+
|
|
3
|
+
from opensees_cli.main import app
|
|
4
|
+
from opensees_cli.version import _status_label
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_status_label_partial_1():
|
|
8
|
+
assert _status_label({
|
|
9
|
+
"status": "partial",
|
|
10
|
+
"lambda_arn": "arn:aws:lambda:us-west-2:1:function:opensees",
|
|
11
|
+
"fargate_task_def": "",
|
|
12
|
+
}) == "partial 1"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_status_label_partial_2():
|
|
16
|
+
assert _status_label({
|
|
17
|
+
"status": "partial",
|
|
18
|
+
"lambda_arn": "",
|
|
19
|
+
"fargate_task_def": "arn:aws:ecs:us-west-2:1:task-definition/opensees:1",
|
|
20
|
+
}) == "partial 2"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_status_label_active_and_inactive():
|
|
24
|
+
assert _status_label({"status": "active", "lambda_arn": "x", "fargate_task_def": "y"}) == "active"
|
|
25
|
+
assert _status_label({"status": "inactive"}) == "inactive"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_version_list_shows_partial_numbers(cli_runner, patch_httpx, mock_router):
|
|
29
|
+
mock_router.add("GET", "/prod/run/versions", json={
|
|
30
|
+
"default": "3.7.0.0",
|
|
31
|
+
"versions": [
|
|
32
|
+
{
|
|
33
|
+
"version": "3.7.0.0",
|
|
34
|
+
"status": "active",
|
|
35
|
+
"lambda_arn": "arn:aws:lambda:fn-a",
|
|
36
|
+
"fargate_task_def": "arn:aws:ecs:td-a",
|
|
37
|
+
"updated_at": "2026-09-01T00:00:00Z",
|
|
38
|
+
"is_default": True,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"version": "3.6.0.0",
|
|
42
|
+
"status": "partial",
|
|
43
|
+
"lambda_arn": "arn:aws:lambda:fn-b",
|
|
44
|
+
"fargate_task_def": "",
|
|
45
|
+
"updated_at": "2026-08-01T00:00:00Z",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"version": "3.5.0.0",
|
|
49
|
+
"status": "partial",
|
|
50
|
+
"lambda_arn": "",
|
|
51
|
+
"fargate_task_def": "arn:aws:ecs:td-c",
|
|
52
|
+
"updated_at": "2026-07-01T00:00:00Z",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
})
|
|
56
|
+
result = cli_runner.invoke(app, ["version", "list"])
|
|
57
|
+
assert result.exit_code == 0
|
|
58
|
+
assert "partial 1" in result.output
|
|
59
|
+
assert "partial 2" in result.output
|
|
60
|
+
assert "active" in result.output
|
|
61
|
+
assert "arn:aws:lambda" not in result.output
|
|
62
|
+
assert "arn:aws:ecs" not in result.output
|
|
63
|
+
assert "Lambda" not in result.output
|
|
64
|
+
assert "Fargate" not in result.output
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_version_list_rejects_arns_flag(cli_runner, patch_httpx, mock_router):
|
|
68
|
+
result = cli_runner.invoke(app, ["version", "list", "-a"])
|
|
69
|
+
assert result.exit_code != 0
|
|
70
|
+
assert "No such option" in result.output or "no such option" in result.output.lower()
|
|
71
|
+
result = cli_runner.invoke(app, ["version", "list", "--arns"])
|
|
72
|
+
assert result.exit_code != 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|