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 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"
@@ -0,0 +1,4 @@
1
+ from quapp.cli.app import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
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`.")