remotivelabs-cli 0.0.30__py3-none-any.whl → 0.0.32__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.
cli/cloud/cloud_cli.py CHANGED
@@ -2,7 +2,7 @@ import typer
2
2
 
3
3
  from cli.cloud.rest_helper import RestHelper
4
4
 
5
- from . import auth, brokers, configs, filestorage, organisations, projects, recordings, sample_recordings, service_accounts
5
+ from . import auth, brokers, configs, organisations, projects, recordings, sample_recordings, service_accounts, storage
6
6
 
7
7
  app = typer.Typer()
8
8
 
@@ -21,7 +21,7 @@ app.add_typer(auth.app, name="auth")
21
21
  app.add_typer(brokers.app, name="brokers", help="Manage cloud broker lifecycle")
22
22
  app.add_typer(recordings.app, name="recordings", help="Manage recordings")
23
23
  app.add_typer(configs.app, name="signal-databases", help="Manage signal databases")
24
- app.add_typer(filestorage.app, name="storage")
24
+ app.add_typer(storage.app, name="storage")
25
25
  app.add_typer(service_accounts.app, name="service-accounts", help="Manage project service account keys")
26
26
  app.add_typer(sample_recordings.app, name="samples", help="Manage sample recordings")
27
27
 
cli/cloud/configs.py CHANGED
@@ -48,10 +48,14 @@ def upload(
48
48
  """
49
49
  Uploads signal database to project
50
50
  """
51
- res = Rest.handle_put(url=f"/api/project/{project}/files/config/{os.path.basename(path)}/uploadfile", return_response=True)
52
- if res is not None:
51
+ res_text = Rest.handle_put(url=f"/api/project/{project}/files/config/{os.path.basename(path)}/uploadfile", return_response=True)
52
+ if res_text is not None:
53
+ res_json = res_text.json()
53
54
  Rest.upload_file_with_signed_url(
54
- path=path, url=res.text, upload_headers={"Content-Type": "application/octet-stream"}, progress_label=f"Uploading {path}..."
55
+ path=path,
56
+ url=res_json["url"],
57
+ upload_headers={"Content-Type": "application/octet-stream"},
58
+ progress_label=f"Uploading {path}...",
55
59
  )
56
60
 
57
61
 
cli/cloud/recordings.py CHANGED
@@ -9,7 +9,7 @@ import tempfile
9
9
  import time
10
10
  import urllib.parse
11
11
  from pathlib import Path
12
- from typing import Any, Dict, List, Tuple, Union
12
+ from typing import Any, Dict, List, Optional, Tuple, Union
13
13
  from urllib.parse import quote
14
14
 
15
15
  import grpc
@@ -119,12 +119,10 @@ def __check_rcs_path(path: str) -> str:
119
119
 
120
120
 
121
121
  def do_start(name: str, project: str, api_key: str, return_response: bool = False) -> requests.Response:
122
- if api_key == "":
123
- body = {"size": "S"}
124
- else:
125
- body = {"size": "S", "apiKey": api_key}
122
+ body = {"size": "S"}
123
+ if not api_key:
124
+ body["apiKey"] = api_key
126
125
 
127
- name = name if name is not None else "personal"
128
126
  return Rest.handle_post(
129
127
  f"/api/project/{project}/brokers/{name}",
130
128
  body=json.dumps(body),
@@ -136,7 +134,7 @@ def do_start(name: str, project: str, api_key: str, return_response: bool = Fals
136
134
  @app.command(help="Prepares all recording files and transformations to be available for playback")
137
135
  def mount( # noqa: C901
138
136
  recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
139
- broker: str = typer.Option(None, help="Broker to use"),
137
+ broker: Optional[str] = typer.Option(None, help="Broker to use"),
140
138
  ensure_broker_started: bool = typer.Option(default=False, help="Ensure broker exists, start otherwise"),
141
139
  transformation_name: str = typer.Option("default", help="Specify a custom signal transformation to use"),
142
140
  project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
@@ -153,7 +151,7 @@ def mount( # noqa: C901
153
151
  broker = r.json()["shortName"]
154
152
 
155
153
  elif r.status_code == 404:
156
- r = do_start(None, project, "", return_response=True)
154
+ r = do_start("personal", project, "", return_response=True)
157
155
  if r.status_code != 200:
158
156
  print(r.text)
159
157
  sys.exit(0)
@@ -208,7 +206,7 @@ def download_recording_file(
208
206
  )
209
207
  if get_signed_url_resp.status_code == 200:
210
208
  # Next download the actual file
211
- Rest.download_file(recording_file_name, get_signed_url_resp.json()["downloadUrl"])
209
+ Rest.download_file(Path(recording_file_name), get_signed_url_resp.json()["downloadUrl"])
212
210
  print(f"Downloaded {recording_file_name}")
213
211
  else:
214
212
  print(get_signed_url_resp)
cli/cloud/rest_helper.py CHANGED
@@ -250,7 +250,7 @@ class RestHelper:
250
250
  return p
251
251
 
252
252
  @staticmethod
253
- def download_file(save_file_name: str, url: str) -> None:
253
+ def download_file(save_file_name: Path, url: str) -> None:
254
254
  # Next download the actual file
255
255
  download_resp = requests.get(url=url, stream=True, timeout=60)
256
256
  if download_resp.status_code == 200:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import sys
5
+ from pathlib import Path
5
6
  from typing import Dict
6
7
 
7
8
  import requests
@@ -66,7 +67,7 @@ def with_resumable_upload_signed_url(signed_url: str, source_file_name: str, con
66
67
  print(f"File {source_file_name} uploaded successfully.")
67
68
 
68
69
 
69
- def upload_signed_url(signed_url: str, source_file_name: str, headers: Dict[str, str]) -> None:
70
+ def upload_signed_url(signed_url: str, source_file_name: Path, headers: Dict[str, str]) -> None:
70
71
  """
71
72
  Upload file to file storage with signed url and resumable upload.
72
73
  Resumable upload will only work with the same URL and not if a new signed URL is requested with the
@@ -76,10 +77,11 @@ def upload_signed_url(signed_url: str, source_file_name: str, headers: Dict[str,
76
77
  :param source_file_name:
77
78
  :return:
78
79
  """
79
- with open(source_file_name, "rb") as f:
80
- response = requests.put(signed_url, headers=headers, timeout=60, data=f)
81
- if response.status_code not in (200, 201, 308):
82
- ErrorPrinter.print_generic_error(f"Failed to upload file: {response.status_code} - {response.text}")
83
- sys.exit(1)
80
+ with open(source_file_name, "rb") as file:
81
+ with wrap_file(file, os.stat(source_file_name).st_size, description=f"Uploading {source_file_name}...") as f:
82
+ response = requests.put(signed_url, headers=headers, timeout=60, data=f)
83
+ if response.status_code not in (200, 201, 308):
84
+ ErrorPrinter.print_generic_error(f"Failed to upload file: {response.status_code} - {response.text}")
85
+ sys.exit(1)
84
86
 
85
87
  print(f"File {source_file_name} uploaded successfully.")
@@ -0,0 +1,5 @@
1
+ from cli.cloud.storage.cmd import app
2
+ from cli.cloud.storage.uri import URI
3
+ from cli.cloud.storage.uri_or_path import UriOrPath
4
+
5
+ __all__ = ["URI", "UriOrPath", "app"]
@@ -0,0 +1,77 @@
1
+ import sys
2
+
3
+ import typer
4
+ from typing_extensions import Annotated
5
+
6
+ from cli.cloud.rest_helper import RestHelper as Rest
7
+ from cli.cloud.storage.copy import copy
8
+ from cli.cloud.storage.uri import URI, InvalidURIError, JoinURIError
9
+ from cli.cloud.storage.uri_or_path import UriOrPath
10
+ from cli.cloud.storage.uri_or_path import uri as uri_parser
11
+ from cli.errors import ErrorPrinter
12
+
13
+ app = typer.Typer(
14
+ rich_markup_mode="rich",
15
+ help="""
16
+ Manage files ([yellow]Beta feature not available for all customers[/yellow])
17
+
18
+ Copy file from local to remote storage and vice versa, list and delete files.
19
+
20
+ """,
21
+ )
22
+
23
+
24
+ @app.command(name="ls")
25
+ def list_files(
26
+ project: Annotated[str, typer.Option(help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT", show_default=False)],
27
+ uri: Annotated[URI, typer.Argument(help="Remote storage path", parser=URI)] = "rcs://", # type: ignore
28
+ ) -> None:
29
+ """
30
+ Listing remote files
31
+
32
+ This will list files and directories in project top level directory
33
+ remotive cloud storage ls rcs://
34
+
35
+ This will list all files and directories matching the path
36
+ remotive cloud storage ls rcs://fileOrDirectoryPrefix
37
+
38
+ This will list all files and directories in the specified directory
39
+ remotive cloud storage ls rcs://fileOrDirectory/
40
+ """
41
+ Rest.handle_get(f"/api/project/{project}/files/storage{uri.path}")
42
+
43
+
44
+ @app.command(name="rm")
45
+ def delete_file(
46
+ project: Annotated[str, typer.Option(help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT", show_default=False)],
47
+ uri: Annotated[URI, typer.Argument(help="Remote storage path", parser=URI, show_default=False)],
48
+ ) -> None:
49
+ """
50
+ [red]Deletes[/red] a file from remote storage, this cannot be undone :fire:
51
+
52
+ [white]remotive cloud storage rm rcs://directory/filename[/white]
53
+ """
54
+ Rest.handle_delete(f"/api/project/{project}/files/storage{uri.path}")
55
+
56
+
57
+ @app.command(name="cp")
58
+ def copy_file(
59
+ project: Annotated[str, typer.Option(help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT", show_default=False)],
60
+ source: Annotated[UriOrPath, typer.Argument(help="Remote or local path to source file", parser=uri_parser, show_default=False)],
61
+ dest: Annotated[UriOrPath, typer.Argument(help="Remote or local path to destination file", parser=uri_parser, show_default=False)],
62
+ overwrite: Annotated[bool, typer.Option(help="Overwrite existing file on RCS")] = False,
63
+ ) -> None:
64
+ """
65
+ Copies a file to or from remote storage
66
+
67
+ remotive cloud storage cp rcs://dir/filename .
68
+ remotive cloud storage cp rcs://dir/filename filename
69
+
70
+ remotive cloud storage cp filename rcs://dir/
71
+ remotive cloud storage cp filename rcs://dir/filename
72
+ """
73
+ try:
74
+ return copy(project=project, source=source.value, dest=dest.value, overwrite=overwrite)
75
+ except (InvalidURIError, JoinURIError, ValueError, FileNotFoundError, FileExistsError) as e:
76
+ ErrorPrinter.print_hint(str(e))
77
+ sys.exit(1)
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from cli.cloud.rest_helper import RestHelper as Rest
7
+ from cli.cloud.resumable_upload import upload_signed_url
8
+ from cli.cloud.storage.uri import URI
9
+
10
+ _RCS_STORAGE_PATH = "/api/project/{project}/files/storage{path}"
11
+
12
+
13
+ def copy(project: str, source: URI | Path, dest: URI | Path, overwrite: bool = False) -> None:
14
+ if isinstance(source, Path) and isinstance(dest, Path):
15
+ raise ValueError("Either source or destination must be an rcs:// uri")
16
+
17
+ if isinstance(source, URI) and isinstance(dest, URI):
18
+ raise ValueError("Either source or destination must be a local path")
19
+
20
+ if isinstance(source, URI) and isinstance(dest, Path):
21
+ _download(source=source, dest=dest, project=project, overwrite=overwrite)
22
+
23
+ elif isinstance(source, Path) and isinstance(dest, URI):
24
+ _upload(source=source, dest=dest, project=project, overwrite=overwrite)
25
+
26
+ else:
27
+ raise ValueError("invalid copy operation")
28
+
29
+
30
+ def _upload(source: Path, dest: URI, project: str, overwrite: bool = False) -> None:
31
+ if not source.exists():
32
+ raise FileNotFoundError(source)
33
+
34
+ files_to_upload = _list_files_for_upload(source, dest)
35
+
36
+ for file_path, target_uri in files_to_upload:
37
+ _upload_single_file(file_path, target_uri, project, overwrite)
38
+
39
+
40
+ def _list_files_for_upload(source: Path, dest: URI) -> list[tuple[Path, URI]]:
41
+ upload_pairs = []
42
+
43
+ if source.is_dir():
44
+ for file_path in source.rglob("*"):
45
+ if file_path.is_file():
46
+ relative_path = file_path.relative_to(source)
47
+ target_uri = dest / relative_path
48
+ upload_pairs.append((file_path, target_uri))
49
+ else:
50
+ target_uri = dest / source.name if dest.is_dir() else dest
51
+ upload_pairs.append((source, target_uri))
52
+
53
+ return upload_pairs
54
+
55
+
56
+ def _upload_single_file(source: Path, target_uri: URI, project: str, overwrite: bool = False) -> None:
57
+ target = _RCS_STORAGE_PATH.format(project=project, path=target_uri.path)
58
+ upload_options = {"overwrite": "always" if overwrite else "never"}
59
+ res = Rest.handle_post(target, return_response=True, body=json.dumps(upload_options))
60
+
61
+ json_res = res.json()
62
+ url = json_res["url"]
63
+ headers = json_res["headers"]
64
+ upload_signed_url(url, source, headers)
65
+
66
+ print(f"Uploaded {source} to {target_uri.path}")
67
+
68
+
69
+ def _download(source: URI, dest: Path, project: str, overwrite: bool = False) -> None:
70
+ if dest.is_dir():
71
+ if not dest.exists():
72
+ raise FileNotFoundError(f"Destination directory {dest} does not exist")
73
+ # create a target file name if destination is a dir
74
+ dest = dest / source.filename
75
+
76
+ else:
77
+ if not dest.parent.is_dir() or not dest.parent.exists():
78
+ raise FileNotFoundError(f"Destination directory {dest.parent} does not exist")
79
+
80
+ if dest.exists() and not overwrite:
81
+ raise FileExistsError(f"Destination file {dest} already exists")
82
+
83
+ target = _RCS_STORAGE_PATH.format(project=project, path=source.path) + "?download=true"
84
+ res = Rest.handle_get(target, return_response=True)
85
+
86
+ Rest.download_file(save_file_name=dest.absolute(), url=res.text)
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from os import PathLike
4
+ from pathlib import PurePosixPath
5
+ from urllib.parse import urlparse
6
+
7
+
8
+ class InvalidURIError(Exception):
9
+ """Raised when an invalid URI is encountered"""
10
+
11
+
12
+ class JoinURIError(Exception):
13
+ """Raised when an error occurs while joining URIs"""
14
+
15
+
16
+ class URI:
17
+ """
18
+ Custom type for rcs (Remotive Cloud Storage) URIs.
19
+ """
20
+
21
+ def __init__(self, value: str, scheme: str = "rcs"):
22
+ self.original_uri = value
23
+ self.scheme = scheme
24
+
25
+ parsed = urlparse(value)
26
+ if parsed.scheme != self.scheme:
27
+ raise InvalidURIError(f"Invalid URI scheme. Expected '{self.scheme}://', got '{parsed.scheme}://'")
28
+ if parsed.netloc.startswith((".", "-", "#", " ", "/", "\\")):
29
+ raise InvalidURIError(f"Invalid URI. Path cannot start with invalid characters: '{value}'")
30
+ if not parsed.netloc and parsed.path == "/":
31
+ raise InvalidURIError(f"Invalid URI: '{value}'")
32
+
33
+ self.path = f"/{parsed.netloc}{parsed.path}" if parsed.netloc else f"/{parsed.path}"
34
+
35
+ self._posix_path = PurePosixPath(self.path)
36
+ self.filename = self._posix_path.name
37
+
38
+ def is_dir(self) -> bool:
39
+ return self.path.endswith("/")
40
+
41
+ def __truediv__(self, other: PathLike[str] | str) -> URI:
42
+ """Returns a new URI object with the joined path"""
43
+ if str(other).startswith("/"):
44
+ raise JoinURIError(f"Cannot join absolute path '{other}' to URI")
45
+
46
+ new_path = self._posix_path / other
47
+
48
+ # handle relative paths
49
+ for part in new_path.parts:
50
+ if part == "..":
51
+ new_path = new_path.parent
52
+ elif part != ".":
53
+ new_path = new_path / part
54
+
55
+ new_uri = f"{self.scheme}://{new_path.relative_to('/')}" # we need to strip the starting '/'
56
+ return URI(new_uri, scheme=self.scheme)
57
+
58
+ def __str__(self) -> str:
59
+ return self.original_uri
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from cli.cloud.storage.uri import URI, InvalidURIError
6
+
7
+
8
+ def uri(path: str) -> UriOrPath:
9
+ """
10
+ Parses a path and returns a UriOrPath object.
11
+
12
+ NOTE: The name of this function is important as it is used by Typer to determine the name/type of the argument
13
+ """
14
+ try:
15
+ p: Path | URI = URI(path)
16
+ except InvalidURIError:
17
+ p = Path(path)
18
+ return UriOrPath(p)
19
+
20
+
21
+ class UriOrPath:
22
+ """
23
+ Union type for handling local and remote paths for Remotive Cloud Storage
24
+
25
+ Note: This custom type only exists because Typer currently does not support union types
26
+ TODO: Move to commands package when refactored
27
+ """
28
+
29
+ def __init__(self, value: Path | URI) -> None:
30
+ self._value = value
31
+
32
+ @property
33
+ def uri(self) -> URI | None:
34
+ return self._value if isinstance(self._value, URI) else None
35
+
36
+ @property
37
+ def path(self) -> Path | None:
38
+ return self._value if isinstance(self._value, Path) else None
39
+
40
+ @property
41
+ def value(self) -> Path | URI:
42
+ return self._value
43
+
44
+ def __str__(self) -> str:
45
+ return str(self._value)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: remotivelabs-cli
3
- Version: 0.0.30
3
+ Version: 0.0.32
4
4
  Summary: CLI for operating RemotiveCloud and RemotiveBroker
5
5
  Author: Johan Rask
6
6
  Author-email: johan.rask@remotivelabs.com
@@ -19,7 +19,7 @@ Requires-Dist: python-socketio (>=4.6.1)
19
19
  Requires-Dist: remotivelabs-broker (>=0.1.17,<0.2.0)
20
20
  Requires-Dist: rich (>=13.7.0,<13.8.0)
21
21
  Requires-Dist: trogon (>=0.5.0)
22
- Requires-Dist: typer (>=0.9.0,<0.10.0)
22
+ Requires-Dist: typer (==0.12.5)
23
23
  Requires-Dist: types-requests (>=2.32.0.20240622,<3.0.0.0)
24
24
  Requires-Dist: websocket-client (>=1.6,<2.0)
25
25
  Requires-Dist: zeroconf (>=0.127.0,<0.128.0)
@@ -14,18 +14,22 @@ cli/cloud/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  cli/cloud/auth.py,sha256=RBTDUGRBVsK28u-aeqzRIzHnW7FqH4KADlVlQEgBCww,2534
15
15
  cli/cloud/auth_tokens.py,sha256=0U60Gk2-TnAUff5anZmTB1rOEninNvYy1o5ihCqgj8A,4525
16
16
  cli/cloud/brokers.py,sha256=DNj79MTkPylKUQbr-iPUhQgfNJLAW8UehnvgpEmNH_k,3890
17
- cli/cloud/cloud_cli.py,sha256=09YCHs8IivYsVJOsxlM5OMEqBdq3QUXtDsktcO8Kjyw,1263
18
- cli/cloud/configs.py,sha256=xg3J-kaS-Pp0p9otV2cWl_oOWJzs_jZhXwFHz0gQxvc,4625
19
- cli/cloud/filestorage.py,sha256=cCPDYwCyJxP4V_qK1_Gnsg_T-zVsw6QaZdY_l4s4vC0,5445
17
+ cli/cloud/cloud_cli.py,sha256=0VvFexnVnYvb1vueYxraHiH_WjbQp0Y42ttL_koDFh8,1255
18
+ cli/cloud/configs.py,sha256=bZzZF-yNkZkfJK5baU0Xh_vQRrJkmuzEv8xx4_IkXZ8,4714
20
19
  cli/cloud/organisations.py,sha256=txKQmSQEpTmeqlqngai8pwgQQEvRgeDd0dT_VzZ7RNc,752
21
20
  cli/cloud/projects.py,sha256=YrwPJClC2Sq_y1HjPd_tzaiv4GEnnsXSXHBhtQCPdK0,1431
22
- cli/cloud/recordings.py,sha256=jai5Gim28UmZFGniUI9qKDwtLoi2Nllv4eyPeIk3OAc,25366
21
+ cli/cloud/recordings.py,sha256=RK-CfYEZ_K4o_nOuRBQ6JCsNpTc8yGWQUMV_BN4aWQA,25315
23
22
  cli/cloud/recordings_playback.py,sha256=PRzftmvG2iePrL9f6qTEXVOnyJ-etcyzn5w9CCxcSto,11539
24
- cli/cloud/rest_helper.py,sha256=lZp0NjQ8yOaggQGNiqNxHex_YFOmuq0rnLPtpLq3Z3Q,11470
25
- cli/cloud/resumable_upload.py,sha256=sYThyhseXRniOMbctbO5p4BGVb9b7BXVBcmcZXwnClM,3550
23
+ cli/cloud/rest_helper.py,sha256=_3RLk8C101PH5pCq5GhCCYLSiS2TZtXh69Yt5TfiJsY,11471
24
+ cli/cloud/resumable_upload.py,sha256=8lEIdncJZoTZzNsQVHH3gm_GunxEmN5JbmWX7awy3p4,3713
26
25
  cli/cloud/sample_recordings.py,sha256=OVX32U1dkkkJZysbgr5Dy515oOQKnwBAbZYzV_QUu1g,690
27
26
  cli/cloud/service_account_tokens.py,sha256=263u1bRmBKfYsxL6TV6YjReUBUaVHWc3ETCd7AS3DTU,2297
28
27
  cli/cloud/service_accounts.py,sha256=XOIPobUamCLIaufjyvb33XJDwy6uRqW5ZljZx3GYEfo,1659
28
+ cli/cloud/storage/__init__.py,sha256=y6V0QswTVY7lHcy5kVUbNKIPc5tyWDqvhakcRFKV6HA,167
29
+ cli/cloud/storage/cmd.py,sha256=kukFatuL-YOy6VC7FC4PvjjS1teQya7R__fNX0jdubY,2929
30
+ cli/cloud/storage/copy.py,sha256=thjdhqnFHSstsA5Iidz74c2o4zHfEaDXfRUGOpjWh1o,3228
31
+ cli/cloud/storage/uri.py,sha256=uGEGggJp7Nnu3oUadBs9bAMUDf2e5YjhZ6gAdeYLBFI,1963
32
+ cli/cloud/storage/uri_or_path.py,sha256=rTjg9G0h8-jb4QUlza1YMqqs99ifrydnEL7o7rmidKQ,1166
29
33
  cli/connect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
34
  cli/connect/connect.py,sha256=U--6dtHxUlvE81J37rABFez4TbF7AXWOpZYZnL7sPMY,3994
31
35
  cli/connect/protopie/protopie.py,sha256=KBMbBwdkUVgV2X7AXTHweVqYVHv4akG875FVc36gsyg,6349
@@ -36,8 +40,8 @@ cli/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
40
  cli/tools/can/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
41
  cli/tools/can/can.py,sha256=8uATViSFlpkdSiIm4fzbuQi1_m7V9Pym-K17TaJQRHU,2262
38
42
  cli/tools/tools.py,sha256=0KU-hXR1f9xHP4BOG9A9eXfmICLmNuQCOU8ueF6iGg0,198
39
- remotivelabs_cli-0.0.30.dist-info/LICENSE,sha256=qDPP_yfuv1fF-u7EfexN-cN3M8aFgGVndGhGLovLKz0,608
40
- remotivelabs_cli-0.0.30.dist-info/METADATA,sha256=82x7il8xKpkWGgoxjahiE9bW3QBSiPC--YuUG2d-Prs,1318
41
- remotivelabs_cli-0.0.30.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
42
- remotivelabs_cli-0.0.30.dist-info/entry_points.txt,sha256=lvDhPgagLqW_KTnLPCwKSqfYlEp-1uYVosRiPjsVj10,45
43
- remotivelabs_cli-0.0.30.dist-info/RECORD,,
43
+ remotivelabs_cli-0.0.32.dist-info/LICENSE,sha256=qDPP_yfuv1fF-u7EfexN-cN3M8aFgGVndGhGLovLKz0,608
44
+ remotivelabs_cli-0.0.32.dist-info/METADATA,sha256=pvFD_mvS0qOntQP-7EAivZAPl1vKcNJDfzGSh-jwKEw,1311
45
+ remotivelabs_cli-0.0.32.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
46
+ remotivelabs_cli-0.0.32.dist-info/entry_points.txt,sha256=lvDhPgagLqW_KTnLPCwKSqfYlEp-1uYVosRiPjsVj10,45
47
+ remotivelabs_cli-0.0.32.dist-info/RECORD,,
cli/cloud/filestorage.py DELETED
@@ -1,170 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os.path
4
- import sys
5
- from pathlib import Path
6
-
7
- import typer
8
-
9
- from ..errors import ErrorPrinter
10
- from . import resumable_upload as upload
11
- from .rest_helper import RestHelper as Rest
12
-
13
- app = typer.Typer(
14
- rich_markup_mode="rich",
15
- help="""
16
- Manage files ([yellow]Beta feature not available for all customers[/yellow])
17
-
18
- Copy file from local to remote storage and vice versa, list and delete files.
19
-
20
- """,
21
- )
22
-
23
-
24
- @app.command(name="ls")
25
- def list_files(
26
- project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
27
- prefix: str = typer.Argument(default="rcs://", help="Remote storage path"),
28
- ) -> None:
29
- """
30
- Listing remote files
31
-
32
- This will list files and directories in project top level directory
33
- remotive cloud storage ls rcs://
34
-
35
- This will list all files and directories matching the path
36
- remotive cloud storage ls rcs://fileOrDirectoryPrefix
37
-
38
- This will list all files and directories in the specified directory
39
- remotive cloud storage ls rcs://fileOrDirectory/
40
- """
41
-
42
- if prefix.startswith("rcs://"):
43
- prefix = __check_rcs_path(prefix)
44
- else:
45
- ErrorPrinter.print_hint("Path must start with rcs://")
46
- sys.exit(1)
47
-
48
- Rest.handle_get(
49
- f"/api/project/{project}/files/storage{prefix}",
50
- )
51
-
52
-
53
- @app.command(name="rm")
54
- def delete_file(
55
- project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
56
- path: str = typer.Argument(default=..., help="Remote storage path to file to delete"),
57
- ) -> None:
58
- """
59
- [red]Deletes[/red] a file from remote storage, this cannot be undone :fire:
60
-
61
- [white]remotive cloud storage rm rcs://directory/filename[/white]
62
- """
63
- if path.startswith("rcs://"):
64
- prefix = __check_rcs_path(path)
65
- else:
66
- ErrorPrinter.print_hint("Path must start with rcs://")
67
- sys.exit(1)
68
-
69
- Rest.handle_delete(
70
- f"/api/project/{project}/files/storage{prefix}",
71
- )
72
-
73
-
74
- @app.command(name="cp")
75
- def copy_file( # noqa: C901 # type: ignore[too-many-branches] # pylint: disable=no-member
76
- source: str = typer.Argument(default=..., help="Remote or local path to source file"),
77
- dest: str = typer.Argument(default=..., help="Remote or local path to destination file"),
78
- project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
79
- ) -> None:
80
- """
81
- Copies a file to or from remote storage
82
-
83
- remotive cloud storage cp rcs://dir/filename .
84
- remotive cloud storage cp rcs://dir/filename filename
85
-
86
- remotive cloud storage cp filename rcs://dir/
87
- remotive cloud storage cp filename rcs://dir/filename
88
- """
89
-
90
- if not source.startswith("rcs://") and not dest.startswith("rcs://"):
91
- ErrorPrinter.print_hint("Source or destination path must be an rcs:// path")
92
- sys.exit(2)
93
-
94
- if source.startswith("rcs://") and dest.startswith("rcs://"):
95
- ErrorPrinter.print_hint("Currently one of source and destination path must be a local path")
96
- sys.exit(2)
97
-
98
- if source.startswith("rcs://"):
99
- __copy_to_local(source=source, dest=dest, project=project)
100
- else:
101
- path = Path(source)
102
- if path.is_dir():
103
- print("is dir")
104
- for file_path in path.rglob("*"):
105
- if file_path.is_file():
106
- print(file_path)
107
- __copy_to_remote(source=source, dest=dest, project=project)
108
- sys.exit(1)
109
- else:
110
- __copy_to_remote(source=source, dest=dest, project=project)
111
-
112
-
113
- def __copy_to_remote(source: str, dest: str, project: str) -> None:
114
- path = Path(source)
115
- if path.is_dir():
116
- print("is dir")
117
- sys.exit(1)
118
-
119
- if not path.exists():
120
- ErrorPrinter.print_hint("Source file does not exist")
121
- sys.exit(1)
122
- filename = source.rsplit("/", 1)[-1]
123
- rcs_path = __check_rcs_path(dest)
124
- if rcs_path.endswith("/"):
125
- rcs_path = rcs_path + filename
126
- res = Rest.handle_post(f"/api/project/{project}/files/storage{rcs_path}", return_response=True)
127
- if res is None:
128
- return
129
- json_res = res.json()
130
- url = json_res["url"]
131
- headers = json_res["headers"]
132
- try:
133
- upload.upload_signed_url(url, source, headers)
134
- except IsADirectoryError:
135
- ErrorPrinter.print_hint(f"Supplied source file '{source}' is a directory but must be a file")
136
-
137
-
138
- def __copy_to_local(source: str, dest: str, project: str) -> None:
139
- rcs_path = __check_rcs_path(source)
140
- filename = source.rsplit("/", 1)[-1]
141
- path = Path(dest)
142
- if path.is_dir():
143
- if not path.exists():
144
- ErrorPrinter.print_generic_error("Destination directory does not exist")
145
- sys.exit(1)
146
- else:
147
- dest = os.path.join(path.absolute(), filename)
148
-
149
- else:
150
- if not path.parent.is_dir() or not path.parent.exists():
151
- ErrorPrinter.print_generic_error("Destination directory does not exist")
152
- sys.exit(1)
153
- dest = str(Path(dest).absolute())
154
-
155
- res = Rest.handle_get(
156
- f"/api/project/{project}/files/storage{rcs_path}?download=true",
157
- return_response=True,
158
- )
159
- if res is None:
160
- return
161
-
162
- Rest.download_file(save_file_name=dest, url=res.text)
163
-
164
-
165
- def __check_rcs_path(path: str) -> str:
166
- rcs_path = path.replace("rcs://", "/")
167
- if rcs_path.startswith("/."):
168
- ErrorPrinter.print_hint("Invalid path")
169
- sys.exit(1)
170
- return rcs_path