quapp 0.1.0__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.
- quapp-0.1.0/LICENSE +21 -0
- quapp-0.1.0/PKG-INFO +117 -0
- quapp-0.1.0/README.md +79 -0
- quapp-0.1.0/pyproject.toml +33 -0
- quapp-0.1.0/quapp/__init__.py +1 -0
- quapp-0.1.0/quapp/__main__.py +4 -0
- quapp-0.1.0/quapp/api/__init__.py +0 -0
- quapp-0.1.0/quapp/api/auth.py +16 -0
- quapp-0.1.0/quapp/api/client.py +126 -0
- quapp-0.1.0/quapp/api/functions.py +47 -0
- quapp-0.1.0/quapp/api/jobs.py +44 -0
- quapp-0.1.0/quapp/api/projects.py +31 -0
- quapp-0.1.0/quapp/cli/__init__.py +0 -0
- quapp-0.1.0/quapp/cli/app.py +24 -0
- quapp-0.1.0/quapp/cli/auth.py +78 -0
- quapp-0.1.0/quapp/cli/functions.py +136 -0
- quapp-0.1.0/quapp/cli/jobs.py +163 -0
- quapp-0.1.0/quapp/cli/projects.py +84 -0
- quapp-0.1.0/quapp/config.py +47 -0
- quapp-0.1.0/quapp/output.py +58 -0
- quapp-0.1.0/quapp.egg-info/PKG-INFO +117 -0
- quapp-0.1.0/quapp.egg-info/SOURCES.txt +32 -0
- quapp-0.1.0/quapp.egg-info/dependency_links.txt +1 -0
- quapp-0.1.0/quapp.egg-info/entry_points.txt +2 -0
- quapp-0.1.0/quapp.egg-info/requires.txt +6 -0
- quapp-0.1.0/quapp.egg-info/top_level.txt +1 -0
- quapp-0.1.0/setup.cfg +4 -0
- quapp-0.1.0/tests/test_api_client.py +101 -0
- quapp-0.1.0/tests/test_auth.py +47 -0
- quapp-0.1.0/tests/test_config.py +45 -0
- quapp-0.1.0/tests/test_functions.py +71 -0
- quapp-0.1.0/tests/test_handler_qec.py +338 -0
- quapp-0.1.0/tests/test_jobs.py +75 -0
- quapp-0.1.0/tests/test_projects.py +59 -0
quapp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Quapp CLI Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
quapp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quapp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for the Quapp quantum/cloud computing platform
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 Quapp CLI Contributors
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: Homepage, https://gitlab.com/quanghuy.ha/Quapp-CLI
|
|
28
|
+
Project-URL: Repository, https://gitlab.com/quanghuy.ha/Quapp-CLI
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Requires-Dist: typer[all]>=0.12
|
|
33
|
+
Requires-Dist: rich>=13
|
|
34
|
+
Requires-Dist: requests>=2.31
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# Quapp CLI
|
|
40
|
+
|
|
41
|
+
A command-line interface for the [Quapp](https://quapp.cloud) quantum/cloud computing platform.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install quapp
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or download a standalone binary from the [GitLab CI pipeline artifacts](https://gitlab.com/quapp/quapp-cli/-/pipelines) — no Python required.
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Authenticate
|
|
55
|
+
quapp auth login
|
|
56
|
+
|
|
57
|
+
# Create a project
|
|
58
|
+
quapp project create --name my-project
|
|
59
|
+
|
|
60
|
+
# Create a function
|
|
61
|
+
quapp function create --name my-func --sdk-version 0.45.3 --lang qiskit
|
|
62
|
+
|
|
63
|
+
# Upload a function version
|
|
64
|
+
quapp function version <function-id> --description "v1" --files main.py
|
|
65
|
+
|
|
66
|
+
# Run a job
|
|
67
|
+
quapp job run --function <function-id> --provider aws --device sim --shots 1000 --wait
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Commands
|
|
71
|
+
|
|
72
|
+
### Auth
|
|
73
|
+
```
|
|
74
|
+
quapp auth login Authenticate with email + password
|
|
75
|
+
quapp auth logout Invalidate session
|
|
76
|
+
quapp auth status Show login state
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Projects
|
|
80
|
+
```
|
|
81
|
+
quapp project create --name X [--description Y] [--permission private|group]
|
|
82
|
+
quapp project get <id>
|
|
83
|
+
quapp project delete <id>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Functions
|
|
87
|
+
```
|
|
88
|
+
quapp function create --name X --sdk-version X --lang qiskit|cuda [--description Y]
|
|
89
|
+
quapp function version <id> --description X --files file1.py [file2.py ...]
|
|
90
|
+
quapp function get <id>
|
|
91
|
+
quapp function delete <id>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Jobs
|
|
95
|
+
```
|
|
96
|
+
quapp job run --function <id> --provider aws --device <device> --shots N [--wait]
|
|
97
|
+
quapp job status <id>
|
|
98
|
+
quapp job results <id>
|
|
99
|
+
quapp job cancel <id>
|
|
100
|
+
quapp job retry <id>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Global flags
|
|
104
|
+
```
|
|
105
|
+
--json Output raw JSON instead of Rich tables
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Development
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pip install -e .
|
|
112
|
+
python -m pytest tests/
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
quapp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Quapp CLI
|
|
2
|
+
|
|
3
|
+
A command-line interface for the [Quapp](https://quapp.cloud) quantum/cloud computing platform.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install quapp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or download a standalone binary from the [GitLab CI pipeline artifacts](https://gitlab.com/quapp/quapp-cli/-/pipelines) — no Python required.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Authenticate
|
|
17
|
+
quapp auth login
|
|
18
|
+
|
|
19
|
+
# Create a project
|
|
20
|
+
quapp project create --name my-project
|
|
21
|
+
|
|
22
|
+
# Create a function
|
|
23
|
+
quapp function create --name my-func --sdk-version 0.45.3 --lang qiskit
|
|
24
|
+
|
|
25
|
+
# Upload a function version
|
|
26
|
+
quapp function version <function-id> --description "v1" --files main.py
|
|
27
|
+
|
|
28
|
+
# Run a job
|
|
29
|
+
quapp job run --function <function-id> --provider aws --device sim --shots 1000 --wait
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
### Auth
|
|
35
|
+
```
|
|
36
|
+
quapp auth login Authenticate with email + password
|
|
37
|
+
quapp auth logout Invalidate session
|
|
38
|
+
quapp auth status Show login state
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Projects
|
|
42
|
+
```
|
|
43
|
+
quapp project create --name X [--description Y] [--permission private|group]
|
|
44
|
+
quapp project get <id>
|
|
45
|
+
quapp project delete <id>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Functions
|
|
49
|
+
```
|
|
50
|
+
quapp function create --name X --sdk-version X --lang qiskit|cuda [--description Y]
|
|
51
|
+
quapp function version <id> --description X --files file1.py [file2.py ...]
|
|
52
|
+
quapp function get <id>
|
|
53
|
+
quapp function delete <id>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Jobs
|
|
57
|
+
```
|
|
58
|
+
quapp job run --function <id> --provider aws --device <device> --shots N [--wait]
|
|
59
|
+
quapp job status <id>
|
|
60
|
+
quapp job results <id>
|
|
61
|
+
quapp job cancel <id>
|
|
62
|
+
quapp job retry <id>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Global flags
|
|
66
|
+
```
|
|
67
|
+
--json Output raw JSON instead of Rich tables
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install -e .
|
|
74
|
+
python -m pytest tests/
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "quapp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CLI for the Quapp quantum/cloud computing platform"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"typer[all]>=0.12",
|
|
14
|
+
"rich>=13",
|
|
15
|
+
"requests>=2.31",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
quapp = "quapp.cli.app:app"
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://gitlab.com/quanghuy.ha/Quapp-CLI"
|
|
23
|
+
Repository = "https://gitlab.com/quanghuy.ha/Quapp-CLI"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["."]
|
|
27
|
+
include = ["quapp*"]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = ["pytest>=8"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from quapp.api.client import QuappClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def login(base_url: str, email: str, password: str) -> dict:
|
|
7
|
+
"""POST /auth/login — returns {"accessToken": ..., "refreshToken": ...}."""
|
|
8
|
+
client = QuappClient(base_url)
|
|
9
|
+
response = client.post("/auth/login", json={"email": email, "password": password})
|
|
10
|
+
return response.json()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def logout(base_url: str, token: str) -> None:
|
|
14
|
+
"""POST /auth/logout — invalidates the refresh token server-side."""
|
|
15
|
+
client = QuappClient(base_url, token=token)
|
|
16
|
+
client.post("/auth/logout")
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
# ---------------------------------------------------------------------------
|
|
8
|
+
# Typed exceptions
|
|
9
|
+
# ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class QuappError(Exception):
|
|
13
|
+
"""Base exception for all Quapp API errors."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class QuappAuthError(QuappError):
|
|
21
|
+
"""401 — Not logged in or token expired."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class QuappForbiddenError(QuappError):
|
|
25
|
+
"""403 — Permission denied."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class QuappNotFoundError(QuappError):
|
|
29
|
+
"""404 — Resource not found."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class QuappBadRequestError(QuappError):
|
|
33
|
+
"""400 — Bad request / validation error."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class QuappRateLimitError(QuappError):
|
|
37
|
+
"""429 — Rate limit exceeded (triggers retry)."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class QuappServerError(QuappError):
|
|
41
|
+
"""500 / 503 — Server-side error."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_STATUS_MAP: dict[int, type[QuappError]] = {
|
|
45
|
+
400: QuappBadRequestError,
|
|
46
|
+
401: QuappAuthError,
|
|
47
|
+
403: QuappForbiddenError,
|
|
48
|
+
404: QuappNotFoundError,
|
|
49
|
+
429: QuappRateLimitError,
|
|
50
|
+
500: QuappServerError,
|
|
51
|
+
503: QuappServerError,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_RETRY_STATUSES = {429, 503}
|
|
55
|
+
_MAX_RETRIES = 3
|
|
56
|
+
_BACKOFF_BASE = 1.0 # seconds
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _raise_for_status(response: requests.Response) -> None:
|
|
60
|
+
if response.ok:
|
|
61
|
+
return
|
|
62
|
+
code = response.status_code
|
|
63
|
+
try:
|
|
64
|
+
detail = response.json().get("message") or response.json().get("error") or response.text
|
|
65
|
+
except Exception:
|
|
66
|
+
detail = response.text
|
|
67
|
+
|
|
68
|
+
exc_class = _STATUS_MAP.get(code, QuappError)
|
|
69
|
+
default_msgs = {
|
|
70
|
+
401: "Not logged in or token expired. Run `quapp auth login`.",
|
|
71
|
+
403: "Permission denied.",
|
|
72
|
+
404: "Resource not found.",
|
|
73
|
+
429: f"Rate limit exceeded: {detail}",
|
|
74
|
+
500: "Quapp server error. Try again later.",
|
|
75
|
+
503: "Quapp server unavailable. Try again later.",
|
|
76
|
+
}
|
|
77
|
+
msg = default_msgs.get(code, detail or f"HTTP {code}")
|
|
78
|
+
raise exc_class(msg, status_code=code)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Client
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class QuappClient:
|
|
87
|
+
def __init__(self, base_url: str, token: str | None = None) -> None:
|
|
88
|
+
self.base_url = base_url.rstrip("/")
|
|
89
|
+
self.token = token
|
|
90
|
+
self._session = requests.Session()
|
|
91
|
+
|
|
92
|
+
def _headers(self) -> dict[str, str]:
|
|
93
|
+
headers: dict[str, str] = {"Content-Type": "application/json"}
|
|
94
|
+
if self.token:
|
|
95
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
96
|
+
return headers
|
|
97
|
+
|
|
98
|
+
def request(
|
|
99
|
+
self,
|
|
100
|
+
method: str,
|
|
101
|
+
path: str,
|
|
102
|
+
*,
|
|
103
|
+
retries: int = _MAX_RETRIES,
|
|
104
|
+
**kwargs,
|
|
105
|
+
) -> requests.Response:
|
|
106
|
+
url = f"{self.base_url}/{path.lstrip('/')}"
|
|
107
|
+
kwargs.setdefault("headers", {}).update(self._headers())
|
|
108
|
+
attempt = 0
|
|
109
|
+
while True:
|
|
110
|
+
response = self._session.request(method, url, **kwargs)
|
|
111
|
+
if response.status_code in _RETRY_STATUSES and attempt < retries:
|
|
112
|
+
wait = _BACKOFF_BASE * (2 ** attempt)
|
|
113
|
+
time.sleep(wait)
|
|
114
|
+
attempt += 1
|
|
115
|
+
continue
|
|
116
|
+
_raise_for_status(response)
|
|
117
|
+
return response
|
|
118
|
+
|
|
119
|
+
def get(self, path: str, **kwargs) -> requests.Response:
|
|
120
|
+
return self.request("GET", path, **kwargs)
|
|
121
|
+
|
|
122
|
+
def post(self, path: str, **kwargs) -> requests.Response:
|
|
123
|
+
return self.request("POST", path, **kwargs)
|
|
124
|
+
|
|
125
|
+
def delete(self, path: str, **kwargs) -> requests.Response:
|
|
126
|
+
return self.request("DELETE", path, **kwargs)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from quapp.api.client import QuappClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_function(
|
|
7
|
+
client: QuappClient,
|
|
8
|
+
name: str,
|
|
9
|
+
sdk_version: str,
|
|
10
|
+
lang: str,
|
|
11
|
+
description: str | None = None,
|
|
12
|
+
) -> dict:
|
|
13
|
+
"""POST /functions — create a new function and return the response dict."""
|
|
14
|
+
body: dict = {
|
|
15
|
+
"functionName": name,
|
|
16
|
+
"sdkVersion": sdk_version,
|
|
17
|
+
"templateLanguageTag": lang,
|
|
18
|
+
}
|
|
19
|
+
if description:
|
|
20
|
+
body["description"] = description
|
|
21
|
+
response = client.post("/functions", json=body)
|
|
22
|
+
return response.json()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_function_version(
|
|
26
|
+
client: QuappClient,
|
|
27
|
+
function_id: str,
|
|
28
|
+
description: str | None,
|
|
29
|
+
files: list[dict],
|
|
30
|
+
) -> dict:
|
|
31
|
+
"""POST /functions/{id}/versions — create a new version and return the response dict."""
|
|
32
|
+
body: dict = {"templateFiles": files}
|
|
33
|
+
if description:
|
|
34
|
+
body["description"] = description
|
|
35
|
+
response = client.post(f"/functions/{function_id}/versions", json=body)
|
|
36
|
+
return response.json()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_function(client: QuappClient, function_id: str) -> dict:
|
|
40
|
+
"""GET /functions/{id} — return function details dict."""
|
|
41
|
+
response = client.get(f"/functions/{function_id}")
|
|
42
|
+
return response.json()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def delete_function(client: QuappClient, function_id: str) -> None:
|
|
46
|
+
"""DELETE /functions/{id} — delete a function; returns nothing."""
|
|
47
|
+
client.delete(f"/functions/{function_id}")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from quapp.api.client import QuappClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run_job(
|
|
7
|
+
client: QuappClient,
|
|
8
|
+
function_id: str,
|
|
9
|
+
provider: str,
|
|
10
|
+
device: str,
|
|
11
|
+
shots: int,
|
|
12
|
+
) -> dict:
|
|
13
|
+
"""POST /jobs — submit a job and return the response dict."""
|
|
14
|
+
body = {
|
|
15
|
+
"function_id": function_id,
|
|
16
|
+
"provider": provider,
|
|
17
|
+
"device": device,
|
|
18
|
+
"shots": shots,
|
|
19
|
+
}
|
|
20
|
+
response = client.post("/jobs", json=body)
|
|
21
|
+
return response.json()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_job_status(client: QuappClient, job_id: str) -> dict:
|
|
25
|
+
"""GET /jobs/{id} — return job status dict."""
|
|
26
|
+
response = client.get(f"/jobs/{job_id}")
|
|
27
|
+
return response.json()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_job_results(client: QuappClient, job_id: str) -> dict:
|
|
31
|
+
"""GET /jobs/{id}/results — return job results dict."""
|
|
32
|
+
response = client.get(f"/jobs/{job_id}/results")
|
|
33
|
+
return response.json()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def cancel_job(client: QuappClient, job_id: str) -> None:
|
|
37
|
+
"""DELETE /jobs/{id} — cancel a job; returns nothing."""
|
|
38
|
+
client.delete(f"/jobs/{job_id}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def retry_job(client: QuappClient, job_id: str) -> dict:
|
|
42
|
+
"""POST /jobs/{id}/retry — retry a failed job and return the response dict."""
|
|
43
|
+
response = client.post(f"/jobs/{job_id}/retry")
|
|
44
|
+
return response.json()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from quapp.api.client import QuappClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_project(
|
|
7
|
+
client: QuappClient,
|
|
8
|
+
name: str,
|
|
9
|
+
description: str | None = None,
|
|
10
|
+
members: list[str] | None = None,
|
|
11
|
+
permission: str = "private",
|
|
12
|
+
) -> dict:
|
|
13
|
+
"""POST /projects — create a new project and return the response dict."""
|
|
14
|
+
body: dict = {"name": name, "permission": permission}
|
|
15
|
+
if description:
|
|
16
|
+
body["description"] = description
|
|
17
|
+
if members:
|
|
18
|
+
body["members"] = members
|
|
19
|
+
response = client.post("/projects", json=body)
|
|
20
|
+
return response.json() if response.content else {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_project(client: QuappClient, project_id: str) -> dict:
|
|
24
|
+
"""GET /projects/{id} — return project details dict."""
|
|
25
|
+
response = client.get(f"/projects/{project_id}")
|
|
26
|
+
return response.json()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def delete_project(client: QuappClient, project_id: str) -> None:
|
|
30
|
+
"""DELETE /projects/{id} — delete a project; returns nothing."""
|
|
31
|
+
client.delete(f"/projects/{project_id}")
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from quapp import output
|
|
6
|
+
from quapp.cli import auth, functions, jobs, projects
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(
|
|
9
|
+
name="quapp",
|
|
10
|
+
help="Quapp quantum/cloud computing platform CLI.",
|
|
11
|
+
no_args_is_help=True,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
app.add_typer(auth.app, name="auth")
|
|
15
|
+
app.add_typer(projects.app, name="project")
|
|
16
|
+
app.add_typer(functions.app, name="function")
|
|
17
|
+
app.add_typer(jobs.app, name="job")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.callback()
|
|
21
|
+
def main(
|
|
22
|
+
json: bool = typer.Option(False, "--json", help="Output raw JSON instead of Rich tables."),
|
|
23
|
+
) -> None:
|
|
24
|
+
output.set_json_mode(json)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from quapp import api
|
|
9
|
+
from quapp.config import load_config, save_config
|
|
10
|
+
from quapp import output
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Manage authentication.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def login() -> None:
|
|
17
|
+
"""Authenticate with email + password and save token."""
|
|
18
|
+
config = load_config()
|
|
19
|
+
email = typer.prompt("Email")
|
|
20
|
+
password = typer.prompt("Password", hide_input=True)
|
|
21
|
+
try:
|
|
22
|
+
from quapp.api.auth import login as api_login
|
|
23
|
+
data = api_login(config.base_url, email, password)
|
|
24
|
+
except Exception as exc:
|
|
25
|
+
output.print_error(str(exc))
|
|
26
|
+
raise typer.Exit(1)
|
|
27
|
+
|
|
28
|
+
token = data.get("accessToken") or data.get("token")
|
|
29
|
+
if not token:
|
|
30
|
+
output.print_error("Login response did not contain an access token.")
|
|
31
|
+
raise typer.Exit(1)
|
|
32
|
+
|
|
33
|
+
config.token = token
|
|
34
|
+
save_config(config)
|
|
35
|
+
|
|
36
|
+
if output.is_json_mode():
|
|
37
|
+
output.print_json({"status": "logged_in", "email": email})
|
|
38
|
+
else:
|
|
39
|
+
output.print_success(f"Logged in as {email}.")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command()
|
|
43
|
+
def logout() -> None:
|
|
44
|
+
"""Invalidate session and clear local token."""
|
|
45
|
+
config = load_config()
|
|
46
|
+
if not config.token:
|
|
47
|
+
output.print_error("Not logged in.")
|
|
48
|
+
raise typer.Exit(1)
|
|
49
|
+
try:
|
|
50
|
+
from quapp.api.auth import logout as api_logout
|
|
51
|
+
api_logout(config.base_url, config.token)
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
output.print_error(str(exc))
|
|
54
|
+
raise typer.Exit(1)
|
|
55
|
+
finally:
|
|
56
|
+
config.token = None
|
|
57
|
+
save_config(config)
|
|
58
|
+
|
|
59
|
+
if output.is_json_mode():
|
|
60
|
+
output.print_json({"status": "logged_out"})
|
|
61
|
+
else:
|
|
62
|
+
output.print_success("Logged out.")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command()
|
|
66
|
+
def status() -> None:
|
|
67
|
+
"""Show whether you are logged in."""
|
|
68
|
+
config = load_config()
|
|
69
|
+
if config.token:
|
|
70
|
+
if output.is_json_mode():
|
|
71
|
+
output.print_json({"logged_in": True})
|
|
72
|
+
else:
|
|
73
|
+
output.print_success("Logged in.")
|
|
74
|
+
else:
|
|
75
|
+
if output.is_json_mode():
|
|
76
|
+
output.print_json({"logged_in": False})
|
|
77
|
+
else:
|
|
78
|
+
output.print_error("Not logged in. Run `quapp auth login`.")
|