remotivelabs-cli 0.3.4__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.

Files changed (78) hide show
  1. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/PKG-INFO +3 -2
  2. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/broker/__init__.py +3 -3
  3. remotivelabs_cli-0.3.6b1/cli/broker/defaults.py +1 -0
  4. remotivelabs_cli-0.3.6b1/cli/broker/recording_session/__init__.py +3 -0
  5. remotivelabs_cli-0.3.6b1/cli/broker/recording_session/client.py +68 -0
  6. remotivelabs_cli-0.3.6b1/cli/broker/recording_session/cmd.py +223 -0
  7. remotivelabs_cli-0.3.6b1/cli/broker/recording_session/time.py +49 -0
  8. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/broker/signals.py +2 -3
  9. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/settings/core.py +12 -1
  10. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/utils/console.py +57 -19
  11. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/utils/rest_helper.py +3 -1
  12. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/pyproject.toml +3 -2
  13. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/LICENSE +0 -0
  14. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/README.md +0 -0
  15. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/.DS_Store +0 -0
  16. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/__init__.py +0 -0
  17. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/api/cloud/tokens.py +0 -0
  18. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/broker/discovery.py +0 -0
  19. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/broker/export.py +0 -0
  20. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/broker/files.py +0 -0
  21. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/broker/lib/__about__.py +0 -0
  22. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/broker/lib/broker.py +0 -0
  23. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/broker/lib/client.py +0 -0
  24. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/broker/lib/helper.py +0 -0
  25. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/broker/lib/signalcreator.py +0 -0
  26. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/broker/license_flows.py +0 -0
  27. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/broker/licenses.py +0 -0
  28. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/broker/playback.py +0 -0
  29. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/broker/record.py +0 -0
  30. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/broker/scripting.py +0 -0
  31. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/__init__.py +0 -0
  32. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/auth/__init__.py +0 -0
  33. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/auth/cmd.py +0 -0
  34. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/auth/login.py +0 -0
  35. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/auth_tokens.py +0 -0
  36. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/brokers.py +0 -0
  37. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/configs.py +0 -0
  38. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/licenses/__init__.py +0 -0
  39. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/licenses/cmd.py +0 -0
  40. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/organisations.py +0 -0
  41. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/projects.py +0 -0
  42. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/recordings.py +0 -0
  43. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/recordings_playback.py +0 -0
  44. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/resumable_upload.py +0 -0
  45. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/sample_recordings.py +0 -0
  46. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/service_account_tokens.py +0 -0
  47. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/service_accounts.py +0 -0
  48. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/storage/__init__.py +0 -0
  49. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/storage/cmd.py +0 -0
  50. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/storage/copy.py +0 -0
  51. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/storage/uri_or_path.py +0 -0
  52. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/cloud/uri.py +0 -0
  53. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/connect/__init__.py +0 -0
  54. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/connect/connect.py +0 -0
  55. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/connect/protopie/protopie.py +0 -0
  56. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/remotive.py +0 -0
  57. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/settings/__init__.py +0 -0
  58. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/settings/config_file.py +0 -0
  59. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/settings/migration/__init__.py +0 -0
  60. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/settings/migration/migrate_all_token_files.py +0 -0
  61. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/settings/migration/migrate_config_file.py +0 -0
  62. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/settings/migration/migrate_legacy_dirs.py +0 -0
  63. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/settings/migration/migrate_token_file.py +0 -0
  64. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/settings/migration/migration_tools.py +0 -0
  65. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/settings/state_file.py +0 -0
  66. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/settings/token_file.py +0 -0
  67. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/tools/__init__.py +0 -0
  68. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/tools/can/__init__.py +0 -0
  69. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/tools/can/can.py +0 -0
  70. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/tools/tools.py +0 -0
  71. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/topology/__init__.py +0 -0
  72. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/topology/cmd.py +0 -0
  73. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/topology/start_trial.py +0 -0
  74. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/typer/__init__.py +0 -0
  75. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/typer/typer_utils.py +0 -0
  76. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/utils/__init__.py +0 -0
  77. {remotivelabs_cli-0.3.4 → remotivelabs_cli-0.3.6b1}/cli/utils/time.py +0 -0
  78. {remotivelabs_cli-0.3.4 → 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.4
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 (>=0.9.1,<0.10.0)
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,3 @@
1
+ from cli.broker.recording_session.cmd import app
2
+
3
+ __all__ = ["app"]
@@ -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
@@ -198,7 +197,7 @@ def namespaces(
198
197
 
199
198
  @app.command()
200
199
  def frame_distribution(
201
- url: str = typer.Option(..., help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
200
+ url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
202
201
  api_key: str = typer.Option(None, help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
203
202
  namespace: str = typer.Option(..., help="Namespace"),
204
203
  ) -> None:
@@ -22,6 +22,10 @@ CONFIG_DIR_PATH = Path.home() / ".config" / "remotive"
22
22
  CLI_CONFIG_FILE_NAME = "config.json"
23
23
  CLI_INTERNAL_STATE_FILE_NAME = "app-state.json"
24
24
 
25
+ TOKEN_ENV = "REMOTIVE_CLOUD_AUTH_TOKEN"
26
+ # Deprecated in favour of name used in topology-cli
27
+ DEPR_TOKEN_ENV = "REMOTIVE_CLOUD_ACCESS_TOKEN"
28
+
25
29
 
26
30
  class InvalidSettingsFilePathError(Exception):
27
31
  """Raised when trying to access an invalid settings file or file path"""
@@ -91,8 +95,15 @@ class Settings:
91
95
 
92
96
  def get_active_token(self) -> str | None:
93
97
  """
94
- Get the token secret for the current active account
98
+ Get the token secret for the current active account or token specified by env variable
95
99
  """
100
+
101
+ token = os.environ[DEPR_TOKEN_ENV] if DEPR_TOKEN_ENV in os.environ else None
102
+ if not token:
103
+ token = os.environ[TOKEN_ENV] if TOKEN_ENV in os.environ else None
104
+ if token:
105
+ return token
106
+
96
107
  token_file = self.get_active_token_file()
97
108
  return token_file.token if token_file else None
98
109
 
@@ -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: is this needed?"""
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: should we allow this?"""
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: should we allow this?"""
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))
@@ -85,6 +85,7 @@ class RestHelper:
85
85
  TODO: remove setting org, as we already set the default organization as env in remotive.py?
86
86
  TODO: don't sys.exit, raise error instead
87
87
  """
88
+
88
89
  if "REMOTIVE_CLOUD_ORGANIZATION" not in os.environ:
89
90
  active_account = settings.get_active_account()
90
91
  if active_account:
@@ -94,7 +95,8 @@ class RestHelper:
94
95
 
95
96
  token = access_token
96
97
  if not token:
97
- token = os.environ.get("REMOTIVE_CLOUD_ACCESS_TOKEN", settings.get_active_token())
98
+ token = settings.get_active_token()
99
+
98
100
  if not token:
99
101
  if quiet:
100
102
  return
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "remotivelabs-cli"
3
- version = "0.3.4"
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 = "~=0.9.1"
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"