remotivelabs-cli 0.5.0a1__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.
- remotivelabs/cli/__init__.py +0 -0
- remotivelabs/cli/api/cloud/tokens.py +62 -0
- remotivelabs/cli/broker/__init__.py +33 -0
- remotivelabs/cli/broker/defaults.py +1 -0
- remotivelabs/cli/broker/discovery.py +43 -0
- remotivelabs/cli/broker/export.py +92 -0
- remotivelabs/cli/broker/files.py +119 -0
- remotivelabs/cli/broker/lib/__about__.py +4 -0
- remotivelabs/cli/broker/lib/broker.py +625 -0
- remotivelabs/cli/broker/lib/client.py +224 -0
- remotivelabs/cli/broker/lib/helper.py +277 -0
- remotivelabs/cli/broker/lib/signalcreator.py +196 -0
- remotivelabs/cli/broker/license_flows.py +167 -0
- remotivelabs/cli/broker/licenses.py +98 -0
- remotivelabs/cli/broker/playback.py +117 -0
- remotivelabs/cli/broker/record.py +41 -0
- remotivelabs/cli/broker/recording_session/__init__.py +3 -0
- remotivelabs/cli/broker/recording_session/client.py +67 -0
- remotivelabs/cli/broker/recording_session/cmd.py +254 -0
- remotivelabs/cli/broker/recording_session/time.py +49 -0
- remotivelabs/cli/broker/scripting.py +129 -0
- remotivelabs/cli/broker/signals.py +220 -0
- remotivelabs/cli/broker/version.py +31 -0
- remotivelabs/cli/cloud/__init__.py +17 -0
- remotivelabs/cli/cloud/auth/__init__.py +3 -0
- remotivelabs/cli/cloud/auth/cmd.py +128 -0
- remotivelabs/cli/cloud/auth/login.py +283 -0
- remotivelabs/cli/cloud/auth_tokens.py +149 -0
- remotivelabs/cli/cloud/brokers.py +109 -0
- remotivelabs/cli/cloud/configs.py +109 -0
- remotivelabs/cli/cloud/licenses/__init__.py +0 -0
- remotivelabs/cli/cloud/licenses/cmd.py +14 -0
- remotivelabs/cli/cloud/organisations.py +112 -0
- remotivelabs/cli/cloud/projects.py +44 -0
- remotivelabs/cli/cloud/recordings.py +580 -0
- remotivelabs/cli/cloud/recordings_playback.py +274 -0
- remotivelabs/cli/cloud/resumable_upload.py +87 -0
- remotivelabs/cli/cloud/sample_recordings.py +25 -0
- remotivelabs/cli/cloud/service_account_tokens.py +62 -0
- remotivelabs/cli/cloud/service_accounts.py +72 -0
- remotivelabs/cli/cloud/storage/__init__.py +5 -0
- remotivelabs/cli/cloud/storage/cmd.py +76 -0
- remotivelabs/cli/cloud/storage/copy.py +86 -0
- remotivelabs/cli/cloud/storage/uri_or_path.py +45 -0
- remotivelabs/cli/cloud/uri.py +113 -0
- remotivelabs/cli/connect/__init__.py +0 -0
- remotivelabs/cli/connect/connect.py +118 -0
- remotivelabs/cli/connect/protopie/protopie.py +185 -0
- remotivelabs/cli/py.typed +0 -0
- remotivelabs/cli/remotive.py +123 -0
- remotivelabs/cli/settings/__init__.py +20 -0
- remotivelabs/cli/settings/config_file.py +113 -0
- remotivelabs/cli/settings/core.py +333 -0
- remotivelabs/cli/settings/migration/__init__.py +0 -0
- remotivelabs/cli/settings/migration/migrate_all_token_files.py +80 -0
- remotivelabs/cli/settings/migration/migrate_config_file.py +64 -0
- remotivelabs/cli/settings/migration/migrate_legacy_dirs.py +50 -0
- remotivelabs/cli/settings/migration/migrate_token_file.py +52 -0
- remotivelabs/cli/settings/migration/migration_tools.py +38 -0
- remotivelabs/cli/settings/state_file.py +67 -0
- remotivelabs/cli/settings/token_file.py +128 -0
- remotivelabs/cli/tools/__init__.py +0 -0
- remotivelabs/cli/tools/can/__init__.py +0 -0
- remotivelabs/cli/tools/can/can.py +78 -0
- remotivelabs/cli/tools/tools.py +9 -0
- remotivelabs/cli/topology/__init__.py +28 -0
- remotivelabs/cli/topology/all.py +322 -0
- remotivelabs/cli/topology/cli/__init__.py +3 -0
- remotivelabs/cli/topology/cli/run_in_docker.py +58 -0
- remotivelabs/cli/topology/cli/topology_cli.py +16 -0
- remotivelabs/cli/topology/cmd.py +130 -0
- remotivelabs/cli/topology/start_trial.py +134 -0
- remotivelabs/cli/typer/__init__.py +0 -0
- remotivelabs/cli/typer/typer_utils.py +27 -0
- remotivelabs/cli/utils/__init__.py +0 -0
- remotivelabs/cli/utils/console.py +99 -0
- remotivelabs/cli/utils/rest_helper.py +369 -0
- remotivelabs/cli/utils/time.py +11 -0
- remotivelabs/cli/utils/versions.py +120 -0
- remotivelabs_cli-0.5.0a1.dist-info/METADATA +51 -0
- remotivelabs_cli-0.5.0a1.dist-info/RECORD +84 -0
- remotivelabs_cli-0.5.0a1.dist-info/WHEEL +4 -0
- remotivelabs_cli-0.5.0a1.dist-info/entry_points.txt +3 -0
- remotivelabs_cli-0.5.0a1.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# ruff: noqa
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from remotivelabs.cli.topology.cmd import app as topology
|
|
7
|
+
from remotivelabs.cli.topology.cli import run_topology_cli
|
|
8
|
+
from remotivelabs.cli.typer import typer_utils
|
|
9
|
+
|
|
10
|
+
app = topology
|
|
11
|
+
|
|
12
|
+
topology_show = typer_utils.create_typer_sorted(
|
|
13
|
+
rich_markup_mode="rich",
|
|
14
|
+
help="""
|
|
15
|
+
Show information from database file
|
|
16
|
+
""",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
topology.add_typer(topology_show, name="show")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@topology_show.command("signals")
|
|
23
|
+
def topology_show_signals(
|
|
24
|
+
ctx: typer.Context,
|
|
25
|
+
path: str = typer.Argument(
|
|
26
|
+
..., help="Path to the Database file, supported extensions: extensions: '.arxml', '.dbc', '.xml', '.ldf', '.signaldb.yaml'"
|
|
27
|
+
),
|
|
28
|
+
out_path: str | None = typer.Argument(None, help="Write content to provided file path"),
|
|
29
|
+
json: bool = typer.Option(False, "--json", help="Output results in JSON format"),
|
|
30
|
+
yaml: bool = typer.Option(False, "--yaml", help="Output results in YAML format (default)"),
|
|
31
|
+
jsonl: bool = typer.Option(False, "--jsonl", help="Output results in JSONL format"),
|
|
32
|
+
broker_format: bool = typer.Option(False, "--bf", help="Output results in broker format"),
|
|
33
|
+
) -> None:
|
|
34
|
+
"""
|
|
35
|
+
[EXPERIMENTAL] Show signals in the provided database file
|
|
36
|
+
"""
|
|
37
|
+
cmd = ["show", "signals"]
|
|
38
|
+
cmd.append(path)
|
|
39
|
+
if out_path is not None:
|
|
40
|
+
cmd.append(out_path)
|
|
41
|
+
if json:
|
|
42
|
+
cmd.append("--json")
|
|
43
|
+
if yaml:
|
|
44
|
+
cmd.append("--yaml")
|
|
45
|
+
if jsonl:
|
|
46
|
+
cmd.append("--jsonl")
|
|
47
|
+
if broker_format:
|
|
48
|
+
cmd.append("--bf")
|
|
49
|
+
run_topology_cli(ctx, cmd)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@topology_show.command("constants")
|
|
53
|
+
def topology_show_constants(
|
|
54
|
+
ctx: typer.Context,
|
|
55
|
+
path: str = typer.Argument(..., help="Path to the Database file, supported extensions: extensions: '.arxml'"),
|
|
56
|
+
json: bool = typer.Option(False, "--json", help="Output results in JSON format"),
|
|
57
|
+
yaml: bool = typer.Option(False, "--yaml", help="Output results in YAML format (default)"),
|
|
58
|
+
) -> None:
|
|
59
|
+
"""
|
|
60
|
+
[EXPERIMENTAL] Extract constants from database file
|
|
61
|
+
"""
|
|
62
|
+
cmd = ["show", "constants"]
|
|
63
|
+
cmd.append(path)
|
|
64
|
+
if json:
|
|
65
|
+
cmd.append("--json")
|
|
66
|
+
if yaml:
|
|
67
|
+
cmd.append("--yaml")
|
|
68
|
+
run_topology_cli(ctx, cmd)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@topology_show.command("topology")
|
|
72
|
+
def topology_show_topology(
|
|
73
|
+
ctx: typer.Context,
|
|
74
|
+
path: str = typer.Argument(
|
|
75
|
+
...,
|
|
76
|
+
help="Path to the Database file, supported extensions: extensions: '.arxml', '.xml', '.instance.yaml', '.platform.yaml', '.dbc', '.ldf', '.signaldb.yaml'",
|
|
77
|
+
),
|
|
78
|
+
json: bool = typer.Option(False, "--json", help="Output results in JSON format"),
|
|
79
|
+
yaml: bool = typer.Option(False, "--yaml", help="Output results in YAML format (default)"),
|
|
80
|
+
resolve: bool = typer.Option(False, "--resolve", help="Show the resolved topology"),
|
|
81
|
+
check: bool = typer.Option(False, "--check", help="Check topology, requires --resolve"),
|
|
82
|
+
verbose: bool = typer.Option(False, "--verbose", help="Show verbose output"),
|
|
83
|
+
out_path: str | None = typer.Option(None, "-o", "--out-path", help="Write content to provided file path"),
|
|
84
|
+
) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Shows the topology from database or instance file
|
|
87
|
+
"""
|
|
88
|
+
cmd = ["show", "topology"]
|
|
89
|
+
cmd.append(path)
|
|
90
|
+
if json:
|
|
91
|
+
cmd.append("--json")
|
|
92
|
+
if yaml:
|
|
93
|
+
cmd.append("--yaml")
|
|
94
|
+
if resolve:
|
|
95
|
+
cmd.append("--resolve")
|
|
96
|
+
if check:
|
|
97
|
+
cmd.append("--check")
|
|
98
|
+
if verbose:
|
|
99
|
+
cmd.append("--verbose")
|
|
100
|
+
if out_path is not None:
|
|
101
|
+
cmd.extend(["--out-path", out_path])
|
|
102
|
+
run_topology_cli(ctx, cmd)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@topology_show.command("ethernet")
|
|
106
|
+
def topology_show_ethernet(
|
|
107
|
+
ctx: typer.Context,
|
|
108
|
+
path: str = typer.Argument(..., help="Path to the Database file, supported extensions: extensions: '.arxml'"),
|
|
109
|
+
json: bool = typer.Option(False, "--json", help="Output results in JSON format"),
|
|
110
|
+
yaml: bool = typer.Option(False, "--yaml", help="Output results in YAML format (default)"),
|
|
111
|
+
) -> None:
|
|
112
|
+
"""
|
|
113
|
+
[EXPERIMENTAL] Shows Ethernet configuration from database file
|
|
114
|
+
"""
|
|
115
|
+
cmd = ["show", "ethernet"]
|
|
116
|
+
cmd.append(path)
|
|
117
|
+
if json:
|
|
118
|
+
cmd.append("--json")
|
|
119
|
+
if yaml:
|
|
120
|
+
cmd.append("--yaml")
|
|
121
|
+
run_topology_cli(ctx, cmd)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@topology_show.command("services")
|
|
125
|
+
def topology_show_services(
|
|
126
|
+
ctx: typer.Context,
|
|
127
|
+
path: str = typer.Argument(..., help="Path to the Database file, supported extensions: extensions: '.arxml'"),
|
|
128
|
+
json: bool = typer.Option(False, "--json", help="Output results in JSON format"),
|
|
129
|
+
yaml: bool = typer.Option(False, "--yaml", help="Output results in YAML format (default)"),
|
|
130
|
+
services: list[str] = typer.Option([], "-s", "--services", help="List of SOME/IP services to process"),
|
|
131
|
+
) -> None:
|
|
132
|
+
"""
|
|
133
|
+
[EXPERIMENTAL] Show SOME/IP services from database file
|
|
134
|
+
"""
|
|
135
|
+
cmd = ["show", "services"]
|
|
136
|
+
cmd.append(path)
|
|
137
|
+
if json:
|
|
138
|
+
cmd.append("--json")
|
|
139
|
+
if yaml:
|
|
140
|
+
cmd.append("--yaml")
|
|
141
|
+
for item in services:
|
|
142
|
+
cmd.extend(["--services", item])
|
|
143
|
+
run_topology_cli(ctx, cmd)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@topology.command("generate")
|
|
147
|
+
def topology_generate(
|
|
148
|
+
ctx: typer.Context,
|
|
149
|
+
topology_output_path: str = typer.Argument(..., help="Path to where generated files should be written"),
|
|
150
|
+
name: str | None = typer.Option(None, "-n", "--name", help="Name of the generated topology"),
|
|
151
|
+
topology_files: list[str] = typer.Option(
|
|
152
|
+
[], "-f", "--file", help="RemotiveTopology .instance.yaml file(s), applied in the order they are provided"
|
|
153
|
+
),
|
|
154
|
+
) -> None:
|
|
155
|
+
"""
|
|
156
|
+
Generate topology environment based on instance descriptions
|
|
157
|
+
"""
|
|
158
|
+
cmd = ["generate"]
|
|
159
|
+
cmd.append(topology_output_path)
|
|
160
|
+
if name is not None:
|
|
161
|
+
cmd.extend(["--name", name])
|
|
162
|
+
for item in topology_files:
|
|
163
|
+
cmd.extend(["--file", item])
|
|
164
|
+
run_topology_cli(ctx, cmd)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
topology_recording_session = typer_utils.create_typer_sorted(
|
|
168
|
+
rich_markup_mode="rich",
|
|
169
|
+
help="""
|
|
170
|
+
Work with recording sessions
|
|
171
|
+
""",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
topology.add_typer(topology_recording_session, name="recording-session")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@topology_recording_session.command("create")
|
|
178
|
+
def topology_recording_session_create(
|
|
179
|
+
ctx: typer.Context,
|
|
180
|
+
out_path: str | None = typer.Argument(None, help="Write content to provided file path"),
|
|
181
|
+
json: bool = typer.Option(False, "--json", help="Output results in JSON format"),
|
|
182
|
+
yaml: bool = typer.Option(False, "--yaml", help="Output results in YAML format (default)"),
|
|
183
|
+
recording_files: list[str] = typer.Option(
|
|
184
|
+
[], "-f", "--file", help="Recording file(s), applied in the order they are provided, e.g. candump.log, .csv"
|
|
185
|
+
),
|
|
186
|
+
) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Create a new recording session from recording files
|
|
189
|
+
"""
|
|
190
|
+
cmd = ["recording_session", "create"]
|
|
191
|
+
if out_path is not None:
|
|
192
|
+
cmd.append(out_path)
|
|
193
|
+
if json:
|
|
194
|
+
cmd.append("--json")
|
|
195
|
+
if yaml:
|
|
196
|
+
cmd.append("--yaml")
|
|
197
|
+
for item in recording_files:
|
|
198
|
+
cmd.extend(["--file", item])
|
|
199
|
+
run_topology_cli(ctx, cmd)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@topology_recording_session.command("instantiate")
|
|
203
|
+
def topology_recording_session_instantiate(
|
|
204
|
+
ctx: typer.Context,
|
|
205
|
+
recording_session: str = typer.Argument(..., help="Path to the recording session file"),
|
|
206
|
+
out_path: str | None = typer.Argument(None, help="Write content to provided file path"),
|
|
207
|
+
json: bool = typer.Option(False, "--json", help="Output results in JSON format"),
|
|
208
|
+
yaml: bool = typer.Option(False, "--yaml", help="Output results in YAML format (default)"),
|
|
209
|
+
) -> None:
|
|
210
|
+
"""
|
|
211
|
+
Create a topology instance ready to play a recording session
|
|
212
|
+
"""
|
|
213
|
+
cmd = ["recording_session", "instantiate"]
|
|
214
|
+
cmd.append(recording_session)
|
|
215
|
+
if out_path is not None:
|
|
216
|
+
cmd.append(out_path)
|
|
217
|
+
if json:
|
|
218
|
+
cmd.append("--json")
|
|
219
|
+
if yaml:
|
|
220
|
+
cmd.append("--yaml")
|
|
221
|
+
run_topology_cli(ctx, cmd)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@topology_recording_session.command("recalculate")
|
|
225
|
+
def topology_recording_session_recalculate(
|
|
226
|
+
ctx: typer.Context,
|
|
227
|
+
recording_session: str = typer.Argument(..., help="Path to the recording session file"),
|
|
228
|
+
out_path: str | None = typer.Argument(None, help="Write content to provided file path"),
|
|
229
|
+
json: bool = typer.Option(False, "--json", help="Output results in JSON format"),
|
|
230
|
+
yaml: bool = typer.Option(False, "--yaml", help="Output results in YAML format (default)"),
|
|
231
|
+
) -> None:
|
|
232
|
+
"""
|
|
233
|
+
Recalculate the recording session start and end time based on the files and their offsets
|
|
234
|
+
"""
|
|
235
|
+
cmd = ["recording_session", "recalculate"]
|
|
236
|
+
cmd.append(recording_session)
|
|
237
|
+
if out_path is not None:
|
|
238
|
+
cmd.append(out_path)
|
|
239
|
+
if json:
|
|
240
|
+
cmd.append("--json")
|
|
241
|
+
if yaml:
|
|
242
|
+
cmd.append("--yaml")
|
|
243
|
+
run_topology_cli(ctx, cmd)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@topology.command("split")
|
|
247
|
+
def topology_split(
|
|
248
|
+
ctx: typer.Context,
|
|
249
|
+
topology_output_path: str = typer.Argument(..., help="Path to a folder where generated signal databases should be written"),
|
|
250
|
+
topology_description_files: list[str] = typer.Option(
|
|
251
|
+
[], "-f", "--file", help="RemotiveTopology platform file(s), applied in the order they are provided"
|
|
252
|
+
),
|
|
253
|
+
) -> None:
|
|
254
|
+
"""
|
|
255
|
+
[DEPRECATED] Extract individual signal databases for each channel in a RemotiveTopology platform.
|
|
256
|
+
"""
|
|
257
|
+
cmd = ["split"]
|
|
258
|
+
cmd.append(topology_output_path)
|
|
259
|
+
for item in topology_description_files:
|
|
260
|
+
cmd.extend(["--file", item])
|
|
261
|
+
run_topology_cli(ctx, cmd)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@topology.command("convert")
|
|
265
|
+
def topology_convert(
|
|
266
|
+
ctx: typer.Context,
|
|
267
|
+
topology_description_files: str = typer.Option(..., "-f", "--file", help="RemotiveTopology platform file"),
|
|
268
|
+
output_file: str | None = typer.Option(
|
|
269
|
+
None,
|
|
270
|
+
"-o",
|
|
271
|
+
"--output",
|
|
272
|
+
help="Optional path to a file where generated signal database should be written. If not provided, the result will be printed to stdout.",
|
|
273
|
+
),
|
|
274
|
+
format: str | None = typer.Option(None, "--format", help="Format to convert to. Currently only 'remotive' is supported."),
|
|
275
|
+
channel_names: list[str] = typer.Option(
|
|
276
|
+
[], "-c", "--channel", help="Optional channel(s) to convert. If not provided, all CAN channels will be converted."
|
|
277
|
+
),
|
|
278
|
+
) -> None:
|
|
279
|
+
"""
|
|
280
|
+
[EXPERIMENTAL] Convert signal database to different format. Currently only CAN frames is supported.
|
|
281
|
+
"""
|
|
282
|
+
cmd = ["convert"]
|
|
283
|
+
cmd.extend(["--file", topology_description_files])
|
|
284
|
+
if output_file is not None:
|
|
285
|
+
cmd.extend(["--output", output_file])
|
|
286
|
+
if format is not None:
|
|
287
|
+
cmd.extend(["--format", format])
|
|
288
|
+
for item in channel_names:
|
|
289
|
+
cmd.extend(["--channel", item])
|
|
290
|
+
run_topology_cli(ctx, cmd)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@topology.command("validate")
|
|
294
|
+
def topology_validate(
|
|
295
|
+
ctx: typer.Context, path: str = typer.Argument(..., help="Path to the Database file, supported extensions: extensions: '.arxml'")
|
|
296
|
+
) -> None:
|
|
297
|
+
"""
|
|
298
|
+
[EXPERIMENTAL] Validate file
|
|
299
|
+
"""
|
|
300
|
+
cmd = ["validate"]
|
|
301
|
+
cmd.append(path)
|
|
302
|
+
run_topology_cli(ctx, cmd)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@topology.command("gateway-mapping")
|
|
306
|
+
def topology_gateway_mapping(
|
|
307
|
+
ctx: typer.Context,
|
|
308
|
+
out_path: str = typer.Argument(..., help="Path to file where extracted gateway mapping should be written"),
|
|
309
|
+
only_fully_resolved: bool = typer.Option(False, "-o", "--only-fully-resolved", help="Only show fully resolved mapping"),
|
|
310
|
+
gateway_ecu: str = typer.Option(..., "-g", "--gateway-ecu", help="Gateway ECU name"),
|
|
311
|
+
platform: str = typer.Option(..., "-p", "--platform", help="Topology platform"),
|
|
312
|
+
) -> None:
|
|
313
|
+
"""
|
|
314
|
+
[EXPERIMENTAL] List gateway mapping for ECU
|
|
315
|
+
"""
|
|
316
|
+
cmd = ["gateway_mapping"]
|
|
317
|
+
cmd.append(out_path)
|
|
318
|
+
if only_fully_resolved:
|
|
319
|
+
cmd.append("--only-fully-resolved")
|
|
320
|
+
cmd.extend(["--gateway-ecu", gateway_ecu])
|
|
321
|
+
cmd.extend(["--platform", platform])
|
|
322
|
+
run_topology_cli(ctx, cmd)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Delegate all arguments to the remotive-topology CLI inside Docker.
|
|
3
|
+
Example:
|
|
4
|
+
remotive topology status --help
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def check_docker_installed() -> None:
|
|
15
|
+
"""Check if Docker is installed and accessible."""
|
|
16
|
+
if shutil.which("docker") is None:
|
|
17
|
+
typer.echo("❌ Docker is not installed or not in PATH.", err=True)
|
|
18
|
+
typer.echo("👉 Please install Docker: https://docs.docker.com/get-docker/", err=True)
|
|
19
|
+
raise typer.Exit(code=1)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_topology_cli_in_docker(ctx: typer.Context, args: list[str]) -> None:
|
|
23
|
+
check_docker_installed()
|
|
24
|
+
|
|
25
|
+
topology_image = ctx.obj.get("topology_image") or "remotivelabs/remotive-topology:0.14.1"
|
|
26
|
+
container_engine = ctx.obj.get("container_engine")
|
|
27
|
+
# Build base docker command equivalent to your alias
|
|
28
|
+
docker_cmd = [
|
|
29
|
+
container_engine,
|
|
30
|
+
"run",
|
|
31
|
+
]
|
|
32
|
+
# -u flag only works on Unix (os.getuid/getgid not available on Windows)
|
|
33
|
+
if hasattr(os, "getuid") and hasattr(os, "getgid"):
|
|
34
|
+
docker_cmd += ["-u", f"{os.getuid()}:{os.getgid()}"] # type: ignore[unused-ignore, attr-defined] # needed on Windows
|
|
35
|
+
docker_cmd += [
|
|
36
|
+
"--rm",
|
|
37
|
+
"-v",
|
|
38
|
+
f"{os.path.expanduser('~')}/.config/remotive/:/.config/remotive",
|
|
39
|
+
"-v",
|
|
40
|
+
f"{os.getcwd()}:{os.getcwd()}",
|
|
41
|
+
"-w",
|
|
42
|
+
os.getcwd(),
|
|
43
|
+
"-e",
|
|
44
|
+
f"REMOTIVE_CLOUD_ORGANIZATION={os.environ.get('REMOTIVE_CLOUD_ORGANIZATION', '')}",
|
|
45
|
+
"-e",
|
|
46
|
+
f"REMOTIVE_CLOUD_AUTH_TOKEN={os.environ.get('REMOTIVE_CLOUD_AUTH_TOKEN', '')}",
|
|
47
|
+
"-e",
|
|
48
|
+
"REMOTIVE_CONFIG_DIR=/.config/remotive",
|
|
49
|
+
]
|
|
50
|
+
docker_cmd += [topology_image] + args
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
subprocess.run(docker_cmd, check=True)
|
|
54
|
+
except subprocess.CalledProcessError as e:
|
|
55
|
+
raise typer.Exit(code=e.returncode)
|
|
56
|
+
except FileNotFoundError:
|
|
57
|
+
typer.echo(f"❌ {container_engine} not found. Make sure {container_engine} is installed and in PATH.", err=True)
|
|
58
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from remotivelabs.cli.topology.cli.run_in_docker import run_topology_cli_in_docker
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_topology_cli(ctx: typer.Context, args: list[str]) -> None:
|
|
9
|
+
if ctx.obj["topology_cmd"] is None:
|
|
10
|
+
run_topology_cli_in_docker(ctx, args)
|
|
11
|
+
else:
|
|
12
|
+
cmd = [ctx.obj["topology_cmd"]] + args
|
|
13
|
+
try:
|
|
14
|
+
subprocess.run(cmd, check=True)
|
|
15
|
+
except subprocess.CalledProcessError as e:
|
|
16
|
+
raise typer.Exit(code=e.returncode)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from remotivelabs.cli.topology.cli.topology_cli import run_topology_cli
|
|
6
|
+
from remotivelabs.cli.topology.start_trial import (
|
|
7
|
+
MissingOrganizationError,
|
|
8
|
+
NoActiveAccountError,
|
|
9
|
+
NoOrganizationOrPermissionError,
|
|
10
|
+
NotAuthorizedError,
|
|
11
|
+
NotAuthorizedToStartTrialError,
|
|
12
|
+
NotSignedInError,
|
|
13
|
+
SubscriptionExpiredError,
|
|
14
|
+
get_organization_and_account,
|
|
15
|
+
start_trial,
|
|
16
|
+
)
|
|
17
|
+
from remotivelabs.cli.typer import typer_utils
|
|
18
|
+
from remotivelabs.cli.utils.console import print_generic_error, print_generic_message, print_hint
|
|
19
|
+
|
|
20
|
+
HELP = """
|
|
21
|
+
Manage RemotiveTopology resources
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
app = typer_utils.create_typer_sorted(rich_markup_mode="rich", help=HELP)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command("start-trial")
|
|
28
|
+
def start_trial_cmd( # noqa: C901
|
|
29
|
+
organization: str = typer.Option(None, help="Organization to start trial for", envvar="REMOTIVE_CLOUD_ORGANIZATION"),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Allows you ta start a 30 day trial subscription for running RemotiveTopology.
|
|
33
|
+
|
|
34
|
+
You can read more at https://docs.remotivelabs.com/docs/remotive-topology.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
(valid_organization, _) = get_organization_and_account(organization_uid=organization)
|
|
39
|
+
ok_to_start_trial = typer.confirm(
|
|
40
|
+
f"You are about to start trial of RemotiveTopology for {valid_organization.display_name}"
|
|
41
|
+
f" (uid={valid_organization.uid}), continue?",
|
|
42
|
+
default=True,
|
|
43
|
+
)
|
|
44
|
+
if not ok_to_start_trial:
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
(subscription, created) = start_trial(organization)
|
|
48
|
+
|
|
49
|
+
def print_start_trial_result() -> None:
|
|
50
|
+
assert subscription.type in ("trial", "paid"), f"Unexpected subscription type: {subscription.type}"
|
|
51
|
+
kind = None
|
|
52
|
+
status = None
|
|
53
|
+
if subscription.type == "trial":
|
|
54
|
+
status = "started now" if created else "is already active"
|
|
55
|
+
kind = "trial subscription"
|
|
56
|
+
elif subscription.type == "paid":
|
|
57
|
+
status = "started now" if created else "is already started"
|
|
58
|
+
kind = "subscription"
|
|
59
|
+
|
|
60
|
+
end_text = subscription.end_date or "Never"
|
|
61
|
+
print_generic_message(f"RemotiveTopology {kind} for {valid_organization.display_name} {status}, expires {end_text}")
|
|
62
|
+
|
|
63
|
+
print_start_trial_result()
|
|
64
|
+
|
|
65
|
+
except NotSignedInError:
|
|
66
|
+
print_generic_error(
|
|
67
|
+
"You must first sign in to RemotiveCloud, please use [bold]remotive cloud auth login[/bold] to sign-in"
|
|
68
|
+
"This requires a RemotiveCloud account, if you do not have an account you can sign-up at https://cloud.remotivelabs.com"
|
|
69
|
+
)
|
|
70
|
+
raise typer.Exit(2)
|
|
71
|
+
|
|
72
|
+
except NoActiveAccountError:
|
|
73
|
+
print_hint(
|
|
74
|
+
"You have not activated your account, please run [bold]remotive cloud auth activate[/bold] to choose an account"
|
|
75
|
+
"or [bold]remotive cloud auth login[/bold] to sign-in"
|
|
76
|
+
)
|
|
77
|
+
raise typer.Exit(3)
|
|
78
|
+
|
|
79
|
+
except NotAuthorizedError:
|
|
80
|
+
print_hint(
|
|
81
|
+
"Your current active credentials are not valid, please run [bold]remotive cloud auth login[/bold] to sign-in again."
|
|
82
|
+
"This requires a RemotiveCloud account, if you do not have an account you can sign-up at https://cloud.remotivelabs.com"
|
|
83
|
+
)
|
|
84
|
+
raise typer.Exit(4)
|
|
85
|
+
|
|
86
|
+
except MissingOrganizationError:
|
|
87
|
+
print_hint("You have not specified any organization and no default organization is set")
|
|
88
|
+
raise typer.Exit(5)
|
|
89
|
+
|
|
90
|
+
except NotAuthorizedToStartTrialError as e:
|
|
91
|
+
print_generic_error(f"You are not allowed to start-trial topology in organization {e.organization.display_name}.")
|
|
92
|
+
raise typer.Exit(6)
|
|
93
|
+
|
|
94
|
+
except SubscriptionExpiredError as e:
|
|
95
|
+
if e.subscription.type == "trial":
|
|
96
|
+
print_generic_error(
|
|
97
|
+
f"RemotiveTopology trial in {e.organization.display_name} expired"
|
|
98
|
+
f" {e.subscription.end_date}, please contact support@remotivelabs.com"
|
|
99
|
+
)
|
|
100
|
+
raise typer.Exit(7)
|
|
101
|
+
|
|
102
|
+
print_generic_error(
|
|
103
|
+
f"RemotiveTopology subscription in {e.organization.display_name} has expired"
|
|
104
|
+
f" {e.subscription.end_date}, please contact support@remotivelabs.com"
|
|
105
|
+
)
|
|
106
|
+
raise typer.Exit(7)
|
|
107
|
+
|
|
108
|
+
except NoOrganizationOrPermissionError as e:
|
|
109
|
+
print_generic_error(f"Organization id {e.organization} does not exist or you do not have permission to access it.")
|
|
110
|
+
raise typer.Exit(7)
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
print_generic_error(f"Unexpected error: {e}")
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
|
|
118
|
+
def cli(args: list[str] = typer.Argument(None), ctx: typer.Context = typer.Option(None)) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Run any RemotiveTopology CLI command.
|
|
121
|
+
"""
|
|
122
|
+
run_topology_cli(ctx, args or ["--help"])
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.command("version")
|
|
126
|
+
def version(ctx: typer.Context = typer.Option(None)) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Show RemotiveTopology CLI version.
|
|
129
|
+
"""
|
|
130
|
+
run_topology_cli(ctx, ["--version"])
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import date
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from remotivelabs.cli.settings import settings
|
|
9
|
+
from remotivelabs.cli.settings.config_file import Account
|
|
10
|
+
from remotivelabs.cli.utils.rest_helper import RestHelper
|
|
11
|
+
from remotivelabs.cli.utils.time import parse_date
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NoActiveAccountError(Exception):
|
|
15
|
+
"""Raised when the user has no active account, but there are available accounts to activate"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NotSignedInError(Exception):
|
|
19
|
+
"""Raised when the user has no active account, and no available accounts to activate"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NotAuthorizedError(Exception):
|
|
23
|
+
"""Raised when the user is not authorized"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, account: Account):
|
|
26
|
+
self.account = account
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NoOrganizationOrPermissionError(Exception):
|
|
30
|
+
"""Raised when the user is trying to access an organization that it does not have access to or it does not exist"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, account: Account, organization_uid: str):
|
|
33
|
+
self.account = account
|
|
34
|
+
self.organization = organization_uid
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MissingOrganizationError(Exception):
|
|
38
|
+
"""Raised when the user has not specified an organization and no default organization is set"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NotAuthorizedToStartTrialError(Exception):
|
|
42
|
+
"""Raised when the user is not authorized to start a topology trial"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, account: Account, organization: Organization):
|
|
45
|
+
self.account = account
|
|
46
|
+
self.organization = organization
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SubscriptionExpiredError(Exception):
|
|
50
|
+
"""Raised when the subscription has expired"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, subscription: Subscription, organization: Organization):
|
|
53
|
+
self.subscription = subscription
|
|
54
|
+
self.organization = organization
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Organization:
|
|
59
|
+
uid: str
|
|
60
|
+
display_name: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Subscription:
|
|
65
|
+
type: str
|
|
66
|
+
display_name: str
|
|
67
|
+
feature: str
|
|
68
|
+
start_date: date
|
|
69
|
+
end_date: date
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def from_dict(data: Any) -> Subscription:
|
|
73
|
+
if not isinstance(data, dict):
|
|
74
|
+
raise ValueError(f"Invalid subscription data {data}")
|
|
75
|
+
|
|
76
|
+
return Subscription(
|
|
77
|
+
type=data["subscriptionType"],
|
|
78
|
+
display_name=data["displayName"],
|
|
79
|
+
feature=data["feature"],
|
|
80
|
+
start_date=parse_date(data["startDate"]),
|
|
81
|
+
end_date=parse_date(data["endDate"]),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_organization_and_account(organization_uid: str | None = None) -> tuple[Organization, Account]:
|
|
86
|
+
active_account = settings.get_active_account()
|
|
87
|
+
active_token_file = settings.get_active_token_file()
|
|
88
|
+
|
|
89
|
+
if not active_account or not active_token_file:
|
|
90
|
+
if len(settings.list_accounts()) == 0:
|
|
91
|
+
raise NotSignedInError()
|
|
92
|
+
raise NoActiveAccountError()
|
|
93
|
+
|
|
94
|
+
if not RestHelper.has_access("/api/whoami"):
|
|
95
|
+
raise NotAuthorizedError(account=active_account)
|
|
96
|
+
|
|
97
|
+
org_uid = organization_uid or active_account.default_organization
|
|
98
|
+
if not org_uid:
|
|
99
|
+
raise MissingOrganizationError()
|
|
100
|
+
|
|
101
|
+
response = RestHelper.handle_get(f"/api/bu/{organization_uid}", return_response=True, allow_status_codes=[403, 404])
|
|
102
|
+
if response.status_code in (403, 404):
|
|
103
|
+
raise NoOrganizationOrPermissionError(account=active_account, organization_uid=org_uid)
|
|
104
|
+
|
|
105
|
+
display_name = response.json()["displayName"]
|
|
106
|
+
|
|
107
|
+
return Organization(uid=org_uid, display_name=display_name), active_account
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def start_trial(organization_uid: str | None = None) -> tuple[Subscription, bool]:
|
|
111
|
+
"""
|
|
112
|
+
Start a 30 day trial subscription for running RemotiveTopology,
|
|
113
|
+
If a trial is already started in this organization, returns the existing trial instead.
|
|
114
|
+
Returns (subscription, created_now).
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
(organization, active_account) = get_organization_and_account(organization_uid)
|
|
118
|
+
|
|
119
|
+
res = RestHelper.handle_get(f"/api/bu/{organization.uid}/features/topology", return_response=True, allow_status_codes=[403, 404])
|
|
120
|
+
if res.status_code == 403:
|
|
121
|
+
raise NotAuthorizedToStartTrialError(account=active_account, organization=organization)
|
|
122
|
+
|
|
123
|
+
created_now = False
|
|
124
|
+
if res.status_code == 404:
|
|
125
|
+
created = RestHelper.handle_post(f"/api/bu/{organization.uid}/features/topology", return_response=True)
|
|
126
|
+
subscription = Subscription.from_dict(created.json())
|
|
127
|
+
created_now = True
|
|
128
|
+
else:
|
|
129
|
+
subscription = Subscription.from_dict(res.json())
|
|
130
|
+
|
|
131
|
+
if subscription.end_date < datetime.datetime.now().date():
|
|
132
|
+
raise SubscriptionExpiredError(subscription=subscription, organization=organization)
|
|
133
|
+
|
|
134
|
+
return subscription, created_now
|
|
File without changes
|