remotivelabs-cli 0.5.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. remotivelabs/cli/__init__.py +0 -0
  2. remotivelabs/cli/api/cloud/tokens.py +62 -0
  3. remotivelabs/cli/broker/__init__.py +33 -0
  4. remotivelabs/cli/broker/defaults.py +1 -0
  5. remotivelabs/cli/broker/discovery.py +43 -0
  6. remotivelabs/cli/broker/export.py +92 -0
  7. remotivelabs/cli/broker/files.py +119 -0
  8. remotivelabs/cli/broker/lib/__about__.py +4 -0
  9. remotivelabs/cli/broker/lib/broker.py +625 -0
  10. remotivelabs/cli/broker/lib/client.py +224 -0
  11. remotivelabs/cli/broker/lib/helper.py +277 -0
  12. remotivelabs/cli/broker/lib/signalcreator.py +196 -0
  13. remotivelabs/cli/broker/license_flows.py +167 -0
  14. remotivelabs/cli/broker/licenses.py +98 -0
  15. remotivelabs/cli/broker/playback.py +117 -0
  16. remotivelabs/cli/broker/record.py +41 -0
  17. remotivelabs/cli/broker/recording_session/__init__.py +3 -0
  18. remotivelabs/cli/broker/recording_session/client.py +67 -0
  19. remotivelabs/cli/broker/recording_session/cmd.py +254 -0
  20. remotivelabs/cli/broker/recording_session/time.py +49 -0
  21. remotivelabs/cli/broker/scripting.py +129 -0
  22. remotivelabs/cli/broker/signals.py +220 -0
  23. remotivelabs/cli/broker/version.py +31 -0
  24. remotivelabs/cli/cloud/__init__.py +17 -0
  25. remotivelabs/cli/cloud/auth/__init__.py +3 -0
  26. remotivelabs/cli/cloud/auth/cmd.py +128 -0
  27. remotivelabs/cli/cloud/auth/login.py +283 -0
  28. remotivelabs/cli/cloud/auth_tokens.py +149 -0
  29. remotivelabs/cli/cloud/brokers.py +109 -0
  30. remotivelabs/cli/cloud/configs.py +109 -0
  31. remotivelabs/cli/cloud/licenses/__init__.py +0 -0
  32. remotivelabs/cli/cloud/licenses/cmd.py +14 -0
  33. remotivelabs/cli/cloud/organisations.py +112 -0
  34. remotivelabs/cli/cloud/projects.py +44 -0
  35. remotivelabs/cli/cloud/recordings.py +580 -0
  36. remotivelabs/cli/cloud/recordings_playback.py +274 -0
  37. remotivelabs/cli/cloud/resumable_upload.py +87 -0
  38. remotivelabs/cli/cloud/sample_recordings.py +25 -0
  39. remotivelabs/cli/cloud/service_account_tokens.py +62 -0
  40. remotivelabs/cli/cloud/service_accounts.py +72 -0
  41. remotivelabs/cli/cloud/storage/__init__.py +5 -0
  42. remotivelabs/cli/cloud/storage/cmd.py +76 -0
  43. remotivelabs/cli/cloud/storage/copy.py +86 -0
  44. remotivelabs/cli/cloud/storage/uri_or_path.py +45 -0
  45. remotivelabs/cli/cloud/uri.py +113 -0
  46. remotivelabs/cli/connect/__init__.py +0 -0
  47. remotivelabs/cli/connect/connect.py +118 -0
  48. remotivelabs/cli/connect/protopie/protopie.py +185 -0
  49. remotivelabs/cli/py.typed +0 -0
  50. remotivelabs/cli/remotive.py +123 -0
  51. remotivelabs/cli/settings/__init__.py +20 -0
  52. remotivelabs/cli/settings/config_file.py +113 -0
  53. remotivelabs/cli/settings/core.py +333 -0
  54. remotivelabs/cli/settings/migration/__init__.py +0 -0
  55. remotivelabs/cli/settings/migration/migrate_all_token_files.py +80 -0
  56. remotivelabs/cli/settings/migration/migrate_config_file.py +64 -0
  57. remotivelabs/cli/settings/migration/migrate_legacy_dirs.py +50 -0
  58. remotivelabs/cli/settings/migration/migrate_token_file.py +52 -0
  59. remotivelabs/cli/settings/migration/migration_tools.py +38 -0
  60. remotivelabs/cli/settings/state_file.py +67 -0
  61. remotivelabs/cli/settings/token_file.py +128 -0
  62. remotivelabs/cli/tools/__init__.py +0 -0
  63. remotivelabs/cli/tools/can/__init__.py +0 -0
  64. remotivelabs/cli/tools/can/can.py +78 -0
  65. remotivelabs/cli/tools/tools.py +9 -0
  66. remotivelabs/cli/topology/__init__.py +28 -0
  67. remotivelabs/cli/topology/all.py +322 -0
  68. remotivelabs/cli/topology/cli/__init__.py +3 -0
  69. remotivelabs/cli/topology/cli/run_in_docker.py +58 -0
  70. remotivelabs/cli/topology/cli/topology_cli.py +16 -0
  71. remotivelabs/cli/topology/cmd.py +130 -0
  72. remotivelabs/cli/topology/start_trial.py +134 -0
  73. remotivelabs/cli/typer/__init__.py +0 -0
  74. remotivelabs/cli/typer/typer_utils.py +27 -0
  75. remotivelabs/cli/utils/__init__.py +0 -0
  76. remotivelabs/cli/utils/console.py +99 -0
  77. remotivelabs/cli/utils/rest_helper.py +369 -0
  78. remotivelabs/cli/utils/time.py +11 -0
  79. remotivelabs/cli/utils/versions.py +120 -0
  80. remotivelabs_cli-0.5.0a1.dist-info/METADATA +51 -0
  81. remotivelabs_cli-0.5.0a1.dist-info/RECORD +84 -0
  82. remotivelabs_cli-0.5.0a1.dist-info/WHEEL +4 -0
  83. remotivelabs_cli-0.5.0a1.dist-info/entry_points.txt +3 -0
  84. remotivelabs_cli-0.5.0a1.dist-info/licenses/LICENSE +17 -0
@@ -0,0 +1,625 @@
1
+ from __future__ import annotations
2
+
3
+ import binascii
4
+ import hashlib
5
+ import itertools
6
+ import ntpath
7
+ import os
8
+ import posixpath
9
+ import queue
10
+ import signal as os_signal
11
+ import sys
12
+ import tempfile
13
+ import time
14
+ import zipfile
15
+ from dataclasses import dataclass
16
+ from glob import glob
17
+ from threading import Thread
18
+ from typing import Any, BinaryIO, Callable, Dict, Generator, Iterable, List, Sequence, Union
19
+
20
+ import grpc
21
+ import typer
22
+ from google.protobuf.json_format import MessageToDict
23
+
24
+ import remotivelabs.broker._generated.common_pb2 as common
25
+ import remotivelabs.broker._generated.network_api_pb2 as network_api
26
+ import remotivelabs.broker._generated.network_api_pb2_grpc as network_api_grpc
27
+ import remotivelabs.broker._generated.system_api_pb2 as system_api
28
+ import remotivelabs.broker._generated.system_api_pb2_grpc as system_api_grpc
29
+ import remotivelabs.broker._generated.traffic_api_pb2 as traffic_api
30
+ import remotivelabs.broker._generated.traffic_api_pb2_grpc as traffic_api_grpc
31
+ from remotivelabs.cli.broker.lib.helper import act_on_scripted_signal, act_on_signal, create_channel
32
+ from remotivelabs.cli.broker.lib.signalcreator import SignalCreator
33
+ from remotivelabs.cli.settings import settings
34
+ from remotivelabs.cli.utils.console import print_generic_error, print_hint, print_success
35
+
36
+
37
+ @dataclass
38
+ class SubscribableSignal:
39
+ name: str
40
+ namespace: str
41
+
42
+
43
+ @dataclass
44
+ class LicenseInfo:
45
+ valid: bool
46
+ expires: str
47
+ email: str
48
+ machine_id: str
49
+
50
+
51
+ def get_sha256(path: str) -> str:
52
+ """
53
+ Calculate SHA256 for a file.
54
+ """
55
+ with open(path, "rb") as f:
56
+ b = f.read() # read entire file as bytes
57
+ return hashlib.sha256(b).hexdigest()
58
+
59
+
60
+ def generate_data(file: BinaryIO, dest_path: str, chunk_size: int, sha256: str) -> Generator[system_api.FileUploadRequest, None, None]:
61
+ for x in itertools.count(start=0):
62
+ if x == 0:
63
+ file_description = system_api.FileDescription(sha256=sha256, path=dest_path)
64
+ yield system_api.FileUploadRequest(fileDescription=file_description)
65
+ else:
66
+ buf = file.read(chunk_size)
67
+ if not buf:
68
+ break
69
+ yield system_api.FileUploadRequest(chunk=buf)
70
+
71
+
72
+ class Broker:
73
+ def __init__(self, url: str, api_key: Union[str, None] = None) -> None:
74
+ self.url = url
75
+ self.api_key = api_key
76
+ self.q: queue.Queue[Any] = queue.Queue()
77
+ """Main function, checking arguments passed to script, setting up stubs, configuration and starting Threads."""
78
+ # Setting up stubs and configuration
79
+
80
+ if api_key is None or api_key == "":
81
+ if url.startswith("https"):
82
+ self.intercept_channel = create_channel(url, None, settings.get_active_token())
83
+ # TODO - Temporary solution to print proper error message, remove ENV once api-key is gone
84
+ os.environ["ACCESS_TOKEN"] = "true"
85
+ else:
86
+ os.environ["ACCESS_TOKEN"] = "false"
87
+ self.intercept_channel = create_channel(url, None, None)
88
+ else:
89
+ print_hint("Option --api-key will is deprecated and will be removed. Use access access tokens by logging in with cli.")
90
+ os.environ["ACCESS_TOKEN"] = "false"
91
+ self.intercept_channel = create_channel(url, api_key, None)
92
+
93
+ self.network_stub = network_api_grpc.NetworkServiceStub(self.intercept_channel)
94
+ self.system_stub = system_api_grpc.SystemServiceStub(self.intercept_channel)
95
+ self.traffic_stub = traffic_api_grpc.TrafficServiceStub(self.intercept_channel)
96
+ self.signal_creator = SignalCreator(self.system_stub)
97
+
98
+ @staticmethod
99
+ def __check_playbackmode_result(status: traffic_api.PlaybackInfos) -> None:
100
+ err_cnt = 0
101
+ for mode in status.playbackInfo:
102
+ if mode.playbackMode.errorMessage:
103
+ print_generic_error(mode.playbackMode.errorMessage)
104
+ err_cnt = err_cnt + 1
105
+ if err_cnt > 0:
106
+ raise typer.Exit(1)
107
+
108
+ def seek(self, recording_and_namespace: List[Any], offset: int, silent: bool = True) -> traffic_api.PlaybackInfos:
109
+ def to_playback(rec: Any) -> Dict[str, Any]:
110
+ return {"namespace": rec["namespace"], "path": rec["recording"], "mode": traffic_api.Mode.SEEK, "offsettime": offset}
111
+
112
+ playback_list = map(to_playback, recording_and_namespace)
113
+
114
+ infos = traffic_api.PlaybackInfos(playbackInfo=list(map(self.__create_playback_config, playback_list)))
115
+ status: traffic_api.PlaybackInfos = self.traffic_stub.PlayTraffic(infos)
116
+ if not silent:
117
+ self.__check_playbackmode_result(status)
118
+ return status
119
+
120
+ def play(self, recording_and_namespace: List[Any], silent: bool = False) -> traffic_api.PlaybackInfos:
121
+ def to_playback(rec: Any) -> Dict[str, Any]:
122
+ return {
123
+ "namespace": rec["namespace"],
124
+ "path": rec["recording"],
125
+ "mode": traffic_api.Mode.PLAY,
126
+ }
127
+
128
+ playback_list = map(to_playback, recording_and_namespace)
129
+
130
+ status: traffic_api.PlaybackInfos = self.traffic_stub.PlayTraffic(
131
+ traffic_api.PlaybackInfos(playbackInfo=list(map(self.__create_playback_config, playback_list)))
132
+ )
133
+
134
+ if not silent:
135
+ self.__check_playbackmode_result(status)
136
+ return status
137
+
138
+ def stop_play(self, recording_and_namespace: List[Any], silent: bool = False) -> traffic_api.PlaybackInfos:
139
+ def to_playback(rec: Any) -> Dict[str, Any]:
140
+ return {
141
+ "namespace": rec["namespace"],
142
+ "path": rec["recording"],
143
+ "mode": traffic_api.Mode.STOP,
144
+ }
145
+
146
+ playback_list = map(to_playback, recording_and_namespace)
147
+
148
+ status: traffic_api.PlaybackInfos = self.traffic_stub.PlayTraffic(
149
+ traffic_api.PlaybackInfos(playbackInfo=list(map(self.__create_playback_config, playback_list)))
150
+ )
151
+ if not silent:
152
+ self.__check_playbackmode_result(status)
153
+ return status
154
+
155
+ def pause_play(self, recording_and_namespace: List[Any], silent: bool = False) -> traffic_api.PlaybackInfos:
156
+ def to_playback(rec: Any) -> Dict[str, Any]:
157
+ return {
158
+ "namespace": rec["namespace"],
159
+ "path": rec["recording"],
160
+ "mode": traffic_api.Mode.PAUSE,
161
+ }
162
+
163
+ playback_list = map(to_playback, recording_and_namespace)
164
+
165
+ status: traffic_api.PlaybackInfos = self.traffic_stub.PlayTraffic(
166
+ traffic_api.PlaybackInfos(playbackInfo=list(map(self.__create_playback_config, playback_list)))
167
+ )
168
+ if not silent:
169
+ self.__check_playbackmode_result(status)
170
+ return status
171
+
172
+ def record_multiple(self, namespaces: List[str], path: str) -> traffic_api.PlaybackInfos:
173
+ def to_playback(namespace: str) -> Dict[str, Any]:
174
+ return {
175
+ "namespace": namespace,
176
+ "path": path + "_" + namespace,
177
+ "mode": traffic_api.Mode.RECORD,
178
+ }
179
+
180
+ playback_list = list(map(to_playback, namespaces))
181
+
182
+ status: traffic_api.PlaybackInfos = self.traffic_stub.PlayTraffic(
183
+ traffic_api.PlaybackInfos(playbackInfo=list(map(self.__create_playback_config, playback_list)))
184
+ )
185
+ self.__check_playbackmode_result(status)
186
+ return status
187
+
188
+ def record(self, namespace: str, path: str) -> traffic_api.PlaybackInfos:
189
+ playback_list = [
190
+ {
191
+ "namespace": namespace,
192
+ "path": path,
193
+ "mode": traffic_api.Mode.RECORD,
194
+ }
195
+ ]
196
+
197
+ status: traffic_api.PlaybackInfos = self.traffic_stub.PlayTraffic(
198
+ traffic_api.PlaybackInfos(playbackInfo=list(map(self.__create_playback_config, playback_list)))
199
+ )
200
+ self.__check_playbackmode_result(status)
201
+ return status
202
+
203
+ def stop(self, namespace: str, path: str, silent: bool = False) -> traffic_api.PlaybackInfos:
204
+ playback_list = [
205
+ {
206
+ "namespace": namespace,
207
+ "path": path,
208
+ "mode": traffic_api.Mode.STOP,
209
+ }
210
+ ]
211
+
212
+ status: traffic_api.PlaybackInfos = self.traffic_stub.PlayTraffic(
213
+ traffic_api.PlaybackInfos(playbackInfo=list(map(self.__create_playback_config, playback_list)))
214
+ )
215
+ if not silent:
216
+ self.__check_playbackmode_result(status)
217
+ return status
218
+
219
+ def listen_on_playback(self, repeat: bool, recording_and_namespace: List[Any], callback: Callable[[int, int, str], None]) -> None:
220
+ # include recording_and_namespace if we want to loop the recording
221
+ # This can probably be improved
222
+ def get_mode(mode: int) -> str:
223
+ if mode == 0:
224
+ return "playing"
225
+ if mode == 1:
226
+ return "paused"
227
+ if mode == 2:
228
+ return "stopped"
229
+ raise ValueError("Unknown Mode")
230
+
231
+ sub = self.traffic_stub.PlayTrafficStatus(common.Empty())
232
+ for playback_state in sub:
233
+ # p = typing.cast(br.traffic_api_pb2.PlaybackInfos, playback_state) # REDUNDANT CAST
234
+ p = playback_state
235
+ offset_length = int(p.playbackInfo[0].playbackMode.offsetTime / 1000000)
236
+ start_time = p.playbackInfo[0].playbackMode.startTime
237
+ end_time = p.playbackInfo[0].playbackMode.endTime
238
+ mode = p.playbackInfo[0].playbackMode.mode
239
+
240
+ total_length = int((end_time - start_time) / 1000000)
241
+
242
+ if mode == 2 and repeat:
243
+ # If we get a stop and is fairly (this is mostly not 100%) close to the end
244
+ # we repeat the recording when files are included
245
+ if abs(total_length - offset_length) < 5:
246
+ self.play(recording_and_namespace)
247
+ callback(offset_length, total_length, get_mode(mode))
248
+
249
+ def listen_on_frame_distribution(self, namespace: str, callback: Callable[[Dict[str, Any]], None]) -> None:
250
+ config = network_api.FramesDistributionConfig(namespace=common.NameSpace(name=namespace))
251
+ frame_distribution_stream = self.network_stub.SubscribeToFramesDistribution(config)
252
+ for frame in frame_distribution_stream:
253
+ f = MessageToDict(frame, preserving_proto_field_name=True)
254
+ callback(f)
255
+
256
+ def pause(self, namespace: str, path: str, silent: bool = False) -> traffic_api.PlaybackInfos:
257
+ playback_list = [
258
+ {
259
+ "namespace": namespace,
260
+ "path": path,
261
+ "mode": traffic_api.Mode.PAUSE,
262
+ }
263
+ ]
264
+
265
+ status: traffic_api.PlaybackInfos = self.traffic_stub.PlayTraffic(
266
+ traffic_api.PlaybackInfos(playbackInfo=list(map(self.__create_playback_config, playback_list)))
267
+ )
268
+ if not silent:
269
+ self.__check_playbackmode_result(status)
270
+ return status
271
+
272
+ def stop_multiple(self, namespaces: List[str], path: str) -> traffic_api.PlaybackInfos:
273
+ def to_playback(namespace: str) -> Dict[str, Any]:
274
+ return {
275
+ "namespace": namespace,
276
+ "path": path + "_" + namespace,
277
+ "mode": traffic_api.Mode.STOP,
278
+ }
279
+
280
+ playback_list = list(map(to_playback, namespaces))
281
+
282
+ status: traffic_api.PlaybackInfos = self.traffic_stub.PlayTraffic(
283
+ traffic_api.PlaybackInfos(playbackInfo=list(map(self.__create_playback_config, playback_list)))
284
+ )
285
+ self.__check_playbackmode_result(status)
286
+ return status
287
+
288
+ def diagnose_stop(self, namespace: List[str]) -> None:
289
+ recording_name = "diagnose__"
290
+ self.stop_multiple(namespace, recording_name)
291
+
292
+ def diagnose(self, namespace: List[str], wait_for_traffic: bool = False) -> None:
293
+ recording_name = "diagnose__"
294
+
295
+ keep_running = True
296
+ keep_running_during_recording = True
297
+
298
+ def exit_on_ctrlc(_sig: Any, _frame: Any) -> None:
299
+ nonlocal keep_running
300
+ keep_running = False
301
+ nonlocal keep_running_during_recording
302
+ keep_running_during_recording = False
303
+ # progress.add_task(description=f"Cleaning up, please wait...", total=None)
304
+
305
+ os_signal.signal(os_signal.SIGINT, exit_on_ctrlc)
306
+
307
+ while keep_running:
308
+ keep_running = wait_for_traffic
309
+ self.record_multiple(namespace, recording_name)
310
+ for i in range(5):
311
+ if keep_running_during_recording:
312
+ time.sleep(1)
313
+
314
+ self.stop_multiple(namespace, recording_name)
315
+
316
+ response = []
317
+ with tempfile.TemporaryDirectory() as tmpdirname:
318
+ for ns in namespace:
319
+ path = recording_name + "_" + ns
320
+ tmp_file = os.path.join(tmpdirname, path)
321
+ self.download(path, tmp_file)
322
+ self.delete_files([path], False)
323
+ with zipfile.ZipFile(tmp_file, "r") as zip_ref:
324
+ zip_ref.extractall(tmpdirname)
325
+
326
+ file_stat = os.stat(os.path.join(tmpdirname, path + ".raw"))
327
+ response.append({"namespace": ns, "data": file_stat.st_size > 0})
328
+
329
+ for r in response:
330
+ if r["data"]:
331
+ print_success(f"Received traffic on {r['namespace']}")
332
+ keep_running = False
333
+ elif not wait_for_traffic or (not keep_running and not keep_running_during_recording):
334
+ print_generic_error(f"Namespace {r['namespace']} did not receive any traffic")
335
+
336
+ def upload(self, file: str, dest: str) -> None:
337
+ sha256 = get_sha256(file)
338
+ with open(file, "rb") as f:
339
+ upload_iterator = generate_data(f, dest.replace(ntpath.sep, posixpath.sep), 1000000, sha256)
340
+ try:
341
+ self.system_stub.UploadFile(upload_iterator, compression=grpc.Compression.Gzip)
342
+ except grpc.RpcError as rpc_error:
343
+ print_generic_error(f"Failed to upload file - {rpc_error.details()} ({rpc_error.code()})")
344
+ raise typer.Exit(1)
345
+
346
+ def delete_files(self, path: List[str], exit_on_faliure: bool) -> None:
347
+ for file in path:
348
+ try:
349
+ self.system_stub.BatchDeleteFiles(
350
+ system_api.FileDescriptions(fileDescriptions=[system_api.FileDescription(path=file.replace(ntpath.sep, posixpath.sep))])
351
+ )
352
+ except grpc.RpcError as rpc_error:
353
+ print_generic_error(f"Failed to delete file - {rpc_error.details()} ({rpc_error.code()})")
354
+ if exit_on_faliure:
355
+ raise typer.Exit(1)
356
+
357
+ def upload_folder(self, folder: str) -> None:
358
+ files = [y for x in os.walk(folder) for y in glob(os.path.join(x[0], "*")) if not os.path.isdir(y)]
359
+ assert len(files) != 0, "Specified upload folder is empty or does not exist"
360
+ for file in files:
361
+ self.upload(file, file.replace(folder, ""))
362
+
363
+ def download(self, file: str, dest: str, delegate_err: bool = False) -> None:
364
+ try:
365
+ with open(dest, "wb") as f:
366
+ for response in self.system_stub.BatchDownloadFiles(
367
+ system_api.FileDescriptions(fileDescriptions=[system_api.FileDescription(path=file.replace(ntpath.sep, posixpath.sep))])
368
+ ):
369
+ assert not response.HasField("errorMessage"), f"Error uploading file, message is: {response.errorMessage}"
370
+ f.write(response.chunk)
371
+ except grpc.RpcError as rpc_error:
372
+ if delegate_err:
373
+ raise rpc_error
374
+ print_generic_error(f"Failed to download file - {rpc_error.details()} ({rpc_error.code()})")
375
+ # There will be an empty file if the download fails so remove that one here
376
+ os.remove(dest)
377
+ raise typer.Exit(1)
378
+
379
+ def reload_config(self) -> None:
380
+ try:
381
+ request = common.Empty()
382
+ response = self.system_stub.ReloadConfiguration(request, timeout=60000)
383
+ if response.errorMessage:
384
+ print_generic_error(f"Failed to reload config: {response.errorMessage}")
385
+ raise typer.Exit(1)
386
+ # br.helper.reload_configuration(system_stub=self.system_stub)
387
+ except grpc.RpcError as rpc_error:
388
+ print_generic_error(f"Failed to reload configuration - {rpc_error.details()} ({rpc_error.code()})")
389
+ raise typer.Exit(1)
390
+
391
+ def list_namespaces(self) -> List[str]:
392
+ # Lists available signals
393
+ configuration = self.system_stub.GetConfiguration(common.Empty())
394
+ namespaces = []
395
+ for network_info in configuration.networkInfo:
396
+ namespaces.append(network_info.namespace.name)
397
+ return namespaces
398
+
399
+ def list_signal_names(self, prefix: Union[str, None], suffix: Union[str, None]) -> List[Dict[str, Any]]:
400
+ # Lists available signals
401
+ configuration = self.system_stub.GetConfiguration(common.Empty())
402
+
403
+ signal_names = []
404
+ for network_info in configuration.networkInfo:
405
+ res = self.system_stub.ListSignals(network_info.namespace)
406
+ for finfo in res.frame:
407
+ if (prefix is None or finfo.signalInfo.id.name.startswith(prefix)) and (
408
+ suffix is None or finfo.signalInfo.id.name.endswith(suffix)
409
+ ):
410
+ metadata_dict = MessageToDict(
411
+ finfo.signalInfo.metaData,
412
+ preserving_proto_field_name=True,
413
+ )
414
+ sig_dict = {
415
+ "signal": finfo.signalInfo.id.name,
416
+ "namespace": network_info.namespace.name,
417
+ }
418
+ signal_names.append({**sig_dict, **metadata_dict})
419
+
420
+ for sinfo in finfo.childInfo:
421
+ # For signals we can simply skip if prefix and suffix exists does not match
422
+ if (prefix is not None and not sinfo.id.name.startswith(prefix)) or (
423
+ suffix is not None and not sinfo.id.name.endswith(suffix)
424
+ ):
425
+ continue
426
+
427
+ metadata_dict = MessageToDict(
428
+ sinfo.metaData,
429
+ preserving_proto_field_name=True,
430
+ )
431
+ sig_dict = {
432
+ "signal": sinfo.id.name,
433
+ "namespace": network_info.namespace.name,
434
+ }
435
+ signal_names.append({**sig_dict, **metadata_dict})
436
+
437
+ return signal_names
438
+
439
+ def subscribe_on_script(
440
+ self,
441
+ script: bytes,
442
+ on_frame: Callable[[Sequence[network_api.Signal]], None],
443
+ changed_values_only: bool = False,
444
+ ) -> Any:
445
+ client_id = common.ClientId(id="cli")
446
+ thread = Thread(
447
+ target=act_on_scripted_signal,
448
+ args=(
449
+ client_id,
450
+ self.network_stub,
451
+ script,
452
+ changed_values_only, # True: only report when signal changes
453
+ lambda frame: self.__each_signal(frame, on_frame),
454
+ lambda sub: (self.q.put(("cli", sub))),
455
+ ),
456
+ )
457
+ thread.start()
458
+ # wait for subscription to settle
459
+ return self.q.get()
460
+
461
+ def validate_and_get_subscribed_signals(
462
+ self, subscribed_namespaces: List[str], subscribed_signals: List[str]
463
+ ) -> List[SubscribableSignal]:
464
+ # Since we cannot know which list[signals] belongs to which namespace we need to fetch
465
+ # all signals from the broker and find the proper signal with namespace. Finally we
466
+ # also filter out namespaces that we do not need since we might have duplicated signal names
467
+ # over namespaces
468
+ # Begin
469
+
470
+ def verify_namespace(available_signal: List[Dict[str, str]]) -> List[str]:
471
+ return list(filter(lambda namespace: available_signal["namespace"] == namespace, subscribed_namespaces)) # type: ignore
472
+
473
+ def find_subscribed_signal(available_signal: List[Dict[str, str]]) -> List[str]:
474
+ return list(filter(lambda s: available_signal["signal"] == s, subscribed_signals)) # type: ignore
475
+
476
+ existing_signals = self.list_signal_names(prefix=None, suffix=None)
477
+ existing_ns = set(map(lambda s: s["namespace"], existing_signals))
478
+ ns_not_matching = []
479
+ for ns in subscribed_namespaces:
480
+ if ns not in existing_ns:
481
+ ns_not_matching.append(ns)
482
+ if len(ns_not_matching) > 0:
483
+ print_hint(f"Namespace(s) {ns_not_matching} does not exist on broker. Namespaces found on broker: {existing_ns}")
484
+ sys.exit(1)
485
+
486
+ available_signals = list(filter(verify_namespace, existing_signals)) # type: ignore
487
+ signals_to_subscribe_to = list(filter(find_subscribed_signal, available_signals)) # type: ignore
488
+
489
+ # Check if subscription is done on signal that is not in any of these namespaces
490
+ signals_subscribed_to_but_does_not_exist = set(subscribed_signals) - set(map(lambda s: s["signal"], signals_to_subscribe_to))
491
+
492
+ if len(signals_subscribed_to_but_does_not_exist) > 0:
493
+ print_hint(f"One or more signals you subscribed to does not exist {signals_subscribed_to_but_does_not_exist}")
494
+ sys.exit(1)
495
+
496
+ return list(map(lambda s: SubscribableSignal(s["signal"], s["namespace"]), signals_to_subscribe_to))
497
+
498
+ def long_name_subscribe(
499
+ self, signals_to_subscribe_to: List[SubscribableSignal], on_frame: Callable[..., Any], changed_values_only: bool = True
500
+ ) -> Any:
501
+ client_id = common.ClientId(id="cli")
502
+
503
+ # TODO - This can be improved moving forward and we also need to move the validation into api
504
+ self.validate_and_get_subscribed_signals(
505
+ list(map(lambda s: s.namespace, signals_to_subscribe_to)), (list(map(lambda s: s.name, signals_to_subscribe_to)))
506
+ )
507
+
508
+ def to_protobuf_signal(s: SubscribableSignal) -> common.SignalId:
509
+ return self.signal_creator.signal(s.name, s.namespace)
510
+
511
+ signals_to_subscribe_on = list(map(to_protobuf_signal, signals_to_subscribe_to))
512
+
513
+ Thread(
514
+ target=act_on_signal,
515
+ args=(
516
+ client_id,
517
+ self.network_stub,
518
+ signals_to_subscribe_on,
519
+ changed_values_only, # True: only report when signal changes
520
+ lambda frame: self.__each_signal(frame, on_frame),
521
+ lambda sub: (self.q.put(("cloud_demo", sub))),
522
+ ),
523
+ ).start()
524
+ # Wait for subscription
525
+ ecu, subscription = self.q.get()
526
+ return subscription
527
+
528
+ def subscribe(
529
+ self,
530
+ subscribed_signals: list[str],
531
+ subscribed_namespaces: list[str],
532
+ on_frame: Callable[..., Any],
533
+ changed_values_only: bool = True,
534
+ ) -> Any:
535
+ client_id = common.ClientId(id="cli")
536
+
537
+ signals_to_subscribe_to: List[SubscribableSignal] = self.validate_and_get_subscribed_signals(
538
+ subscribed_namespaces, subscribed_signals
539
+ )
540
+
541
+ def to_protobuf_signal(s: SubscribableSignal) -> common.SignalId:
542
+ return self.signal_creator.signal(s.name, s.namespace)
543
+
544
+ signals_to_subscribe_on = list(map(to_protobuf_signal, signals_to_subscribe_to))
545
+
546
+ Thread(
547
+ target=act_on_signal,
548
+ args=(
549
+ client_id,
550
+ self.network_stub,
551
+ signals_to_subscribe_on,
552
+ changed_values_only, # True: only report when signal changes
553
+ lambda frame: self.__each_signal(frame, on_frame),
554
+ lambda sub: (self.q.put(("cloud_demo", sub))),
555
+ ),
556
+ ).start()
557
+ # Wait for subscription
558
+ ecu, subscription = self.q.get()
559
+ return subscription
560
+
561
+ def __each_signal(self, signals: Iterable[network_api.Signal], callback: Callable[..., Any]) -> None:
562
+ callback(
563
+ map(
564
+ lambda s: {"timestamp_us": s.timestamp, "namespace": s.id.namespace.name, "name": s.id.name, "value": self.__get_value(s)},
565
+ signals,
566
+ )
567
+ )
568
+
569
+ @staticmethod
570
+ def __get_value(signal: network_api.Signal) -> Any:
571
+ if signal.raw != b"":
572
+ return "0x" + binascii.hexlify(signal.raw).decode("ascii")
573
+ if signal.HasField("integer"):
574
+ return signal.integer
575
+ if signal.HasField("double"):
576
+ return signal.double
577
+ if signal.HasField("arbitration"):
578
+ return signal.arbitration
579
+ return "empty"
580
+
581
+ @staticmethod
582
+ def __create_playback_config(item: Dict[str, Any]) -> traffic_api.PlaybackInfo:
583
+ """Creating configuration for playback
584
+
585
+ Parameters
586
+ ----------
587
+ item : dict
588
+ Dictionary containing 'path', 'namespace' and 'mode'
589
+
590
+ Returns
591
+ -------
592
+ PlaybackInfo
593
+ Object instance of class
594
+
595
+ """
596
+
597
+ def get_offset_time() -> int:
598
+ if "offsettime" in item:
599
+ return int(item["offsettime"])
600
+ return 0
601
+
602
+ playback_config = traffic_api.PlaybackConfig(
603
+ fileDescription=system_api.FileDescription(path=item["path"]),
604
+ namespace=common.NameSpace(name=item["namespace"]),
605
+ )
606
+ return traffic_api.PlaybackInfo(
607
+ playbackConfig=playback_config,
608
+ playbackMode=traffic_api.PlaybackMode(mode=item["mode"], offsetTime=get_offset_time()),
609
+ )
610
+
611
+ def get_license(self) -> LicenseInfo:
612
+ license_info = self.system_stub.GetLicenseInfo(common.Empty())
613
+ return LicenseInfo(
614
+ valid=license_info.status == system_api.LicenseStatus.VALID,
615
+ expires=license_info.expires,
616
+ email=license_info.requestId,
617
+ machine_id=license_info.requestMachineId.decode("utf-8"),
618
+ )
619
+
620
+ def apply_license(self, license_data_b64: bytes) -> LicenseInfo:
621
+ license = system_api.License()
622
+ license.data = license_data_b64
623
+ license.termsAgreement = True
624
+ self.system_stub.SetLicense(license)
625
+ return self.get_license()