remotivelabs-cli 0.2.0a2__py3-none-any.whl → 0.2.2__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.

Potentially problematic release.


This version of remotivelabs-cli might be problematic. Click here for more details.

@@ -1,22 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
- import dataclasses
4
- import json
5
3
  import re
6
- from dataclasses import dataclass
7
4
  from datetime import date, datetime
8
5
  from typing import Any, Literal
9
6
 
10
- # from cli.settings.core import PERSONAL_TOKEN_FILE_PREFIX, SERVICE_ACCOUNT_TOKEN_FILE_PREFIX
7
+ from pydantic import BaseModel, EmailStr, Field, field_validator, model_validator
8
+
9
+ from cli.utils.time import parse_date
11
10
 
12
11
  DEFAULT_EMAIL = "unknown@remotivecloud.com"
12
+ PERSONAL_TOKEN_FILE_PREFIX = "personal-token-"
13
+ SERVICE_ACCOUNT_TOKEN_FILE_PREFIX = "service-account-token-"
13
14
 
14
15
  TokenType = Literal["authorized_user", "service_account"]
15
16
 
16
17
 
17
- def _parse_date(date_str: str) -> date:
18
- normalized = date_str.replace("Z", "+00:00")
19
- return datetime.fromisoformat(normalized).date()
18
+ def _email_to_safe_filename(email: str) -> str:
19
+ """Replace any invalid character with an underscore"""
20
+ return re.sub(r'[<>:"/\\|?*]', "_", email)
20
21
 
21
22
 
22
23
  def _parse_token_type(token: str) -> TokenType:
@@ -27,79 +28,109 @@ def _parse_token_type(token: str) -> TokenType:
27
28
  raise ValueError(f"Unknown token type for token: {token}")
28
29
 
29
30
 
30
- def _from_dict(d: dict[str, Any]) -> TokenFile:
31
- if "version" not in d:
32
- token_type = _parse_token_type(d["token"])
33
- return TokenFile(
34
- version="1.0",
35
- type=token_type,
36
- name=d["name"],
37
- token=d["token"],
38
- created=_parse_date(d["created"]),
39
- expires=_parse_date(d["expires"]),
40
- account=TokenFileAccount(email=DEFAULT_EMAIL),
41
- )
42
-
43
- account_email = d.get("account", {}).get("email", DEFAULT_EMAIL)
44
- return TokenFile(
45
- version=d["version"],
46
- type=d["type"],
47
- name=d["name"],
48
- token=d["token"],
49
- created=_parse_date(d["created"]),
50
- expires=_parse_date(d["expires"]),
51
- account=TokenFileAccount(email=account_email),
52
- )
31
+ class TokenFileAccount(BaseModel):
32
+ email: EmailStr = DEFAULT_EMAIL
53
33
 
54
34
 
55
- def loads(data: str) -> TokenFile:
56
- return _from_dict(json.loads(data))
57
-
35
+ class TokenFile(BaseModel):
36
+ version: str = "1.0"
37
+ type: TokenType
38
+ name: str
39
+ token: str
40
+ created: date
41
+ expires: date
42
+ account: TokenFileAccount = Field(default_factory=TokenFileAccount)
58
43
 
59
- def dumps(token: TokenFile) -> str:
60
- return json.dumps(dataclasses.asdict(token), default=str)
44
+ @field_validator("created", "expires", mode="before")
45
+ @classmethod
46
+ def validate_parse_date(cls, value: str | date) -> date:
47
+ if isinstance(value, date):
48
+ return value
49
+ return parse_date(value)
61
50
 
51
+ @model_validator(mode="before")
52
+ @classmethod
53
+ def init_with_defaults(cls, json_data: Any) -> Any:
54
+ """
55
+ Try to migrate old formats and missing fields as best we can.
62
56
 
63
- @dataclass
64
- class TokenFileAccount:
65
- email: str
57
+ NOTE: If we ever need to add a new version (like 2.0), we should add explicit classes for each version (e.g. TokenFileV1,
58
+ TokenFileV2, etc.), each with their own fields. This will allow us to migrate to new versions without breaking
59
+ backwards compatibility.
60
+ """
61
+ if not isinstance(json_data, dict):
62
+ return json_data
66
63
 
64
+ if "version" not in json_data:
65
+ json_data["version"] = "1.0"
67
66
 
68
- PERSONAL_TOKEN_FILE_PREFIX = "personal-token-"
69
- SERVICE_ACCOUNT_TOKEN_FILE_PREFIX = "service-account-token-"
67
+ if "type" not in json_data and "token" in json_data:
68
+ json_data["type"] = _parse_token_type(json_data["token"])
70
69
 
70
+ if "account" not in json_data:
71
+ json_data["account"] = {"email": DEFAULT_EMAIL}
72
+ elif isinstance(json_data["account"], str):
73
+ json_data["account"] = {"email": json_data["account"]}
71
74
 
72
- @dataclass
73
- class TokenFile:
74
- version: str
75
- type: TokenType
76
- name: str
77
- token: str
78
- created: date
79
- expires: date
80
- account: TokenFileAccount
75
+ return json_data
81
76
 
82
77
  def get_token_file_name(self) -> str:
83
- def email_to_safe_filename(email: str) -> str:
84
- # Replace any invalid character with an underscore
85
- return re.sub(r'[<>:"/\\|?*]', "_", email)
86
-
87
- # From now, user will never be None when adding a token so in this case token_file.user is never None
88
- email = email_to_safe_filename(self.account.email) if self.account is not None else "unknown"
78
+ """
79
+ Returns the name of the token file using the proper file name format.
80
+ """
81
+ email = _email_to_safe_filename(self.account.email) if self.account is not None else "unknown"
89
82
  if self.type == "authorized_user":
90
83
  return f"{PERSONAL_TOKEN_FILE_PREFIX}{self.name}-{email}.json"
91
84
  return f"{SERVICE_ACCOUNT_TOKEN_FILE_PREFIX}{self.name}-{email}.json"
92
85
 
93
86
  def is_expired(self) -> bool:
87
+ """
88
+ Returns True if the token is expired, False otherwise.
89
+ """
94
90
  return datetime.today().date() > self.expires
95
91
 
96
92
  def expires_in_days(self) -> int:
93
+ """
94
+ Returns the number of days until the token expires.
95
+ """
97
96
  return (self.expires - datetime.today().date()).days
98
97
 
99
- @staticmethod
100
- def from_json_str(data: str) -> TokenFile:
101
- return loads(data)
98
+ @classmethod
99
+ def from_json_str(cls, data: str) -> TokenFile:
100
+ """
101
+ Creates a TokenFile from a JSON string.
102
+ """
103
+ return cls.model_validate_json(data)
104
+
105
+ @classmethod
106
+ def from_dict(cls, data: dict[str, Any]) -> TokenFile:
107
+ """
108
+ Creates a TokenFile from a dictionary.
109
+ """
110
+ return cls.model_validate(data)
111
+
112
+ def to_json_str(self) -> str:
113
+ """
114
+ Returns the JSON string representation of the TokenFile.
115
+ """
116
+ return self.model_dump_json()
117
+
118
+ def to_dict(self) -> dict[str, Any]:
119
+ """
120
+ Returns the dictionary representation of the TokenFile.
121
+ """
122
+ return self.model_dump()
123
+
124
+
125
+ def loads(data: str) -> TokenFile:
126
+ """
127
+ Creates a TokenFile from a JSON string.
128
+ """
129
+ return TokenFile.from_json_str(data)
130
+
102
131
 
103
- @staticmethod
104
- def from_dict(data: dict[str, Any]) -> TokenFile:
105
- return _from_dict(data)
132
+ def dumps(token_file: TokenFile) -> str:
133
+ """
134
+ Returns the JSON string representation of the TokenFile.
135
+ """
136
+ return token_file.to_json_str()
cli/topology/cmd.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import dataclasses
2
4
  import datetime
3
5
  from typing import Any
@@ -22,30 +24,31 @@ class Subscription:
22
24
  type: str
23
25
  display_name: str
24
26
  feature: str
25
- start_date: str
26
- end_date: str
27
+ start_date: str # TODO: add datetime
28
+ end_date: str # TODO: add datetime
27
29
 
28
30
 
29
31
  def _print_current_subscription(subscription_info: dict[str, Any]) -> None:
30
- subscription_type = subscription_info["subscriptionType"]
32
+ subscription_type = subscription_info.get("subscriptionType")
33
+ end_date_str = subscription_info.get("endDate")
34
+ now = datetime.datetime.now()
35
+
36
+ def parse_date(date_str: str | None) -> datetime.datetime | None:
37
+ return datetime.datetime.fromisoformat(date_str) if date_str else None
38
+
39
+ expires = parse_date(end_date_str)
31
40
 
32
41
  if subscription_type == "trial":
33
- expires = datetime.datetime.fromisoformat(subscription_info["endDate"])
34
- if expires < datetime.datetime.now():
35
- console.print(f"Your Topology trial expired {subscription_info['endDate']}, please contact support@remotivelabs.com")
42
+ if expires and expires < now:
43
+ console.print(f"Your Topology trial expired {end_date_str}, please contact support@remotivelabs.com")
36
44
  else:
37
- console.print(f"You already have an active topology trial, it expires {subscription_info['endDate']}")
38
- # A paid subscription might not have an endDate
39
- elif subscription_type == "paid":
40
- if "endDate" in subscription_type:
41
- expires = datetime.datetime.fromisoformat(subscription_info["endDate"])
42
- else:
43
- expires = None
45
+ console.print(f"You already have an active topology trial, it expires {end_date_str}")
44
46
 
45
- if expires is not None and expires < datetime.datetime.now():
46
- console.print(f"Topology subscription has ended, expired {subscription_info['endDate']}")
47
+ elif subscription_type == "paid":
48
+ if expires and expires < now:
49
+ console.print(f"Topology subscription has ended, expired {end_date_str}")
47
50
  else:
48
- console.print(f"You already have an active topology subscription, it expires {expires if expires is not None else 'Never'}")
51
+ console.print(f"You already have an active topology subscription, it expires {end_date_str or 'Never'}")
49
52
 
50
53
  else:
51
54
  ErrorPrinter.print_generic_error("Unexpected exception, please contact support@remotivelabs.com")
cli/typer/typer_utils.py CHANGED
@@ -17,7 +17,7 @@ console = Console()
17
17
  def create_typer(**kwargs: Any) -> typer.Typer:
18
18
  """Create a Typer instance with default settings."""
19
19
  # return typer.Typer(no_args_is_help=True, **kwargs)
20
- return typer.Typer(cls=OrderCommands, no_args_is_help=True, **kwargs)
20
+ return typer.Typer(cls=OrderCommands, no_args_is_help=True, invoke_without_command=True, **kwargs)
21
21
 
22
22
 
23
23
  def print_padded(label: str, right_text: str, length: int = 30) -> None:
cli/utils/rest_helper.py CHANGED
@@ -52,7 +52,7 @@ class RestHelper:
52
52
  # token = os.environ["REMOTIVE_CLOUD_AUTH_TOKEN"]
53
53
  # headers = {"Authorization": "Bearer " + token}
54
54
 
55
- __headers: Dict[str, str] = {"User-Agent": f"remotivelabs-cli {version('remotivelabs-cli')}"}
55
+ __headers: Dict[str, str] = {"User-Agent": f"remotivelabs-cli/{version('remotivelabs-cli')}"}
56
56
  __org: str = ""
57
57
 
58
58
  __token: str = ""
cli/utils/time.py ADDED
@@ -0,0 +1,11 @@
1
+ from datetime import date, datetime
2
+
3
+
4
+ def parse_date(date_str: str) -> date:
5
+ return parse_datetime(date_str).date()
6
+
7
+
8
+ def parse_datetime(date_str: str) -> datetime:
9
+ """Required for pre 3.11"""
10
+ normalized = date_str.replace("Z", "+00:00")
11
+ return datetime.fromisoformat(normalized)
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import json
5
+ import os
6
+ import urllib.request
7
+ from datetime import timedelta
8
+ from importlib import metadata as importlib_metadata
9
+
10
+ from packaging.version import InvalidVersion, Version
11
+
12
+ from cli.errors import ErrorPrinter
13
+ from cli.settings import Settings
14
+
15
+
16
+ def _pypi_latest(
17
+ project: str, *, include_prereleases: bool, timeout: float = 2.5, user_agent: str | None = None
18
+ ) -> tuple[str | None, str | None]:
19
+ """Return (latest_version, project_url) from PyPI, skipping yanked files."""
20
+ url = f"https://pypi.org/pypi/{project}/json"
21
+ headers = {"Accept": "application/json"}
22
+ if user_agent:
23
+ headers["User-Agent"] = user_agent
24
+ req = urllib.request.Request(url, headers=headers)
25
+
26
+ try:
27
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
28
+ data = json.load(resp)
29
+ except Exception:
30
+ return None, None # network/404/etc.
31
+
32
+ releases = data.get("releases") or {}
33
+ candidates: list[Version] = []
34
+ for s, files in releases.items():
35
+ try:
36
+ v = Version(s)
37
+ except InvalidVersion:
38
+ continue
39
+ the_files = files or []
40
+ if any(f.get("yanked", False) for f in the_files):
41
+ continue
42
+ if (v.is_prerelease or v.is_devrelease) and not include_prereleases:
43
+ continue
44
+ candidates.append(v)
45
+
46
+ if not candidates:
47
+ return None, None
48
+
49
+ latest = str(max(candidates))
50
+ info = data.get("info") or {}
51
+ proj_url = info.get("project_url") or info.get("package_url") or f"https://pypi.org/project/{project}/"
52
+ return latest, proj_url
53
+
54
+
55
+ def _installed_version(distribution_name: str, fallback: str | None = None) -> str | None:
56
+ try:
57
+ return importlib_metadata.version(distribution_name)
58
+ except importlib_metadata.PackageNotFoundError:
59
+ return fallback
60
+
61
+
62
+ def check_for_update(project: str, current_version: str, settings: Settings) -> None:
63
+ # Make it possible to disable update check, i.e in CI
64
+ if os.environ.get("PYTHON_DISABLE_UPDATE_CHECK"):
65
+ return
66
+
67
+ # Determine current version
68
+ cur = current_version or _installed_version(project)
69
+ if not cur:
70
+ return # unknown version → skip silently
71
+
72
+ state = settings.read_state_file()
73
+
74
+ if not state.last_update_check_time:
75
+ if os.environ.get("RUNS_IN_DOCKER"):
76
+ # To prevent that we always check update in docker due to ephemeral disks we write an "old" check if the state
77
+ # is missing. If no disk is mounted we will never get the update check but if its mounted properly we will get
78
+ # it on the second attempt. This is good enough
79
+ state.last_update_check_time = (datetime.datetime.now() - timedelta(hours=10)).isoformat()
80
+ settings.write_state_file(state)
81
+ return
82
+
83
+ elif not state.should_perform_update_check():
84
+ return
85
+
86
+ # We end up here if last_update_check_time is None or should_perform_update_check is true
87
+
88
+ include_prereleases = Version(cur).is_prerelease or Version(cur).is_devrelease
89
+
90
+ latest, proj_url = _pypi_latest(
91
+ project, include_prereleases=include_prereleases, user_agent=f"{project}/{cur} (+https://pypi.org/project/{project}/)"
92
+ )
93
+ if latest:
94
+ if Version(latest) > Version(cur):
95
+ _print_update_info(
96
+ cur,
97
+ latest,
98
+ )
99
+ state.last_update_check_time = datetime.datetime.now().isoformat()
100
+ settings.write_state_file(state)
101
+
102
+
103
+ def _print_update_info(cur: str, latest: str) -> None:
104
+ instructions = (
105
+ "upgrade with: docker pull remotivelabs/remotivelabs-cli"
106
+ if os.environ.get("RUNS_IN_DOCKER")
107
+ else "upgrade with: pipx install -U remotivelabs-cli"
108
+ )
109
+
110
+ ErrorPrinter.print_hint(
111
+ f"Update available: remotivelabs-cli {cur} → {latest} , ({instructions}) we always recommend to use latest version"
112
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: remotivelabs-cli
3
- Version: 0.2.0a2
3
+ Version: 0.2.2
4
4
  Summary: CLI for operating RemotiveCloud and RemotiveBroker
5
5
  Author: Johan Rask
6
6
  Author-email: johan.rask@remotivelabs.com
@@ -13,9 +13,11 @@ Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Requires-Dist: click (<8.2.0)
15
15
  Requires-Dist: dacite (>=1.9.2,<2.0.0)
16
+ Requires-Dist: email-validator (>=2.2.0,<3.0.0)
16
17
  Requires-Dist: grpc-stubs (>=1.53.0.5)
17
18
  Requires-Dist: mypy-protobuf (>=3.0.0)
18
19
  Requires-Dist: plotext (>=5.2,<6.0)
20
+ Requires-Dist: pydantic (>=2.11.7,<3.0.0)
19
21
  Requires-Dist: pyjwt (>=2.6,<3.0)
20
22
  Requires-Dist: python-can (>=4.3.1)
21
23
  Requires-Dist: python-socketio (>=4.6.1)
@@ -14,15 +14,15 @@ cli/broker/scripting.py,sha256=LFLdaBNxe2sfpcxhDmRlAbEorjL3SJZNK-zEdLQ9ySU,3854
14
14
  cli/broker/signals.py,sha256=MFj_bOLIxHY1v3XPkKk6n8U3JLaY8nrXHahRQaVse6s,8207
15
15
  cli/cloud/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  cli/cloud/auth/__init__.py,sha256=MtQ01-n8CgZb9Y_SvxwZUgj44Yo0dFAU3_XwhQiUYtw,54
17
- cli/cloud/auth/cmd.py,sha256=1QEYkaRO4Q1wbOemEwnLCl6YJivC8OdtOguH-bc6N8Y,2930
17
+ cli/cloud/auth/cmd.py,sha256=gLmfjIN9Vrytk9BwGH1cq4WeeryOfwTTnQ4yuZrbRcs,2757
18
18
  cli/cloud/auth/login.py,sha256=dX6M5ysE0n9Zg3gVT7hJbChxTsmuba-Z-1Or6DCFYis,11511
19
- cli/cloud/auth_tokens.py,sha256=RNvvHN9bJ_gfUUa4JL0YXpvTKFYKf922BWjvltElwMg,12964
19
+ cli/cloud/auth_tokens.py,sha256=K_HSBto2XfbD-Hxhb0SAFhxSDZdSXo961UcvdVWNkZI,12831
20
20
  cli/cloud/brokers.py,sha256=QTA9bmaK06LKEccF6IBgWBonC4VFrKwFQBsACX_IzYw,3896
21
21
  cli/cloud/cloud_cli.py,sha256=q-oiaLcKC-BRamXfIFGn-BskRmJ3utA7-tI39lSs3Cs,1309
22
22
  cli/cloud/configs.py,sha256=uv46nUoGXOr99smQHahv_ageDv6bGYfUnlRlxcS5D9A,5125
23
- cli/cloud/organisations.py,sha256=dX8h1SMLxKBvOaxuiRP6nFnkYdek91fjTRtuNRLRHeQ,4116
23
+ cli/cloud/organisations.py,sha256=iEmGMEzOIvuWomoJZ0WBa3Rmrkrup5UH7wjPMoElSn4,4092
24
24
  cli/cloud/projects.py,sha256=ecn5Y8UKhgYnHSJQACUk1GNZt9EF8ug4B-6MCr8rZqM,1487
25
- cli/cloud/recordings.py,sha256=B0XOj8LIm3hBqBzVKPLPvPUCXCKZBTEISssrijK481w,24855
25
+ cli/cloud/recordings.py,sha256=In2fKX668CPsEVBAy7zkU92lEnmu3UcnqiVrqsvLNDQ,24961
26
26
  cli/cloud/recordings_playback.py,sha256=XZoVyujufMQFN2v_Nwsf8tOqn61yLEpAf2z_u5uhXik,11532
27
27
  cli/cloud/resumable_upload.py,sha256=8lEIdncJZoTZzNsQVHH3gm_GunxEmN5JbmWX7awy3p4,3713
28
28
  cli/cloud/sample_recordings.py,sha256=RmuT-a2iMwGj3LXVcPkV5l66uFcf7nyWyJciUjnYkk4,721
@@ -37,24 +37,31 @@ cli/connect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  cli/connect/connect.py,sha256=SH2DNTTVLu2dNpk6xIah1-KJZAqrK_7Skt8RKp8Mjh8,4231
38
38
  cli/connect/protopie/protopie.py,sha256=ElmrGaV0ivb85wo0gLzCAXZhmSmIDASaCVlF1iQblLI,6532
39
39
  cli/errors.py,sha256=djODw6sdMJXzOsuAUOP3N13nfmm1sIP3Pe6tllGdozM,1657
40
- cli/remotive.py,sha256=5Qt9jayyj1wGSyj2eyLzeJaZ1F8UrtznFzrPPcDqZnY,2988
41
- cli/settings/__init__.py,sha256=ocADDtwE8qsVN_0RM71f2VftFFCHdJrcG37hP7QmC3A,238
42
- cli/settings/config_file.py,sha256=y5wUg2OILhpkxcamyiTkcCQOgy6ZkiEsLkTZnGdpmfA,4510
43
- cli/settings/core.py,sha256=3VQ78muCAcM7buRIhanr6QgF73m3MP0Kdupc6cz3sww,15059
44
- cli/settings/migrate_all_token_files.py,sha256=7kvHbpP4BtILJ8kPtb_bFnTnBYX9isZ4rwB5lfnEkbA,2722
45
- cli/settings/migrate_token_file.py,sha256=Fp7Z_lNqSdoWY05TYwFW2QH8q9QhmB2TYSok6hV1Mic,1530
46
- cli/settings/token_file.py,sha256=RKtdZYOIb9CJjs2dHFlv6TZHgWuPVonAQphRuosXP0I,3050
40
+ cli/remotive.py,sha256=xfeekzG6tojXsWZdiGN5ceKCCd8xVNf8v9EUAY2Gnjc,4033
41
+ cli/settings/__init__.py,sha256=t1qkaGrJ4xx8WMHlmBTbQ1VdJL4YOcz8VFfRkGa2_jQ,711
42
+ cli/settings/config_file.py,sha256=6sdHUtZSUIgubwpfwEEn7GarTK1M_iQhtRJZzFDdP5o,2784
43
+ cli/settings/core.py,sha256=IJ62CzPrrvwO46zmvsjKIn6VD4oR9VG5IX29ctd2RO4,11611
44
+ cli/settings/migration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
+ cli/settings/migration/migrate_all_token_files.py,sha256=xoVvAqn_tGskEW148uf3xZx1mpJKUnERMTcBo0nkCnI,3010
46
+ cli/settings/migration/migrate_config_file.py,sha256=hw4EpRwJz1zUNxfCOk0PvMuZjAlaGy4m_rDbMsHZO_w,2047
47
+ cli/settings/migration/migrate_legacy_dirs.py,sha256=N0t2io3bT_ub8BcVPw1CeQ4eeexRUiu3jXq3DL018OE,1819
48
+ cli/settings/migration/migrate_token_file.py,sha256=Fp7Z_lNqSdoWY05TYwFW2QH8q9QhmB2TYSok6hV1Mic,1530
49
+ cli/settings/migration/migration_tools.py,sha256=P72tuw6-aS_Kd0qn-0ZecplsYxMTu0LTXM5sMSNTVEM,1378
50
+ cli/settings/state_file.py,sha256=ujTOJgCts-gpM-66EhRYXwO803HAFcmia2Pf_nYGldc,837
51
+ cli/settings/token_file.py,sha256=Po3Vwu5cdT5ZgLO3_ZLEX13_57coqHz1PPu2SQ-202o,4177
47
52
  cli/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
53
  cli/tools/can/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
54
  cli/tools/can/can.py,sha256=TtP5w8vb0QG4ObNhkWIDpRMdNelirFffoc_lFZy8ePM,2260
50
55
  cli/tools/tools.py,sha256=jhLfrFDqkmWV3eBAzNwBf6WgDGrz7sOhgVCia36Twn8,232
51
- cli/topology/cmd.py,sha256=Fs6tLomKhaie275Tc3FoKGY5cyWx2EEYdroUcfJBnmk,4033
56
+ cli/topology/cmd.py,sha256=SQ5wi7KDoh4iR2Ed7gyfGLNj6UE0K6UkksmBMSD2XAk,3981
52
57
  cli/typer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
- cli/typer/typer_utils.py,sha256=8SkvG9aKkfK9fTRsLD9pOBtWn9XSwtOXWg2RAk9FhOI,708
58
+ cli/typer/typer_utils.py,sha256=TaJuK1EtE9Gv3DfmoyHPTNKmhiAimuQCHKxQjnUZ7bs,737
54
59
  cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
- cli/utils/rest_helper.py,sha256=b_FJY6MxnFSqo11qaHxkBFHfVlKf7Zj28Uxv9Oj7XY4,14141
56
- remotivelabs_cli-0.2.0a2.dist-info/LICENSE,sha256=qDPP_yfuv1fF-u7EfexN-cN3M8aFgGVndGhGLovLKz0,608
57
- remotivelabs_cli-0.2.0a2.dist-info/METADATA,sha256=rxP2gpSWY3w0Lw47fAveqxqkLySPvo9qdMR67QHNoq4,1430
58
- remotivelabs_cli-0.2.0a2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
59
- remotivelabs_cli-0.2.0a2.dist-info/entry_points.txt,sha256=lvDhPgagLqW_KTnLPCwKSqfYlEp-1uYVosRiPjsVj10,45
60
- remotivelabs_cli-0.2.0a2.dist-info/RECORD,,
60
+ cli/utils/rest_helper.py,sha256=De-1Z75p-zjA4hZrylVxWn2wqdPB2gvyvA-ixm_dRFo,14141
61
+ cli/utils/time.py,sha256=TEKcNZ-pQoJ7cZ6hQmVD0sTRwRm2rBy51-MuDNdO4S4,296
62
+ cli/utils/version_check.py,sha256=sAkTzNRlgGn4Hto_56J0KHLe-3zK7he7gbUTjrtY1lc,3940
63
+ remotivelabs_cli-0.2.2.dist-info/LICENSE,sha256=qDPP_yfuv1fF-u7EfexN-cN3M8aFgGVndGhGLovLKz0,608
64
+ remotivelabs_cli-0.2.2.dist-info/METADATA,sha256=Dyz6ht0Psz5TCHsxJXxsKIWckmwKEPVsO3amT4GkDng,1518
65
+ remotivelabs_cli-0.2.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
66
+ remotivelabs_cli-0.2.2.dist-info/entry_points.txt,sha256=lvDhPgagLqW_KTnLPCwKSqfYlEp-1uYVosRiPjsVj10,45
67
+ remotivelabs_cli-0.2.2.dist-info/RECORD,,