remotivelabs-cli 0.0.24__py3-none-any.whl → 0.0.26__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 (42) hide show
  1. cli/broker/brokers.py +2 -2
  2. cli/broker/export.py +5 -5
  3. cli/broker/files.py +5 -5
  4. cli/broker/lib/broker.py +59 -49
  5. cli/broker/license_flows.py +11 -9
  6. cli/broker/licenses.py +2 -2
  7. cli/broker/playback.py +14 -34
  8. cli/broker/record.py +3 -3
  9. cli/broker/scripting.py +4 -4
  10. cli/broker/signals.py +11 -11
  11. cli/cloud/__init__.py +0 -1
  12. cli/cloud/auth.py +40 -35
  13. cli/cloud/auth_tokens.py +39 -36
  14. cli/cloud/brokers.py +24 -33
  15. cli/cloud/cloud_cli.py +11 -7
  16. cli/cloud/configs.py +19 -11
  17. cli/cloud/filestorage.py +155 -0
  18. cli/cloud/projects.py +10 -7
  19. cli/cloud/recordings.py +127 -108
  20. cli/cloud/recordings_playback.py +52 -39
  21. cli/cloud/rest_helper.py +248 -190
  22. cli/cloud/resumable_upload.py +62 -0
  23. cli/cloud/sample_recordings.py +5 -5
  24. cli/cloud/service_account_tokens.py +18 -16
  25. cli/cloud/service_accounts.py +9 -9
  26. cli/connect/__init__.py +0 -1
  27. cli/connect/connect.py +7 -6
  28. cli/connect/protopie/protopie.py +32 -16
  29. cli/errors.py +6 -5
  30. cli/remotive.py +13 -9
  31. cli/requirements.txt +4 -1
  32. cli/settings.py +9 -9
  33. cli/tools/__init__.py +0 -1
  34. cli/tools/can/__init__.py +0 -1
  35. cli/tools/can/can.py +8 -8
  36. cli/tools/tools.py +2 -2
  37. {remotivelabs_cli-0.0.24.dist-info → remotivelabs_cli-0.0.26.dist-info}/METADATA +6 -4
  38. remotivelabs_cli-0.0.26.dist-info/RECORD +44 -0
  39. remotivelabs_cli-0.0.24.dist-info/RECORD +0 -42
  40. {remotivelabs_cli-0.0.24.dist-info → remotivelabs_cli-0.0.26.dist-info}/LICENSE +0 -0
  41. {remotivelabs_cli-0.0.24.dist-info → remotivelabs_cli-0.0.26.dist-info}/WHEEL +0 -0
  42. {remotivelabs_cli-0.0.24.dist-info → remotivelabs_cli-0.0.26.dist-info}/entry_points.txt +0 -0
cli/cloud/rest_helper.py CHANGED
@@ -5,11 +5,13 @@ import json
5
5
  import logging
6
6
  import os
7
7
  import shutil
8
+ import sys
8
9
  from importlib.metadata import version
9
10
  from pathlib import Path
10
- from typing import Dict, List, Union
11
+ from typing import Any, Dict, List, Union
11
12
 
12
13
  import requests
14
+ from requests.exceptions import JSONDecodeError
13
15
  from rich.console import Console
14
16
  from rich.progress import Progress, SpinnerColumn, TextColumn, wrap_file
15
17
 
@@ -23,204 +25,260 @@ if "REMOTIVE_CLOUD_HTTP_LOGGING" in os.environ:
23
25
  requests_log = logging.getLogger("requests.packages.urllib3")
24
26
  requests_log.setLevel(logging.DEBUG)
25
27
  requests_log.propagate = True
26
- global baseurl
27
- global license_server_base_url
28
- base_url = "https://cloud.remotivelabs.com"
29
- license_server_base_url = "https://license.cloud.remotivelabs.com"
30
-
31
- if "REMOTIVE_CLOUD_BASE_URL" in os.environ:
32
- base_url = os.environ["REMOTIVE_CLOUD_BASE_URL"]
33
-
34
- if "cloud-dev" in base_url:
35
- license_server_base_url = "https://license.cloud-dev.remotivelabs.com"
36
-
37
- # if 'REMOTIVE_CLOUD_AUTH_TOKEN' not in os.environ:
38
- # print('export REMOTIVE_CLOUD_AUTH_TOKEN=auth must be set')
39
- # exit(0)
40
-
41
- # token = os.environ["REMOTIVE_CLOUD_AUTH_TOKEN"]
42
- # headers = {"Authorization": "Bearer " + token}
43
-
44
- headers = {}
45
- org = ""
46
-
47
- token = ""
48
-
49
- cli_version = version("remotivelabs-cli")
50
-
51
-
52
- def ensure_auth_token():
53
- global token
54
-
55
- # if 'REMOTIVE_CLOUD_ORGANISATION' not in os.environ:
56
- # print('You must first set the organisation id to use: export REMOTIVE_CLOUD_ORGANISATION=organisationUid')
57
- # raise typer.Exit()
58
- global org
59
- # org = os.environ["REMOTIVE_CLOUD_ORGANISATION"]
60
-
61
- # if not exists(str(Path.home()) + "/.config/.remotive/cloud.secret.token"):
62
- # print("Access token not found, please login first")
63
- # raise typer.Exit()
64
-
65
- # f = open(str(Path.home()) + "/.config/.remotive/cloud.secret.token", "r")
66
- token = settings.read_token()
67
- # os.environ['REMOTIVE_CLOUD_AUTH_TOKEN'] = token
68
- global headers
69
-
70
- headers["Authorization"] = "Bearer " + token.strip()
71
- headers["User-Agent"] = f"remotivelabs-cli {cli_version}"
72
-
73
-
74
- def handle_get(
75
- url,
76
- params=None,
77
- return_response: bool = False,
78
- allow_status_codes=None,
79
- progress_label="Fetching...",
80
- use_progress_indicator: bool = True,
81
- timeout: int = 20,
82
- ):
83
- if params is None:
84
- params = {}
85
- ensure_auth_token()
86
- if use_progress_indicator:
87
- with use_progress(progress_label):
88
- r = requests.get(f"{base_url}{url}", headers=headers, params=params, timeout=timeout)
89
- else:
90
- r = requests.get(f"{base_url}{url}", headers=headers, params=params, timeout=timeout)
91
-
92
- if return_response:
93
- check_api_result(r, allow_status_codes)
94
- return r
95
- print_api_result(r)
96
-
97
-
98
- def has_access(url, params={}):
99
- ensure_auth_token()
100
- r = requests.get(f"{base_url}{url}", headers=headers, params=params)
101
- if r.status_code == 401:
102
- return False
103
- return True
104
-
105
-
106
- def check_api_result(response, allow_status_codes: List[int] = None):
107
- if response.status_code > 299:
108
- if allow_status_codes is not None and response.status_code in allow_status_codes:
109
- return
110
- err_console.print(f":boom: [bold red]Got status code[/bold red]: {response.status_code}")
111
- if response.status_code == 401:
112
- err_console.print("Your token has expired, please login again")
113
- else:
114
- err_console.print(response.text)
115
- exit(1)
116
-
117
-
118
- def print_api_result(response):
119
- if response.status_code >= 200 and response.status_code < 300:
120
- if len(response.content) > 4:
121
- print(json.dumps(response.json()))
122
- exit(0)
123
- else:
124
- err_console.print(f":boom: [bold red]Got status code[/bold red]: {response.status_code}")
125
- if response.status_code == 401:
126
- err_console.print("Your token has expired, please login again")
127
- else:
128
- err_console.print(response.text)
129
- exit(1)
130
- # typer.Exit(1) did not work as expected
131
-
132
-
133
- def handle_delete(url, params={}, quiet=False, success_msg="Successfully deleted", progress_label="Deleting..."):
134
- ensure_auth_token()
135
- with use_progress(progress_label):
136
- r = requests.delete(f"{base_url}{url}", headers=headers, params=params)
137
- if r.status_code == 200 or r.status_code == 204:
138
- if not quiet:
139
- print_api_result(r)
140
- else:
141
- print_api_result(r)
142
-
143
-
144
- def handle_post(url, body=None, params={}, progress_label: str = "Processing...", return_response: bool = False):
145
- ensure_auth_token()
146
- headers["content-type"] = "application/json"
147
-
148
- with use_progress(progress_label):
149
- r = requests.post(f"{base_url}{url}", headers=headers, params=params, data=body)
150
-
151
- if return_response:
152
- check_api_result(r)
153
- return r
154
28
 
155
- print_api_result(r)
156
29
 
30
+ class RestHelper:
31
+ """Static Class with various helper functions for the rest API"""
32
+
33
+ __base_url = "https://cloud.remotivelabs.com"
34
+ __license_server_base_url = "https://license.cloud.remotivelabs.com"
35
+
36
+ if "REMOTIVE_CLOUD_BASE_URL" in os.environ:
37
+ __base_url = os.environ["REMOTIVE_CLOUD_BASE_URL"]
38
+
39
+ if "cloud-dev" in __base_url:
40
+ __license_server_base_url = "https://license.cloud-dev.remotivelabs.com"
41
+
42
+ # if 'REMOTIVE_CLOUD_AUTH_TOKEN' not in os.environ:
43
+ # print('export REMOTIVE_CLOUD_AUTH_TOKEN=auth must be set')
44
+ # exit(0)
45
+
46
+ # token = os.environ["REMOTIVE_CLOUD_AUTH_TOKEN"]
47
+ # headers = {"Authorization": "Bearer " + token}
48
+
49
+ __headers: Dict[str, str] = {}
50
+ __org: str = ""
51
+
52
+ __token: str = ""
53
+
54
+ @staticmethod
55
+ def get_cli_version() -> str:
56
+ return version("remotivelabs-cli")
57
+
58
+ @staticmethod
59
+ def get_base_url() -> str:
60
+ return RestHelper.__base_url
61
+
62
+ @staticmethod
63
+ def get_license_server_base_url() -> str:
64
+ return RestHelper.__license_server_base_url
65
+
66
+ @staticmethod
67
+ def get_headers() -> Dict[str, str]:
68
+ return RestHelper.__headers
69
+
70
+ @staticmethod
71
+ def get_org() -> str:
72
+ return RestHelper.__org
73
+
74
+ @staticmethod
75
+ def get_token() -> str:
76
+ return RestHelper.__token
77
+
78
+ @staticmethod
79
+ def ensure_auth_token() -> None:
80
+ # if 'REMOTIVE_CLOUD_ORGANISATION' not in os.environ:
81
+ # print('You must first set the organisation id to use: export REMOTIVE_CLOUD_ORGANISATION=organisationUid')
82
+ # raise typer.Exit()
83
+ # org = os.environ["REMOTIVE_CLOUD_ORGANISATION"]
84
+
85
+ # if not exists(str(Path.home()) + "/.config/.remotive/cloud.secret.token"):
86
+ # print("Access token not found, please login first")
87
+ # raise typer.Exit()
88
+
89
+ # f = open(str(Path.home()) + "/.config/.remotive/cloud.secret.token", "r")
90
+ token = settings.read_token()
91
+ # os.environ['REMOTIVE_CLOUD_AUTH_TOKEN'] = token
92
+
93
+ RestHelper.__headers["Authorization"] = "Bearer " + token.strip()
94
+ RestHelper.__headers["User-Agent"] = f"remotivelabs-cli {RestHelper.get_cli_version()}"
95
+
96
+ @staticmethod
97
+ def handle_get(
98
+ url: str,
99
+ params: Any = None,
100
+ return_response: bool = False,
101
+ allow_status_codes: List[int] | None = None,
102
+ progress_label: str = "Fetching...",
103
+ use_progress_indicator: bool = True,
104
+ timeout: int = 20,
105
+ ) -> requests.Response | None:
106
+ # pylint: disable=R0913
107
+ # Returns a Response object if succesfull otherwise None
108
+ if params is None:
109
+ params = {}
110
+ RestHelper.ensure_auth_token()
111
+ if use_progress_indicator:
112
+ with RestHelper.use_progress(progress_label):
113
+ r = requests.get(f"{RestHelper.__base_url}{url}", headers=RestHelper.__headers, params=params, timeout=timeout)
114
+ else:
115
+ r = requests.get(f"{RestHelper.__base_url}{url}", headers=RestHelper.__headers, params=params, timeout=timeout)
157
116
 
158
- def handle_put(url, body=None, params={}, return_response: bool = False):
159
- ensure_auth_token()
160
- headers["content-type"] = "application/json"
161
- r = requests.put(f"{base_url}{url}", headers=headers, params=params, data=body)
117
+ if return_response:
118
+ RestHelper.check_api_result(r, allow_status_codes)
119
+ return r
120
+ RestHelper.print_api_result(r)
121
+ return requests.Response()
122
+
123
+ @staticmethod
124
+ def has_access(url: str, params: Any = {}) -> bool: # pylint: disable=W0102
125
+ RestHelper.ensure_auth_token()
126
+ r = requests.get(f"{RestHelper.__base_url}{url}", headers=RestHelper.__headers, params=params, timeout=60)
127
+ if r.status_code == 401:
128
+ return False
129
+ return True
130
+
131
+ @staticmethod
132
+ def check_api_result(response: requests.Response, allow_status_codes: List[int] | None = None) -> None:
133
+ if response.status_code > 299:
134
+ if allow_status_codes is not None and response.status_code in allow_status_codes:
135
+ return
136
+ err_console.print(f":boom: [bold red]Got status code[/bold red]: {response.status_code}")
137
+ if response.status_code == 401:
138
+ err_console.print("Your token has expired, please login again")
139
+ else:
140
+ err_console.print(response.text)
141
+ sys.exit(1)
142
+
143
+ @staticmethod
144
+ def print_api_result(response: requests.Response) -> None:
145
+ if response.status_code >= 200 and response.status_code < 300:
146
+ if len(response.content) >= 2:
147
+ try:
148
+ print(json.dumps(response.json()))
149
+ except JSONDecodeError:
150
+ err_console.print(":boom: [bold red]Json parse error[/bold red]: Please try again and report if the error persists")
151
+ sys.exit(0)
152
+ else:
153
+ err_console.print(f":boom: [bold red]Got status code[/bold red]: {response.status_code}")
154
+ if response.status_code == 401:
155
+ err_console.print("Your token has expired, please login again")
156
+ else:
157
+ err_console.print(response.text)
158
+ sys.exit(1)
159
+ # typer.Exit(1) did not work as expected
160
+
161
+ @staticmethod
162
+ def handle_delete( # pylint: disable=W0102
163
+ url: str, params: Any = {}, quiet: bool = False, progress_label: str = "Deleting..."
164
+ ) -> None:
165
+ RestHelper.ensure_auth_token()
166
+ with RestHelper.use_progress(progress_label):
167
+ r = requests.delete(f"{RestHelper.__base_url}{url}", headers=RestHelper.__headers, params=params, timeout=60)
168
+ if r.status_code in (200, 204):
169
+ if not quiet:
170
+ RestHelper.print_api_result(r)
171
+ else:
172
+ RestHelper.print_api_result(r)
162
173
 
163
- if return_response:
164
- check_api_result(r)
165
- return r
166
- print_api_result(r)
174
+ @staticmethod
175
+ def handle_post( # pylint: disable=W0102
176
+ url: str, body: Any = None, params: Any = {}, progress_label: str = "Processing...", return_response: bool = False
177
+ ) -> requests.Response | None:
178
+ # Returns a Response object if succesfull otherwise, None
179
+ RestHelper.ensure_auth_token()
180
+ RestHelper.__headers["content-type"] = "application/json"
167
181
 
182
+ with RestHelper.use_progress(progress_label):
183
+ r = requests.post(f"{RestHelper.__base_url}{url}", headers=RestHelper.__headers, params=params, data=body, timeout=60)
168
184
 
169
- def upload_file(
170
- path: Union[str, Path], url: str, upload_headers: dict[str, str] = None, return_response: bool = False, progress_label="Uploading..."
171
- ):
172
- ensure_auth_token()
173
- if upload_headers is not None:
174
- headers.update(upload_headers)
175
- with open(path, "rb") as file:
176
- with wrap_file(file, os.stat(path).st_size, description=progress_label) as f:
177
- r = requests.post(f"{base_url}{url}", files={os.path.basename(path): f}, headers=headers)
178
185
  if return_response:
179
- check_api_result(r)
186
+ RestHelper.check_api_result(r)
180
187
  return r
181
- print_api_result(r)
182
188
 
189
+ RestHelper.print_api_result(r)
190
+ return None
191
+
192
+ @staticmethod
193
+ def handle_put(url: str, body: Any = None, params: Any = {}, return_response: bool = False) -> requests.Response | None: # pylint: disable=W0102
194
+ # Returns a Response object if succesfull otherwise, None
195
+ RestHelper.ensure_auth_token()
196
+ RestHelper.__headers["content-type"] = "application/json"
197
+ r = requests.put(f"{RestHelper.__base_url}{url}", headers=RestHelper.__headers, params=params, data=body, timeout=60)
183
198
 
184
- def upload_file_with_signed_url(
185
- path: Union[str, Path], url: str, upload_headers: dict[str, str], return_response: bool = False, progress_label="Uploading..."
186
- ):
187
- with open(path, "rb") as file:
188
- with wrap_file(file, os.stat(path).st_size, description=progress_label, transient=True) as f:
189
- r = requests.put(url, data=f, headers=upload_headers)
190
199
  if return_response:
191
- check_api_result(r)
200
+ RestHelper.check_api_result(r)
192
201
  return r
193
- print_api_result(r)
194
-
195
-
196
- def use_progress(label: str):
197
- p = Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True)
198
- p.add_task(label, total=1)
199
- return p
200
-
201
-
202
- def download_file(save_file_name: str, url: str):
203
- # Next download the actual file
204
- download_resp = requests.get(url=url, stream=True)
205
- if download_resp.status_code == 200:
206
- content_length = int(download_resp.headers["Content-Length"])
207
- with open(save_file_name, "wb") as out_file:
208
- with wrap_file(download_resp.raw, content_length, refresh_per_second=100) as stream_with_progress:
209
- shutil.copyfileobj(stream_with_progress, out_file)
210
- else:
211
- check_api_result(download_resp)
212
-
213
-
214
- def request_license(email: str, machine_id: Dict[str, any]) -> str:
215
- # Lets keep the email here so we have the same interface for both authenticated
216
- # and not authenticated license requests.
217
- # email will be validated in the license server to make sure it matches with the user of the
218
- # access token so not any email is sent here
219
- ensure_auth_token()
220
- payload = {"id": email, "machine_id": machine_id}
221
- b64_encoded_bytes = base64.encodebytes(json.dumps(payload).encode())
222
- license_jsonb64 = {"licensejsonb64": b64_encoded_bytes.decode("utf-8")}
223
- headers["content-type"] = "application/json"
224
- r = requests.post(url=f"{license_server_base_url}/api/license/request", headers=headers, data=json.dumps(license_jsonb64))
225
- check_api_result(r)
226
- return r.json()["license_data"]
202
+ RestHelper.print_api_result(r)
203
+ return None
204
+
205
+ @staticmethod
206
+ def upload_file(
207
+ path: Union[str, Path],
208
+ url: str,
209
+ upload_headers: Dict[str, str] | None = None,
210
+ return_response: bool = False,
211
+ progress_label: str = "Uploading...",
212
+ ) -> requests.Response | None:
213
+ # Returns a Response object if succesfull otherwise, None
214
+ RestHelper.ensure_auth_token()
215
+ if upload_headers is not None:
216
+ RestHelper.__headers.update(upload_headers)
217
+ with open(path, "rb") as file:
218
+ with wrap_file(file, os.stat(path).st_size, description=progress_label) as f:
219
+ r = requests.post(
220
+ f"{RestHelper.__base_url}{url}", files={os.path.basename(path): f}, headers=RestHelper.__headers, timeout=60
221
+ )
222
+ if return_response:
223
+ RestHelper.check_api_result(r)
224
+ return r
225
+ RestHelper.print_api_result(r)
226
+ return None
227
+
228
+ @staticmethod
229
+ def upload_file_with_signed_url(
230
+ path: Union[str, Path],
231
+ url: str,
232
+ upload_headers: Dict[str, str],
233
+ return_response: bool = False,
234
+ progress_label: str = "Uploading...",
235
+ ) -> requests.Response | None:
236
+ # Returns a Response object if succesfull otherwise, None
237
+ with open(path, "rb") as file:
238
+ with wrap_file(file, os.stat(path).st_size, description=progress_label, transient=False) as f:
239
+ r = requests.put(url, data=f, headers=upload_headers, timeout=60)
240
+ if return_response:
241
+ RestHelper.check_api_result(r)
242
+ return r
243
+ RestHelper.print_api_result(r)
244
+ return None
245
+
246
+ @staticmethod
247
+ def use_progress(label: str) -> Progress:
248
+ p = Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True)
249
+ p.add_task(label, total=1)
250
+ return p
251
+
252
+ @staticmethod
253
+ def download_file(save_file_name: str, url: str) -> None:
254
+ # Next download the actual file
255
+ download_resp = requests.get(url=url, stream=True, timeout=60)
256
+ if download_resp.status_code == 200:
257
+ content_length = int(download_resp.headers["Content-Length"])
258
+ with open(save_file_name, "wb") as out_file:
259
+ with wrap_file(
260
+ download_resp.raw, content_length, refresh_per_second=100, description=f"Downloading to {save_file_name}"
261
+ ) as stream_with_progress:
262
+ shutil.copyfileobj(stream_with_progress, out_file)
263
+ else:
264
+ RestHelper.check_api_result(download_resp)
265
+
266
+ @staticmethod
267
+ def request_license(email: str, machine_id: Dict[str, Any]) -> str:
268
+ # Lets keep the email here so we have the same interface for both authenticated
269
+ # and not authenticated license requests.
270
+ # email will be validated in the license server to make sure it matches with the user of the
271
+ # access token so not any email is sent here
272
+ RestHelper.ensure_auth_token()
273
+ payload = {"id": email, "machine_id": machine_id}
274
+ b64_encoded_bytes = base64.encodebytes(json.dumps(payload).encode())
275
+ license_jsonb64 = {"licensejsonb64": b64_encoded_bytes.decode("utf-8")}
276
+ RestHelper.__headers["content-type"] = "application/json"
277
+ r = requests.post(
278
+ url=f"{RestHelper.__license_server_base_url}/api/license/request",
279
+ headers=RestHelper.__headers,
280
+ data=json.dumps(license_jsonb64),
281
+ timeout=60,
282
+ )
283
+ RestHelper.check_api_result(r)
284
+ return str(r.json()["license_data"])
@@ -0,0 +1,62 @@
1
+ import os
2
+ import sys
3
+
4
+ import requests
5
+ from rich.progress import wrap_file
6
+
7
+ from ..errors import ErrorPrinter
8
+
9
+
10
+ def __get_uploaded_bytes(upload_url: str) -> int:
11
+ headers = {"Content-Range": "bytes */*"}
12
+ response = requests.put(upload_url, headers=headers, timeout=60)
13
+ if response.status_code != 308:
14
+ raise ValueError(f"Failed to retrieve upload status: {response.status_code} {response.text}")
15
+
16
+ # Parse the Range header to get the last byte uploaded
17
+ range_header = response.headers.get("Range")
18
+ if range_header:
19
+ last_byte = int(range_header.split("-")[1])
20
+ return last_byte + 1
21
+ return 0
22
+
23
+
24
+ def upload_signed_url(signed_url: str, source_file_name: str, content_type: str) -> None:
25
+ """
26
+ Upload file to file storage with signed url and resumable upload.
27
+ Resumable upload will only work with the same URL and not if a new signed URL is requested with the
28
+ same object id.
29
+ :param signed_url:
30
+ :param source_file_name:
31
+ :return:
32
+ """
33
+
34
+ file_size = os.path.getsize(source_file_name)
35
+ headers = {"x-goog-resumable": "start", "content-type": content_type}
36
+ response = requests.post(signed_url, headers=headers, timeout=60)
37
+ if response.status_code not in (200, 201, 308):
38
+ ErrorPrinter.print_generic_error(f"Failed to upload file: {response.status_code} - {response.text}")
39
+ sys.exit(1)
40
+
41
+ upload_url = response.headers["Location"]
42
+
43
+ # Check how many bytes have already been uploaded
44
+ uploaded_bytes = __get_uploaded_bytes(upload_url)
45
+
46
+ # Upload the remaining file in chunks
47
+ # Not sure what a good chunk size is or if we even should have resumable uploads here, probably not..
48
+ chunk_size = 256 * 1024 * 10
49
+ # Upload the file in chunks
50
+ with open(source_file_name, "rb") as f:
51
+ with wrap_file(f, os.stat(source_file_name).st_size, description=f"Uploading {source_file_name}...") as file:
52
+ file.seek(uploaded_bytes) # Seek to the position of the last uploaded byte
53
+ for chunk_start in range(uploaded_bytes, file_size, chunk_size):
54
+ chunk_end = min(chunk_start + chunk_size, file_size) - 1
55
+ chunk = file.read(chunk_end - chunk_start + 1)
56
+ headers = {"Content-Range": f"bytes {chunk_start}-{chunk_end}/{file_size}"}
57
+ response = requests.put(upload_url, headers=headers, data=chunk, timeout=60)
58
+ if response.status_code not in (200, 201, 308):
59
+ ErrorPrinter.print_generic_error(f"Failed to upload file: {response.status_code} - {response.text}")
60
+ sys.exit(1)
61
+
62
+ print(f"File {source_file_name} uploaded successfully.")
@@ -2,7 +2,7 @@ import json
2
2
 
3
3
  import typer
4
4
 
5
- from . import rest_helper as rest
5
+ from .rest_helper import RestHelper as Rest
6
6
 
7
7
  app = typer.Typer()
8
8
 
@@ -11,14 +11,14 @@ app = typer.Typer()
11
11
  def do_import(
12
12
  recording_session: str = typer.Argument(..., help="Recording session id"),
13
13
  project: str = typer.Option(..., help="Project to import sample recording into", envvar="REMOTIVE_CLOUD_PROJECT"),
14
- ):
15
- rest.handle_post(url=f"/api/samples/files/recording/{recording_session}/copy", body=json.dumps({"projectUid": project}))
14
+ ) -> None:
15
+ Rest.handle_post(url=f"/api/samples/files/recording/{recording_session}/copy", body=json.dumps({"projectUid": project}))
16
16
 
17
17
 
18
18
  @app.command("list")
19
- def list():
19
+ def list() -> None: # pylint: disable=W0622
20
20
  """
21
21
  List available sample recordings
22
22
  """
23
23
 
24
- rest.handle_get("/api/samples/files/recording")
24
+ Rest.handle_get("/api/samples/files/recording")
@@ -4,12 +4,12 @@ from pathlib import Path
4
4
 
5
5
  import typer
6
6
 
7
- from . import rest_helper as rest
7
+ from .rest_helper import RestHelper as Rest
8
8
 
9
9
  app = typer.Typer()
10
10
 
11
- config_dir_name = str(Path.home()) + "/.config/.remotive/"
12
- token_file_name = str(Path.home()) + "/.config/.remotive/cloud.secret.token2"
11
+ CONFIG_DIR_NAME = str(Path.home()) + "/.config/.remotive/"
12
+ TOKEN_FILE_NAME = str(Path.home()) + "/.config/.remotive/cloud.secret.token2"
13
13
 
14
14
 
15
15
  @app.command(name="create", help="Create new access token")
@@ -17,13 +17,16 @@ def create(
17
17
  expire_in_days: int = typer.Option(default=365, help="Number of this token is valid"),
18
18
  service_account: str = typer.Option(..., help="Service account name"),
19
19
  project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
20
- ):
21
- response = rest.handle_post(
20
+ ) -> None:
21
+ response = Rest.handle_post(
22
22
  url=f"/api/project/{project}/admin/accounts/{service_account}/keys",
23
23
  return_response=True,
24
24
  body=json.dumps({"daysUntilExpiry": expire_in_days}),
25
25
  )
26
26
 
27
+ if response is None:
28
+ return
29
+
27
30
  if response.status_code == 200:
28
31
  name = response.json()["name"]
29
32
  write_token(f"service-account-{service_account}-{name}-token.json", response.text)
@@ -36,16 +39,16 @@ def create(
36
39
  def list_keys(
37
40
  service_account: str = typer.Option(..., help="Service account name"),
38
41
  project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
39
- ):
40
- rest.handle_get(f"/api/project/{project}/admin/accounts/{service_account}/keys")
42
+ ) -> None:
43
+ Rest.handle_get(f"/api/project/{project}/admin/accounts/{service_account}/keys")
41
44
 
42
45
 
43
46
  @app.command(name="list-files")
44
- def list_files():
47
+ def list_files() -> None:
45
48
  """
46
49
  List personal access token files in remotivelabs config directory
47
50
  """
48
- personal_files = filter(lambda f: f.startswith("service-account"), os.listdir(config_dir_name))
51
+ personal_files = filter(lambda f: f.startswith("service-account"), os.listdir(CONFIG_DIR_NAME))
49
52
  for file in personal_files:
50
53
  print(file)
51
54
 
@@ -55,13 +58,12 @@ def revoke(
55
58
  name: str = typer.Argument(..., help="Access token name"),
56
59
  service_account: str = typer.Option(..., help="Service account name"),
57
60
  project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
58
- ):
59
- rest.handle_delete(f"/api/project/{project}/admin/accounts/{service_account}/keys/{name}")
61
+ ) -> None:
62
+ Rest.handle_delete(f"/api/project/{project}/admin/accounts/{service_account}/keys/{name}")
60
63
 
61
64
 
62
- def write_token(file, token):
65
+ def write_token(file: str, token: str) -> None:
63
66
  path = str(Path.home()) + f"/.config/.remotive/{file}"
64
- f = open(path, "w")
65
- f.write(token)
66
- f.close()
67
- print(f"Secret token written to {path}")
67
+ with open(path, "w", encoding="utf8") as f:
68
+ f.write(token)
69
+ print(f"Secret token written to {path}")