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.
Files changed (84) hide show
  1. remotivelabs/cli/__init__.py +0 -0
  2. remotivelabs/cli/api/cloud/tokens.py +62 -0
  3. remotivelabs/cli/broker/__init__.py +33 -0
  4. remotivelabs/cli/broker/defaults.py +1 -0
  5. remotivelabs/cli/broker/discovery.py +43 -0
  6. remotivelabs/cli/broker/export.py +92 -0
  7. remotivelabs/cli/broker/files.py +119 -0
  8. remotivelabs/cli/broker/lib/__about__.py +4 -0
  9. remotivelabs/cli/broker/lib/broker.py +625 -0
  10. remotivelabs/cli/broker/lib/client.py +224 -0
  11. remotivelabs/cli/broker/lib/helper.py +277 -0
  12. remotivelabs/cli/broker/lib/signalcreator.py +196 -0
  13. remotivelabs/cli/broker/license_flows.py +167 -0
  14. remotivelabs/cli/broker/licenses.py +98 -0
  15. remotivelabs/cli/broker/playback.py +117 -0
  16. remotivelabs/cli/broker/record.py +41 -0
  17. remotivelabs/cli/broker/recording_session/__init__.py +3 -0
  18. remotivelabs/cli/broker/recording_session/client.py +67 -0
  19. remotivelabs/cli/broker/recording_session/cmd.py +254 -0
  20. remotivelabs/cli/broker/recording_session/time.py +49 -0
  21. remotivelabs/cli/broker/scripting.py +129 -0
  22. remotivelabs/cli/broker/signals.py +220 -0
  23. remotivelabs/cli/broker/version.py +31 -0
  24. remotivelabs/cli/cloud/__init__.py +17 -0
  25. remotivelabs/cli/cloud/auth/__init__.py +3 -0
  26. remotivelabs/cli/cloud/auth/cmd.py +128 -0
  27. remotivelabs/cli/cloud/auth/login.py +283 -0
  28. remotivelabs/cli/cloud/auth_tokens.py +149 -0
  29. remotivelabs/cli/cloud/brokers.py +109 -0
  30. remotivelabs/cli/cloud/configs.py +109 -0
  31. remotivelabs/cli/cloud/licenses/__init__.py +0 -0
  32. remotivelabs/cli/cloud/licenses/cmd.py +14 -0
  33. remotivelabs/cli/cloud/organisations.py +112 -0
  34. remotivelabs/cli/cloud/projects.py +44 -0
  35. remotivelabs/cli/cloud/recordings.py +580 -0
  36. remotivelabs/cli/cloud/recordings_playback.py +274 -0
  37. remotivelabs/cli/cloud/resumable_upload.py +87 -0
  38. remotivelabs/cli/cloud/sample_recordings.py +25 -0
  39. remotivelabs/cli/cloud/service_account_tokens.py +62 -0
  40. remotivelabs/cli/cloud/service_accounts.py +72 -0
  41. remotivelabs/cli/cloud/storage/__init__.py +5 -0
  42. remotivelabs/cli/cloud/storage/cmd.py +76 -0
  43. remotivelabs/cli/cloud/storage/copy.py +86 -0
  44. remotivelabs/cli/cloud/storage/uri_or_path.py +45 -0
  45. remotivelabs/cli/cloud/uri.py +113 -0
  46. remotivelabs/cli/connect/__init__.py +0 -0
  47. remotivelabs/cli/connect/connect.py +118 -0
  48. remotivelabs/cli/connect/protopie/protopie.py +185 -0
  49. remotivelabs/cli/py.typed +0 -0
  50. remotivelabs/cli/remotive.py +123 -0
  51. remotivelabs/cli/settings/__init__.py +20 -0
  52. remotivelabs/cli/settings/config_file.py +113 -0
  53. remotivelabs/cli/settings/core.py +333 -0
  54. remotivelabs/cli/settings/migration/__init__.py +0 -0
  55. remotivelabs/cli/settings/migration/migrate_all_token_files.py +80 -0
  56. remotivelabs/cli/settings/migration/migrate_config_file.py +64 -0
  57. remotivelabs/cli/settings/migration/migrate_legacy_dirs.py +50 -0
  58. remotivelabs/cli/settings/migration/migrate_token_file.py +52 -0
  59. remotivelabs/cli/settings/migration/migration_tools.py +38 -0
  60. remotivelabs/cli/settings/state_file.py +67 -0
  61. remotivelabs/cli/settings/token_file.py +128 -0
  62. remotivelabs/cli/tools/__init__.py +0 -0
  63. remotivelabs/cli/tools/can/__init__.py +0 -0
  64. remotivelabs/cli/tools/can/can.py +78 -0
  65. remotivelabs/cli/tools/tools.py +9 -0
  66. remotivelabs/cli/topology/__init__.py +28 -0
  67. remotivelabs/cli/topology/all.py +322 -0
  68. remotivelabs/cli/topology/cli/__init__.py +3 -0
  69. remotivelabs/cli/topology/cli/run_in_docker.py +58 -0
  70. remotivelabs/cli/topology/cli/topology_cli.py +16 -0
  71. remotivelabs/cli/topology/cmd.py +130 -0
  72. remotivelabs/cli/topology/start_trial.py +134 -0
  73. remotivelabs/cli/typer/__init__.py +0 -0
  74. remotivelabs/cli/typer/typer_utils.py +27 -0
  75. remotivelabs/cli/utils/__init__.py +0 -0
  76. remotivelabs/cli/utils/console.py +99 -0
  77. remotivelabs/cli/utils/rest_helper.py +369 -0
  78. remotivelabs/cli/utils/time.py +11 -0
  79. remotivelabs/cli/utils/versions.py +120 -0
  80. remotivelabs_cli-0.5.0a1.dist-info/METADATA +51 -0
  81. remotivelabs_cli-0.5.0a1.dist-info/RECORD +84 -0
  82. remotivelabs_cli-0.5.0a1.dist-info/WHEEL +4 -0
  83. remotivelabs_cli-0.5.0a1.dist-info/entry_points.txt +3 -0
  84. 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,3 @@
1
+ from remotivelabs.cli.topology.cli.topology_cli import run_topology_cli
2
+
3
+ __all__ = ["run_topology_cli"]
@@ -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