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,27 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from click import Context
|
|
5
|
+
from typer.core import TyperGroup
|
|
6
|
+
|
|
7
|
+
from remotivelabs.cli.utils.console import print_generic_message
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OrderCommands(TyperGroup):
|
|
11
|
+
def list_commands(self, _ctx: Context): # type: ignore
|
|
12
|
+
return list(self.commands)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_typer(**kwargs: Any) -> typer.Typer:
|
|
16
|
+
"""Create a Typer instance with default settings."""
|
|
17
|
+
return typer.Typer(cls=OrderCommands, no_args_is_help=True, invoke_without_command=True, **kwargs)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_typer_sorted(**kwargs: Any) -> typer.Typer:
|
|
21
|
+
"""Create a Typer instance with default settings."""
|
|
22
|
+
return typer.Typer(no_args_is_help=True, invoke_without_command=True, **kwargs)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def print_padded(label: str, right_text: str, length: int = 30) -> None:
|
|
26
|
+
padded_label = label.ljust(length) # pad to 30 characters
|
|
27
|
+
print_generic_message(f"{padded_label} {right_text}")
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import grpc
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
console = Console(soft_wrap=True)
|
|
12
|
+
err_console = Console(stderr=True, soft_wrap=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def print_grpc_error(error: grpc.RpcError) -> None:
|
|
16
|
+
"""TODO: remove me"""
|
|
17
|
+
if error.code() == grpc.StatusCode.UNAUTHENTICATED:
|
|
18
|
+
is_access_token = os.environ["ACCESS_TOKEN"]
|
|
19
|
+
if is_access_token is not None and is_access_token == "true":
|
|
20
|
+
err_console.print(f":boom: [bold red]Authentication failed[/bold red]: {error.details()}")
|
|
21
|
+
err_console.print("Please login again")
|
|
22
|
+
else:
|
|
23
|
+
err_console.print(":boom: [bold red]Authentication failed[/bold red]")
|
|
24
|
+
err_console.print("Failed to verify api-key")
|
|
25
|
+
else:
|
|
26
|
+
err_console.print(f":boom: [bold red]Unexpected error, status code[/bold red]: {error.code()}")
|
|
27
|
+
err_console.print(error.details())
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def print_newline() -> None:
|
|
32
|
+
"""TODO: remove me"""
|
|
33
|
+
console.print("\n")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def print_url(url: str) -> None:
|
|
37
|
+
console.print(url, style="bold")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def print_unformatted(message: Any) -> None:
|
|
41
|
+
"""TODO: remove me"""
|
|
42
|
+
console.print(message)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def print_unformatted_to_stderr(message: Any) -> None:
|
|
46
|
+
"""TODO: remove me"""
|
|
47
|
+
err_console.print(message)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def print_success(message: str | None = None) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Print a success message to stdout
|
|
53
|
+
|
|
54
|
+
TODO: use stderr instead.
|
|
55
|
+
"""
|
|
56
|
+
msg = "[bold green]Success![/bold green]"
|
|
57
|
+
if message:
|
|
58
|
+
msg += f" {message}"
|
|
59
|
+
console.print(msg)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def print_generic_error(message: str | None = None) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Print a failure message to stderr
|
|
65
|
+
|
|
66
|
+
TODO: rename to print_failure
|
|
67
|
+
"""
|
|
68
|
+
msg = ":boom: [bold red]Failed[/bold red]"
|
|
69
|
+
if message:
|
|
70
|
+
msg += f": {message}"
|
|
71
|
+
err_console.print(msg)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def print_generic_message(message: str) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Print a message to the user.
|
|
77
|
+
|
|
78
|
+
TODO: rename to print_message
|
|
79
|
+
TODO: use stderr instead.
|
|
80
|
+
"""
|
|
81
|
+
console.print(f"[bold]{message}[/bold]")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def print_hint(message: str) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Print a hint to stderr.
|
|
87
|
+
|
|
88
|
+
Useful when nudging the user to a suitable solution.
|
|
89
|
+
"""
|
|
90
|
+
err_console.print(f":point_right: [bold]{message}[/bold]")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def print_result(result: Any, default: Any = None) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Print a result to stdout
|
|
96
|
+
|
|
97
|
+
TODO: Decide on how to handle output. In broker lib (to_json)?
|
|
98
|
+
"""
|
|
99
|
+
console.print(json.dumps(result, indent=2, default=default))
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
from importlib.metadata import version
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, BinaryIO, Dict, List, Optional, Union, cast
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
from requests.exceptions import JSONDecodeError
|
|
15
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, wrap_file
|
|
16
|
+
|
|
17
|
+
from remotivelabs.cli.settings import settings
|
|
18
|
+
from remotivelabs.cli.utils import versions
|
|
19
|
+
from remotivelabs.cli.utils.console import (
|
|
20
|
+
print_generic_error,
|
|
21
|
+
print_generic_message,
|
|
22
|
+
print_hint,
|
|
23
|
+
print_unformatted,
|
|
24
|
+
print_unformatted_to_stderr,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if "REMOTIVE_CLOUD_HTTP_LOGGING" in os.environ:
|
|
28
|
+
logging.basicConfig()
|
|
29
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
30
|
+
requests_log = logging.getLogger("requests.packages.urllib3")
|
|
31
|
+
requests_log.setLevel(logging.DEBUG)
|
|
32
|
+
requests_log.propagate = True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RestHelper:
|
|
36
|
+
"""Static Class with various helper functions for the rest API"""
|
|
37
|
+
|
|
38
|
+
__base_url = "https://cloud.remotivelabs.com"
|
|
39
|
+
__frontend_url = __base_url
|
|
40
|
+
__license_server_base_url = "https://license.cloud.remotivelabs.com"
|
|
41
|
+
|
|
42
|
+
if "REMOTIVE_CLOUD_BASE_URL" in os.environ:
|
|
43
|
+
__base_url = os.environ["REMOTIVE_CLOUD_BASE_URL"]
|
|
44
|
+
__frontend_url = os.environ["REMOTIVE_CLOUD_BASE_URL"]
|
|
45
|
+
|
|
46
|
+
if "REMOTIVE_CLOUD_FRONTEND_BASE_URL" in os.environ:
|
|
47
|
+
__frontend_url = os.environ["REMOTIVE_CLOUD_FRONTEND_BASE_URL"]
|
|
48
|
+
|
|
49
|
+
if "cloud-dev" in __base_url:
|
|
50
|
+
__license_server_base_url = "https://license.cloud-dev.remotivelabs.com"
|
|
51
|
+
|
|
52
|
+
__headers: Dict[str, str] = {"User-Agent": f"remotivelabs-cli/{versions.cli_version()} ({versions.platform_info()})"}
|
|
53
|
+
__org: str = ""
|
|
54
|
+
|
|
55
|
+
__token: str = ""
|
|
56
|
+
|
|
57
|
+
def _cli_version(self) -> str:
|
|
58
|
+
return ""
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def get_cli_version() -> str:
|
|
62
|
+
return version("remotivelabs-cli")
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def get_base_url() -> str:
|
|
66
|
+
return RestHelper.__base_url
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def get_base_frontend_url() -> str:
|
|
70
|
+
return RestHelper.__frontend_url
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def get_license_server_base_url() -> str:
|
|
74
|
+
return RestHelper.__license_server_base_url
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def get_headers() -> Dict[str, str]:
|
|
78
|
+
return RestHelper.__headers
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def get_org() -> str:
|
|
82
|
+
return RestHelper.__org
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def get_token() -> str:
|
|
86
|
+
return RestHelper.__token
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def ensure_auth_token(quiet: bool = False, access_token: Optional[str] = None) -> None:
|
|
90
|
+
"""
|
|
91
|
+
TODO: remove setting org, as we already set the default organization as env in remotive.py?
|
|
92
|
+
TODO: don't sys.exit, raise error instead
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
if "REMOTIVE_CLOUD_ORGANIZATION" not in os.environ:
|
|
96
|
+
active_account = settings.get_active_account()
|
|
97
|
+
if active_account:
|
|
98
|
+
org = active_account.default_organization
|
|
99
|
+
if org:
|
|
100
|
+
os.environ["REMOTIVE_CLOUD_ORGANIZATION"] = org
|
|
101
|
+
|
|
102
|
+
token = access_token
|
|
103
|
+
if not token:
|
|
104
|
+
token = settings.get_active_token()
|
|
105
|
+
|
|
106
|
+
if not token:
|
|
107
|
+
if quiet:
|
|
108
|
+
return
|
|
109
|
+
print_hint("you are not logged in, please login using [green]remotive cloud auth login[/green]")
|
|
110
|
+
sys.exit(1)
|
|
111
|
+
|
|
112
|
+
RestHelper.__headers["Authorization"] = f"Bearer {token.strip()}"
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def handle_get( # noqa: PLR0913
|
|
116
|
+
url: str,
|
|
117
|
+
params: Any = None,
|
|
118
|
+
return_response: bool = False,
|
|
119
|
+
allow_status_codes: List[int] | None = None,
|
|
120
|
+
progress_label: str = "Fetching...",
|
|
121
|
+
use_progress_indicator: bool = True,
|
|
122
|
+
allow_redirects: bool = False,
|
|
123
|
+
timeout: int = 60,
|
|
124
|
+
access_token: Optional[str] = None,
|
|
125
|
+
skip_access_token: bool = False,
|
|
126
|
+
) -> requests.Response:
|
|
127
|
+
# Returns a Response object if succesfull otherwise None
|
|
128
|
+
if params is None:
|
|
129
|
+
params = {}
|
|
130
|
+
if not skip_access_token:
|
|
131
|
+
RestHelper.ensure_auth_token(access_token=access_token)
|
|
132
|
+
if use_progress_indicator:
|
|
133
|
+
with RestHelper.use_progress(progress_label):
|
|
134
|
+
r = requests.get(
|
|
135
|
+
f"{RestHelper.__base_url}{url}",
|
|
136
|
+
headers=RestHelper.__headers,
|
|
137
|
+
params=params,
|
|
138
|
+
timeout=timeout,
|
|
139
|
+
allow_redirects=allow_redirects,
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
r = requests.get(
|
|
143
|
+
f"{RestHelper.__base_url}{url}",
|
|
144
|
+
headers=RestHelper.__headers,
|
|
145
|
+
params=params,
|
|
146
|
+
timeout=timeout,
|
|
147
|
+
allow_redirects=allow_redirects,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if return_response:
|
|
151
|
+
RestHelper.check_api_result(r, allow_status_codes)
|
|
152
|
+
return r
|
|
153
|
+
RestHelper.print_api_result(r)
|
|
154
|
+
sys.exit(0)
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def has_access(url: str, params: Any = {}, access_token: Optional[str] = None) -> bool:
|
|
158
|
+
RestHelper.ensure_auth_token(quiet=True, access_token=access_token)
|
|
159
|
+
r = requests.get(f"{RestHelper.__base_url}{url}", headers=RestHelper.__headers, params=params, timeout=60)
|
|
160
|
+
if 200 <= r.status_code <= 299:
|
|
161
|
+
return True
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def check_api_result(response: requests.Response, allow_status_codes: List[int] | None = None) -> None:
|
|
166
|
+
"""
|
|
167
|
+
TODO: don't sys.exit, raise error instead
|
|
168
|
+
"""
|
|
169
|
+
if response.status_code == 426: # CLI upgrade
|
|
170
|
+
print_hint(response.text)
|
|
171
|
+
sys.exit(1)
|
|
172
|
+
if response.status_code > 299:
|
|
173
|
+
if allow_status_codes is not None and response.status_code in allow_status_codes:
|
|
174
|
+
return
|
|
175
|
+
print_generic_error(f"Got status code: {response.status_code}")
|
|
176
|
+
if response.status_code == 401:
|
|
177
|
+
print_generic_message("Your token is not valid or has expired, please login again or activate another account")
|
|
178
|
+
else:
|
|
179
|
+
print_unformatted_to_stderr(response.text)
|
|
180
|
+
sys.exit(1)
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def print_api_result(response: requests.Response) -> None:
|
|
184
|
+
"""
|
|
185
|
+
TODO: don't sys.exit, raise error instead
|
|
186
|
+
TODO: dont print from here, return and let caller print instead
|
|
187
|
+
"""
|
|
188
|
+
if response.status_code == 426: # CLI upgrade
|
|
189
|
+
print_hint(response.text)
|
|
190
|
+
sys.exit(1)
|
|
191
|
+
|
|
192
|
+
if response.status_code >= 200 and response.status_code < 300:
|
|
193
|
+
if len(response.content) >= 2:
|
|
194
|
+
try:
|
|
195
|
+
print_unformatted(json.dumps(response.json()))
|
|
196
|
+
except JSONDecodeError:
|
|
197
|
+
print_generic_error("Json parse error: Please try again and report if the error persists")
|
|
198
|
+
sys.exit(0)
|
|
199
|
+
else:
|
|
200
|
+
print_generic_error(f"Got status code: {response.status_code}")
|
|
201
|
+
if response.status_code == 401:
|
|
202
|
+
print_generic_message("Your token is not valid or has expired, please login again or activate another account")
|
|
203
|
+
else:
|
|
204
|
+
print_unformatted_to_stderr(response.text)
|
|
205
|
+
sys.exit(1)
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def handle_patch( # noqa: PLR0913
|
|
209
|
+
url: str,
|
|
210
|
+
params: Any = {},
|
|
211
|
+
quiet: bool = False,
|
|
212
|
+
progress_label: str = "Deleting...",
|
|
213
|
+
access_token: Optional[str] = None,
|
|
214
|
+
allow_status_codes: Optional[List[int]] = None,
|
|
215
|
+
) -> requests.Response:
|
|
216
|
+
if allow_status_codes is None:
|
|
217
|
+
allow_status_codes = []
|
|
218
|
+
RestHelper.ensure_auth_token(access_token=access_token)
|
|
219
|
+
with RestHelper.use_progress(progress_label):
|
|
220
|
+
r = requests.patch(f"{RestHelper.__base_url}{url}", headers=RestHelper.__headers, params=params, timeout=60)
|
|
221
|
+
if r.status_code in (200, 204):
|
|
222
|
+
if not quiet:
|
|
223
|
+
RestHelper.print_api_result(r)
|
|
224
|
+
elif r.status_code not in allow_status_codes:
|
|
225
|
+
RestHelper.print_api_result(r)
|
|
226
|
+
return r
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def handle_delete( # noqa: PLR0913
|
|
230
|
+
url: str,
|
|
231
|
+
params: Any = {},
|
|
232
|
+
quiet: bool = False,
|
|
233
|
+
progress_label: str = "Deleting...",
|
|
234
|
+
access_token: Optional[str] = None,
|
|
235
|
+
allow_status_codes: Optional[List[int]] = None,
|
|
236
|
+
) -> requests.Response:
|
|
237
|
+
if allow_status_codes is None:
|
|
238
|
+
allow_status_codes = []
|
|
239
|
+
RestHelper.ensure_auth_token(access_token=access_token)
|
|
240
|
+
with RestHelper.use_progress(progress_label):
|
|
241
|
+
r = requests.delete(f"{RestHelper.__base_url}{url}", headers=RestHelper.__headers, params=params, timeout=60)
|
|
242
|
+
if r.status_code in (200, 204):
|
|
243
|
+
if not quiet:
|
|
244
|
+
RestHelper.print_api_result(r)
|
|
245
|
+
elif r.status_code not in allow_status_codes:
|
|
246
|
+
RestHelper.print_api_result(r)
|
|
247
|
+
return r
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
def handle_post( # noqa: PLR0913
|
|
251
|
+
url: str,
|
|
252
|
+
body: Any = None,
|
|
253
|
+
params: Any = {},
|
|
254
|
+
progress_label: str = "Processing...",
|
|
255
|
+
return_response: bool = False,
|
|
256
|
+
access_token: Optional[str] = None,
|
|
257
|
+
) -> requests.Response:
|
|
258
|
+
# Returns a Response object if succesfull otherwise, None
|
|
259
|
+
|
|
260
|
+
RestHelper.ensure_auth_token(access_token=access_token)
|
|
261
|
+
RestHelper.__headers["content-type"] = "application/json"
|
|
262
|
+
|
|
263
|
+
with RestHelper.use_progress(progress_label):
|
|
264
|
+
r = requests.post(f"{RestHelper.__base_url}{url}", headers=RestHelper.__headers, params=params, data=body, timeout=60)
|
|
265
|
+
|
|
266
|
+
if return_response:
|
|
267
|
+
RestHelper.check_api_result(r)
|
|
268
|
+
return r
|
|
269
|
+
|
|
270
|
+
RestHelper.print_api_result(r)
|
|
271
|
+
sys.exit(0)
|
|
272
|
+
|
|
273
|
+
@staticmethod
|
|
274
|
+
def handle_put(url: str, body: Any = None, params: Any = {}, return_response: bool = False) -> requests.Response | None:
|
|
275
|
+
# Returns a Response object if succesfull otherwise, None
|
|
276
|
+
RestHelper.ensure_auth_token()
|
|
277
|
+
RestHelper.__headers["content-type"] = "application/json"
|
|
278
|
+
r = requests.put(f"{RestHelper.__base_url}{url}", headers=RestHelper.__headers, params=params, data=body, timeout=60)
|
|
279
|
+
|
|
280
|
+
if return_response:
|
|
281
|
+
RestHelper.check_api_result(r)
|
|
282
|
+
return r
|
|
283
|
+
RestHelper.print_api_result(r)
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def upload_file(
|
|
288
|
+
path: Union[str, Path],
|
|
289
|
+
url: str,
|
|
290
|
+
upload_headers: Dict[str, str] | None = None,
|
|
291
|
+
return_response: bool = False,
|
|
292
|
+
progress_label: str = "Uploading...",
|
|
293
|
+
) -> requests.Response | None:
|
|
294
|
+
# Returns a Response object if succesfull otherwise, None
|
|
295
|
+
RestHelper.ensure_auth_token()
|
|
296
|
+
if upload_headers is not None:
|
|
297
|
+
RestHelper.__headers.update(upload_headers)
|
|
298
|
+
with open(path, "rb") as file:
|
|
299
|
+
with wrap_file(file, os.stat(path).st_size, description=progress_label) as f:
|
|
300
|
+
r = requests.post(
|
|
301
|
+
f"{RestHelper.__base_url}{url}", files={os.path.basename(path): f}, headers=RestHelper.__headers, timeout=60
|
|
302
|
+
)
|
|
303
|
+
if return_response:
|
|
304
|
+
RestHelper.check_api_result(r)
|
|
305
|
+
return r
|
|
306
|
+
RestHelper.print_api_result(r)
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def upload_file_with_signed_url(
|
|
311
|
+
path: Union[str, Path],
|
|
312
|
+
url: str,
|
|
313
|
+
upload_headers: Dict[str, str],
|
|
314
|
+
return_response: bool = False,
|
|
315
|
+
progress_label: str = "Uploading...",
|
|
316
|
+
) -> requests.Response | None:
|
|
317
|
+
# Returns a Response object if succesfull otherwise, None
|
|
318
|
+
with open(path, "rb") as file:
|
|
319
|
+
with wrap_file(file, os.stat(path).st_size, description=progress_label, transient=False) as f:
|
|
320
|
+
r = requests.put(url, data=f, headers=upload_headers, timeout=60)
|
|
321
|
+
if return_response:
|
|
322
|
+
RestHelper.check_api_result(r)
|
|
323
|
+
return r
|
|
324
|
+
RestHelper.print_api_result(r)
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
@staticmethod
|
|
328
|
+
def use_progress(label: str, transient: bool = True) -> Progress:
|
|
329
|
+
p = Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=transient)
|
|
330
|
+
p.add_task(label, total=1)
|
|
331
|
+
return p
|
|
332
|
+
|
|
333
|
+
@staticmethod
|
|
334
|
+
def download_file(save_file_name: Path, url: str) -> None:
|
|
335
|
+
# Next download the actual file
|
|
336
|
+
download_resp = requests.get(url=url, stream=True, timeout=60)
|
|
337
|
+
if download_resp.status_code == 200:
|
|
338
|
+
content_length = int(download_resp.headers["Content-Length"])
|
|
339
|
+
with open(save_file_name, "wb") as out_file:
|
|
340
|
+
stream = cast(BinaryIO, download_resp.raw) # we know this is a binary stream, as stream=True is set in the request
|
|
341
|
+
with wrap_file(
|
|
342
|
+
stream,
|
|
343
|
+
content_length,
|
|
344
|
+
refresh_per_second=100,
|
|
345
|
+
description=f"Downloading to {save_file_name}",
|
|
346
|
+
) as stream_with_progress:
|
|
347
|
+
shutil.copyfileobj(stream_with_progress, out_file)
|
|
348
|
+
else:
|
|
349
|
+
RestHelper.check_api_result(download_resp)
|
|
350
|
+
|
|
351
|
+
@staticmethod
|
|
352
|
+
def request_license(email: str, machine_id: Dict[str, Any]) -> str:
|
|
353
|
+
# Lets keep the email here so we have the same interface for both authenticated
|
|
354
|
+
# and not authenticated license requests.
|
|
355
|
+
# email will be validated in the license server to make sure it matches with the user of the
|
|
356
|
+
# access token so not any email is sent here
|
|
357
|
+
RestHelper.ensure_auth_token()
|
|
358
|
+
payload = {"id": email, "machine_id": machine_id}
|
|
359
|
+
b64_encoded_bytes = base64.encodebytes(json.dumps(payload).encode())
|
|
360
|
+
license_jsonb64 = {"licensejsonb64": b64_encoded_bytes.decode("utf-8")}
|
|
361
|
+
RestHelper.__headers["content-type"] = "application/json"
|
|
362
|
+
r = requests.post(
|
|
363
|
+
url=f"{RestHelper.__license_server_base_url}/api/license/request",
|
|
364
|
+
headers=RestHelper.__headers,
|
|
365
|
+
data=json.dumps(license_jsonb64),
|
|
366
|
+
timeout=60,
|
|
367
|
+
)
|
|
368
|
+
RestHelper.check_api_result(r)
|
|
369
|
+
return str(r.json()["license_data"])
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from datetime import date, datetime
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def parse_date(date_str: str) -> date:
|
|
5
|
+
return parse_datetime(date_str).date()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_datetime(date_str: str) -> datetime:
|
|
9
|
+
"""Required for pre 3.11"""
|
|
10
|
+
normalized = date_str.replace("Z", "+00:00")
|
|
11
|
+
return datetime.fromisoformat(normalized)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import urllib.request
|
|
8
|
+
from importlib import metadata as importlib_metadata
|
|
9
|
+
from importlib.metadata import version as python_project_version
|
|
10
|
+
|
|
11
|
+
from packaging.version import InvalidVersion, Version
|
|
12
|
+
|
|
13
|
+
from remotivelabs.cli.settings import Settings
|
|
14
|
+
from remotivelabs.cli.utils.console import print_hint
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cli_version() -> str:
|
|
18
|
+
return python_project_version("remotivelabs-cli")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def python_version() -> str:
|
|
22
|
+
return platform.python_version()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def host_os() -> str:
|
|
26
|
+
return platform.system().lower() # 'linux', 'darwin', 'windows'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def host_env() -> str:
|
|
30
|
+
return "docker" if os.environ.get("RUNS_IN_DOCKER") else "native"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def platform_info() -> str:
|
|
34
|
+
return f"python {python_version()}; {host_os()}; {host_env()}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _pypi_latest(
|
|
38
|
+
project: str, *, include_prereleases: bool, timeout: float = 2.5, user_agent: str | None = None
|
|
39
|
+
) -> tuple[str | None, str | None]:
|
|
40
|
+
"""Return (latest_version, project_url) from PyPI, skipping yanked files."""
|
|
41
|
+
url = f"https://pypi.org/pypi/{project}/json"
|
|
42
|
+
headers = {"Accept": "application/json"}
|
|
43
|
+
if user_agent:
|
|
44
|
+
headers["User-Agent"] = user_agent
|
|
45
|
+
req = urllib.request.Request(url, headers=headers)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
49
|
+
data = json.load(resp)
|
|
50
|
+
except Exception:
|
|
51
|
+
return None, None # network/404/etc.
|
|
52
|
+
|
|
53
|
+
releases = data.get("releases") or {}
|
|
54
|
+
candidates: list[Version] = []
|
|
55
|
+
for s, files in releases.items():
|
|
56
|
+
try:
|
|
57
|
+
v = Version(s)
|
|
58
|
+
except InvalidVersion:
|
|
59
|
+
continue
|
|
60
|
+
the_files = files or []
|
|
61
|
+
if any(f.get("yanked", False) for f in the_files):
|
|
62
|
+
continue
|
|
63
|
+
if (v.is_prerelease or v.is_devrelease) and not include_prereleases:
|
|
64
|
+
continue
|
|
65
|
+
candidates.append(v)
|
|
66
|
+
|
|
67
|
+
if not candidates:
|
|
68
|
+
return None, None
|
|
69
|
+
|
|
70
|
+
latest = str(max(candidates))
|
|
71
|
+
info = data.get("info") or {}
|
|
72
|
+
proj_url = info.get("project_url") or info.get("package_url") or f"https://pypi.org/project/{project}/"
|
|
73
|
+
return latest, proj_url
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _installed_version(distribution_name: str, fallback: str | None = None) -> str | None:
|
|
77
|
+
try:
|
|
78
|
+
return importlib_metadata.version(distribution_name)
|
|
79
|
+
except importlib_metadata.PackageNotFoundError:
|
|
80
|
+
return fallback
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def check_for_update(settings: Settings) -> None:
|
|
84
|
+
# Make it possible to disable update check, i.e in CI
|
|
85
|
+
if os.environ.get("PYTHON_DISABLE_UPDATE_CHECK"):
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
# Check if we are allowed to perform an update check
|
|
89
|
+
if not settings.should_perform_update_check():
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Determine current version
|
|
93
|
+
project = "remotivelabs-cli"
|
|
94
|
+
cur = cli_version() or _installed_version(project)
|
|
95
|
+
if not cur:
|
|
96
|
+
return # unknown version → skip silently
|
|
97
|
+
|
|
98
|
+
# We end up here if last_update_check_time is None or should_perform_update_check is true
|
|
99
|
+
include_prereleases = Version(cur).is_prerelease or Version(cur).is_devrelease
|
|
100
|
+
|
|
101
|
+
latest, proj_url = _pypi_latest(
|
|
102
|
+
project, include_prereleases=include_prereleases, user_agent=f"{project}/{cur} (+https://pypi.org/project/{project}/)"
|
|
103
|
+
)
|
|
104
|
+
if latest:
|
|
105
|
+
if Version(latest) > Version(cur):
|
|
106
|
+
_print_update_info(
|
|
107
|
+
cur,
|
|
108
|
+
latest,
|
|
109
|
+
)
|
|
110
|
+
settings.set_last_update_check_time(datetime.datetime.now().isoformat())
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _print_update_info(cur: str, latest: str) -> None:
|
|
114
|
+
instructions = (
|
|
115
|
+
"upgrade with: docker pull remotivelabs/remotivelabs-cli"
|
|
116
|
+
if os.environ.get("RUNS_IN_DOCKER")
|
|
117
|
+
else "upgrade with: pipx upgrade remotivelabs-cli"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
print_hint(f"Update available: remotivelabs-cli {cur} → {latest} , ({instructions}) we always recommend to use latest version")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: remotivelabs-cli
|
|
3
|
+
Version: 0.5.0a1
|
|
4
|
+
Summary: CLI for operating RemotiveCloud and RemotiveBroker
|
|
5
|
+
Keywords: automotive,autotech,networking,CAN
|
|
6
|
+
Author: Remotivelabs
|
|
7
|
+
Author-email: Remotivelabs <support@remotivelabs.com>
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Manufacturing
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: Other/Proprietary License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Communications
|
|
22
|
+
Classifier: Topic :: Internet
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
24
|
+
Requires-Dist: trogon>=0.5.0
|
|
25
|
+
Requires-Dist: typer==0.12.5
|
|
26
|
+
Requires-Dist: click<8.2.0
|
|
27
|
+
Requires-Dist: remotivelabs-broker~=0.11.1
|
|
28
|
+
Requires-Dist: rich~=13.7.0
|
|
29
|
+
Requires-Dist: pyjwt~=2.6
|
|
30
|
+
Requires-Dist: zeroconf~=0.127.0
|
|
31
|
+
Requires-Dist: websocket-client~=1.6
|
|
32
|
+
Requires-Dist: plotext~=5.2
|
|
33
|
+
Requires-Dist: python-socketio>=4.6.1
|
|
34
|
+
Requires-Dist: python-can>=4.3.1
|
|
35
|
+
Requires-Dist: grpc-stubs>=1.53.0.5
|
|
36
|
+
Requires-Dist: mypy-protobuf>=3.0.0
|
|
37
|
+
Requires-Dist: types-requests~=2.32.0.20240622
|
|
38
|
+
Requires-Dist: pydantic~=2.12.4
|
|
39
|
+
Requires-Dist: email-validator~=2.2.0
|
|
40
|
+
Requires-Dist: requests~=2.32.4
|
|
41
|
+
Requires-Dist: semver>=3.0.4
|
|
42
|
+
Requires-Python: >=3.10, <4.0
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# RemotiveLabs - CLI
|
|
46
|
+
[](https://pypi.org/project/remotivelabs-cli)
|
|
47
|
+
|
|
48
|
+
Use this CLI with our cloud and broker as a compliment to code and web tools.
|
|
49
|
+
|
|
50
|
+
Read more at https://docs.remotivelabs.com/docs/remotive-cli
|
|
51
|
+
|