remotivelabs-cli 0.1.0a1__tar.gz → 0.1.0a3__tar.gz

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 (58) hide show
  1. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/PKG-INFO +1 -1
  2. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/auth/cmd.py +19 -14
  3. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/auth/login.py +51 -52
  4. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/auth_tokens.py +61 -37
  5. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/cloud_cli.py +4 -4
  6. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/organisations.py +17 -6
  7. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/projects.py +4 -4
  8. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/errors.py +6 -1
  9. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/remotive.py +2 -2
  10. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/settings/config_file.py +9 -2
  11. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/settings/core.py +15 -21
  12. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/utils/rest_helper.py +15 -11
  13. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/pyproject.toml +3 -1
  14. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/LICENSE +0 -0
  15. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/README.md +0 -0
  16. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/.DS_Store +0 -0
  17. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/__init__.py +0 -0
  18. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/api/cloud/tokens.py +0 -0
  19. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/broker/brokers.py +0 -0
  20. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/broker/export.py +0 -0
  21. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/broker/files.py +0 -0
  22. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/broker/lib/__about__.py +0 -0
  23. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/broker/lib/broker.py +0 -0
  24. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/broker/license_flows.py +0 -0
  25. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/broker/licenses.py +0 -0
  26. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/broker/playback.py +0 -0
  27. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/broker/record.py +0 -0
  28. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/broker/scripting.py +0 -0
  29. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/broker/signals.py +0 -0
  30. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/__init__.py +0 -0
  31. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/auth/__init__.py +0 -0
  32. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/brokers.py +0 -0
  33. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/configs.py +0 -0
  34. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/recordings.py +0 -0
  35. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/recordings_playback.py +0 -0
  36. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/resumable_upload.py +0 -0
  37. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/sample_recordings.py +0 -0
  38. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/service_account_tokens.py +0 -0
  39. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/service_accounts.py +0 -0
  40. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/storage/__init__.py +0 -0
  41. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/storage/cmd.py +0 -0
  42. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/storage/copy.py +0 -0
  43. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/storage/uri_or_path.py +0 -0
  44. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/cloud/uri.py +0 -0
  45. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/connect/__init__.py +0 -0
  46. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/connect/connect.py +0 -0
  47. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/connect/protopie/protopie.py +0 -0
  48. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/settings/__init__.py +0 -0
  49. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/settings/migrate_all_token_files.py +0 -0
  50. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/settings/migrate_token_file.py +0 -0
  51. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/settings/token_file.py +0 -0
  52. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/tools/__init__.py +0 -0
  53. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/tools/can/__init__.py +0 -0
  54. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/tools/can/can.py +0 -0
  55. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/tools/tools.py +0 -0
  56. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/typer/__init__.py +0 -0
  57. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/typer/typer_utils.py +0 -0
  58. {remotivelabs_cli-0.1.0a1 → remotivelabs_cli-0.1.0a3}/cli/utils/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: remotivelabs-cli
3
- Version: 0.1.0a1
3
+ Version: 0.1.0a3
4
4
  Summary: CLI for operating RemotiveCloud and RemotiveBroker
5
5
  Author: Johan Rask
6
6
  Author-email: johan.rask@remotivelabs.com
@@ -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() -> None:
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
- try:
62
- print(settings.get_active_token())
63
- except TokenNotFoundError as e:
64
- ErrorPrinter.print_hint(str(e))
65
- sys.exit(1)
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:
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import base64
4
4
  import hashlib
5
- import json
6
5
  import os
7
6
  import secrets
8
7
  import sys
@@ -17,9 +16,9 @@ import typer
17
16
  from rich.console import Console
18
17
  from typing_extensions import override
19
18
 
20
- from cli.cloud.auth_tokens import list_and_select_personal_token
19
+ from cli.cloud.auth_tokens import do_activate, prompt_to_set_org
21
20
  from cli.errors import ErrorPrinter
22
- from cli.settings import TokenNotFoundError, settings
21
+ from cli.settings import TokenFile, TokenNotFoundError, settings
23
22
  from cli.utils.rest_helper import RestHelper as Rest
24
23
 
25
24
  httpd: HTTPServer
@@ -79,6 +78,7 @@ class S(BaseHTTPRequestHandler):
79
78
  allow_status_codes=[401, 400],
80
79
  )
81
80
  if res.status_code != 200:
81
+ print(res.text)
82
82
  ErrorPrinter.print_generic_error(
83
83
  "Failed to fetch token. Please try again, if the problem persists please reach out to support@remotivelabs.com"
84
84
  )
@@ -88,20 +88,16 @@ class S(BaseHTTPRequestHandler):
88
88
  )
89
89
  )
90
90
  sys.exit(1)
91
- self.wfile.write("Successfully setup CLI, return to your terminal to continue".encode("utf-8"))
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")
@@ -136,8 +145,8 @@ def create_personal_token() -> None:
136
145
  response = Rest.handle_post(
137
146
  url="/api/me/keys",
138
147
  return_response=True,
139
- body=json.dumps({"alias": "roine"}),
140
148
  access_token=short_lived_token,
149
+ # TODO - add body with alias
141
150
  )
142
151
  token = response.json()
143
152
  email = token["account"]["email"]
@@ -150,7 +159,7 @@ def create_personal_token() -> None:
150
159
  access_token=short_lived_token,
151
160
  allow_status_codes=[400, 404],
152
161
  )
153
- if res is not None and res.status_code == 200:
162
+ if res is not None and res.status_code == 204:
154
163
  Rest.handle_delete(
155
164
  f"/api/me/keys/{existing_file.name}",
156
165
  quiet=True,
@@ -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
- console.print(f"You are already signed in with valid credentials that expires in {activate_token.expires_in_days()} days")
182
- files = settings.list_personal_token_files()
183
- if len(files) > 0:
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
- files = settings.list_personal_token_files()
205
- if len(files) > 0:
206
- print("")
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
 
@@ -234,7 +244,7 @@ def login(headless: bool = False) -> bool: # noqa: C901, PLR0912, PLR0915
234
244
  f"&code_challenge={code_challenge}"
235
245
  f"&redirect_uri=http://localhost:{httpd.server_address[1]}"
236
246
  )
237
- console.print(url, style="bold")
247
+ console.print(url, style="bold", soft_wrap=True)
238
248
  httpd.serve_forever()
239
249
 
240
250
  def login_headless() -> None:
@@ -251,7 +261,7 @@ def login(headless: bool = False) -> bool: # noqa: C901, PLR0912, PLR0915
251
261
  f"&response_type=code"
252
262
  f"&code_challenge={code_challenge}"
253
263
  )
254
- console.print(url, style="bold")
264
+ console.print(url, style="bold", soft_wrap=True)
255
265
 
256
266
  code = typer.prompt(
257
267
  "Once finished, enter the verification code provided in your browser",
@@ -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
- # current_user = res.json()
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()
@@ -301,17 +301,16 @@ def login(headless: bool = False) -> bool: # noqa: C901, PLR0912, PLR0915
301
301
 
302
302
  if not could_open:
303
303
  print(
304
- "Could not open a browser on this machine, this is likely because you are in an environment where no browser is avaialble"
304
+ "Could not open a browser on this machine, this is likely because you are in an environment where no browser is available"
305
305
  )
306
306
  print("")
307
307
  if force_use_webserver_callback():
308
308
  login_with_callback_but_copy_url()
309
+ create_personal_token()
309
310
  else:
310
311
  login_headless()
311
312
  else:
312
313
  httpd.serve_forever()
313
-
314
- # Once we received our callback or code we are logged in and ready to go
315
- create_personal_token()
314
+ create_personal_token()
316
315
 
317
316
  return True
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import List, Optional, Union
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[Union[TokenFile, None]]:
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 active_account is not None and active_account.credentials_name == choice.name else "",
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
- selection = typer.prompt(f"Enter the number(# 1-{len(included_tokens)}) of the token to select (or q to skip)")
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
- Prompts user to select and activate one of the available credential files
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
- else:
172
- ErrorPrinter.print_hint("No credentials available, login to activate credentials")
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
- if settings.get_cli_config().get_active_default_organisation() is None:
175
- set_default_organisation = typer.confirm(
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
- ) -> bool:
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
- return True
201
- return False
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() -> None:
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
- personal_files = settings.list_personal_token_files()
279
- service_account_files = settings.list_service_account_token_files()
280
- personal_files.extend(service_account_files)
281
- for file in personal_files:
282
- print(file)
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:
@@ -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 organisation")
10
+ @app.command(help="List licenses for an organization")
11
11
  def licenses(
12
- organisation: str = typer.Option(..., help="Organisation ID", envvar="REMOTIVE_CLOUD_ORGANISATION"),
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/{organisation}/licenses", {"filter": filter_option})
15
+ RestHelper.handle_get(f"/api/bu/{organization}/licenses", {"filter": filter_option})
16
16
 
17
17
 
18
- app.add_typer(organisations.app, name="organisations", help="Manage organizations")
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")
@@ -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 organisation to select (or q to quit)")
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
- organisation_uid: str = typer.Argument(None, help="Organisation uid or empty to select one"),
60
- get: bool = typer.Option(False, help="Print current default organisation"),
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(organisation_uid, get)
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 organisation for the currently activated user, empty to choose from available organizations or organization uid as argument
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 organisation set")
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]
@@ -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(organisation: str = typer.Option(..., help="Organisation ID", envvar="REMOTIVE_CLOUD_ORGANISATION")) -> None:
13
- r = Rest.handle_get(url=f"/api/bu/{organisation}/me", return_response=True)
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
- organisation: str = typer.Option(..., help="Organisation ID", envvar="REMOTIVE_CLOUD_ORGANISATION"),
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/{organisation}/project", body=json.dumps(create_project_req))
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")
@@ -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:
@@ -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 "REMOTIVE_CLOUD_ORGANISATION" not in os.environ:
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["REMOTIVE_CLOUD_ORGANISATION"] = org
66
+ os.environ["REMOTIVE_CLOUD_ORGANIZATION"] = org
67
67
 
68
68
 
69
69
  @app.callback()
@@ -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
- d = json.loads(data)
13
- return from_dict(ConfigFile, d)
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:
@@ -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 organisation")
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}{email}-{token_file.name}.json"
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
 
@@ -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 "REMOTIVE_CLOUD_ORGANISATION" not in os.environ:
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["REMOTIVE_CLOUD_ORGANISATION"] = org
96
+ os.environ["REMOTIVE_CLOUD_ORGANIZATION"] = org
97
97
 
98
98
  token = None
99
+
99
100
  if access_token is None:
100
- try:
101
- token = settings.get_active_token()
102
- except TokenNotFoundError:
103
- if quiet:
104
- return
105
- ErrorPrinter.print_hint("you are not logged in, please login using [green]remotive cloud auth login[/green]")
106
- sys.exit(1)
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)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "remotivelabs-cli"
3
- version = "0.1.0a1"
3
+ version = "0.1.0a3"
4
4
  description = "CLI for operating RemotiveCloud and RemotiveBroker"
5
5
  authors = ["Johan Rask <johan.rask@remotivelabs.com>"]
6
6
  readme = "README.md"
@@ -37,6 +37,8 @@ friendlywords = "^1.1.3"
37
37
  ruff = "^0.9.3"
38
38
  mypy = "^1.13.0"
39
39
 
40
+ # This fails on windows on github so take a look the workflow, we do manual installation of poethepoet
41
+ # https://github.com/python-poetry/poetry/issues/10028
40
42
  [tool.poetry.requires-plugins]
41
43
  poethepoet = ">=0.34.0"
42
44