remotivelabs-cli 0.1.0a1__py3-none-any.whl → 0.1.0a2__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/cloud/auth/cmd.py +19 -14
- cli/cloud/auth/login.py +43 -43
- cli/cloud/auth_tokens.py +61 -37
- cli/cloud/cloud_cli.py +4 -4
- cli/cloud/organisations.py +17 -6
- cli/cloud/projects.py +4 -4
- cli/errors.py +6 -1
- cli/remotive.py +2 -2
- cli/settings/config_file.py +9 -2
- cli/settings/core.py +15 -21
- cli/utils/rest_helper.py +15 -11
- {remotivelabs_cli-0.1.0a1.dist-info → remotivelabs_cli-0.1.0a2.dist-info}/METADATA +1 -1
- {remotivelabs_cli-0.1.0a1.dist-info → remotivelabs_cli-0.1.0a2.dist-info}/RECORD +16 -16
- {remotivelabs_cli-0.1.0a1.dist-info → remotivelabs_cli-0.1.0a2.dist-info}/LICENSE +0 -0
- {remotivelabs_cli-0.1.0a1.dist-info → remotivelabs_cli-0.1.0a2.dist-info}/WHEEL +0 -0
- {remotivelabs_cli-0.1.0a1.dist-info → remotivelabs_cli-0.1.0a2.dist-info}/entry_points.txt +0 -0
cli/cloud/auth/cmd.py
CHANGED
@@ -11,7 +11,6 @@ from cli.typer import typer_utils
|
|
11
11
|
from cli.utils.rest_helper import RestHelper as Rest
|
12
12
|
|
13
13
|
from .. import auth_tokens
|
14
|
-
from ..organisations import do_select_default_org
|
15
14
|
|
16
15
|
HELP = """
|
17
16
|
Manage how you authenticate with our cloud platform
|
@@ -33,12 +32,6 @@ def login(browser: bool = typer.Option(default=True, help="Does not automaticall
|
|
33
32
|
be the same as activating a personal access key or service-account access key.
|
34
33
|
"""
|
35
34
|
do_login(headless=not browser)
|
36
|
-
if settings.get_cli_config().get_active_default_organisation() is None:
|
37
|
-
set_default_organisation = typer.confirm(
|
38
|
-
"You have not set a default organisation\nWould you like to choose one now?", abort=False, default=True
|
39
|
-
)
|
40
|
-
if set_default_organisation:
|
41
|
-
do_select_default_org(get=False)
|
42
35
|
|
43
36
|
|
44
37
|
@app.command()
|
@@ -54,15 +47,27 @@ def whoami() -> None:
|
|
54
47
|
|
55
48
|
|
56
49
|
@app.command()
|
57
|
-
def print_access_token(
|
50
|
+
def print_access_token(
|
51
|
+
account: str = typer.Option(None, help="Email of the account you want to print access token for, defaults to active"),
|
52
|
+
) -> None:
|
58
53
|
"""
|
59
|
-
Print current active token
|
54
|
+
Print current active access token or the token for the specified account
|
60
55
|
"""
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
56
|
+
if account is None:
|
57
|
+
try:
|
58
|
+
print(settings.get_active_token())
|
59
|
+
except TokenNotFoundError:
|
60
|
+
ErrorPrinter.print_generic_error("You have no active account", exit_code=1)
|
61
|
+
else:
|
62
|
+
config = settings.get_cli_config()
|
63
|
+
if account in config.accounts:
|
64
|
+
token_name = config.accounts[account].credentials_name
|
65
|
+
try:
|
66
|
+
print(settings.get_token_file(token_name).token)
|
67
|
+
except TokenNotFoundError:
|
68
|
+
ErrorPrinter.print_generic_error(f"Token file for {account} could not be found", exit_code=1)
|
69
|
+
else:
|
70
|
+
ErrorPrinter.print_generic_error(f"No account for {account} was found", exit_code=1)
|
66
71
|
|
67
72
|
|
68
73
|
def print_access_token_file() -> None:
|
cli/cloud/auth/login.py
CHANGED
@@ -17,9 +17,9 @@ import typer
|
|
17
17
|
from rich.console import Console
|
18
18
|
from typing_extensions import override
|
19
19
|
|
20
|
-
from cli.cloud.auth_tokens import
|
20
|
+
from cli.cloud.auth_tokens import do_activate, prompt_to_set_org
|
21
21
|
from cli.errors import ErrorPrinter
|
22
|
-
from cli.settings import TokenNotFoundError, settings
|
22
|
+
from cli.settings import TokenFile, TokenNotFoundError, settings
|
23
23
|
from cli.utils.rest_helper import RestHelper as Rest
|
24
24
|
|
25
25
|
httpd: HTTPServer
|
@@ -88,20 +88,16 @@ class S(BaseHTTPRequestHandler):
|
|
88
88
|
)
|
89
89
|
)
|
90
90
|
sys.exit(1)
|
91
|
-
|
91
|
+
|
92
|
+
# TODO - This is written before we are done...
|
93
|
+
self.wfile.write(
|
94
|
+
"""Successfully setup CLI, you may close this window now. Return to your terminal to continue""".encode("utf-8")
|
95
|
+
)
|
92
96
|
access_token = res.json()["access_token"]
|
93
|
-
# token = tf.TokenFile(
|
94
|
-
# name="CLI_login_token",
|
95
|
-
# token=access_token,
|
96
|
-
# created=str(datetime.datetime.now().isoformat()),
|
97
|
-
# expires="unknown",
|
98
|
-
# )
|
99
97
|
|
100
98
|
global short_lived_token # noqa: PLW0603
|
101
99
|
short_lived_token = access_token
|
102
100
|
|
103
|
-
# settings.add_and_activate_short_lived_cli_token(tf.dumps(token))
|
104
|
-
# print("Successfully logged on, you are ready to go with cli")
|
105
101
|
else:
|
106
102
|
if error_value == "no_consent":
|
107
103
|
self.wfile.write(
|
@@ -113,6 +109,19 @@ class S(BaseHTTPRequestHandler):
|
|
113
109
|
""".encode("utf-8")
|
114
110
|
)
|
115
111
|
ErrorPrinter.print_generic_error("You did not grant access to RemotiveCloud, login aborted")
|
112
|
+
elif error_value == "user_not_exists":
|
113
|
+
self.wfile.write(
|
114
|
+
"""
|
115
|
+
It seems like you do not have an account at RemotiveCloud with that user<br/>
|
116
|
+
To use RemotiveCLI you must first sign up at <a href="https://cloud.remotivelabs.com">cloud.remotivelabs.com</a>
|
117
|
+
and approve our agreements.<br/>
|
118
|
+
<br/><br/>
|
119
|
+
Once you are signed up, Run `remotive cloud auth login` again.
|
120
|
+
""".encode("utf-8")
|
121
|
+
)
|
122
|
+
ErrorPrinter.print_generic_error(
|
123
|
+
"To use RemotiveCLI you must first sign up at https://cloud.remotivelabs.com and approve our agreements"
|
124
|
+
)
|
116
125
|
else:
|
117
126
|
self.wfile.write(f"Unknown error {error_value}, please contact support@remotivelabs.com".encode("utf-8"))
|
118
127
|
ErrorPrinter.print_generic_error(f"Unexpected error {error_value}, please contact support@remotivelabs.com")
|
@@ -163,6 +172,22 @@ def create_personal_token() -> None:
|
|
163
172
|
print("Successfully logged on")
|
164
173
|
|
165
174
|
|
175
|
+
def _do_prompt_to_use_existing_credentials() -> Optional[TokenFile]:
|
176
|
+
files = settings.list_personal_token_files()
|
177
|
+
if len(files) > 0:
|
178
|
+
should_select_token = typer.confirm(
|
179
|
+
"You have credentials available already, would you like to choose one of these instead?", default=True
|
180
|
+
)
|
181
|
+
if should_select_token:
|
182
|
+
token = do_activate(token_name=None)
|
183
|
+
# token = list_and_select_personal_token(skip_prompt=False, include_service_accounts=True)
|
184
|
+
if token is not None:
|
185
|
+
return token
|
186
|
+
# TODO - fix so this is not needed
|
187
|
+
sys.exit(0)
|
188
|
+
return None
|
189
|
+
|
190
|
+
|
166
191
|
def login(headless: bool = False) -> bool: # noqa: C901, PLR0912, PLR0915
|
167
192
|
"""
|
168
193
|
Initiate login
|
@@ -178,18 +203,9 @@ def login(headless: bool = False) -> bool: # noqa: C901, PLR0912, PLR0915
|
|
178
203
|
|
179
204
|
if not activate_token.is_expired():
|
180
205
|
if Rest.has_access("/api/whoami"):
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
print("")
|
185
|
-
console.print("You have available credentials on disk, [bold]choose one or press q to login again[/bold]")
|
186
|
-
token_selected = list_and_select_personal_token(skip_prompt=False)
|
187
|
-
if token_selected:
|
188
|
-
return True
|
189
|
-
# list_and_select_personal_token(skip_prompt=True)
|
190
|
-
# print("")
|
191
|
-
# typer.confirm("Are you sure you want to login again?", abort=True)
|
192
|
-
# If we are here, user still wants to login
|
206
|
+
token = _do_prompt_to_use_existing_credentials()
|
207
|
+
if token is not None:
|
208
|
+
return True
|
193
209
|
else:
|
194
210
|
settings.clear_active_token()
|
195
211
|
raise TokenNotFoundError()
|
@@ -201,15 +217,9 @@ def login(headless: bool = False) -> bool: # noqa: C901, PLR0912, PLR0915
|
|
201
217
|
#
|
202
218
|
# 2. If no token was found, let user choose an existing if exists
|
203
219
|
#
|
204
|
-
|
205
|
-
if
|
206
|
-
|
207
|
-
token_selected = list_and_select_personal_token(
|
208
|
-
skip_prompt=False,
|
209
|
-
info_message="You have available credentials on disk, choose one or press q to login again",
|
210
|
-
)
|
211
|
-
if token_selected:
|
212
|
-
return True
|
220
|
+
token = _do_prompt_to_use_existing_credentials()
|
221
|
+
if token is not None:
|
222
|
+
return True
|
213
223
|
|
214
224
|
prepare_local_webserver()
|
215
225
|
|
@@ -273,17 +283,7 @@ def login(headless: bool = False) -> bool: # noqa: C901, PLR0912, PLR0915
|
|
273
283
|
global short_lived_token # noqa: PLW0603
|
274
284
|
short_lived_token = access_token
|
275
285
|
create_personal_token()
|
276
|
-
|
277
|
-
# token = tf.TokenFile(
|
278
|
-
# type="authorized_user",
|
279
|
-
# name="CLI_login_token",
|
280
|
-
# token=access_token,
|
281
|
-
# created=str(datetime.datetime.now().isoformat()),
|
282
|
-
# expires="unknown",
|
283
|
-
# account=TokenFileUser(email=current_user["email"], uid=current_user["uid"], project=None),
|
284
|
-
# )
|
285
|
-
# settings.add_and_activate_short_lived_cli_token(tf.dumps(token))
|
286
|
-
# console.print("Successfully logged on, you are ready to go with cli", style="green bold")
|
286
|
+
prompt_to_set_org()
|
287
287
|
|
288
288
|
if headless and not force_use_webserver_callback():
|
289
289
|
login_headless()
|
cli/cloud/auth_tokens.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from typing import List,
|
3
|
+
from typing import List, Literal, Optional
|
4
4
|
|
5
5
|
import typer
|
6
6
|
from rich.console import Console
|
@@ -18,14 +18,15 @@ err_console = Console(stderr=True)
|
|
18
18
|
|
19
19
|
app = typer_utils.create_typer()
|
20
20
|
|
21
|
+
PromptType = Literal["activate", "login"]
|
22
|
+
|
21
23
|
|
22
24
|
def _prompt_choice( # noqa: C901, PLR0912
|
23
25
|
choices: List[TokenFile],
|
24
26
|
skip_prompt: bool = False,
|
25
27
|
info_message: Optional[str] = None,
|
26
|
-
) -> Optional[
|
28
|
+
) -> Optional[TokenFile]:
|
27
29
|
accounts = settings.get_cli_config().accounts
|
28
|
-
|
29
30
|
try:
|
30
31
|
active_account = settings.get_cli_config().get_active()
|
31
32
|
except TokenNotFoundError:
|
@@ -42,30 +43,25 @@ def _prompt_choice( # noqa: C901, PLR0912
|
|
42
43
|
included_tokens.append(token)
|
43
44
|
else:
|
44
45
|
excluded_tokens.append(token)
|
45
|
-
if len(excluded_tokens) > 0:
|
46
|
-
err_console.print("The following credentials were not included in account config and cannot be activated")
|
47
|
-
for token in excluded_tokens:
|
48
|
-
err_console.print(f" * {token.name} - {token.account.email}")
|
49
46
|
|
50
47
|
if len(included_tokens) == 0:
|
51
48
|
return None
|
52
49
|
|
53
|
-
if info_message is not None:
|
54
|
-
console.print(info_message)
|
55
|
-
|
56
50
|
included_tokens.sort(key=lambda token: token.created, reverse=True)
|
57
51
|
|
52
|
+
active_token_index = None
|
58
53
|
for idx, choice in enumerate(included_tokens, start=1):
|
54
|
+
is_active = active_account is not None and active_account.credentials_name == choice.name
|
55
|
+
active_token_index = idx if is_active else active_token_index
|
59
56
|
table.add_row(
|
60
57
|
f"[yellow]{idx}",
|
61
|
-
":white_check_mark:" if
|
58
|
+
":white_check_mark:" if is_active else "",
|
62
59
|
"user" if choice.type == "authorized_user" else "sa",
|
63
60
|
choice.name,
|
64
61
|
f"[bold]{choice.account.email if choice.account else 'unknown'}[/bold]",
|
65
62
|
str(choice.created),
|
66
63
|
str(choice.expires),
|
67
64
|
)
|
68
|
-
|
69
65
|
# console.print("It seems like you have access tokens from previous login, you can select one of these instead of logging in")
|
70
66
|
console.print(table)
|
71
67
|
|
@@ -73,7 +69,13 @@ def _prompt_choice( # noqa: C901, PLR0912
|
|
73
69
|
return None
|
74
70
|
|
75
71
|
typer.echo("")
|
76
|
-
|
72
|
+
if info_message is not None:
|
73
|
+
console.print(info_message)
|
74
|
+
|
75
|
+
selection = typer.prompt(
|
76
|
+
f"Enter the number(# 1-{len(included_tokens)}) of the account to select (q to quit)",
|
77
|
+
default=f"{active_token_index}" if active_token_index is not None else None,
|
78
|
+
)
|
77
79
|
|
78
80
|
if selection == "q":
|
79
81
|
return None
|
@@ -141,51 +143,65 @@ def activate(
|
|
141
143
|
err_console.print(f":boom: [bold red] Error: [/bold red] Token with filename or name {token_name} could not be found")
|
142
144
|
|
143
145
|
|
146
|
+
def prompt_to_set_org() -> None:
|
147
|
+
if settings.get_cli_config().get_active_default_organisation() is None:
|
148
|
+
set_default_organisation = typer.confirm(
|
149
|
+
"You have not set a default organization\nWould you like to choose one now?",
|
150
|
+
abort=False,
|
151
|
+
default=True,
|
152
|
+
)
|
153
|
+
if set_default_organisation:
|
154
|
+
do_select_default_org(get=False)
|
155
|
+
|
156
|
+
|
144
157
|
@app.command("activate")
|
145
158
|
def select_personal_token(
|
146
159
|
token_name: str = typer.Argument(None, help="Name, filename or path to a credentials file"),
|
147
160
|
) -> None:
|
148
161
|
"""
|
149
|
-
|
162
|
+
Activates is setting the current active credentials to use by the CLI, this can be done by specifying a name
|
163
|
+
of the token or getting prompted and choosing from existing.
|
150
164
|
"""
|
165
|
+
do_activate(token_name)
|
166
|
+
|
167
|
+
|
168
|
+
def do_activate(
|
169
|
+
token_name: Optional[str],
|
170
|
+
) -> Optional[TokenFile]:
|
151
171
|
if token_name is not None:
|
152
|
-
token_selected = True
|
153
172
|
try:
|
154
173
|
token_file = settings.get_token_file(token_name)
|
155
174
|
settings.activate_token(token_file)
|
175
|
+
return token_file
|
156
176
|
except TokenNotFoundError:
|
157
|
-
token_selected = False
|
158
177
|
err_console.print(f":boom: [bold red] Error: [/bold red] Token with filename or name {token_name} could not be found")
|
178
|
+
return None
|
159
179
|
else:
|
160
180
|
token_files = settings.list_personal_tokens()
|
161
181
|
token_files.extend(settings.list_service_account_tokens())
|
162
182
|
if len(token_files) > 0:
|
163
183
|
token_selected = list_and_select_personal_token(include_service_accounts=True)
|
164
|
-
|
165
|
-
if token_selected:
|
184
|
+
if token_selected is not None:
|
166
185
|
is_logged_in = Rest.has_access("/api/whoami")
|
167
186
|
if not is_logged_in:
|
168
187
|
ErrorPrinter.print_generic_error("Could not access RemotiveCloud with selected token")
|
169
188
|
else:
|
170
189
|
console.print("[green]Success![/green] Access to RemotiveCloud granted")
|
171
|
-
|
172
|
-
|
190
|
+
# Only select default if activate was done with selection and successful
|
191
|
+
# and not SA since SA cannot list available organizations
|
192
|
+
if token_selected.type == "authorized_user":
|
193
|
+
prompt_to_set_org()
|
194
|
+
return token_selected
|
173
195
|
|
174
|
-
|
175
|
-
|
176
|
-
"You have not set a default organisation\nWould you like to choose one now?",
|
177
|
-
abort=False,
|
178
|
-
default=True,
|
179
|
-
)
|
180
|
-
if set_default_organisation:
|
181
|
-
do_select_default_org(get=False)
|
196
|
+
ErrorPrinter.print_hint("No credentials available, login to activate credentials")
|
197
|
+
return None
|
182
198
|
|
183
199
|
|
184
200
|
def list_and_select_personal_token(
|
185
201
|
skip_prompt: bool = False,
|
186
202
|
include_service_accounts: bool = False,
|
187
203
|
info_message: Optional[str] = None,
|
188
|
-
) ->
|
204
|
+
) -> Optional[TokenFile]:
|
189
205
|
personal_tokens = settings.list_personal_tokens()
|
190
206
|
|
191
207
|
if include_service_accounts:
|
@@ -197,8 +213,8 @@ def list_and_select_personal_token(
|
|
197
213
|
selected_token = _prompt_choice(personal_tokens, skip_prompt=skip_prompt, info_message=info_message)
|
198
214
|
if selected_token is not None:
|
199
215
|
settings.activate_token(selected_token)
|
200
|
-
|
201
|
-
return
|
216
|
+
|
217
|
+
return selected_token
|
202
218
|
|
203
219
|
|
204
220
|
# @app.command("select-revoke")
|
@@ -271,15 +287,23 @@ def list_sats_files() -> None:
|
|
271
287
|
|
272
288
|
|
273
289
|
@app.command(name="list")
|
274
|
-
def list_pats_files(
|
290
|
+
def list_pats_files(
|
291
|
+
accounts: bool = typer.Option(True, help="Lists all available accounts"),
|
292
|
+
files: bool = typer.Option(False, help="Shows all token files in config directory"),
|
293
|
+
) -> None:
|
275
294
|
"""
|
276
295
|
Lists available credential files on filesystem
|
277
296
|
"""
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
297
|
+
|
298
|
+
if accounts:
|
299
|
+
list_and_select_personal_token(skip_prompt=True, include_service_accounts=True, info_message="hello")
|
300
|
+
|
301
|
+
if files:
|
302
|
+
personal_files = settings.list_personal_token_files()
|
303
|
+
service_account_files = settings.list_service_account_token_files()
|
304
|
+
personal_files.extend(service_account_files)
|
305
|
+
for file in personal_files:
|
306
|
+
print(file)
|
283
307
|
|
284
308
|
|
285
309
|
def _revoke_and_delete_personal_token(name: str, delete: bool) -> None:
|
cli/cloud/cloud_cli.py
CHANGED
@@ -7,15 +7,15 @@ from cli.utils.rest_helper import RestHelper
|
|
7
7
|
app = typer_utils.create_typer()
|
8
8
|
|
9
9
|
|
10
|
-
@app.command(help="List licenses for an
|
10
|
+
@app.command(help="List licenses for an organization")
|
11
11
|
def licenses(
|
12
|
-
|
12
|
+
organization: str = typer.Option(..., help="Organization ID", envvar="REMOTIVE_CLOUD_ORGANIZATION"),
|
13
13
|
filter_option: str = typer.Option("all", help="all, valid, expired"),
|
14
14
|
) -> None:
|
15
|
-
RestHelper.handle_get(f"/api/bu/{
|
15
|
+
RestHelper.handle_get(f"/api/bu/{organization}/licenses", {"filter": filter_option})
|
16
16
|
|
17
17
|
|
18
|
-
app.add_typer(organisations.app, name="
|
18
|
+
app.add_typer(organisations.app, name="organizations", help="Manage organizations")
|
19
19
|
app.add_typer(projects.app, name="projects", help="Manage projects")
|
20
20
|
app.add_typer(auth.app, name="auth")
|
21
21
|
app.add_typer(brokers.app, name="brokers", help="Manage cloud broker lifecycle")
|
cli/cloud/organisations.py
CHANGED
@@ -8,6 +8,7 @@ import typer
|
|
8
8
|
from rich.console import Console
|
9
9
|
from rich.table import Table
|
10
10
|
|
11
|
+
from cli.errors import ErrorPrinter
|
11
12
|
from cli.settings import settings
|
12
13
|
from cli.typer import typer_utils
|
13
14
|
from cli.utils.rest_helper import RestHelper
|
@@ -40,7 +41,7 @@ def _prompt_choice(choices: List[Organisation]) -> Optional[Organisation]:
|
|
40
41
|
console.print(table)
|
41
42
|
|
42
43
|
typer.echo("")
|
43
|
-
selection = typer.prompt(f"Enter the number(# 1-{len(choices)}) of the
|
44
|
+
selection = typer.prompt(f"Enter the number(# 1-{len(choices)}) of the organization to select (or q to quit)")
|
44
45
|
|
45
46
|
if selection == "q":
|
46
47
|
return None
|
@@ -56,15 +57,15 @@ def _prompt_choice(choices: List[Organisation]) -> Optional[Organisation]:
|
|
56
57
|
|
57
58
|
@app.command("default")
|
58
59
|
def select_default_org(
|
59
|
-
|
60
|
-
get: bool = typer.Option(False, help="Print current default
|
60
|
+
organization_uid: str = typer.Argument(None, help="Organization uid or empty to select one"),
|
61
|
+
get: bool = typer.Option(False, help="Print current default organization"),
|
61
62
|
) -> None:
|
62
|
-
do_select_default_org(
|
63
|
+
do_select_default_org(organization_uid, get)
|
63
64
|
|
64
65
|
|
65
66
|
def do_select_default_org(organisation_uid: Optional[str] = None, get: bool = False) -> None:
|
66
67
|
r"""
|
67
|
-
Set default
|
68
|
+
Set default organization for the currently activated user, empty to choose from available organizations or organization uid as argument
|
68
69
|
|
69
70
|
remotive cloud organizations default my_org \[set specific org uid]
|
70
71
|
remotive cloud organizations default \[select one from prompt]
|
@@ -78,10 +79,20 @@ def do_select_default_org(organisation_uid: Optional[str] = None, get: bool = Fa
|
|
78
79
|
if default_organisation is not None:
|
79
80
|
console.print(default_organisation)
|
80
81
|
else:
|
81
|
-
console.print("No default
|
82
|
+
console.print("No default organization set")
|
82
83
|
elif organisation_uid is not None:
|
83
84
|
settings.set_default_organisation(organisation_uid)
|
84
85
|
else:
|
86
|
+
account = settings.get_cli_config().get_active()
|
87
|
+
if account is not None:
|
88
|
+
token = settings.get_token_file(account.credentials_name)
|
89
|
+
if token.type != "authorized_user":
|
90
|
+
ErrorPrinter.print_hint(
|
91
|
+
"You must supply the organization name as argument when using a service-account since the "
|
92
|
+
"service-account is not allowed to list"
|
93
|
+
)
|
94
|
+
return
|
95
|
+
|
85
96
|
r = RestHelper.handle_get("/api/bu", return_response=True)
|
86
97
|
orgs = r.json()
|
87
98
|
orgs = [Organisation(display_name=o["organisation"]["displayName"], uid=o["organisation"]["uid"]) for o in orgs]
|
cli/cloud/projects.py
CHANGED
@@ -9,8 +9,8 @@ app = typer_utils.create_typer()
|
|
9
9
|
|
10
10
|
|
11
11
|
@app.command(name="list", help="List your projects")
|
12
|
-
def list_projects(
|
13
|
-
r = Rest.handle_get(url=f"/api/bu/{
|
12
|
+
def list_projects(organization: str = typer.Option(..., help="Organization ID", envvar="REMOTIVE_CLOUD_ORGANIZATION")) -> None:
|
13
|
+
r = Rest.handle_get(url=f"/api/bu/{organization}/me", return_response=True)
|
14
14
|
if r is None:
|
15
15
|
return
|
16
16
|
|
@@ -27,7 +27,7 @@ def list_projects(organisation: str = typer.Option(..., help="Organisation ID",
|
|
27
27
|
@app.command(name="create")
|
28
28
|
def create_project(
|
29
29
|
project_uid: str = typer.Argument(help="Project UID"),
|
30
|
-
|
30
|
+
organization: str = typer.Option(..., help="Organization ID", envvar="REMOTIVE_CLOUD_ORGANIZATION"),
|
31
31
|
project_display_name: str = typer.Option(default="", help="Project display name"),
|
32
32
|
) -> None:
|
33
33
|
create_project_req = {
|
@@ -36,7 +36,7 @@ def create_project(
|
|
36
36
|
"description": "",
|
37
37
|
}
|
38
38
|
|
39
|
-
Rest.handle_post(url=f"/api/bu/{
|
39
|
+
Rest.handle_post(url=f"/api/bu/{organization}/project", body=json.dumps(create_project_req))
|
40
40
|
|
41
41
|
|
42
42
|
@app.command(name="delete")
|
cli/errors.py
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import os
|
2
4
|
import sys
|
5
|
+
from typing import Optional
|
3
6
|
|
4
7
|
import grpc
|
5
8
|
from rich.console import Console
|
@@ -31,8 +34,10 @@ class ErrorPrinter:
|
|
31
34
|
err_console.print(f":point_right: [bold]{message}[/bold]")
|
32
35
|
|
33
36
|
@staticmethod
|
34
|
-
def print_generic_error(message: str) -> None:
|
37
|
+
def print_generic_error(message: str, exit_code: Optional[int] = None) -> None:
|
35
38
|
err_console.print(f":boom: [bold red]Failed[/bold red]: {message}")
|
39
|
+
if exit_code is not None:
|
40
|
+
sys.exit(exit_code)
|
36
41
|
|
37
42
|
@staticmethod
|
38
43
|
def print_generic_message(message: str) -> None:
|
cli/remotive.py
CHANGED
@@ -60,10 +60,10 @@ def _set_default_org_as_env() -> None:
|
|
60
60
|
If not already set, take the default organisation from file and set as env
|
61
61
|
This has to be done early before it is read
|
62
62
|
"""
|
63
|
-
if "
|
63
|
+
if "REMOTIVE_CLOUD_ORGANIZATION" not in os.environ:
|
64
64
|
org = settings.get_cli_config().get_active_default_organisation()
|
65
65
|
if org is not None:
|
66
|
-
os.environ["
|
66
|
+
os.environ["REMOTIVE_CLOUD_ORGANIZATION"] = org
|
67
67
|
|
68
68
|
|
69
69
|
@app.callback()
|
cli/settings/config_file.py
CHANGED
@@ -3,14 +3,21 @@ from __future__ import annotations
|
|
3
3
|
import dataclasses
|
4
4
|
import json
|
5
5
|
from dataclasses import dataclass
|
6
|
+
from json import JSONDecodeError
|
6
7
|
from typing import Dict, Optional
|
7
8
|
|
8
9
|
from dacite import from_dict
|
9
10
|
|
10
11
|
|
11
12
|
def loads(data: str) -> ConfigFile:
|
12
|
-
|
13
|
-
|
13
|
+
try:
|
14
|
+
d = json.loads(data)
|
15
|
+
return from_dict(ConfigFile, d)
|
16
|
+
except JSONDecodeError as e:
|
17
|
+
# ErrorPrinter.print_generic_error("Invalid json format, config.json")
|
18
|
+
raise JSONDecodeError(
|
19
|
+
f"File config.json is not valid json, please edit or remove file to have it re-created ({e.msg})", pos=e.pos, doc=e.doc
|
20
|
+
)
|
14
21
|
|
15
22
|
|
16
23
|
def dumps(config: ConfigFile) -> str:
|
cli/settings/core.py
CHANGED
@@ -6,6 +6,7 @@ import shutil
|
|
6
6
|
import stat
|
7
7
|
import sys
|
8
8
|
from dataclasses import dataclass
|
9
|
+
from json import JSONDecodeError
|
9
10
|
from pathlib import Path
|
10
11
|
from typing import Optional, Tuple, Union
|
11
12
|
|
@@ -103,26 +104,9 @@ class Settings:
|
|
103
104
|
cli_config.set_account_field(token.account.email, organisation)
|
104
105
|
self._write_config_file(cli_config)
|
105
106
|
except TokenNotFoundError:
|
106
|
-
ErrorPrinter.print_hint("You must have an account activated in order to set default
|
107
|
+
ErrorPrinter.print_hint("You must have an account activated in order to set default organization")
|
107
108
|
sys.exit(1)
|
108
109
|
|
109
|
-
# def set_default_config_as_env(self) -> None:
|
110
|
-
# config = self.get_cli_config()
|
111
|
-
# if config.default_organisation is not None:
|
112
|
-
# if "REMOTIVE_CLOUD_ORGANISATION" not in os.environ:
|
113
|
-
# os.environ["REMOTIVE_CLOUD_ORGANISATION"] = config.default_organisation
|
114
|
-
|
115
|
-
# def get_active_cli_account(self) -> Optional[Account]:
|
116
|
-
# try:
|
117
|
-
# token = self.get_active_token_file()
|
118
|
-
# config = self.get_cli_config()
|
119
|
-
# if token.account.email in config.accounts:
|
120
|
-
# return config.accounts[token.account.email]
|
121
|
-
# return None
|
122
|
-
# except TokenNotFoundError:
|
123
|
-
# ErrorPrinter.print_hint("Cannot get active config without activating account credentials first")
|
124
|
-
# sys.exit(1)
|
125
|
-
|
126
110
|
def get_cli_config(self) -> ConfigFile:
|
127
111
|
try:
|
128
112
|
return self._read_config_file()
|
@@ -209,7 +193,6 @@ class Settings:
|
|
209
193
|
# TODO: what about the active token?
|
210
194
|
path = self._get_token_by_name(name)[1]
|
211
195
|
# print("Deleting", path)
|
212
|
-
print(path)
|
213
196
|
return path.unlink()
|
214
197
|
|
215
198
|
# def add_and_activate_short_lived_cli_token(self, token: str) -> TokenFile:
|
@@ -274,10 +257,11 @@ class Settings:
|
|
274
257
|
# From now, user will never be None when adding a token so in this case token_file.user is never None
|
275
258
|
|
276
259
|
email = email_to_safe_filename(token_file.account.email) if token_file.account is not None else "unknown"
|
277
|
-
file = f"{SERVICE_ACCOUNT_TOKEN_FILE_PREFIX}{
|
260
|
+
file = f"{SERVICE_ACCOUNT_TOKEN_FILE_PREFIX}{token_file.name}-{email}.json"
|
278
261
|
path = self.config_dir / file
|
279
262
|
|
280
263
|
self._write_token_file(path, token_file)
|
264
|
+
print(f"Service account token stored at {path}")
|
281
265
|
cli_config = self.get_cli_config()
|
282
266
|
cli_config.init_account(email=token_file.account.email, token_name=token_file.name)
|
283
267
|
self._write_config_file(cli_config)
|
@@ -324,11 +308,21 @@ class Settings:
|
|
324
308
|
def _list_token_files(self, prefix: str = "") -> list[TokenFileMetadata]:
|
325
309
|
"""list all tokens with the correct prefix in the config dir, but omit files that are not token files"""
|
326
310
|
|
311
|
+
def is_valid_json(path: Path) -> bool:
|
312
|
+
try:
|
313
|
+
self._read_token_file(path)
|
314
|
+
return True
|
315
|
+
except JSONDecodeError:
|
316
|
+
# TODO - this should be printed but printing it here causes it to be displayed to many times
|
317
|
+
# err_console.print(f"File is not valid json, skipping. {path}")
|
318
|
+
return False
|
319
|
+
|
327
320
|
def is_valid_token_file(path: Path) -> bool:
|
321
|
+
is_token_file = path.name.startswith(SERVICE_ACCOUNT_TOKEN_FILE_PREFIX) or path.name.startswith(PERSONAL_TOKEN_FILE_PREFIX)
|
328
322
|
has_correct_prefix = path.is_file() and path.name.startswith(prefix)
|
329
323
|
is_active_secret = path == self._active_secret_token_path
|
330
324
|
is_cli_config = path == self._cli_config
|
331
|
-
return has_correct_prefix and not is_active_secret and not is_cli_config
|
325
|
+
return is_token_file and is_valid_json(path) and has_correct_prefix and not is_active_secret and not is_cli_config
|
332
326
|
|
333
327
|
paths = [path for path in self.config_dir.iterdir() if is_valid_token_file(path)]
|
334
328
|
|
cli/utils/rest_helper.py
CHANGED
@@ -90,20 +90,24 @@ class RestHelper:
|
|
90
90
|
|
91
91
|
@staticmethod
|
92
92
|
def ensure_auth_token(quiet: bool = False, access_token: Optional[str] = None) -> None:
|
93
|
-
if "
|
93
|
+
if "REMOTIVE_CLOUD_ORGANIZATION" not in os.environ:
|
94
94
|
org = settings.get_cli_config().get_active_default_organisation()
|
95
95
|
if org is not None:
|
96
|
-
os.environ["
|
96
|
+
os.environ["REMOTIVE_CLOUD_ORGANIZATION"] = org
|
97
97
|
|
98
98
|
token = None
|
99
|
+
|
99
100
|
if access_token is None:
|
100
|
-
|
101
|
-
token =
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
101
|
+
if "REMOTIVE_CLOUD_ACCESS_TOKEN" in os.environ:
|
102
|
+
token = os.environ["REMOTIVE_CLOUD_ACCESS_TOKEN"]
|
103
|
+
else:
|
104
|
+
try:
|
105
|
+
token = settings.get_active_token()
|
106
|
+
except TokenNotFoundError:
|
107
|
+
if quiet:
|
108
|
+
return
|
109
|
+
ErrorPrinter.print_hint("you are not logged in, please login using [green]remotive cloud auth login[/green]")
|
110
|
+
sys.exit(1)
|
107
111
|
|
108
112
|
RestHelper.__headers["Authorization"] = f"Bearer {token.strip() if token is not None else access_token}"
|
109
113
|
|
@@ -167,7 +171,7 @@ class RestHelper:
|
|
167
171
|
return
|
168
172
|
err_console.print(f":boom: [bold red]Got status code[/bold red]: {response.status_code}")
|
169
173
|
if response.status_code == 401:
|
170
|
-
err_console.print("Your token has expired, please login again")
|
174
|
+
err_console.print("Your token is not valid or has expired, please login again or activate another account")
|
171
175
|
else:
|
172
176
|
err_console.print(response.text)
|
173
177
|
sys.exit(1)
|
@@ -188,7 +192,7 @@ class RestHelper:
|
|
188
192
|
else:
|
189
193
|
err_console.print(f":boom: [bold red]Got status code[/bold red]: {response.status_code}")
|
190
194
|
if response.status_code == 401:
|
191
|
-
err_console.print("Your token has expired, please login again")
|
195
|
+
err_console.print("Your token is not valid or has expired, please login again or activate another account")
|
192
196
|
else:
|
193
197
|
err_console.print(response.text)
|
194
198
|
sys.exit(1)
|
@@ -14,14 +14,14 @@ 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=
|
18
|
-
cli/cloud/auth/login.py,sha256=
|
19
|
-
cli/cloud/auth_tokens.py,sha256=
|
17
|
+
cli/cloud/auth/cmd.py,sha256=aOvbrzNb7moClgmde25T0coBs8Ge696xUB_w6qfvN8I,2920
|
18
|
+
cli/cloud/auth/login.py,sha256=PnmD2dMOP55_DOE6yM1lKBVhxyaZfO2DMQATaRuJrB4,11438
|
19
|
+
cli/cloud/auth_tokens.py,sha256=XHXrCwgK_akrCbFxcxtDr7GDM9cd-T_s40PrRovY4Lk,12344
|
20
20
|
cli/cloud/brokers.py,sha256=QTA9bmaK06LKEccF6IBgWBonC4VFrKwFQBsACX_IzYw,3896
|
21
|
-
cli/cloud/cloud_cli.py,sha256=
|
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=
|
24
|
-
cli/cloud/projects.py,sha256=
|
23
|
+
cli/cloud/organisations.py,sha256=QBD2RnCbpZ9XQrTIcvD74o321JHNnKxDleKlYmSZEuI,4116
|
24
|
+
cli/cloud/projects.py,sha256=ecn5Y8UKhgYnHSJQACUk1GNZt9EF8ug4B-6MCr8rZqM,1487
|
25
25
|
cli/cloud/recordings.py,sha256=B0XOj8LIm3hBqBzVKPLPvPUCXCKZBTEISssrijK481w,24855
|
26
26
|
cli/cloud/recordings_playback.py,sha256=XZoVyujufMQFN2v_Nwsf8tOqn61yLEpAf2z_u5uhXik,11532
|
27
27
|
cli/cloud/resumable_upload.py,sha256=8lEIdncJZoTZzNsQVHH3gm_GunxEmN5JbmWX7awy3p4,3713
|
@@ -36,11 +36,11 @@ cli/cloud/uri.py,sha256=QZCus--KJQlVwGCOzZqiglvj8VvSRKxfVvN33Pilgyg,3616
|
|
36
36
|
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
|
-
cli/errors.py,sha256=
|
40
|
-
cli/remotive.py,sha256=
|
39
|
+
cli/errors.py,sha256=_P-qpayY06qgLA7wvoDFsyMaTfo_zjNY--fgHPAuhrs,1658
|
40
|
+
cli/remotive.py,sha256=PsxR3Hhp3zhj-A06sqvtSkmXwv9moWHT_ify8Vg5xQA,2756
|
41
41
|
cli/settings/__init__.py,sha256=5ZRq04PHp3WU_5e7mGwWUoFYUPWfnnS16yF45wUv7mY,248
|
42
|
-
cli/settings/config_file.py,sha256
|
43
|
-
cli/settings/core.py,sha256=
|
42
|
+
cli/settings/config_file.py,sha256=6WHlJT74aQvqc5elcW1FDafcG0NttYvPawmArN5H2MQ,2869
|
43
|
+
cli/settings/core.py,sha256=vRS_3JZDKRMGMa65C3LT178BCUcZ2RAlp2knn158CA4,15669
|
44
44
|
cli/settings/migrate_all_token_files.py,sha256=7kvHbpP4BtILJ8kPtb_bFnTnBYX9isZ4rwB5lfnEkbA,2722
|
45
45
|
cli/settings/migrate_token_file.py,sha256=Fp7Z_lNqSdoWY05TYwFW2QH8q9QhmB2TYSok6hV1Mic,1530
|
46
46
|
cli/settings/token_file.py,sha256=KISVaSVV2pzfduCtJYUtCLtJa3htrpP4_qTODp5IhW8,2210
|
@@ -51,9 +51,9 @@ cli/tools/tools.py,sha256=jhLfrFDqkmWV3eBAzNwBf6WgDGrz7sOhgVCia36Twn8,232
|
|
51
51
|
cli/typer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
52
52
|
cli/typer/typer_utils.py,sha256=8SkvG9aKkfK9fTRsLD9pOBtWn9XSwtOXWg2RAk9FhOI,708
|
53
53
|
cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
54
|
-
cli/utils/rest_helper.py,sha256=
|
55
|
-
remotivelabs_cli-0.1.
|
56
|
-
remotivelabs_cli-0.1.
|
57
|
-
remotivelabs_cli-0.1.
|
58
|
-
remotivelabs_cli-0.1.
|
59
|
-
remotivelabs_cli-0.1.
|
54
|
+
cli/utils/rest_helper.py,sha256=b_FJY6MxnFSqo11qaHxkBFHfVlKf7Zj28Uxv9Oj7XY4,14141
|
55
|
+
remotivelabs_cli-0.1.0a2.dist-info/LICENSE,sha256=qDPP_yfuv1fF-u7EfexN-cN3M8aFgGVndGhGLovLKz0,608
|
56
|
+
remotivelabs_cli-0.1.0a2.dist-info/METADATA,sha256=ikZ-W_JIYwAuG5JTEKQRo8O00WqQn6GvAAJmWvCk3KI,1430
|
57
|
+
remotivelabs_cli-0.1.0a2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
58
|
+
remotivelabs_cli-0.1.0a2.dist-info/entry_points.txt,sha256=lvDhPgagLqW_KTnLPCwKSqfYlEp-1uYVosRiPjsVj10,45
|
59
|
+
remotivelabs_cli-0.1.0a2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|