remotivelabs-cli 0.0.32__py3-none-any.whl → 0.0.34__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cli/broker/lib/broker.py CHANGED
@@ -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
- # f: br.common_pb2.FrameInfo = finfo
372
- receivers = []
373
- for sinfo in finfo.childInfo:
374
- rec = list(map(lambda r: r, sinfo.metaData.receiver))
375
- receivers.extend(rec)
376
- signal_names.append(
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:
cli/broker/signals.py CHANGED
@@ -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", help="List frame and signal metadata on broker")
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
- signal_names(url, api_key)
39
-
40
+ """
41
+ List signal metadata on a broker
40
42
 
41
- @app.command(help="List signals names on broker", deprecated=True)
42
- def signal_names(
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
- # print("Listing available signals")
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,3 @@
1
+ from cli.cloud.auth.cmd import app
2
+
3
+ __all__ = ["app"]
cli/cloud/auth/cmd.py ADDED
@@ -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
- Login to the cli using browser
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.open(
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")
cli/cloud/cloud_cli.py CHANGED
@@ -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
 
cli/cloud/storage/cmd.py CHANGED
@@ -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
- app = typer.Typer(
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")
cli/cloud/storage/uri.py CHANGED
@@ -16,10 +16,37 @@ class JoinURIError(Exception):
16
16
  class URI:
17
17
  """
18
18
  Custom type for rcs (Remotive Cloud Storage) URIs.
19
+
20
+ The URI format follows the pattern: rcs://bucket/path/to/resource
19
21
  """
20
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
+
21
38
  def __init__(self, value: str, scheme: str = "rcs"):
22
- self.original_uri = value
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
23
50
  self.scheme = scheme
24
51
 
25
52
  parsed = urlparse(value)
@@ -34,18 +61,39 @@ class URI:
34
61
 
35
62
  self._posix_path = PurePosixPath(self.path)
36
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)
37
72
 
38
73
  def is_dir(self) -> bool:
74
+ """Check if the URI points to a directory."""
39
75
  return self.path.endswith("/")
40
76
 
41
77
  def __truediv__(self, other: PathLike[str] | str) -> URI:
42
- """Returns a new URI object with the joined path"""
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
+ """
43
91
  if str(other).startswith("/"):
44
92
  raise JoinURIError(f"Cannot join absolute path '{other}' to URI")
45
93
 
94
+ is_dir = str(other).endswith("/")
46
95
  new_path = self._posix_path / other
47
96
 
48
- # handle relative paths
49
97
  for part in new_path.parts:
50
98
  if part == "..":
51
99
  new_path = new_path.parent
@@ -53,7 +101,13 @@ class URI:
53
101
  new_path = new_path / part
54
102
 
55
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
56
105
  return URI(new_uri, scheme=self.scheme)
57
106
 
58
107
  def __str__(self) -> str:
59
- return self.original_uri
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,15 +1,16 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: remotivelabs-cli
3
- Version: 0.0.32
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,<3.12
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)
@@ -3,18 +3,20 @@ cli/broker/brokers.py,sha256=oUadEL6xQ4bhXucBH-ZjL67VuERf19kn1g240v_lEpg,3197
3
3
  cli/broker/export.py,sha256=3sG9i6ZwOQW6snu87NSzOL2_giQTYQMzQlpPg7z8n78,4431
4
4
  cli/broker/files.py,sha256=_MVwitQ5Z9-lNDb3biXqnlkKti8rizTEw0nnAViussU,4181
5
5
  cli/broker/lib/__about__.py,sha256=xnZ5V6ZcHW9dhWLWdMzVjYJbEnMKpeXm0_S_mbNzypE,141
6
- cli/broker/lib/broker.py,sha256=iBv-uegVD6awnhkukV50CcZywIybEO3qvI0YU47KcGo,23949
6
+ cli/broker/lib/broker.py,sha256=Ip9Fg3WskADWjaIO-315s0vCMtZxM5jwBFpK0QuuIPI,24515
7
7
  cli/broker/license_flows.py,sha256=qJplaeugkUiypFGPdEIl5Asqlf7W3geJ-wU-QbYMP_8,7216
8
8
  cli/broker/licenses.py,sha256=Ddl243re8RoeP9CoWWbIzwDePQ9l8r7ixmbd1gqn8f0,3973
9
9
  cli/broker/playback.py,sha256=hdDKXGPuIE3gcT-kgQltgn5jsPzK19Yh9hiNcgtkLX0,3992
10
10
  cli/broker/record.py,sha256=Oa6hUpS0Dgnt0f6Ig33vl0Jy8wN7wMXfemaxXWjRVoQ,1414
11
11
  cli/broker/scripting.py,sha256=8577_C6siOk90s4G1ItIfAoFIUAkS0ItUl5kqR0cD-k,3792
12
- cli/broker/signals.py,sha256=llok_jUGWOzAiiQUK54uRDnDuonBOAYBDbPdnzCFdog,7075
12
+ cli/broker/signals.py,sha256=KflNaVFgsKFi1suec7fSkCfGhcJ0LfPTMRGVQUlsU-0,7058
13
13
  cli/cloud/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- cli/cloud/auth.py,sha256=RBTDUGRBVsK28u-aeqzRIzHnW7FqH4KADlVlQEgBCww,2534
14
+ cli/cloud/auth/__init__.py,sha256=MtQ01-n8CgZb9Y_SvxwZUgj44Yo0dFAU3_XwhQiUYtw,54
15
+ cli/cloud/auth/cmd.py,sha256=T2o-T3KYQPZU5Ujj7IuOYA7icbv930hks14YA-4Gh1s,1077
16
+ cli/cloud/auth/login.py,sha256=yhehYM97tcS6A7srVU4Olg8s58H0JJzfSl7yWp9a1VI,1607
15
17
  cli/cloud/auth_tokens.py,sha256=0U60Gk2-TnAUff5anZmTB1rOEninNvYy1o5ihCqgj8A,4525
16
18
  cli/cloud/brokers.py,sha256=DNj79MTkPylKUQbr-iPUhQgfNJLAW8UehnvgpEmNH_k,3890
17
- cli/cloud/cloud_cli.py,sha256=0VvFexnVnYvb1vueYxraHiH_WjbQp0Y42ttL_koDFh8,1255
19
+ cli/cloud/cloud_cli.py,sha256=c_JMjAOsJnAw4TM7gkPWmEfrrQly6lAppvfHAmaUGmo,1262
18
20
  cli/cloud/configs.py,sha256=bZzZF-yNkZkfJK5baU0Xh_vQRrJkmuzEv8xx4_IkXZ8,4714
19
21
  cli/cloud/organisations.py,sha256=txKQmSQEpTmeqlqngai8pwgQQEvRgeDd0dT_VzZ7RNc,752
20
22
  cli/cloud/projects.py,sha256=YrwPJClC2Sq_y1HjPd_tzaiv4GEnnsXSXHBhtQCPdK0,1431
@@ -26,9 +28,9 @@ cli/cloud/sample_recordings.py,sha256=OVX32U1dkkkJZysbgr5Dy515oOQKnwBAbZYzV_QUu1
26
28
  cli/cloud/service_account_tokens.py,sha256=263u1bRmBKfYsxL6TV6YjReUBUaVHWc3ETCd7AS3DTU,2297
27
29
  cli/cloud/service_accounts.py,sha256=XOIPobUamCLIaufjyvb33XJDwy6uRqW5ZljZx3GYEfo,1659
28
30
  cli/cloud/storage/__init__.py,sha256=y6V0QswTVY7lHcy5kVUbNKIPc5tyWDqvhakcRFKV6HA,167
29
- cli/cloud/storage/cmd.py,sha256=kukFatuL-YOy6VC7FC4PvjjS1teQya7R__fNX0jdubY,2929
31
+ cli/cloud/storage/cmd.py,sha256=Uq8tRJw9AYD8V4F5SeqUGKGfAZRdB-J3vHDdCbhfhCg,2931
30
32
  cli/cloud/storage/copy.py,sha256=thjdhqnFHSstsA5Iidz74c2o4zHfEaDXfRUGOpjWh1o,3228
31
- cli/cloud/storage/uri.py,sha256=uGEGggJp7Nnu3oUadBs9bAMUDf2e5YjhZ6gAdeYLBFI,1963
33
+ cli/cloud/storage/uri.py,sha256=QZCus--KJQlVwGCOzZqiglvj8VvSRKxfVvN33Pilgyg,3616
32
34
  cli/cloud/storage/uri_or_path.py,sha256=rTjg9G0h8-jb4QUlza1YMqqs99ifrydnEL7o7rmidKQ,1166
33
35
  cli/connect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
36
  cli/connect/connect.py,sha256=U--6dtHxUlvE81J37rABFez4TbF7AXWOpZYZnL7sPMY,3994
@@ -37,11 +39,12 @@ cli/errors.py,sha256=CXYArw1W82bRFwJkJ3tD-Ek1huKeah502DGMvPxHYFo,1366
37
39
  cli/remotive.py,sha256=z834JeOwENyUM4bS74_zE95sGwu1efgfDVtCLKV5rV0,1789
38
40
  cli/settings.py,sha256=A5rtp_1oix7Com5aHCAHdwJqxoV2LgxpYXwCe40v7oY,2072
39
41
  cli/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
+ cli/tools/can/RemotiveLabs.can1.log,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
43
  cli/tools/can/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
44
  cli/tools/can/can.py,sha256=8uATViSFlpkdSiIm4fzbuQi1_m7V9Pym-K17TaJQRHU,2262
42
45
  cli/tools/tools.py,sha256=0KU-hXR1f9xHP4BOG9A9eXfmICLmNuQCOU8ueF6iGg0,198
43
- remotivelabs_cli-0.0.32.dist-info/LICENSE,sha256=qDPP_yfuv1fF-u7EfexN-cN3M8aFgGVndGhGLovLKz0,608
44
- remotivelabs_cli-0.0.32.dist-info/METADATA,sha256=pvFD_mvS0qOntQP-7EAivZAPl1vKcNJDfzGSh-jwKEw,1311
45
- remotivelabs_cli-0.0.32.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
46
- remotivelabs_cli-0.0.32.dist-info/entry_points.txt,sha256=lvDhPgagLqW_KTnLPCwKSqfYlEp-1uYVosRiPjsVj10,45
47
- remotivelabs_cli-0.0.32.dist-info/RECORD,,
46
+ remotivelabs_cli-0.0.34.dist-info/LICENSE,sha256=qDPP_yfuv1fF-u7EfexN-cN3M8aFgGVndGhGLovLKz0,608
47
+ remotivelabs_cli-0.0.34.dist-info/METADATA,sha256=_QH02sJMCawf9FDqD071Fs_UBJ9g5FUmauy3Ef53b2k,1356
48
+ remotivelabs_cli-0.0.34.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
49
+ remotivelabs_cli-0.0.34.dist-info/entry_points.txt,sha256=lvDhPgagLqW_KTnLPCwKSqfYlEp-1uYVosRiPjsVj10,45
50
+ remotivelabs_cli-0.0.34.dist-info/RECORD,,