remotivelabs-cli 0.3.5__tar.gz → 0.3.6b1__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.
Potentially problematic release.
This version of remotivelabs-cli might be problematic. Click here for more details.
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/PKG-INFO +3 -2
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/broker/__init__.py +3 -3
- remotivelabs_cli-0.3.6b1/cli/broker/defaults.py +1 -0
- remotivelabs_cli-0.3.6b1/cli/broker/recording_session/__init__.py +3 -0
- remotivelabs_cli-0.3.6b1/cli/broker/recording_session/client.py +68 -0
- remotivelabs_cli-0.3.6b1/cli/broker/recording_session/cmd.py +223 -0
- remotivelabs_cli-0.3.6b1/cli/broker/recording_session/time.py +49 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/broker/signals.py +1 -2
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/utils/console.py +57 -19
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/pyproject.toml +3 -2
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/LICENSE +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/README.md +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/.DS_Store +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/__init__.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/api/cloud/tokens.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/broker/discovery.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/broker/export.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/broker/files.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/broker/lib/__about__.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/broker/lib/broker.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/broker/lib/client.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/broker/lib/helper.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/broker/lib/signalcreator.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/broker/license_flows.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/broker/licenses.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/broker/playback.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/broker/record.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/broker/scripting.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/__init__.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/auth/__init__.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/auth/cmd.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/auth/login.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/auth_tokens.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/brokers.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/configs.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/licenses/__init__.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/licenses/cmd.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/organisations.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/projects.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/recordings.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/recordings_playback.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/resumable_upload.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/sample_recordings.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/service_account_tokens.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/service_accounts.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/storage/__init__.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/storage/cmd.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/storage/copy.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/storage/uri_or_path.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/cloud/uri.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/connect/__init__.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/connect/connect.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/connect/protopie/protopie.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/remotive.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/settings/__init__.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/settings/config_file.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/settings/core.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/settings/migration/__init__.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/settings/migration/migrate_all_token_files.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/settings/migration/migrate_config_file.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/settings/migration/migrate_legacy_dirs.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/settings/migration/migrate_token_file.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/settings/migration/migration_tools.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/settings/state_file.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/settings/token_file.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/tools/__init__.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/tools/can/__init__.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/tools/can/can.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/tools/tools.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/topology/__init__.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/topology/cmd.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/topology/start_trial.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/typer/__init__.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/typer/typer_utils.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/utils/__init__.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/utils/rest_helper.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/utils/time.py +0 -0
- {remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/utils/versions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: remotivelabs-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6b1
|
|
4
4
|
Summary: CLI for operating RemotiveCloud and RemotiveBroker
|
|
5
5
|
Author: Johan Rask
|
|
6
6
|
Author-email: johan.rask@remotivelabs.com
|
|
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: asyncer (>=0.0.9,<0.0.10)
|
|
14
15
|
Requires-Dist: click (<8.2.0)
|
|
15
16
|
Requires-Dist: email-validator (>=2.2.0,<3.0.0)
|
|
16
17
|
Requires-Dist: grpc-stubs (>=1.53.0.5)
|
|
@@ -20,7 +21,7 @@ Requires-Dist: pydantic (>=2.11.7,<3.0.0)
|
|
|
20
21
|
Requires-Dist: pyjwt (>=2.6,<3.0)
|
|
21
22
|
Requires-Dist: python-can (>=4.3.1)
|
|
22
23
|
Requires-Dist: python-socketio (>=4.6.1)
|
|
23
|
-
Requires-Dist: remotivelabs-broker (
|
|
24
|
+
Requires-Dist: remotivelabs-broker (==0.9.2b4)
|
|
24
25
|
Requires-Dist: requests (>=2.32.4,<3.0.0)
|
|
25
26
|
Requires-Dist: rich (>=13.7.0,<13.8.0)
|
|
26
27
|
Requires-Dist: trogon (>=0.5.0)
|
|
@@ -4,6 +4,7 @@ import os
|
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
|
+
from cli.broker import recording_session
|
|
7
8
|
from cli.broker.discovery import discover as discover_cmd
|
|
8
9
|
from cli.typer import typer_utils
|
|
9
10
|
|
|
@@ -25,12 +26,11 @@ app.callback()(cb)
|
|
|
25
26
|
|
|
26
27
|
# subcommands
|
|
27
28
|
app.add_typer(playback.app, name="playback", help="Manage playing recordings")
|
|
29
|
+
if "PLAYBACK_V2" in os.environ:
|
|
30
|
+
app.add_typer(recording_session.app, name="playback-v2")
|
|
28
31
|
app.add_typer(record.app, name="record", help="Record data on buses")
|
|
29
32
|
app.add_typer(files.app, name="files", help="Upload/Download configurations and recordings")
|
|
30
33
|
app.add_typer(signals.app, name="signals", help="Find and subscribe to signals")
|
|
31
34
|
app.add_typer(export.app, name="export", help="Export to external formats")
|
|
32
35
|
app.add_typer(scripting.app, name="scripting", help="LUA scripting utilities")
|
|
33
36
|
app.add_typer(licenses.app, name="license", help="View and request license to broker")
|
|
34
|
-
|
|
35
|
-
if __name__ == "__main__":
|
|
36
|
-
app()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
DEFAULT_GRPC_URL = "http://localhost:50051"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import AsyncGenerator, Iterable, Optional, Set
|
|
4
|
+
|
|
5
|
+
from remotivelabs.broker.auth import ApiKeyAuth, NoAuth
|
|
6
|
+
from remotivelabs.broker.recording_session import File, RecordingSessionClient
|
|
7
|
+
from remotivelabs.broker.recording_session.file import FileType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RecursiveFilesListingClient:
|
|
11
|
+
"""
|
|
12
|
+
Client for recursively listing files using a broker API.
|
|
13
|
+
|
|
14
|
+
This class provides functionality to recursively list files and directories using
|
|
15
|
+
a RecordingSessionClient. It retrieves files from specified paths and can process them
|
|
16
|
+
with customizable options such as filtering by file types or including directories
|
|
17
|
+
in the results.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
broker_url (str): The URL of the broker API.
|
|
21
|
+
api_key (Optional[str]): The API key used for authentication, if required by the
|
|
22
|
+
broker API.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, broker_url: str, api_key: Optional[str]):
|
|
26
|
+
self._broker_client = RecordingSessionClient(url=broker_url, auth=ApiKeyAuth(api_key) if api_key is not None else NoAuth())
|
|
27
|
+
|
|
28
|
+
async def _list_files(self, path: str = "/") -> list[File]:
|
|
29
|
+
return await self._broker_client.list_recording_files(path=path)
|
|
30
|
+
|
|
31
|
+
async def list_all_files(
|
|
32
|
+
self,
|
|
33
|
+
path: str = "/",
|
|
34
|
+
file_types: Optional[Iterable[FileType]] = None,
|
|
35
|
+
) -> list[File]:
|
|
36
|
+
files: list[File] = []
|
|
37
|
+
async for f in self._iter_files_recursive(root=path, return_types=file_types):
|
|
38
|
+
files.append(f)
|
|
39
|
+
return files
|
|
40
|
+
|
|
41
|
+
async def _iter_files_recursive(
|
|
42
|
+
self,
|
|
43
|
+
root: str = "/",
|
|
44
|
+
*,
|
|
45
|
+
return_types: Optional[Iterable[FileType]] = None,
|
|
46
|
+
include_dirs: bool = False,
|
|
47
|
+
) -> AsyncGenerator[File, None]:
|
|
48
|
+
seen: Set[str] = set()
|
|
49
|
+
|
|
50
|
+
async def _walk(path: str) -> AsyncGenerator[File, None]:
|
|
51
|
+
if path in seen:
|
|
52
|
+
return
|
|
53
|
+
seen.add(path)
|
|
54
|
+
|
|
55
|
+
resp = await self._list_files(path)
|
|
56
|
+
for f in resp or []:
|
|
57
|
+
if f.type == FileType.FILE_TYPE_FOLDER:
|
|
58
|
+
# Optionally yield the directory itself
|
|
59
|
+
if include_dirs and (return_types is None or f.type in return_types):
|
|
60
|
+
yield f
|
|
61
|
+
# Recurse into the folder
|
|
62
|
+
async for file in _walk(f.path):
|
|
63
|
+
yield file
|
|
64
|
+
elif return_types is None or f.type in return_types:
|
|
65
|
+
yield f
|
|
66
|
+
|
|
67
|
+
async for file in _walk(root):
|
|
68
|
+
yield file
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from dataclasses import asdict, is_dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from functools import partial
|
|
7
|
+
from typing import Any, AsyncIterator, Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from asyncer import syncify
|
|
11
|
+
from remotivelabs.broker.auth import ApiKeyAuth, NoAuth
|
|
12
|
+
from remotivelabs.broker.recording_session import RecordingSessionClient, RecordingSessionPlaybackStatus
|
|
13
|
+
|
|
14
|
+
from cli.broker.defaults import DEFAULT_GRPC_URL
|
|
15
|
+
from cli.broker.recording_session.client import RecursiveFilesListingClient
|
|
16
|
+
from cli.broker.recording_session.time import time_offset_to_us
|
|
17
|
+
from cli.typer import typer_utils
|
|
18
|
+
from cli.utils.console import print_generic_error, print_result
|
|
19
|
+
|
|
20
|
+
app = typer_utils.create_typer(
|
|
21
|
+
help="""
|
|
22
|
+
Manage playback of recording sessions
|
|
23
|
+
|
|
24
|
+
All offsets are in microseconds (μs)
|
|
25
|
+
"""
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _int_or_none(offset: Optional[str | int]) -> Optional[int]:
|
|
30
|
+
return offset if offset is None else int(offset)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _print_offset_help(cmd: str) -> str:
|
|
34
|
+
return f"""
|
|
35
|
+
Offsets can be specified in minutes (1:15min), seconds(10s), millis(10000ms) or micros(10000000us), default without suffix is micros.
|
|
36
|
+
Samples offsets
|
|
37
|
+
{cmd} 1.15min, 10s, 10000ms, 10000000us, 10000000,
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _custom_types(o: Any) -> Any:
|
|
42
|
+
if isinstance(o, Enum):
|
|
43
|
+
return o.name
|
|
44
|
+
if is_dataclass(type(o)):
|
|
45
|
+
return asdict(o)
|
|
46
|
+
if isinstance(o, datetime.datetime):
|
|
47
|
+
return o.isoformat(timespec="seconds")
|
|
48
|
+
raise TypeError(f"Object of type {o.__class__.__name__} is not JSON serializable")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command()
|
|
52
|
+
@partial(syncify, raise_sync_error=False)
|
|
53
|
+
async def list_files(
|
|
54
|
+
path: str = typer.Argument("/", help="Optional subdirectory to list files in, defaults to /"),
|
|
55
|
+
recursive: bool = typer.Option(False, help="List subdirectories recursively"),
|
|
56
|
+
url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
|
|
57
|
+
api_key: str = typer.Option("offline", help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
|
|
58
|
+
) -> None:
|
|
59
|
+
"""
|
|
60
|
+
List files on broker.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
if recursive:
|
|
64
|
+
file_listing_client = RecursiveFilesListingClient(broker_url=url, api_key=api_key)
|
|
65
|
+
print_result(
|
|
66
|
+
await file_listing_client.list_all_files(path, file_types=None), # Expose file-types in next version
|
|
67
|
+
default=_custom_types,
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
client = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key is not None else NoAuth())
|
|
71
|
+
print_result(await client.list_recording_files(path), default=_custom_types)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
print_generic_error(str(e))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.command(
|
|
77
|
+
help=f"""
|
|
78
|
+
Starts playing the recording at current offset or from specified offset
|
|
79
|
+
{_print_offset_help("--offset")}
|
|
80
|
+
"""
|
|
81
|
+
)
|
|
82
|
+
@partial(syncify, raise_sync_error=False)
|
|
83
|
+
async def play( # noqa: PLR0913
|
|
84
|
+
path: str = typer.Argument(..., help="Path to the recording session", envvar="REMOTIVE_RECORDING_SESSION_PATH"),
|
|
85
|
+
offset: str = typer.Option(None, callback=time_offset_to_us, help="Offset to play from"),
|
|
86
|
+
url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
|
|
87
|
+
api_key: str = typer.Option("offline", help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
|
|
88
|
+
) -> None:
|
|
89
|
+
try:
|
|
90
|
+
client = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key is not None else NoAuth())
|
|
91
|
+
print_result(await client.get_session(path=path).play(offset=_int_or_none(offset)), default=_custom_types)
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
print_generic_error(str(e))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@app.command(
|
|
98
|
+
help=f"""
|
|
99
|
+
Repeat RecordingSession in specific interval or complete recording
|
|
100
|
+
To remove existing repeat config, use --clear flag.
|
|
101
|
+
{_print_offset_help("--startOffset/--endOffset")}
|
|
102
|
+
"""
|
|
103
|
+
)
|
|
104
|
+
@partial(syncify, raise_sync_error=False)
|
|
105
|
+
async def repeat( # noqa: PLR0913
|
|
106
|
+
path: str = typer.Argument(..., help="Path to the recording session", envvar="REMOTIVE_RECORDING_SESSION_PATH"),
|
|
107
|
+
start_offset: str = typer.Option(0, callback=time_offset_to_us, help="Repeat start offset, defaults to start"),
|
|
108
|
+
end_offset: str = typer.Option(None, callback=time_offset_to_us, help="Repeat end offset, defaults to end"),
|
|
109
|
+
clear: bool = typer.Option(False, help="Clear repeat"),
|
|
110
|
+
url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
|
|
111
|
+
api_key: str = typer.Option("offline", help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
|
|
112
|
+
) -> None:
|
|
113
|
+
""" """
|
|
114
|
+
try:
|
|
115
|
+
session = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key is not None else NoAuth()).get_session(path)
|
|
116
|
+
if clear:
|
|
117
|
+
print_result(await session.set_repeat(start_offset=None, end_offset=None), _custom_types)
|
|
118
|
+
else:
|
|
119
|
+
print_result(
|
|
120
|
+
await session.set_repeat(start_offset=int(start_offset), end_offset=_int_or_none(end_offset)),
|
|
121
|
+
_custom_types,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
print_generic_error(str(e))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@app.command(
|
|
129
|
+
help=f"""
|
|
130
|
+
Pause the recording at current offset or specified offset
|
|
131
|
+
{_print_offset_help("--offset")}
|
|
132
|
+
"""
|
|
133
|
+
)
|
|
134
|
+
@partial(syncify, raise_sync_error=False)
|
|
135
|
+
async def pause(
|
|
136
|
+
path: str = typer.Argument(..., help="Path to the recording session", envvar="REMOTIVE_RECORDING_SESSION_PATH"),
|
|
137
|
+
offset: str = typer.Option(None, callback=time_offset_to_us, help="Offset to play from"),
|
|
138
|
+
url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
|
|
139
|
+
api_key: str = typer.Option("offline", help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
|
|
140
|
+
) -> None:
|
|
141
|
+
try:
|
|
142
|
+
session = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key is not None else NoAuth()).get_session(path)
|
|
143
|
+
print_result(await session.pause(offset=_int_or_none(offset)), default=_custom_types)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
print_generic_error(str(e))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.command(
|
|
149
|
+
help=f"""
|
|
150
|
+
Seek to specified offset
|
|
151
|
+
{_print_offset_help("--offset")}
|
|
152
|
+
"""
|
|
153
|
+
)
|
|
154
|
+
@partial(syncify, raise_sync_error=False)
|
|
155
|
+
async def seek(
|
|
156
|
+
path: str = typer.Argument(..., help="Path to the recording session", envvar="REMOTIVE_RECORDING_SESSION_PATH"),
|
|
157
|
+
offset: str = typer.Option(..., callback=time_offset_to_us, help="Offset to seek to"),
|
|
158
|
+
url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
|
|
159
|
+
api_key: str = typer.Option("offline", help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
|
|
160
|
+
) -> None:
|
|
161
|
+
try:
|
|
162
|
+
session = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key is not None else NoAuth()).get_session(path)
|
|
163
|
+
print_result(await session.seek(offset=int(offset)), default=_custom_types)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
print_generic_error(str(e))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@app.command()
|
|
169
|
+
@partial(syncify, raise_sync_error=False)
|
|
170
|
+
async def open( # noqa: PLR0913
|
|
171
|
+
path: str = typer.Argument(..., help="Path to the recording session", envvar="REMOTIVE_RECORDING_SESSION_PATH"),
|
|
172
|
+
force: bool = typer.Option(False, help="Force close and re-open recording session if exists"),
|
|
173
|
+
url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
|
|
174
|
+
api_key: str = typer.Option("offline", help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
|
|
175
|
+
) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Open a recording session.
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
session = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key is not None else NoAuth()).get_session(path)
|
|
181
|
+
print_result(await session.open(force_reopen=force), default=_custom_types)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
print_generic_error(str(e))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@app.command()
|
|
187
|
+
@partial(syncify, raise_sync_error=False)
|
|
188
|
+
async def close(
|
|
189
|
+
path: str = typer.Argument(..., help="Path to the recording session", envvar="REMOTIVE_RECORDING_SESSION_PATH"),
|
|
190
|
+
url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
|
|
191
|
+
api_key: str = typer.Option("offline", help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
|
|
192
|
+
) -> None:
|
|
193
|
+
"""
|
|
194
|
+
Close a recording session.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
session = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key is not None else NoAuth()).get_session(path)
|
|
199
|
+
print_result(await session.close(), default=_custom_types)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
print_generic_error(str(e))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@app.command()
|
|
205
|
+
@partial(syncify, raise_sync_error=False)
|
|
206
|
+
async def status(
|
|
207
|
+
url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
|
|
208
|
+
api_key: str = typer.Option("offline", help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
|
|
209
|
+
) -> None:
|
|
210
|
+
"""
|
|
211
|
+
Get the status of a recording session.
|
|
212
|
+
"""
|
|
213
|
+
try:
|
|
214
|
+
client = RecordingSessionClient(url, auth=ApiKeyAuth(api_key) if api_key is not None else NoAuth())
|
|
215
|
+
|
|
216
|
+
async def _async_playback_stream() -> None:
|
|
217
|
+
stream: AsyncIterator[list[RecordingSessionPlaybackStatus]] = client.playback_status()
|
|
218
|
+
async for f in stream:
|
|
219
|
+
print_result(f, default=_custom_types)
|
|
220
|
+
|
|
221
|
+
await _async_playback_stream()
|
|
222
|
+
except Exception as e:
|
|
223
|
+
print_generic_error(str(e))
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InvalidOffsetException(typer.BadParameter): # noqa: N818
|
|
10
|
+
"""
|
|
11
|
+
Thrown if invalid time, extends BadParameter to hook into typer validation
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def time_offset_to_us(value: Optional[str]) -> Optional[int]:
|
|
16
|
+
"""
|
|
17
|
+
Parse a time string like '1s', '1ms', '1us', '1.02min' or '30' (default µs) into microseconds (int).
|
|
18
|
+
For minutes: 1.02min == 1 minute 2 seconds
|
|
19
|
+
"""
|
|
20
|
+
if value is None:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
value = value.strip().lower()
|
|
24
|
+
|
|
25
|
+
# Check if it's a minute format with decimal or colon
|
|
26
|
+
min_match = re.fullmatch(r"([+-]?\d+)[.:](\d+)min", value)
|
|
27
|
+
if min_match:
|
|
28
|
+
minutes = int(min_match.group(1))
|
|
29
|
+
seconds = int(min_match.group(2))
|
|
30
|
+
return (minutes * 60 + seconds) * 1_000_000
|
|
31
|
+
|
|
32
|
+
# Support optional + or - sign before the number
|
|
33
|
+
match = re.fullmatch(r"([+-]?\d+(?:\.\d+)?)(?:\s*(us|µs|ms|s|min))?", value)
|
|
34
|
+
if not match:
|
|
35
|
+
raise InvalidOffsetException(f"Invalid time format: '{value}'")
|
|
36
|
+
|
|
37
|
+
amount, unit = match.groups()
|
|
38
|
+
amount = float(amount)
|
|
39
|
+
unit = unit or "us" # Default to microseconds
|
|
40
|
+
|
|
41
|
+
unit_multipliers = {
|
|
42
|
+
"min": 60_000_000,
|
|
43
|
+
"s": 1_000_000,
|
|
44
|
+
"ms": 1_000,
|
|
45
|
+
"us": 1,
|
|
46
|
+
"µs": 1,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return int(amount * unit_multipliers[unit])
|
|
@@ -16,12 +16,11 @@ import typer
|
|
|
16
16
|
from cli.typer import typer_utils
|
|
17
17
|
from cli.utils.console import print_generic_error, print_generic_message, print_grpc_error, print_hint
|
|
18
18
|
|
|
19
|
+
from .defaults import DEFAULT_GRPC_URL
|
|
19
20
|
from .lib.broker import Broker, SubscribableSignal
|
|
20
21
|
|
|
21
22
|
app = typer_utils.create_typer(help=help)
|
|
22
23
|
|
|
23
|
-
DEFAULT_GRPC_URL = "http://localhost:50051"
|
|
24
|
-
|
|
25
24
|
|
|
26
25
|
class Signals(TypedDict):
|
|
27
26
|
name: str
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
4
5
|
import sys
|
|
5
6
|
from typing import Any
|
|
@@ -12,6 +13,7 @@ err_console = Console(stderr=True, soft_wrap=True)
|
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def print_grpc_error(error: grpc.RpcError) -> None:
|
|
16
|
+
"""TODO: remove me"""
|
|
15
17
|
if error.code() == grpc.StatusCode.UNAUTHENTICATED:
|
|
16
18
|
is_access_token = os.environ["ACCESS_TOKEN"]
|
|
17
19
|
if is_access_token is not None and is_access_token == "true":
|
|
@@ -26,24 +28,8 @@ def print_grpc_error(error: grpc.RpcError) -> None:
|
|
|
26
28
|
sys.exit(1)
|
|
27
29
|
|
|
28
30
|
|
|
29
|
-
def print_hint(message: str) -> None:
|
|
30
|
-
err_console.print(f":point_right: [bold]{message}[/bold]")
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def print_generic_error(message: str) -> None:
|
|
34
|
-
err_console.print(f":boom: [bold red]Failed[/bold red]: {message}")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def print_success(message: str) -> None:
|
|
38
|
-
console.print(f"[bold green]Success![/bold green] {message}")
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def print_generic_message(message: str) -> None:
|
|
42
|
-
console.print(f"[bold]{message}[/bold]")
|
|
43
|
-
|
|
44
|
-
|
|
45
31
|
def print_newline() -> None:
|
|
46
|
-
"""TODO:
|
|
32
|
+
"""TODO: remove me"""
|
|
47
33
|
console.print("\n")
|
|
48
34
|
|
|
49
35
|
|
|
@@ -52,10 +38,62 @@ def print_url(url: str) -> None:
|
|
|
52
38
|
|
|
53
39
|
|
|
54
40
|
def print_unformatted(message: Any) -> None:
|
|
55
|
-
"""TODO:
|
|
41
|
+
"""TODO: remove me"""
|
|
56
42
|
console.print(message)
|
|
57
43
|
|
|
58
44
|
|
|
59
45
|
def print_unformatted_to_stderr(message: Any) -> None:
|
|
60
|
-
"""TODO:
|
|
46
|
+
"""TODO: remove me"""
|
|
61
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))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "remotivelabs-cli"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.6b1"
|
|
4
4
|
description = "CLI for operating RemotiveCloud and RemotiveBroker"
|
|
5
5
|
authors = ["Johan Rask <johan.rask@remotivelabs.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -15,7 +15,7 @@ python = ">=3.9,<4"
|
|
|
15
15
|
trogon = ">=0.5.0"
|
|
16
16
|
typer = "0.12.5"
|
|
17
17
|
click = "<8.2.0"
|
|
18
|
-
remotivelabs-broker = "
|
|
18
|
+
remotivelabs-broker = "0.9.2b4"
|
|
19
19
|
rich = "~=13.7.0"
|
|
20
20
|
pyjwt = "~=2.6"
|
|
21
21
|
zeroconf = "~=0.127.0"
|
|
@@ -29,6 +29,7 @@ types-requests = "^2.32.0.20240622"
|
|
|
29
29
|
pydantic = "^2.11.7"
|
|
30
30
|
email-validator = "^2.2.0"
|
|
31
31
|
requests = "^2.32.4"
|
|
32
|
+
asyncer = "^0.0.9"
|
|
32
33
|
|
|
33
34
|
[tool.poetry.group.test.dependencies]
|
|
34
35
|
pytest = "^8.3"
|
|
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
|
|
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
|
{remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/settings/migration/migrate_config_file.py
RENAMED
|
File without changes
|
{remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/settings/migration/migrate_legacy_dirs.py
RENAMED
|
File without changes
|
{remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/settings/migration/migrate_token_file.py
RENAMED
|
File without changes
|
{remotivelabs_cli-0.3.5 → remotivelabs_cli-0.3.6b1}/cli/settings/migration/migration_tools.py
RENAMED
|
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
|