remotivelabs-cli 0.0.25__py3-none-any.whl → 0.0.27__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 (43) hide show
  1. cli/broker/brokers.py +2 -2
  2. cli/broker/export.py +5 -5
  3. cli/broker/files.py +5 -5
  4. cli/broker/lib/broker.py +82 -51
  5. cli/broker/license_flows.py +11 -9
  6. cli/broker/licenses.py +2 -2
  7. cli/broker/playback.py +14 -34
  8. cli/broker/record.py +3 -3
  9. cli/broker/scripting.py +4 -4
  10. cli/broker/signals.py +20 -12
  11. cli/cloud/__init__.py +0 -1
  12. cli/cloud/auth.py +40 -35
  13. cli/cloud/auth_tokens.py +73 -37
  14. cli/cloud/brokers.py +24 -33
  15. cli/cloud/cloud_cli.py +7 -18
  16. cli/cloud/configs.py +28 -11
  17. cli/cloud/filestorage.py +63 -51
  18. cli/cloud/organisations.py +30 -0
  19. cli/cloud/projects.py +11 -8
  20. cli/cloud/recordings.py +148 -117
  21. cli/cloud/recordings_playback.py +52 -39
  22. cli/cloud/rest_helper.py +247 -196
  23. cli/cloud/resumable_upload.py +9 -8
  24. cli/cloud/sample_recordings.py +5 -5
  25. cli/cloud/service_account_tokens.py +18 -16
  26. cli/cloud/service_accounts.py +9 -9
  27. cli/connect/__init__.py +0 -1
  28. cli/connect/connect.py +7 -6
  29. cli/connect/protopie/protopie.py +32 -16
  30. cli/errors.py +6 -5
  31. cli/remotive.py +13 -9
  32. cli/requirements.txt +4 -1
  33. cli/settings.py +9 -9
  34. cli/tools/__init__.py +0 -1
  35. cli/tools/can/__init__.py +0 -1
  36. cli/tools/can/can.py +8 -8
  37. cli/tools/tools.py +2 -2
  38. {remotivelabs_cli-0.0.25.dist-info → remotivelabs_cli-0.0.27.dist-info}/METADATA +5 -3
  39. remotivelabs_cli-0.0.27.dist-info/RECORD +45 -0
  40. remotivelabs_cli-0.0.25.dist-info/RECORD +0 -44
  41. {remotivelabs_cli-0.0.25.dist-info → remotivelabs_cli-0.0.27.dist-info}/LICENSE +0 -0
  42. {remotivelabs_cli-0.0.25.dist-info → remotivelabs_cli-0.0.27.dist-info}/WHEEL +0 -0
  43. {remotivelabs_cli-0.0.25.dist-info → remotivelabs_cli-0.0.27.dist-info}/entry_points.txt +0 -0
cli/cloud/brokers.py CHANGED
@@ -1,49 +1,40 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
  import os
3
5
  import signal as os_signal
6
+ from typing import Any, Union
4
7
 
8
+ import requests
5
9
  import typer
6
10
  import websocket
7
- from rich.progress import Progress, SpinnerColumn, TextColumn
8
11
 
9
- from . import rest_helper as rest
12
+ from .rest_helper import RestHelper as Rest
10
13
 
11
14
  app = typer.Typer()
12
15
 
13
16
 
14
17
  @app.command("list", help="Lists brokers in project")
15
- def list(project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")):
16
- rest.handle_get(f"/api/project/{project}/brokers")
18
+ def list(project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")) -> None: # pylint: disable=W0622
19
+ Rest.handle_get(f"/api/project/{project}/brokers")
17
20
 
18
21
 
19
22
  @app.command("describe", help="Shows details about a specific broker")
20
- def describe(name: str, project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")):
21
- rest.handle_get(f"/api/project/{project}/brokers/{name}")
23
+ def describe(name: str, project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")) -> None:
24
+ Rest.handle_get(f"/api/project/{project}/brokers/{name}")
22
25
 
23
26
 
24
27
  @app.command("delete", help="Stops and deletes a broker from project")
25
- def stop(name: str, project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")):
26
- with Progress(
27
- SpinnerColumn(),
28
- TextColumn("[progress.description]{task.description}"),
29
- transient=True,
30
- ) as progress:
31
- progress.add_task(description=f"Deleting broker {name}...", total=None)
32
- rest.handle_delete(f"/api/project/{project}/brokers/{name}")
28
+ def stop(name: str, project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")) -> None:
29
+ Rest.handle_delete(f"/api/project/{project}/brokers/{name}")
33
30
 
34
31
 
35
32
  @app.command(help="Deletes your personal broker from project")
36
- def delete_personal(project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")):
37
- with Progress(
38
- SpinnerColumn(),
39
- TextColumn("[progress.description]{task.description}"),
40
- transient=True,
41
- ) as progress:
42
- progress.add_task(description="Deleting personal broker...", total=None)
43
- rest.handle_delete(f"/api/project/{project}/brokers/personal")
33
+ def delete_personal(project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")) -> None:
34
+ Rest.handle_delete(f"/api/project/{project}/brokers/personal")
44
35
 
45
36
 
46
- def do_start(name: str, project: str, api_key: str, tag: str, return_response: bool = False):
37
+ def do_start(name: str, project: str, api_key: str, tag: str, return_response: bool = False) -> Union[requests.Response, None]:
47
38
  if tag == "":
48
39
  tag_to_use = None
49
40
  else:
@@ -53,7 +44,7 @@ def do_start(name: str, project: str, api_key: str, tag: str, return_response: b
53
44
  body = {"size": "S", "tag": tag_to_use}
54
45
  else:
55
46
  body = {"size": "S", "apiKey": api_key, "tag": tag_to_use}
56
- return rest.handle_post(
47
+ return Rest.handle_post(
57
48
  f"/api/project/{project}/brokers/{name}",
58
49
  body=json.dumps(body),
59
50
  return_response=return_response,
@@ -68,7 +59,7 @@ def start(
68
59
  tag: str = typer.Option("", help="Optional specific tag/version"),
69
60
  silent: bool = typer.Option(False, help="Optional specific tag/version"),
70
61
  api_key: str = typer.Option("", help="Start with your own api-key"),
71
- ):
62
+ ) -> None:
72
63
  # with Progress(
73
64
  # SpinnerColumn(),
74
65
  # TextColumn("[progress.description]{task.description}"),
@@ -84,7 +75,7 @@ def logs(
84
75
  tail: bool = typer.Option(False, help="Tail the broker log"),
85
76
  history_minutes: str = typer.Option(10, help="History in minutes minutes to fetch"),
86
77
  project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
87
- ):
78
+ ) -> None:
88
79
  """
89
80
  Exposes broker logs history or real-time tail of the broker.
90
81
 
@@ -92,30 +83,30 @@ def logs(
92
83
 
93
84
  """
94
85
 
95
- def exit_on_ctrlc(sig, frame):
86
+ def exit_on_ctrlc(__sig: Any, __frame: Any) -> None:
96
87
  wsapp.close()
97
88
  os._exit(0)
98
89
 
99
90
  os_signal.signal(os_signal.SIGINT, exit_on_ctrlc)
100
91
 
101
- def on_message(wsapp, message):
92
+ def on_message(__wsapp: Any, message: str) -> None:
102
93
  print(message)
103
94
 
104
- def on_error(wsapp, err):
95
+ def on_error(wsapp: Any, err: str) -> None: # pylint: disable=W0613
105
96
  print("EXAMPLE error encountered: ", err)
106
97
 
107
- rest.ensure_auth_token()
98
+ Rest.ensure_auth_token()
108
99
  # This will work with both http -> ws and https -> wss
109
- ws_url = rest.base_url.replace("http", "ws")
100
+ ws_url = Rest.get_base_url().replace("http", "ws")
110
101
 
111
102
  if tail:
112
103
  q = "?tail=yes"
113
- elif history_minutes != 10:
104
+ elif history_minutes != "10":
114
105
  q = f"?history={history_minutes}"
115
106
  else:
116
107
  q = ""
117
108
 
118
109
  wsapp = websocket.WebSocketApp(
119
- f"{ws_url}/api/project/{project}/brokers/{broker_name}/logs{q}", header=rest.headers, on_message=on_message, on_error=on_error
110
+ f"{ws_url}/api/project/{project}/brokers/{broker_name}/logs{q}", header=Rest.get_headers(), on_message=on_message, on_error=on_error
120
111
  )
121
112
  wsapp.run_forever()
cli/cloud/cloud_cli.py CHANGED
@@ -1,9 +1,8 @@
1
- import json
2
-
3
1
  import typer
4
2
 
5
- from . import auth, brokers, configs, filestorage, projects, recordings, sample_recordings, service_accounts
6
- from . import rest_helper as rest
3
+ from cli.cloud.rest_helper import RestHelper
4
+
5
+ from . import auth, brokers, configs, filestorage, organisations, projects, recordings, sample_recordings, service_accounts
7
6
 
8
7
  app = typer.Typer()
9
8
 
@@ -11,22 +10,12 @@ app = typer.Typer()
11
10
  @app.command(help="List licenses for an organisation")
12
11
  def licenses(
13
12
  organisation: str = typer.Option(..., help="Organisation ID", envvar="REMOTIVE_CLOUD_ORGANISATION"),
14
- filter: str = typer.Option("all", help="all, valid, expired"),
15
- ):
16
- rest.handle_get(f"/api/bu/{organisation}/licenses", {"filter": filter})
17
-
18
-
19
- @app.command(help="List your available organisations")
20
- def organisations():
21
- r = rest.handle_get("/api/home", return_response=True)
22
- if r.status_code == 200:
23
- j = list(map(lambda x: x["billableUnitUser"]["billableUnit"]["uid"], r.json()))
24
- print(json.dumps(j))
25
- else:
26
- print(f"Got status code: {r.status_code}")
27
- print(r.text)
13
+ filter_option: str = typer.Option("all", help="all, valid, expired"),
14
+ ) -> None:
15
+ RestHelper.handle_get(f"/api/bu/{organisation}/licenses", {"filter": filter_option})
28
16
 
29
17
 
18
+ app.add_typer(organisations.app, name="organisations", help="Manage organisations")
30
19
  app.add_typer(projects.app, name="projects", help="Manage projects")
31
20
  app.add_typer(auth.app, name="auth")
32
21
  app.add_typer(brokers.app, name="brokers", help="Manage cloud broker lifecycle")
cli/cloud/configs.py CHANGED
@@ -7,22 +7,28 @@ import requests
7
7
  import typer
8
8
  from rich.progress import Progress, SpinnerColumn, TextColumn
9
9
 
10
- from . import rest_helper as rest
10
+ from .rest_helper import RestHelper as Rest
11
11
 
12
12
  app = typer.Typer()
13
13
 
14
14
 
15
15
  @app.command("list")
16
- def list_signal_databases(project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")):
17
- rest.handle_get(f"/api/project/{project}/files/config")
16
+ def list_signal_databases(project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")) -> None:
17
+ """
18
+ List available signal databases in project
19
+ """
20
+ Rest.handle_get(f"/api/project/{project}/files/config")
18
21
 
19
22
 
20
23
  @app.command("delete")
21
24
  def delete(
22
25
  signal_db_file: str = typer.Argument("", help="Signal database file"),
23
26
  project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
24
- ):
25
- rest.handle_delete(f"/api/project/{project}/files/config/{signal_db_file}")
27
+ ) -> None:
28
+ """
29
+ Deletes the specified signal database
30
+ """
31
+ Rest.handle_delete(f"/api/project/{project}/files/config/{signal_db_file}")
26
32
 
27
33
 
28
34
  @app.command()
@@ -38,35 +44,45 @@ def upload(
38
44
  help="Path to signal database file to upload",
39
45
  ),
40
46
  project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
41
- ):
47
+ ) -> None:
42
48
  """
43
49
  Uploads signal database to project
44
50
  """
45
- rest.upload_file(path=path, url=f"/api/project/{project}/files/config/{os.path.basename(path)}/uploadfile")
51
+ res = Rest.handle_put(url=f"/api/project/{project}/files/config/{os.path.basename(path)}/uploadfile", return_response=True)
52
+ if res is not None:
53
+ Rest.upload_file_with_signed_url(
54
+ path=path, url=res.text, upload_headers={"Content-Type": "application/octet-stream"}, progress_label=f"Uploading {path}..."
55
+ )
46
56
 
47
57
 
48
58
  @app.command()
49
59
  def download(
50
60
  signal_db_file: str = typer.Argument("", help="Signal database file"),
51
61
  project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
52
- ):
62
+ ) -> None:
63
+ """
64
+ Downloads the specified signal database to disk
65
+ """
53
66
  with Progress(
54
67
  SpinnerColumn(),
55
68
  TextColumn("[progress.description]{task.description}"),
56
69
  transient=True,
57
70
  ) as progress:
58
- rest.ensure_auth_token()
71
+ Rest.ensure_auth_token()
59
72
 
60
73
  progress.add_task(description=f"Downloading {signal_db_file}", total=None)
61
74
 
62
75
  # First request the download url from cloud. This is a public signed url that is valid
63
76
  # for a short period of time
64
77
  get_signed_url_resp = requests.get(
65
- f"{rest.base_url}/api/project/{project}/files/config/{signal_db_file}/download", headers=rest.headers, allow_redirects=True
78
+ f"{Rest.get_base_url()}/api/project/{project}/files/config/{signal_db_file}/download",
79
+ headers=Rest.get_headers(),
80
+ allow_redirects=True,
81
+ timeout=60,
66
82
  )
67
83
  if get_signed_url_resp.status_code == 200:
68
84
  # Next download the actual file
69
- download_resp = requests.get(url=get_signed_url_resp.text, stream=True)
85
+ download_resp = requests.get(url=get_signed_url_resp.text, stream=True, timeout=60)
70
86
  if download_resp.status_code == 200:
71
87
  with open(signal_db_file, "wb") as out_file:
72
88
  shutil.copyfileobj(download_resp.raw, out_file)
@@ -86,4 +102,5 @@ def download(
86
102
  # print(r.status_code)
87
103
  # print(r.text)
88
104
 
105
+ # pylint: disable=C0301
89
106
  # curl -X PUT -H 'Content-Type: application/octet-stream' --upload-file docker-compose.yml 'https://storage.googleapis.com/beamylabs-fileuploads-dev/projects/beamyhack/recording/myrecording?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=recordings-upload-account%40beamycloud-dev.iam.gserviceaccount.com%2F20220729%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20220729T134012Z&X-Goog-Expires=3000&X-Goog-SignedHeaders=content-type%3Bhost&X-Goog-Signature=d1fa7639349d6453aebfce8814d6e5685af03952d07aa4e3cb0d44dba7cf5e572f684c8120dba17cbc7ea6a0ef5450542a3c745c65e04272b34265d0ddcf1b67e6f2b5bfa446264a62d77bd7faabf45ad6bd2aec5225f57004b0a31cfe0480cba063a3807d86346b1da99ecbae3f3e6da8f44f06396dfc1fdc6f89e475abdf969142cef6f369f03aff41000c8abb28aa82185246746fd6c16b6b381baa2d586382a3d3067b6376ddba2b55b2b6f9d942913a1cbfbc61491ba6a615d7d5a0d9a476c357431143e9cea1411dfad9f01b1e1176dc8c056cbf08cccfd401a55d63c19d038f3ab42b712abc48d759047ac07862c4fae937c341e19b568bb60a4e4086'
cli/cloud/filestorage.py CHANGED
@@ -1,11 +1,12 @@
1
1
  import os.path
2
+ import sys
2
3
  from pathlib import Path
3
4
 
4
5
  import typer
5
6
 
6
7
  from ..errors import ErrorPrinter
7
- from . import rest_helper as rest
8
8
  from . import resumable_upload as upload
9
+ from .rest_helper import RestHelper as Rest
9
10
 
10
11
  app = typer.Typer(
11
12
  rich_markup_mode="rich",
@@ -22,7 +23,7 @@ Copy file from local to remote storage and vice versa, list and delete files.
22
23
  def list_files(
23
24
  project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
24
25
  prefix: str = typer.Argument(default="rcs://", help="Remote storage path"),
25
- ):
26
+ ) -> None:
26
27
  """
27
28
  Listing remote files
28
29
 
@@ -40,9 +41,9 @@ def list_files(
40
41
  prefix = __check_rcs_path(prefix)
41
42
  else:
42
43
  ErrorPrinter.print_hint("Path must start with rcs://")
43
- exit(1)
44
+ sys.exit(1)
44
45
 
45
- rest.handle_get(
46
+ Rest.handle_get(
46
47
  f"/api/project/{project}/files/storage{prefix}",
47
48
  )
48
49
 
@@ -51,7 +52,7 @@ def list_files(
51
52
  def delete_file(
52
53
  project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
53
54
  path: str = typer.Argument(default=..., help="Remote storage path to file to delete"),
54
- ):
55
+ ) -> None:
55
56
  """
56
57
  [red]Deletes[/red] a file from remote storage, this cannot be undone :fire:
57
58
 
@@ -61,19 +62,19 @@ def delete_file(
61
62
  prefix = __check_rcs_path(path)
62
63
  else:
63
64
  ErrorPrinter.print_hint("Path must start with rcs://")
64
- exit(1)
65
+ sys.exit(1)
65
66
 
66
- rest.handle_delete(
67
+ Rest.handle_delete(
67
68
  f"/api/project/{project}/files/storage{prefix}",
68
69
  )
69
70
 
70
71
 
71
72
  @app.command(name="cp")
72
- def copy_file(
73
+ def copy_file( # noqa: C901 # type: ignore[too-many-branches] # pylint: disable=no-member
73
74
  source: str = typer.Argument(default=..., help="Remote or local path to source file"),
74
75
  dest: str = typer.Argument(default=..., help="Remote or local path to destination file"),
75
76
  project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
76
- ):
77
+ ) -> None:
77
78
  """
78
79
  Copies a file to or from remote storage
79
80
 
@@ -86,58 +87,69 @@ def copy_file(
86
87
 
87
88
  if not source.startswith("rcs://") and not dest.startswith("rcs://"):
88
89
  ErrorPrinter.print_hint("Source or destination path must be an rcs:// path")
89
- exit(2)
90
+ sys.exit(2)
90
91
 
91
92
  if source.startswith("rcs://") and dest.startswith("rcs://"):
92
93
  ErrorPrinter.print_hint("Currently one of source and destination path must be a local path")
93
- exit(2)
94
+ sys.exit(2)
94
95
 
95
96
  if source.startswith("rcs://"):
96
- rcs_path = __check_rcs_path(source)
97
- filename = source.rsplit("/", 1)[-1]
98
- path = Path(dest)
99
- if path.is_dir():
100
- if not path.exists():
101
- ErrorPrinter.print_generic_error("Destination directory does not exist")
102
- exit(1)
103
- else:
104
- dest = os.path.join(path.absolute(), filename)
105
-
97
+ __copy_to_local(source=source, dest=dest, project=project)
98
+ else:
99
+ __copy_to_remote(source=source, dest=dest, project=project)
100
+
101
+
102
+ def __copy_to_remote(source: str, dest: str, project: str) -> None:
103
+ path = Path(source)
104
+ if not path.exists():
105
+ ErrorPrinter.print_hint("Source file does not exist")
106
+ sys.exit(1)
107
+ filename = source.rsplit("/", 1)[-1]
108
+ rcs_path = __check_rcs_path(dest)
109
+ if rcs_path.endswith("/"):
110
+ rcs_path = rcs_path + filename
111
+ res = Rest.handle_post(f"/api/project/{project}/files/storage{rcs_path}", return_response=True)
112
+ if res is None:
113
+ return
114
+ json = res.json()
115
+ url = json["url"]
116
+ content_type = json["contentType"]
117
+ try:
118
+ upload.upload_signed_url(url, source, content_type)
119
+ except IsADirectoryError:
120
+ ErrorPrinter.print_hint(f"Supplied source file '{source}' is a directory but must be a file")
121
+
122
+
123
+ def __copy_to_local(source: str, dest: str, project: str) -> None:
124
+ rcs_path = __check_rcs_path(source)
125
+ filename = source.rsplit("/", 1)[-1]
126
+ path = Path(dest)
127
+ if path.is_dir():
128
+ if not path.exists():
129
+ ErrorPrinter.print_generic_error("Destination directory does not exist")
130
+ sys.exit(1)
106
131
  else:
107
- if not path.parent.is_dir() or not path.parent.exists():
108
- ErrorPrinter.print_generic_error("Destination directory does not exist")
109
- exit(1)
110
- dest = Path(dest).absolute()
132
+ dest = os.path.join(path.absolute(), filename)
133
+
134
+ else:
135
+ if not path.parent.is_dir() or not path.parent.exists():
136
+ ErrorPrinter.print_generic_error("Destination directory does not exist")
137
+ sys.exit(1)
138
+ dest = str(Path(dest).absolute())
139
+
140
+ res = Rest.handle_get(
141
+ f"/api/project/{project}/files/storage{rcs_path}?download=true",
142
+ return_response=True,
143
+ )
144
+ if res is None:
145
+ return
111
146
 
112
- res = rest.handle_get(
113
- f"/api/project/{project}/files/storage{rcs_path}?download=true",
114
- return_response=True,
115
- )
147
+ Rest.download_file(save_file_name=dest, url=res.text)
116
148
 
117
- rest.download_file(save_file_name=dest, url=res.text)
118
149
 
119
- else:
120
- path = Path(source)
121
- if not path.exists():
122
- ErrorPrinter.print_hint("Source file does not exist")
123
- exit(1)
124
- filename = source.rsplit("/", 1)[-1]
125
- rcs_path = __check_rcs_path(dest)
126
- if rcs_path.endswith("/"):
127
- rcs_path = rcs_path + filename
128
- res = rest.handle_post(f"/api/project/{project}/files/storage{rcs_path}", return_response=True)
129
- json = res.json()
130
- url = json["url"]
131
- content_type = json["contentType"]
132
- try:
133
- upload.upload_signed_url(url, source, content_type)
134
- except IsADirectoryError:
135
- ErrorPrinter.print_hint(f"Supplied source file '{source}' is a directory but must be a file")
136
-
137
-
138
- def __check_rcs_path(path: str):
150
+ def __check_rcs_path(path: str) -> str:
139
151
  rcs_path = path.replace("rcs://", "/")
140
152
  if rcs_path.startswith("/."):
141
153
  ErrorPrinter.print_hint("Invalid path")
142
- exit(1)
154
+ sys.exit(1)
143
155
  return rcs_path
@@ -0,0 +1,30 @@
1
+ import json
2
+ import sys
3
+
4
+ import typer
5
+
6
+ from cli.cloud.rest_helper import RestHelper
7
+
8
+ app = typer.Typer()
9
+
10
+
11
+ @app.command(name="list", help="List your available organisations")
12
+ def list_orgs() -> None:
13
+ r = RestHelper.handle_get("/api/home", return_response=True)
14
+ if r is None:
15
+ return
16
+ if r.status_code == 200:
17
+ j = list(
18
+ map(
19
+ lambda x: {
20
+ "uid": x["billableUnitUser"]["billableUnit"]["uid"],
21
+ "displayName": x["billableUnitUser"]["billableUnit"]["displayName"],
22
+ },
23
+ r.json(),
24
+ )
25
+ )
26
+ print(json.dumps(j))
27
+ else:
28
+ print(f"Got status code: {r.status_code}")
29
+ print(r.text)
30
+ sys.exit(1)
cli/cloud/projects.py CHANGED
@@ -2,14 +2,17 @@ import json
2
2
 
3
3
  import typer
4
4
 
5
- from . import rest_helper as rest
5
+ from .rest_helper import RestHelper as Rest
6
6
 
7
7
  app = typer.Typer()
8
8
 
9
9
 
10
10
  @app.command(name="list", help="List your projects")
11
- def list_projects(organisation: str = typer.Option(..., help="Organisation ID", envvar="REMOTIVE_CLOUD_ORGANISATION")):
12
- r = rest.handle_get(url=f"/api/bu/{organisation}/me", return_response=True)
11
+ def list_projects(organisation: str = typer.Option(..., help="Organisation ID", envvar="REMOTIVE_CLOUD_ORGANISATION")) -> None:
12
+ r = Rest.handle_get(url=f"/api/bu/{organisation}/me", return_response=True)
13
+ if r is None:
14
+ return
15
+
13
16
  if r.status_code == 200:
14
17
  # extract the project uid parts
15
18
  projects = r.json()["projects"]
@@ -22,19 +25,19 @@ def list_projects(organisation: str = typer.Option(..., help="Organisation ID",
22
25
 
23
26
  @app.command(name="create")
24
27
  def create_project(
28
+ project_uid: str = typer.Argument(help="Project UID"),
25
29
  organisation: str = typer.Option(..., help="Organisation ID", envvar="REMOTIVE_CLOUD_ORGANISATION"),
26
- project_uid: str = typer.Option(..., help="Project UID"),
27
30
  project_display_name: str = typer.Option(default="", help="Project display name"),
28
- ):
31
+ ) -> None:
29
32
  create_project_req = {
30
33
  "uid": project_uid,
31
34
  "displayName": project_display_name if project_display_name != "" else project_uid,
32
35
  "description": "",
33
36
  }
34
37
 
35
- rest.handle_post(url=f"/api/bu/{organisation}/project", body=json.dumps(create_project_req))
38
+ Rest.handle_post(url=f"/api/bu/{organisation}/project", body=json.dumps(create_project_req))
36
39
 
37
40
 
38
41
  @app.command(name="delete")
39
- def delete(project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")):
40
- rest.handle_delete(url=f"/api/project/{project}")
42
+ def delete(project: str = typer.Argument(help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")) -> None:
43
+ Rest.handle_delete(url=f"/api/project/{project}")