remotivelabs-cli 0.0.32__tar.gz → 0.0.34__tar.gz
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-0.0.32 → remotivelabs_cli-0.0.34}/PKG-INFO +3 -2
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/broker/lib/broker.py +30 -22
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/broker/signals.py +9 -11
- remotivelabs_cli-0.0.34/cli/cloud/auth/__init__.py +3 -0
- remotivelabs_cli-0.0.34/cli/cloud/auth/cmd.py +46 -0
- remotivelabs_cli-0.0.32/cli/cloud/auth.py → remotivelabs_cli-0.0.34/cli/cloud/auth/login.py +3 -49
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/cloud_cli.py +1 -2
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/storage/cmd.py +4 -5
- remotivelabs_cli-0.0.34/cli/cloud/storage/uri.py +113 -0
- remotivelabs_cli-0.0.34/cli/tools/can/__init__.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/pyproject.toml +8 -10
- remotivelabs_cli-0.0.32/cli/cloud/storage/uri.py +0 -59
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/LICENSE +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/README.md +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/__init__.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/broker/brokers.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/broker/export.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/broker/files.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/broker/lib/__about__.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/broker/license_flows.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/broker/licenses.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/broker/playback.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/broker/record.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/broker/scripting.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/__init__.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/auth_tokens.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/brokers.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/configs.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/organisations.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/projects.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/recordings.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/recordings_playback.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/rest_helper.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/resumable_upload.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/sample_recordings.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/service_account_tokens.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/service_accounts.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/storage/__init__.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/storage/copy.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/cloud/storage/uri_or_path.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/connect/__init__.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/connect/connect.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/connect/protopie/protopie.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/errors.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/remotive.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/settings.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/tools/__init__.py +0 -0
- /remotivelabs_cli-0.0.32/cli/tools/can/__init__.py → /remotivelabs_cli-0.0.34/cli/tools/can/RemotiveLabs.can1.log +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/tools/can/can.py +0 -0
- {remotivelabs_cli-0.0.32 → remotivelabs_cli-0.0.34}/cli/tools/tools.py +0 -0
@@ -1,15 +1,16 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: remotivelabs-cli
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.34
|
4
4
|
Summary: CLI for operating RemotiveCloud and RemotiveBroker
|
5
5
|
Author: Johan Rask
|
6
6
|
Author-email: johan.rask@remotivelabs.com
|
7
|
-
Requires-Python: >=3.8
|
7
|
+
Requires-Python: >=3.8
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
9
9
|
Classifier: Programming Language :: Python :: 3.8
|
10
10
|
Classifier: Programming Language :: Python :: 3.9
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
13
14
|
Requires-Dist: grpc-stubs (>=1.53.0.5)
|
14
15
|
Requires-Dist: mypy-protobuf (>=3.0.0)
|
15
16
|
Requires-Dist: plotext (>=5.2,<6.0)
|
@@ -18,6 +18,7 @@ import remotivelabs.broker.generated.sync.traffic_api_pb2 as traffic_api
|
|
18
18
|
import remotivelabs.broker.sync as br
|
19
19
|
import remotivelabs.broker.sync.helper as br_helper
|
20
20
|
import typer
|
21
|
+
from google.protobuf.json_format import MessageToDict
|
21
22
|
from rich.console import Console
|
22
23
|
|
23
24
|
from cli import settings
|
@@ -360,7 +361,7 @@ class Broker:
|
|
360
361
|
namespaces.append(network_info.namespace.name)
|
361
362
|
return namespaces
|
362
363
|
|
363
|
-
def list_signal_names(self) -> List[Dict[str, Any]]:
|
364
|
+
def list_signal_names(self, prefix: Union[str, None], suffix: Union[str, None]) -> List[Dict[str, Any]]:
|
364
365
|
# Lists available signals
|
365
366
|
configuration = self.system_stub.GetConfiguration(br.common_pb2.Empty())
|
366
367
|
|
@@ -368,30 +369,37 @@ class Broker:
|
|
368
369
|
for network_info in configuration.networkInfo:
|
369
370
|
res = self.system_stub.ListSignals(network_info.namespace)
|
370
371
|
for finfo in res.frame:
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
"signal": sinfo.id.name,
|
379
|
-
"namespace": network_info.namespace.name,
|
380
|
-
"receivers": rec,
|
381
|
-
"min": sinfo.metaData.min,
|
382
|
-
"max": sinfo.metaData.max,
|
383
|
-
}
|
372
|
+
if (prefix is None or finfo.signalInfo.id.name.startswith(prefix)) and (
|
373
|
+
suffix is None or finfo.signalInfo.id.name.endswith(suffix)
|
374
|
+
):
|
375
|
+
metadata_dict = MessageToDict(
|
376
|
+
finfo.signalInfo.metaData,
|
377
|
+
including_default_value_fields=True,
|
378
|
+
preserving_proto_field_name=True,
|
384
379
|
)
|
385
|
-
|
386
|
-
signal_names.append(
|
387
|
-
{
|
380
|
+
sig_dict = {
|
388
381
|
"signal": finfo.signalInfo.id.name,
|
389
382
|
"namespace": network_info.namespace.name,
|
390
|
-
"senders": list(map(lambda s: s, finfo.signalInfo.metaData.sender)),
|
391
|
-
"receivers": list(set(receivers)),
|
392
|
-
"cycletime": finfo.signalInfo.metaData.cycleTime,
|
393
383
|
}
|
394
|
-
|
384
|
+
signal_names.append({**sig_dict, **metadata_dict})
|
385
|
+
|
386
|
+
for sinfo in finfo.childInfo:
|
387
|
+
# For signals we can simply skip if prefix and suffix exists does not match
|
388
|
+
if (prefix is not None and not sinfo.id.name.startswith(prefix)) or (
|
389
|
+
suffix is not None and not sinfo.id.name.endswith(suffix)
|
390
|
+
):
|
391
|
+
continue
|
392
|
+
|
393
|
+
metadata_dict = MessageToDict(
|
394
|
+
sinfo.metaData,
|
395
|
+
including_default_value_fields=True,
|
396
|
+
preserving_proto_field_name=True,
|
397
|
+
)
|
398
|
+
sig_dict = {
|
399
|
+
"signal": sinfo.id.name,
|
400
|
+
"namespace": network_info.namespace.name,
|
401
|
+
}
|
402
|
+
signal_names.append({**sig_dict, **metadata_dict})
|
395
403
|
|
396
404
|
return signal_names
|
397
405
|
|
@@ -433,7 +441,7 @@ class Broker:
|
|
433
441
|
def find_subscribed_signal(available_signal: List[Dict[str, str]]) -> List[str]:
|
434
442
|
return list(filter(lambda s: available_signal["signal"] == s, subscribed_signals)) # type: ignore
|
435
443
|
|
436
|
-
existing_signals = self.list_signal_names()
|
444
|
+
existing_signals = self.list_signal_names(prefix=None, suffix=None)
|
437
445
|
existing_ns = set(map(lambda s: s["namespace"], existing_signals))
|
438
446
|
ns_not_matching = []
|
439
447
|
for ns in subscribed_namespaces:
|
@@ -5,7 +5,7 @@ import numbers
|
|
5
5
|
import os
|
6
6
|
import signal as os_signal
|
7
7
|
from pathlib import Path
|
8
|
-
from typing import Any, Dict, Iterable, List, TypedDict
|
8
|
+
from typing import Any, Dict, Iterable, List, TypedDict, Union
|
9
9
|
|
10
10
|
import grpc
|
11
11
|
import plotext as plt # type: ignore
|
@@ -30,23 +30,21 @@ class Signals(TypedDict):
|
|
30
30
|
signal_values: Dict[Any, Any] = {}
|
31
31
|
|
32
32
|
|
33
|
-
@app.command(name="list"
|
33
|
+
@app.command(name="list")
|
34
34
|
def list_signals(
|
35
35
|
url: str = typer.Option(..., help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
|
36
36
|
api_key: str = typer.Option(None, help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
|
37
|
+
name_starts_with: Union[str, None] = typer.Option(None, help="Signal name prefix to include"),
|
38
|
+
name_ends_with: Union[str, None] = typer.Option(None, help="Signal name suffix to include"),
|
37
39
|
) -> None:
|
38
|
-
|
39
|
-
|
40
|
+
"""
|
41
|
+
List signal metadata on a broker
|
40
42
|
|
41
|
-
|
42
|
-
|
43
|
-
url: str = typer.Option(..., help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
|
44
|
-
api_key: str = typer.Option(None, help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
|
45
|
-
) -> None:
|
43
|
+
Filter are inclusive so --name-starts-with and --name-ends-with will include name that matches both
|
44
|
+
"""
|
46
45
|
try:
|
47
46
|
broker = Broker(url, api_key)
|
48
|
-
|
49
|
-
available_signals = broker.list_signal_names()
|
47
|
+
available_signals = broker.list_signal_names(prefix=name_starts_with, suffix=name_ends_with)
|
50
48
|
print(json.dumps(available_signals))
|
51
49
|
except grpc.RpcError as rpc_error:
|
52
50
|
ErrorPrinter.print_grpc_error(rpc_error)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import typer
|
2
|
+
|
3
|
+
from cli import settings
|
4
|
+
from cli.cloud.auth.login import login as do_login
|
5
|
+
from cli.cloud.rest_helper import RestHelper as Rest
|
6
|
+
|
7
|
+
from .. import auth_tokens
|
8
|
+
|
9
|
+
HELP = """
|
10
|
+
Manage how you authenticate with our cloud platform
|
11
|
+
"""
|
12
|
+
app = typer.Typer(help=HELP)
|
13
|
+
app.add_typer(auth_tokens.app, name="tokens", help="Manage users personal access tokens")
|
14
|
+
|
15
|
+
|
16
|
+
@app.command(name="login")
|
17
|
+
def login() -> None:
|
18
|
+
"""
|
19
|
+
Login to the cli using browser
|
20
|
+
|
21
|
+
This will be used as the current access token in all subsequent requests. This would
|
22
|
+
be the same as activating a personal access key or service-account access key.
|
23
|
+
"""
|
24
|
+
do_login()
|
25
|
+
|
26
|
+
|
27
|
+
@app.command()
|
28
|
+
def whoami() -> None:
|
29
|
+
"""
|
30
|
+
Validates authentication and fetches your user information
|
31
|
+
"""
|
32
|
+
Rest.handle_get("/api/whoami")
|
33
|
+
|
34
|
+
|
35
|
+
@app.command()
|
36
|
+
def print_access_token() -> None:
|
37
|
+
"""
|
38
|
+
Print current active access token
|
39
|
+
"""
|
40
|
+
print(settings.read_secret_token())
|
41
|
+
|
42
|
+
|
43
|
+
@app.command(help="Clear access token")
|
44
|
+
def logout() -> None:
|
45
|
+
settings.clear_secret_token()
|
46
|
+
print("Access token removed")
|
@@ -4,23 +4,13 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
4
4
|
from threading import Thread
|
5
5
|
from typing import Any
|
6
6
|
|
7
|
-
import typer
|
8
7
|
from typing_extensions import override
|
9
8
|
|
10
9
|
from cli import settings
|
11
|
-
|
12
|
-
from . import auth_tokens
|
13
|
-
from .rest_helper import RestHelper as Rest
|
14
|
-
|
15
|
-
HELP = """
|
16
|
-
Manage how you authenticate with our cloud platform
|
17
|
-
"""
|
10
|
+
from cli.cloud.rest_helper import RestHelper as Rest
|
18
11
|
|
19
12
|
httpd: HTTPServer
|
20
13
|
|
21
|
-
app = typer.Typer(help=HELP)
|
22
|
-
app.add_typer(auth_tokens.app, name="tokens", help="Manage users personal access tokens")
|
23
|
-
|
24
14
|
|
25
15
|
class S(BaseHTTPRequestHandler):
|
26
16
|
def _set_response(self) -> None:
|
@@ -55,46 +45,10 @@ def start_local_webserver(server_class: type = HTTPServer, handler_class: type =
|
|
55
45
|
httpd = server_class(server_address, handler_class)
|
56
46
|
|
57
47
|
|
58
|
-
#
|
59
|
-
# CLI commands go here
|
60
|
-
#
|
61
|
-
|
62
|
-
|
63
|
-
@app.command(name="login")
|
64
48
|
def login() -> None:
|
65
49
|
"""
|
66
|
-
|
67
|
-
|
68
|
-
This will be used as the current access token in all subsequent requests. This would
|
69
|
-
be the same as activating a personal access key or service-account access key.
|
50
|
+
Initiate login using browser
|
70
51
|
"""
|
71
52
|
start_local_webserver()
|
72
|
-
webbrowser.
|
73
|
-
f"{Rest.get_base_url()}/login?redirectUrl=http://localhost:{httpd.server_address[1]}",
|
74
|
-
new=1,
|
75
|
-
autoraise=True,
|
76
|
-
)
|
77
|
-
|
53
|
+
webbrowser.open_new_tab(f"{Rest.get_base_url()}/login?redirectUrl=http://localhost:{httpd.server_address[1]}")
|
78
54
|
httpd.serve_forever()
|
79
|
-
|
80
|
-
|
81
|
-
@app.command()
|
82
|
-
def whoami() -> None:
|
83
|
-
"""
|
84
|
-
Validates authentication and fetches your user information
|
85
|
-
"""
|
86
|
-
Rest.handle_get("/api/whoami")
|
87
|
-
|
88
|
-
|
89
|
-
@app.command()
|
90
|
-
def print_access_token() -> None:
|
91
|
-
"""
|
92
|
-
Print current active access token
|
93
|
-
"""
|
94
|
-
print(settings.read_secret_token())
|
95
|
-
|
96
|
-
|
97
|
-
@app.command(help="Clear access token")
|
98
|
-
def logout() -> None:
|
99
|
-
settings.clear_secret_token()
|
100
|
-
print("Access token removed")
|
@@ -1,9 +1,8 @@
|
|
1
1
|
import typer
|
2
2
|
|
3
|
+
from cli.cloud import auth, brokers, configs, organisations, projects, recordings, sample_recordings, service_accounts, storage
|
3
4
|
from cli.cloud.rest_helper import RestHelper
|
4
5
|
|
5
|
-
from . import auth, brokers, configs, organisations, projects, recordings, sample_recordings, service_accounts, storage
|
6
|
-
|
7
6
|
app = typer.Typer()
|
8
7
|
|
9
8
|
|
@@ -10,15 +10,14 @@ from cli.cloud.storage.uri_or_path import UriOrPath
|
|
10
10
|
from cli.cloud.storage.uri_or_path import uri as uri_parser
|
11
11
|
from cli.errors import ErrorPrinter
|
12
12
|
|
13
|
-
|
14
|
-
rich_markup_mode="rich",
|
15
|
-
help="""
|
13
|
+
HELP = """
|
16
14
|
Manage files ([yellow]Beta feature not available for all customers[/yellow])
|
17
15
|
|
18
16
|
Copy file from local to remote storage and vice versa, list and delete files.
|
19
17
|
|
20
|
-
"""
|
21
|
-
|
18
|
+
"""
|
19
|
+
|
20
|
+
app = typer.Typer(rich_markup_mode="rich", help=HELP)
|
22
21
|
|
23
22
|
|
24
23
|
@app.command(name="ls")
|
@@ -0,0 +1,113 @@
|
|
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
|
+
The URI format follows the pattern: rcs://bucket/path/to/resource
|
21
|
+
"""
|
22
|
+
|
23
|
+
scheme: str
|
24
|
+
"""The URI scheme (default: 'rcs')"""
|
25
|
+
|
26
|
+
path: str
|
27
|
+
"""The full path component, including leading slash"""
|
28
|
+
|
29
|
+
filename: str
|
30
|
+
"""The name of the file or last path segment"""
|
31
|
+
|
32
|
+
bucket: str
|
33
|
+
"""The first path segment after the leading slash"""
|
34
|
+
|
35
|
+
parent: URI
|
36
|
+
"""The parent URI. If at root, returns a copy of itself."""
|
37
|
+
|
38
|
+
def __init__(self, value: str, scheme: str = "rcs"):
|
39
|
+
"""
|
40
|
+
Create a new URI.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
value: The URI string in format "scheme://path/to/resource"
|
44
|
+
scheme: The URI scheme (default: 'rcs')
|
45
|
+
|
46
|
+
Raises:
|
47
|
+
InvalidURIError: If the URI format is invalid
|
48
|
+
"""
|
49
|
+
self._raw = value
|
50
|
+
self.scheme = scheme
|
51
|
+
|
52
|
+
parsed = urlparse(value)
|
53
|
+
if parsed.scheme != self.scheme:
|
54
|
+
raise InvalidURIError(f"Invalid URI scheme. Expected '{self.scheme}://', got '{parsed.scheme}://'")
|
55
|
+
if parsed.netloc.startswith((".", "-", "#", " ", "/", "\\")):
|
56
|
+
raise InvalidURIError(f"Invalid URI. Path cannot start with invalid characters: '{value}'")
|
57
|
+
if not parsed.netloc and parsed.path == "/":
|
58
|
+
raise InvalidURIError(f"Invalid URI: '{value}'")
|
59
|
+
|
60
|
+
self.path = f"/{parsed.netloc}{parsed.path}" if parsed.netloc else f"/{parsed.path}"
|
61
|
+
|
62
|
+
self._posix_path = PurePosixPath(self.path)
|
63
|
+
self.filename = self._posix_path.name
|
64
|
+
self.bucket = self._posix_path.parts[1] if len(self._posix_path.parts) > 1 else ""
|
65
|
+
|
66
|
+
if self._posix_path == PurePosixPath("/"):
|
67
|
+
self.parent = self
|
68
|
+
else:
|
69
|
+
parent_path = self._posix_path.parent
|
70
|
+
new_uri = f"{self.scheme}://{str(parent_path)[1:]}"
|
71
|
+
self.parent = URI(new_uri, scheme=self.scheme)
|
72
|
+
|
73
|
+
def is_dir(self) -> bool:
|
74
|
+
"""Check if the URI points to a directory."""
|
75
|
+
return self.path.endswith("/")
|
76
|
+
|
77
|
+
def __truediv__(self, other: PathLike[str] | str) -> URI:
|
78
|
+
"""
|
79
|
+
Join this URI with another path component.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
other: Path component to join
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
A new URI with the joined path
|
86
|
+
|
87
|
+
Raises:
|
88
|
+
JoinURIError: If trying to join an absolute path
|
89
|
+
TypeError: If the path component is not a string or PathLike
|
90
|
+
"""
|
91
|
+
if str(other).startswith("/"):
|
92
|
+
raise JoinURIError(f"Cannot join absolute path '{other}' to URI")
|
93
|
+
|
94
|
+
is_dir = str(other).endswith("/")
|
95
|
+
new_path = self._posix_path / other
|
96
|
+
|
97
|
+
for part in new_path.parts:
|
98
|
+
if part == "..":
|
99
|
+
new_path = new_path.parent
|
100
|
+
elif part != ".":
|
101
|
+
new_path = new_path / part
|
102
|
+
|
103
|
+
new_uri = f"{self.scheme}://{new_path.relative_to('/')}" # we need to strip the starting '/'
|
104
|
+
new_uri = new_uri if not is_dir else f"{new_uri}/" # and append slash if the added path was a dir
|
105
|
+
return URI(new_uri, scheme=self.scheme)
|
106
|
+
|
107
|
+
def __str__(self) -> str:
|
108
|
+
"""Return the original URI string."""
|
109
|
+
return self._raw
|
110
|
+
|
111
|
+
def __repr__(self) -> str:
|
112
|
+
"""Return the original URI string."""
|
113
|
+
return f"URI({self._raw})"
|
File without changes
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "remotivelabs-cli"
|
3
|
-
version = "0.0.
|
3
|
+
version = "0.0.34"
|
4
4
|
description = "CLI for operating RemotiveCloud and RemotiveBroker"
|
5
5
|
authors = ["Johan Rask <johan.rask@remotivelabs.com>"]
|
6
6
|
readme = "README.md"
|
@@ -11,8 +11,8 @@ packages = [{ include = "cli" }]
|
|
11
11
|
remotive = "cli.remotive:app"
|
12
12
|
|
13
13
|
[tool.poetry.dependencies]
|
14
|
+
python = ">=3.8"
|
14
15
|
trogon = ">=0.5.0"
|
15
|
-
python = ">=3.8,<3.12"
|
16
16
|
typer = "0.12.5"
|
17
17
|
remotivelabs-broker = "~=0.1.17"
|
18
18
|
rich = "~=13.7.0"
|
@@ -29,6 +29,8 @@ types-requests = "^2.32.0.20240622"
|
|
29
29
|
[tool.poetry.group.test.dependencies]
|
30
30
|
pytest = "^8.3"
|
31
31
|
pytest-cov = "^5.0"
|
32
|
+
semver = "^3.0.2"
|
33
|
+
friendlywords = "^1.1.3"
|
32
34
|
|
33
35
|
[tool.poetry.group.lint.dependencies]
|
34
36
|
ruff = "^0.6.5"
|
@@ -36,14 +38,6 @@ pylint = "^3.2.7"
|
|
36
38
|
pylint-protobuf = "^0.22.0"
|
37
39
|
mypy = "^1.11"
|
38
40
|
|
39
|
-
[tool.poe.tasks]
|
40
|
-
test = [{ cmd = "pytest --cov=cli ." }]
|
41
|
-
pylint = [{ cmd = "pylint ." }]
|
42
|
-
lint = [{ cmd = "ruff check ." }, { cmd = "ruff format --check --diff ." }]
|
43
|
-
format = [{ cmd = "ruff format ." }, { cmd = "ruff check --fix ." }]
|
44
|
-
mypy = [{ cmd = "mypy -p cli" }]
|
45
|
-
check = ["lint", "pylint", "mypy"]
|
46
|
-
|
47
41
|
[tool.pytest.ini_options]
|
48
42
|
markers = ["itests: run tests with custom flag"]
|
49
43
|
|
@@ -70,6 +64,7 @@ select = [
|
|
70
64
|
recursive = true
|
71
65
|
ignore-paths = '^.venv/.*$'
|
72
66
|
load-plugins = ["pylint_protobuf"]
|
67
|
+
ignored-modules = ["friendlywords"]
|
73
68
|
|
74
69
|
[tool.pylint.format]
|
75
70
|
max-line-length = 140
|
@@ -104,6 +99,9 @@ hide_error_codes = false
|
|
104
99
|
module = "someip.*"
|
105
100
|
ignore_missing_imports = true
|
106
101
|
|
102
|
+
[[tool.mypy.overrides]]
|
103
|
+
module = "friendlywords.*"
|
104
|
+
ignore_missing_imports = true
|
107
105
|
|
108
106
|
[build-system]
|
109
107
|
requires = ["poetry-core"]
|
@@ -1,59 +0,0 @@
|
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|