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,52 @@
1
+ from __future__ import annotations
2
+
3
+ from remotivelabs.cli.settings.token_file import TokenFile, TokenFileAccount
4
+ from remotivelabs.cli.utils.rest_helper import RestHelper
5
+
6
+
7
+ class InvalidTokenError(Exception):
8
+ """Raised when a token is invalid."""
9
+
10
+
11
+ class UnsupportedTokenVersionError(Exception):
12
+ """Raised when a token version is not supported."""
13
+
14
+
15
+ def migrate_legacy_token(token: TokenFile) -> TokenFile:
16
+ """
17
+ Migrate a token from a legacy format to the latest format.
18
+
19
+ Args:
20
+ token: The token to migrate.
21
+
22
+ Returns:
23
+ TokenFile: The migrated token.
24
+
25
+ Raises:
26
+ InvalidTokenError: If the token is invalid.
27
+ UnsupportedTokenVersionError: If the token version is not supported.
28
+ """
29
+ # use a naive approach to compare versions for now
30
+ version = float(token.version)
31
+
32
+ # already migrated
33
+ if version >= 1.1:
34
+ return token
35
+
36
+ if version == 1.0:
37
+ res = RestHelper.handle_get("/api/whoami", return_response=True, allow_status_codes=[401, 400, 403], access_token=token.token)
38
+ if res.status_code != 200:
39
+ raise InvalidTokenError(f"Token {token.name} is invalid")
40
+
41
+ email = res.json()["email"]
42
+ return TokenFile(
43
+ version="1.1",
44
+ type=token.type,
45
+ name=token.name,
46
+ token=token.token,
47
+ created=token.created,
48
+ expires=token.expires,
49
+ account=TokenFileAccount(email=email),
50
+ )
51
+
52
+ raise UnsupportedTokenVersionError(f"Unsupported token version: {token.version}")
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from itertools import chain
4
+ from pathlib import Path
5
+
6
+ from remotivelabs.cli.settings.token_file import TokenFile
7
+ from remotivelabs.cli.utils.console import print_generic_message
8
+
9
+
10
+ def list_token_files(config_dir: Path) -> list[TokenFile]:
11
+ """
12
+ List all token files in the config directory
13
+
14
+ Note! Dont use settings, as that will couple settings to the old config and token formats we want to migrate away from.
15
+ """
16
+ token_files = []
17
+ patterns = ["personal-token-*.json", "service-account-token-*.json"]
18
+ files = list(chain.from_iterable(config_dir.glob(pattern) for pattern in patterns))
19
+ for file in files:
20
+ try:
21
+ token_file = TokenFile.from_json_str(file.read_text())
22
+ token_files.append(token_file)
23
+ except Exception:
24
+ print_generic_message(f"warning: invalid token file {file}. Consider removing it.")
25
+ return token_files
26
+
27
+
28
+ def get_token_file(cred_name: str, config_dir: Path) -> TokenFile | None:
29
+ """
30
+ Get the token file for a given credentials name.
31
+
32
+ Note! Dont use settings, as that will couple settings to the old config and token formats we want to migrate away from.
33
+ """
34
+ token_files = list_token_files(config_dir)
35
+ matches = [token_file for token_file in token_files if token_file.name == cred_name]
36
+ if len(matches) != 1:
37
+ return None
38
+ return matches[0]
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from datetime import datetime, timedelta
5
+ from typing import Any, Optional
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from remotivelabs.cli.utils.time import parse_datetime
10
+
11
+
12
+ class StateFile(BaseModel):
13
+ """
14
+ Contains CLI state and other application specific data.
15
+ """
16
+
17
+ version: str = "1.0"
18
+ last_update_check_time: Optional[str] = None
19
+
20
+ def should_perform_update_check(self) -> bool:
21
+ """
22
+ Check if we should perform an update check.
23
+
24
+ Returns True if the last update check time is older than 2 hours.
25
+
26
+ For Docker environments, returns False and sets a backdated timestamp
27
+ to prevent constant update checks due to ephemeral disks.
28
+ """
29
+ if not self.last_update_check_time:
30
+ if os.environ.get("RUNS_IN_DOCKER"):
31
+ # To prevent that we always check update in docker due to ephemeral disks we write an "old" check if the state
32
+ # is missing. If no disk is mounted we will never get the update check but if its mounted properly we will get
33
+ # it on the second attempt. This is good enough
34
+ self.last_update_check_time = (datetime.now() - timedelta(hours=10)).isoformat()
35
+ return False
36
+ return True # Returning True will trigger a check, which will properly set last_update_check_time
37
+
38
+ seconds = (datetime.now() - parse_datetime(self.last_update_check_time)).seconds
39
+ return (seconds / 3600) > 2
40
+
41
+ @classmethod
42
+ def from_json_str(cls, data: str) -> StateFile:
43
+ return cls.model_validate_json(data)
44
+
45
+ @classmethod
46
+ def from_dict(cls, data: dict[str, Any]) -> StateFile:
47
+ return cls.model_validate(data)
48
+
49
+ def to_json_str(self) -> str:
50
+ return self.model_dump_json()
51
+
52
+ def to_dict(self) -> dict[str, Any]:
53
+ return self.model_dump()
54
+
55
+
56
+ def loads(data: str) -> StateFile:
57
+ """
58
+ Creates a StateFile from a JSON string.
59
+ """
60
+ return StateFile.from_json_str(data)
61
+
62
+
63
+ def dumps(state: StateFile) -> str:
64
+ """
65
+ Returns the JSON string representation of the StateFile.
66
+ """
67
+ return state.to_json_str()
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from datetime import date, datetime
5
+ from typing import Any, Literal
6
+
7
+ from pydantic import BaseModel, EmailStr, Field, field_validator, model_validator
8
+
9
+ from remotivelabs.cli.utils.time import parse_date
10
+
11
+ DEFAULT_EMAIL = "unknown@remotivecloud.com"
12
+ PERSONAL_TOKEN_FILE_PREFIX = "personal-token-"
13
+ SERVICE_ACCOUNT_TOKEN_FILE_PREFIX = "service-account-token-"
14
+
15
+ TokenType = Literal["authorized_user", "service_account"]
16
+
17
+
18
+ def _email_to_safe_filename(email: str) -> str:
19
+ """Replace any invalid character with an underscore"""
20
+ return re.sub(r'[<>:"/\\|?*]', "_", email)
21
+
22
+
23
+ def _parse_token_type(token: str) -> TokenType:
24
+ if token.startswith("pa"):
25
+ return "authorized_user"
26
+ if token.startswith("sa"):
27
+ return "service_account"
28
+ raise ValueError(f"Unknown token type for token: {token}")
29
+
30
+
31
+ class TokenFileAccount(BaseModel):
32
+ """
33
+ TokenFileAccount represents the account information for a token file.
34
+ """
35
+
36
+ email: EmailStr = DEFAULT_EMAIL
37
+
38
+
39
+ class TokenFile(BaseModel):
40
+ """
41
+ TokenFile represents a token file for the CLI.
42
+
43
+ TODO: Should all setters return a new instance of the TokenFile?
44
+ """
45
+
46
+ version: str = "1.0"
47
+ type: TokenType
48
+ name: str
49
+ token: str
50
+ created: date
51
+ expires: date
52
+ account: TokenFileAccount = Field(default_factory=TokenFileAccount)
53
+
54
+ @field_validator("created", "expires", mode="before")
55
+ @classmethod
56
+ def _validate_parse_date(cls, value: str | date) -> date:
57
+ if isinstance(value, date):
58
+ return value
59
+ return parse_date(value)
60
+
61
+ @model_validator(mode="before")
62
+ @classmethod
63
+ def _validate_json_data(cls, json_data: Any) -> Any:
64
+ """
65
+ Try to migrate old formats and missing fields as best we can.
66
+
67
+ NOTE: If we ever need to add a new version (like 2.0), we should add explicit classes for each version (e.g. TokenFileV1,
68
+ TokenFileV2, etc.), each with their own fields. This will allow us to migrate to new versions without breaking
69
+ backwards compatibility.
70
+ """
71
+ if not isinstance(json_data, dict):
72
+ return json_data
73
+
74
+ if "version" not in json_data:
75
+ json_data["version"] = "1.0"
76
+
77
+ if "type" not in json_data and "token" in json_data:
78
+ json_data["type"] = _parse_token_type(json_data["token"])
79
+
80
+ if "account" not in json_data:
81
+ json_data["account"] = {"email": DEFAULT_EMAIL}
82
+ elif isinstance(json_data["account"], str):
83
+ json_data["account"] = {"email": json_data["account"]}
84
+
85
+ return json_data
86
+
87
+ def get_token_file_name(self) -> str:
88
+ """
89
+ Returns the name of the token_file following a predictable naming format.
90
+ """
91
+ email = _email_to_safe_filename(self.account.email) if self.account is not None else "unknown"
92
+ if self.type == "authorized_user":
93
+ return f"{PERSONAL_TOKEN_FILE_PREFIX}{self.name}-{email}.json"
94
+ return f"{SERVICE_ACCOUNT_TOKEN_FILE_PREFIX}{self.name}-{email}.json"
95
+
96
+ def is_expired(self) -> bool:
97
+ return datetime.today().date() > self.expires
98
+
99
+ def expires_in_days(self) -> int:
100
+ return (self.expires - datetime.today().date()).days
101
+
102
+ @classmethod
103
+ def from_json_str(cls, data: str) -> TokenFile:
104
+ return cls.model_validate_json(data)
105
+
106
+ @classmethod
107
+ def from_dict(cls, data: dict[str, Any]) -> TokenFile:
108
+ return cls.model_validate(data)
109
+
110
+ def to_json_str(self) -> str:
111
+ return self.model_dump_json()
112
+
113
+ def to_dict(self) -> dict[str, Any]:
114
+ return self.model_dump()
115
+
116
+
117
+ def loads(data: str) -> TokenFile:
118
+ """
119
+ Creates a TokenFile from a JSON string.
120
+ """
121
+ return TokenFile.from_json_str(data)
122
+
123
+
124
+ def dumps(token_file: TokenFile) -> str:
125
+ """
126
+ Returns the JSON string representation of the TokenFile.
127
+ """
128
+ return token_file.to_json_str()
File without changes
File without changes
@@ -0,0 +1,78 @@
1
+ from pathlib import Path
2
+
3
+ import can
4
+ import typer
5
+
6
+ from remotivelabs.cli.typer import typer_utils
7
+ from remotivelabs.cli.utils.console import print_generic_error, print_success
8
+
9
+ HELP = """
10
+ CAN related tools
11
+ """
12
+
13
+ app = typer_utils.create_typer(help=HELP)
14
+
15
+
16
+ @app.command("convert")
17
+ def convert(
18
+ in_file: Path = typer.Argument(
19
+ exists=True,
20
+ file_okay=True,
21
+ dir_okay=False,
22
+ writable=False,
23
+ readable=True,
24
+ resolve_path=True,
25
+ help="File to convert from (.blf .asc .log)",
26
+ ),
27
+ out_file: Path = typer.Argument(
28
+ exists=False,
29
+ file_okay=True,
30
+ dir_okay=False,
31
+ writable=True,
32
+ readable=True,
33
+ resolve_path=True,
34
+ help="File to convert to (.blf .asc .log)",
35
+ ),
36
+ ) -> None:
37
+ r"""
38
+ Converts between ASC, BLF and LOG files. Files must end with .asc, .blf or .log.
39
+
40
+ remotive tools can convert \[my_file.blf|.log|.asc] \[my_file.blf|.log|.asc]
41
+ """
42
+
43
+ with can.LogReader(in_file, relative_timestamp=False) as reader:
44
+ try:
45
+ with can.Logger(out_file) as writer:
46
+ for msg in reader:
47
+ writer.on_message_received(msg)
48
+ except Exception as e:
49
+ print_generic_error(f"Failed to convert file: {e}")
50
+
51
+
52
+ @app.command("validate")
53
+ def validate(
54
+ in_file: Path = typer.Argument(
55
+ exists=True,
56
+ file_okay=True,
57
+ dir_okay=False,
58
+ writable=False,
59
+ readable=True,
60
+ resolve_path=True,
61
+ help="File to validate (.blf .asc .log)",
62
+ ),
63
+ print_to_terminal: bool = typer.Option(False, help="Print file contents to terminal"),
64
+ ) -> None:
65
+ r"""
66
+ Validates that the input file is an ASC, BLF and LOG file
67
+
68
+ remotive tools can validate \[my_file.blf|.log|.asc]
69
+ """
70
+ with can.LogReader(in_file, relative_timestamp=False) as reader:
71
+ try:
72
+ with can.Printer() as writer:
73
+ for msg in reader:
74
+ if print_to_terminal:
75
+ writer.on_message_received(msg)
76
+ print_success(f"{in_file} verified")
77
+ except Exception as e:
78
+ print_generic_error(f"Failed to convert file: {e}")
@@ -0,0 +1,9 @@
1
+ from remotivelabs.cli.tools.can.can import app as can_app
2
+ from remotivelabs.cli.typer import typer_utils
3
+
4
+ HELP_TEXT = """
5
+ CLI tools unrelated to cloud or broker
6
+ """
7
+
8
+ app = typer_utils.create_typer(help=HELP_TEXT)
9
+ app.add_typer(can_app, name="can", help="CAN tools")
@@ -0,0 +1,28 @@
1
+ from enum import Enum
2
+
3
+ import typer
4
+
5
+ from remotivelabs.cli.topology.all import app
6
+
7
+
8
+ class ContainerEngine(str, Enum):
9
+ docker = "docker"
10
+ podman = "podman"
11
+
12
+
13
+ @app.callback()
14
+ def service_callback(
15
+ ctx: typer.Context,
16
+ topology_cmd: str = typer.Option(
17
+ None, envvar="REMOTIVE_TOPOLOGY_COMMAND", hidden=True, help="Optional path to RemotiveTopology command"
18
+ ),
19
+ topology_image: str = typer.Option(None, envvar="REMOTIVE_TOPOLOGY_IMAGE", help="Optional docker image for RemotiveTopology "),
20
+ container_engine: ContainerEngine = typer.Option(ContainerEngine.docker, envvar="CONTAINER_ENGINE", help="Specify container engine"),
21
+ ) -> None:
22
+ ctx.obj = ctx.obj or {}
23
+ ctx.obj["topology_cmd"] = topology_cmd
24
+ ctx.obj["topology_image"] = topology_image
25
+ ctx.obj["container_engine"] = container_engine.name
26
+
27
+
28
+ __all__ = ["app"]