qubecli 1.0.2__tar.gz → 1.0.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubecli
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: QubeCore CLI — Command Line Interface for QubeCore
5
5
  License: MIT License
6
6
 
@@ -25,7 +25,7 @@ License: MIT License
25
25
  SOFTWARE.
26
26
  License-File: LICENSE
27
27
  Requires-Python: >=3.11
28
- Requires-Dist: qubecore-client>=1.0.10
28
+ Requires-Dist: qubecore-client>=1.0.12
29
29
  Requires-Dist: typer>=0.12.0
30
30
  Provides-Extra: dev
31
31
  Requires-Dist: mypy>=1.8.0; extra == 'dev'
@@ -58,8 +58,10 @@ pip install qubecli
58
58
  # Set server address
59
59
  qubecore config set host 192.168.1.100:50051
60
60
 
61
- # Submit a job (use single quotes to avoid shell interpretation of \n)
62
- qubecore submit --backend backend1 --circuit 'OPENQASM 2.0;\ninclude "qelib1.inc";\nqreg q[2];\ncreg c[2];\nh q[0];\ncx q[0],q[1];\nmeasure q -> c;' --shots 100
61
+ # Submit a gate job
62
+ qubecore submit --backend backend1 \
63
+ --circuit 'OPENQASM 2.0;\ninclude "qelib1.inc";\nqreg q[2];\ncreg c[2];\nh q[0];\ncx q[0],q[1];\nmeasure q -> c;' \
64
+ --shots 100
63
65
 
64
66
  # Check status
65
67
  qubecore status --job-id <job_id>
@@ -67,7 +69,7 @@ qubecore status --job-id <job_id>
67
69
  # Fetch result
68
70
  qubecore result --job-id <job_id>
69
71
 
70
- # Cancel job
72
+ # Cancel job (only possible while status is PENDING)
71
73
  qubecore cancel --job-id <job_id>
72
74
  ```
73
75
 
@@ -92,17 +94,26 @@ qubecore config unset host
92
94
  ### Job Commands
93
95
 
94
96
  ```bash
95
- # Submit
97
+ # Gate job
96
98
  qubecore submit --backend <backend> --circuit <circuit_string> --shots <n>
97
99
  qubecore submit --backend backend1 --circuit '...' --shots 100 --priority 2 --optimization-level 1
98
100
 
101
+ # Pulse job
102
+ qubecore submit-pulse --backend <backend> --pulse <pulse_json> --shots <n>
103
+
104
+ # Reset job
105
+ qubecore submit-reset --backend <backend> --qubits qubit_0,qubit_1 --shots <n>
106
+
107
+ # Calibration job
108
+ qubecore submit-calibration --backend <backend> --calibration-type widescan --params '{"qubit": "qubit_0", "span": 100000000, "n_points": 200, "num_shots": 100}'
109
+
99
110
  # Status
100
111
  qubecore status --job-id <job_id>
101
112
 
102
113
  # Result
103
114
  qubecore result --job-id <job_id>
104
115
 
105
- # Cancel (PENDING 상태일 때만 가능)
116
+ # Cancel
106
117
  qubecore cancel --job-id <job_id>
107
118
  ```
108
119
 
@@ -117,12 +128,39 @@ qubecore --host 10.0.0.1:50051 submit --backend backend1 --circuit "..." --shots
117
128
  ### `submit`
118
129
 
119
130
  | Option | Required | Description |
120
- |--------|----------|-------------|
121
- | `--backend` | Yes | 백엔드 이름 (`backend1`, `kreo.sc-20` ) |
122
- | `--circuit` | Yes | 회로 문자열 (QASM2/QASM3/JSON) |
123
- | `--shots` | Yes | 실행 횟수 |
124
- | `--priority` | No | 우선순위 1~4 (1=CRITICAL, 2=HIGH, 3=NORMAL, 4=LOW) |
125
- | `--optimization-level` | No | 최적화 레벨 0~3 |
131
+ |--------|:--------:|-------------|
132
+ | `--backend` | Yes | Backend name (`backend1`, `kreo.sc-20`, etc.) |
133
+ | `--circuit` | Yes | Circuit string (QASM2/QASM3/JSON) |
134
+ | `--shots` | Yes | Number of shots |
135
+ | `--priority` | No | Priority 14 (1=CRITICAL, 2=HIGH, 3=NORMAL, 4=LOW) |
136
+ | `--optimization-level` | No | Transpile optimization level 03 |
137
+
138
+ ### `submit-pulse`
139
+
140
+ | Option | Required | Description |
141
+ |--------|:--------:|-------------|
142
+ | `--backend` | Yes | Backend name |
143
+ | `--pulse` | Yes | Pulse program string (backend-specific JSON) |
144
+ | `--shots` | Yes | Number of shots |
145
+ | `--priority` | No | Priority 1–4 (default: 3=NORMAL) |
146
+
147
+ ### `submit-reset`
148
+
149
+ | Option | Required | Description |
150
+ |--------|:--------:|-------------|
151
+ | `--backend` | Yes | Backend name |
152
+ | `--qubits` | Yes | Comma-separated qubit names (e.g. `qubit_0,qubit_1`) |
153
+ | `--shots` | Yes | Number of verification shots after reset |
154
+ | `--priority` | No | Priority 1–4 (default: 3=NORMAL) |
155
+
156
+ ### `submit-calibration`
157
+
158
+ | Option | Required | Description |
159
+ |--------|:--------:|-------------|
160
+ | `--backend` | Yes | Backend name |
161
+ | `--calibration-type` | Yes | Calibration type: `widescan` or `punchout` |
162
+ | `--params` | Yes | Calibration parameters as JSON string |
163
+ | `--priority` | No | Priority 1–4 (default: 3=NORMAL) |
126
164
 
127
165
  ## Compatibility
128
166
 
@@ -0,0 +1,139 @@
1
+ # QubeCli
2
+
3
+ Command Line Interface for QubeCore — Quantum Computing Operating System.
4
+
5
+ ## Overview
6
+
7
+ QubeCli provides a terminal-based interface for submitting and managing
8
+ quantum jobs on QubeCore. Built on [qubecore-client](https://github.com/sdt-quantum/qubecore-client).
9
+
10
+ ## Requirements
11
+
12
+ - Python 3.11+
13
+ - QubeCore Server running and accessible
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install qubecli
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```bash
24
+ # Set server address
25
+ qubecore config set host 192.168.1.100:50051
26
+
27
+ # Submit a gate job
28
+ qubecore submit --backend backend1 \
29
+ --circuit 'OPENQASM 2.0;\ninclude "qelib1.inc";\nqreg q[2];\ncreg c[2];\nh q[0];\ncx q[0],q[1];\nmeasure q -> c;' \
30
+ --shots 100
31
+
32
+ # Check status
33
+ qubecore status --job-id <job_id>
34
+
35
+ # Fetch result
36
+ qubecore result --job-id <job_id>
37
+
38
+ # Cancel job (only possible while status is PENDING)
39
+ qubecore cancel --job-id <job_id>
40
+ ```
41
+
42
+ ## Commands
43
+
44
+ ### Version
45
+
46
+ ```bash
47
+ qubecore -v
48
+ qubecore --version
49
+ ```
50
+
51
+ ### Server Configuration
52
+
53
+ ```bash
54
+ qubecore config set host 192.168.1.100:50051
55
+ qubecore config get
56
+ qubecore config get host
57
+ qubecore config unset host
58
+ ```
59
+
60
+ ### Job Commands
61
+
62
+ ```bash
63
+ # Gate job
64
+ qubecore submit --backend <backend> --circuit <circuit_string> --shots <n>
65
+ qubecore submit --backend backend1 --circuit '...' --shots 100 --priority 2 --optimization-level 1
66
+
67
+ # Pulse job
68
+ qubecore submit-pulse --backend <backend> --pulse <pulse_json> --shots <n>
69
+
70
+ # Reset job
71
+ qubecore submit-reset --backend <backend> --qubits qubit_0,qubit_1 --shots <n>
72
+
73
+ # Calibration job
74
+ qubecore submit-calibration --backend <backend> --calibration-type widescan --params '{"qubit": "qubit_0", "span": 100000000, "n_points": 200, "num_shots": 100}'
75
+
76
+ # Status
77
+ qubecore status --job-id <job_id>
78
+
79
+ # Result
80
+ qubecore result --job-id <job_id>
81
+
82
+ # Cancel
83
+ qubecore cancel --job-id <job_id>
84
+ ```
85
+
86
+ One-time server override:
87
+
88
+ ```bash
89
+ qubecore --host 10.0.0.1:50051 submit --backend backend1 --circuit "..." --shots 100
90
+ ```
91
+
92
+ ## Options
93
+
94
+ ### `submit`
95
+
96
+ | Option | Required | Description |
97
+ |--------|:--------:|-------------|
98
+ | `--backend` | Yes | Backend name (`backend1`, `kreo.sc-20`, etc.) |
99
+ | `--circuit` | Yes | Circuit string (QASM2/QASM3/JSON) |
100
+ | `--shots` | Yes | Number of shots |
101
+ | `--priority` | No | Priority 1–4 (1=CRITICAL, 2=HIGH, 3=NORMAL, 4=LOW) |
102
+ | `--optimization-level` | No | Transpile optimization level 0–3 |
103
+
104
+ ### `submit-pulse`
105
+
106
+ | Option | Required | Description |
107
+ |--------|:--------:|-------------|
108
+ | `--backend` | Yes | Backend name |
109
+ | `--pulse` | Yes | Pulse program string (backend-specific JSON) |
110
+ | `--shots` | Yes | Number of shots |
111
+ | `--priority` | No | Priority 1–4 (default: 3=NORMAL) |
112
+
113
+ ### `submit-reset`
114
+
115
+ | Option | Required | Description |
116
+ |--------|:--------:|-------------|
117
+ | `--backend` | Yes | Backend name |
118
+ | `--qubits` | Yes | Comma-separated qubit names (e.g. `qubit_0,qubit_1`) |
119
+ | `--shots` | Yes | Number of verification shots after reset |
120
+ | `--priority` | No | Priority 1–4 (default: 3=NORMAL) |
121
+
122
+ ### `submit-calibration`
123
+
124
+ | Option | Required | Description |
125
+ |--------|:--------:|-------------|
126
+ | `--backend` | Yes | Backend name |
127
+ | `--calibration-type` | Yes | Calibration type: `widescan` or `punchout` |
128
+ | `--params` | Yes | Calibration parameters as JSON string |
129
+ | `--priority` | No | Priority 1–4 (default: 3=NORMAL) |
130
+
131
+ ## Compatibility
132
+
133
+ | qubecli | qubecore-client | qubecore |
134
+ |---------|----------------|----------|
135
+ | 1.x | 1.x | 1.x |
136
+
137
+ ## License
138
+
139
+ MIT
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "qubecli"
7
- version = "1.0.2"
7
+ version = "1.0.3"
8
8
  description = "QubeCore CLI — Command Line Interface for QubeCore"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
11
11
  requires-python = ">=3.11"
12
12
  dependencies = [
13
13
  "typer>=0.12.0",
14
- "qubecore-client>=1.0.10",
14
+ "qubecore-client>=1.0.12",
15
15
  ]
16
16
 
17
17
  [project.optional-dependencies]
@@ -0,0 +1,268 @@
1
+ import json
2
+ from contextlib import contextmanager
3
+ from importlib.metadata import version
4
+ from pathlib import Path
5
+ from typing import Annotated, Generator, Optional
6
+
7
+ import grpc
8
+ import typer
9
+ from qubecore_client import QubeClient
10
+
11
+ _CONFIG_PATH = Path.home() / ".qubecore" / "config.json"
12
+
13
+ app = typer.Typer(help="QubeCore CLI — Quantum Computing Operating System", invoke_without_command=True)
14
+ config_app = typer.Typer(help="Manage server configuration")
15
+ app.add_typer(config_app, name="config")
16
+
17
+
18
+ _address: str = "localhost:50051"
19
+
20
+
21
+ def _load_config() -> dict:
22
+ if _CONFIG_PATH.exists():
23
+ return json.loads(_CONFIG_PATH.read_text())
24
+ return {}
25
+
26
+
27
+ def _save_config(data: dict) -> None:
28
+ _CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
29
+ _CONFIG_PATH.write_text(json.dumps(data, indent=2))
30
+
31
+
32
+ def _resolve_address(host: Optional[str]) -> str:
33
+ if host is not None:
34
+ return host
35
+ return _load_config().get("host", "localhost:50051")
36
+
37
+
38
+ @contextmanager
39
+ def _get_client(address: str) -> Generator[QubeClient, None, None]:
40
+ client = QubeClient(address=address)
41
+ try:
42
+ yield client
43
+ except grpc.RpcError as e:
44
+ if e.code() == grpc.StatusCode.UNAVAILABLE:
45
+ typer.echo(f"Cannot connect to server: {address}", err=True)
46
+ typer.echo(" Run 'qubecore config set host <host:port>' to update the server address.", err=True)
47
+ else:
48
+ typer.echo(f"gRPC error: {e.details()}", err=True)
49
+ raise typer.Exit(1)
50
+ finally:
51
+ client.close()
52
+
53
+
54
+ def _priority_label(priority: int) -> str:
55
+ return {1: "CRITICAL", 2: "HIGH", 3: "NORMAL", 4: "LOW"}.get(priority, str(priority))
56
+
57
+
58
+ @app.callback()
59
+ def _callback(
60
+ host: Annotated[Optional[str], typer.Option(help="QubeCore server address (host:port)")] = None,
61
+ ver: Annotated[bool, typer.Option("-v", "--version", help="Print version", is_eager=True)] = False,
62
+ ) -> None:
63
+ global _address
64
+ if ver:
65
+ typer.echo(f"v{version('qubecli')}")
66
+ raise typer.Exit()
67
+ _address = _resolve_address(host)
68
+
69
+
70
+ @config_app.command("set")
71
+ def config_set(
72
+ key: Annotated[str, typer.Argument(help="Config key")],
73
+ value: Annotated[str, typer.Argument(help="Config value")],
74
+ ) -> None:
75
+ """Save a config value (e.g. qubecore config set host 192.168.1.100:50051)"""
76
+ if key != "host":
77
+ typer.echo(f"Unknown key: {key}. Available: host", err=True)
78
+ raise typer.Exit(1)
79
+ cfg = _load_config()
80
+ cfg[key] = value
81
+ _save_config(cfg)
82
+ typer.echo(f"✓ {key} = {value} saved ({_CONFIG_PATH})")
83
+
84
+
85
+ @config_app.command("get")
86
+ def config_get(
87
+ key: Annotated[Optional[str], typer.Argument(help="Config key (omit to show all)")] = None,
88
+ ) -> None:
89
+ """Show config value(s)"""
90
+ cfg = _load_config()
91
+ if not cfg:
92
+ typer.echo("No config saved")
93
+ return
94
+ if key:
95
+ typer.echo(f" {key} : {cfg.get(key, '(not set)')}")
96
+ else:
97
+ for k, v in cfg.items():
98
+ typer.echo(f" {k} : {v}")
99
+
100
+
101
+ @config_app.command("unset")
102
+ def config_unset(
103
+ key: Annotated[str, typer.Argument(help="Config key to remove")],
104
+ ) -> None:
105
+ """Remove a config value"""
106
+ cfg = _load_config()
107
+ if key not in cfg:
108
+ typer.echo(f" {key} : (not set)")
109
+ return
110
+ del cfg[key]
111
+ _save_config(cfg)
112
+ typer.echo(f"✓ {key} removed")
113
+
114
+
115
+ @app.command()
116
+ def submit(
117
+ backend: Annotated[str, typer.Option(help="Backend name (e.g. backend1, kreo.sc-20)")],
118
+ circuit: Annotated[str, typer.Option(help="Circuit string (QASM2/QASM3/JSON)")],
119
+ shots: Annotated[int, typer.Option(help="Number of shots")],
120
+ priority: Annotated[Optional[int], typer.Option(help="Priority 1–4 (default: 3=NORMAL)")] = None,
121
+ optimization_level: Annotated[Optional[int], typer.Option(help="Transpile optimization level 0–3")] = None,
122
+ ) -> None:
123
+ """Submit a gate-circuit execution job"""
124
+ if priority is not None and priority not in (1, 2, 3, 4):
125
+ typer.echo("priority must be between 1 and 4.", err=True)
126
+ raise typer.Exit(1)
127
+ if optimization_level is not None and optimization_level not in (0, 1, 2, 3):
128
+ typer.echo("optimization-level must be between 0 and 3.", err=True)
129
+ raise typer.Exit(1)
130
+ with _get_client(_address) as client:
131
+ job_id = client.submit_gate_job(
132
+ backend_name=backend,
133
+ gate_circuits=[circuit.replace("\\n", "\n").replace("\\t", "\t")],
134
+ shots=shots,
135
+ priority=priority,
136
+ optimization_level=optimization_level,
137
+ )
138
+ typer.echo("✓ Job submitted")
139
+ typer.echo(f" job_id : {job_id}")
140
+ typer.echo(f" priority : {_priority_label(priority if priority is not None else 3)}")
141
+
142
+
143
+ @app.command("submit-pulse")
144
+ def submit_pulse(
145
+ backend: Annotated[str, typer.Option(help="Backend name")],
146
+ pulse: Annotated[str, typer.Option(help="Pulse program string (backend-specific JSON)")],
147
+ shots: Annotated[int, typer.Option(help="Number of shots")],
148
+ priority: Annotated[Optional[int], typer.Option(help="Priority 1–4 (default: 3=NORMAL)")] = None,
149
+ ) -> None:
150
+ """Submit a pulse-program execution job"""
151
+ if priority is not None and priority not in (1, 2, 3, 4):
152
+ typer.echo("priority must be between 1 and 4.", err=True)
153
+ raise typer.Exit(1)
154
+ with _get_client(_address) as client:
155
+ job_id = client.submit_pulse_job(
156
+ backend_name=backend,
157
+ pulse_circuits=[pulse],
158
+ shots=shots,
159
+ priority=priority,
160
+ )
161
+ typer.echo("✓ Job submitted")
162
+ typer.echo(f" job_id : {job_id}")
163
+ typer.echo(f" priority : {_priority_label(priority if priority is not None else 3)}")
164
+
165
+
166
+ @app.command("submit-reset")
167
+ def submit_reset(
168
+ backend: Annotated[str, typer.Option(help="Backend name")],
169
+ qubits: Annotated[str, typer.Option(help="Comma-separated qubit names (e.g. qubit_0,qubit_1)")],
170
+ shots: Annotated[int, typer.Option(help="Number of verification shots after reset")],
171
+ priority: Annotated[Optional[int], typer.Option(help="Priority 1–4 (default: 3=NORMAL)")] = None,
172
+ ) -> None:
173
+ """Submit a qubit active-reset job"""
174
+ if priority is not None and priority not in (1, 2, 3, 4):
175
+ typer.echo("priority must be between 1 and 4.", err=True)
176
+ raise typer.Exit(1)
177
+ qubit_list = [q.strip() for q in qubits.split(",")]
178
+ with _get_client(_address) as client:
179
+ job_id = client.submit_reset_job(
180
+ backend_name=backend,
181
+ qubits=qubit_list,
182
+ shots=shots,
183
+ priority=priority,
184
+ )
185
+ typer.echo("✓ Job submitted")
186
+ typer.echo(f" job_id : {job_id}")
187
+ typer.echo(f" qubits : {qubit_list}")
188
+ typer.echo(f" priority : {_priority_label(priority if priority is not None else 3)}")
189
+
190
+
191
+ @app.command("submit-calibration")
192
+ def submit_calibration(
193
+ backend: Annotated[str, typer.Option(help="Backend name")],
194
+ calibration_type: Annotated[str, typer.Option(help="Calibration type: widescan or punchout")],
195
+ params: Annotated[str, typer.Option(help="Calibration parameters as JSON string")],
196
+ priority: Annotated[Optional[int], typer.Option(help="Priority 1–4 (default: 3=NORMAL)")] = None,
197
+ ) -> None:
198
+ """Submit a calibration job"""
199
+ if priority is not None and priority not in (1, 2, 3, 4):
200
+ typer.echo("priority must be between 1 and 4.", err=True)
201
+ raise typer.Exit(1)
202
+ try:
203
+ params_dict = json.loads(params)
204
+ except json.JSONDecodeError as e:
205
+ typer.echo(f"Failed to parse params JSON: {e}", err=True)
206
+ raise typer.Exit(1)
207
+ with _get_client(_address) as client:
208
+ job_id = client.submit_calibration_job(
209
+ backend_name=backend,
210
+ calibration_type=calibration_type,
211
+ params=params_dict,
212
+ priority=priority,
213
+ )
214
+ typer.echo("✓ Job submitted")
215
+ typer.echo(f" job_id : {job_id}")
216
+ typer.echo(f" calibration_type : {calibration_type}")
217
+ typer.echo(f" priority : {_priority_label(priority if priority is not None else 3)}")
218
+
219
+
220
+ @app.command()
221
+ def status(
222
+ job_id: Annotated[str, typer.Option(help="Job ID")],
223
+ ) -> None:
224
+ """Show job status"""
225
+ with _get_client(_address) as client:
226
+ job = client.get_job_status(job_id)
227
+ typer.echo(f" job_id : {job['job_id']}")
228
+ typer.echo(f" status : {job['status']}")
229
+ typer.echo(f" priority : {_priority_label(job['priority'])}")
230
+ typer.echo(f" submitted : {job['submitted_at']}")
231
+ typer.echo(f" started : {job['started_at'] or '(not started)'}")
232
+ typer.echo(f" finished : {job['finished_at'] or '(not finished)'}")
233
+ if job["error"]:
234
+ typer.echo(f" error : {job['error']}")
235
+
236
+
237
+ @app.command()
238
+ def result(
239
+ job_id: Annotated[str, typer.Option(help="Job ID")],
240
+ ) -> None:
241
+ """Show job result"""
242
+ with _get_client(_address) as client:
243
+ job = client.get_job_result(job_id)
244
+ typer.echo(f" job_id : {job['job_id']}")
245
+ typer.echo(f" status : {job['status']}")
246
+ if "calibration_result" in job:
247
+ typer.echo(f" calibration_result : {json.dumps(job['calibration_result'], indent=2)}")
248
+ else:
249
+ for i, r in enumerate(job.get("results", [])):
250
+ typer.echo(f" [circuit {i}] counts : {r['counts']}")
251
+
252
+
253
+ @app.command()
254
+ def cancel(
255
+ job_id: Annotated[str, typer.Option(help="Job ID")],
256
+ ) -> None:
257
+ """Cancel a job (only possible while status is PENDING)"""
258
+ with _get_client(_address) as client:
259
+ message = client.cancel_job(job_id)
260
+ typer.echo(f"✓ {message}")
261
+
262
+
263
+ def main() -> None:
264
+ app()
265
+
266
+
267
+ if __name__ == "__main__":
268
+ main()
qubecli-1.0.2/README.md DELETED
@@ -1,101 +0,0 @@
1
- # QubeCli
2
-
3
- Command Line Interface for QubeCore — Quantum Computing Operating System.
4
-
5
- ## Overview
6
-
7
- QubeCli provides a terminal-based interface for submitting and managing
8
- quantum jobs on QubeCore. Built on [qubecore-client](https://github.com/sdt-quantum/qubecore-client).
9
-
10
- ## Requirements
11
-
12
- - Python 3.11+
13
- - QubeCore Server running and accessible
14
-
15
- ## Installation
16
-
17
- ```bash
18
- pip install qubecli
19
- ```
20
-
21
- ## Quick Start
22
-
23
- ```bash
24
- # Set server address
25
- qubecore config set host 192.168.1.100:50051
26
-
27
- # Submit a job (use single quotes to avoid shell interpretation of \n)
28
- qubecore submit --backend backend1 --circuit 'OPENQASM 2.0;\ninclude "qelib1.inc";\nqreg q[2];\ncreg c[2];\nh q[0];\ncx q[0],q[1];\nmeasure q -> c;' --shots 100
29
-
30
- # Check status
31
- qubecore status --job-id <job_id>
32
-
33
- # Fetch result
34
- qubecore result --job-id <job_id>
35
-
36
- # Cancel job
37
- qubecore cancel --job-id <job_id>
38
- ```
39
-
40
- ## Commands
41
-
42
- ### Version
43
-
44
- ```bash
45
- qubecore -v
46
- qubecore --version
47
- ```
48
-
49
- ### Server Configuration
50
-
51
- ```bash
52
- qubecore config set host 192.168.1.100:50051
53
- qubecore config get
54
- qubecore config get host
55
- qubecore config unset host
56
- ```
57
-
58
- ### Job Commands
59
-
60
- ```bash
61
- # Submit
62
- qubecore submit --backend <backend> --circuit <circuit_string> --shots <n>
63
- qubecore submit --backend backend1 --circuit '...' --shots 100 --priority 2 --optimization-level 1
64
-
65
- # Status
66
- qubecore status --job-id <job_id>
67
-
68
- # Result
69
- qubecore result --job-id <job_id>
70
-
71
- # Cancel (PENDING 상태일 때만 가능)
72
- qubecore cancel --job-id <job_id>
73
- ```
74
-
75
- One-time server override:
76
-
77
- ```bash
78
- qubecore --host 10.0.0.1:50051 submit --backend backend1 --circuit "..." --shots 100
79
- ```
80
-
81
- ## Options
82
-
83
- ### `submit`
84
-
85
- | Option | Required | Description |
86
- |--------|----------|-------------|
87
- | `--backend` | Yes | 백엔드 이름 (`backend1`, `kreo.sc-20` 등) |
88
- | `--circuit` | Yes | 회로 문자열 (QASM2/QASM3/JSON) |
89
- | `--shots` | Yes | 실행 횟수 |
90
- | `--priority` | No | 우선순위 1~4 (1=CRITICAL, 2=HIGH, 3=NORMAL, 4=LOW) |
91
- | `--optimization-level` | No | 최적화 레벨 0~3 |
92
-
93
- ## Compatibility
94
-
95
- | qubecli | qubecore-client | qubecore |
96
- |---------|----------------|----------|
97
- | 1.x | 1.x | 1.x |
98
-
99
- ## License
100
-
101
- MIT
@@ -1,188 +0,0 @@
1
- import json
2
- from contextlib import contextmanager
3
- from importlib.metadata import version
4
- from pathlib import Path
5
- from typing import Annotated, Generator, Optional
6
-
7
- import grpc
8
- import typer
9
- from qubecore_client import QubeClient
10
-
11
- _CONFIG_PATH = Path.home() / ".qubecore" / "config.json"
12
-
13
- app = typer.Typer(help="QubeCore CLI — Quantum Computing Operating System", invoke_without_command=True)
14
- config_app = typer.Typer(help="서버 설정 관리")
15
- app.add_typer(config_app, name="config")
16
-
17
-
18
- _address: str = "localhost:50051"
19
-
20
-
21
- def _load_config() -> dict:
22
- if _CONFIG_PATH.exists():
23
- return json.loads(_CONFIG_PATH.read_text())
24
- return {}
25
-
26
-
27
- def _save_config(data: dict) -> None:
28
- _CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
29
- _CONFIG_PATH.write_text(json.dumps(data, indent=2))
30
-
31
-
32
- def _resolve_address(host: Optional[str]) -> str:
33
- if host is not None:
34
- return host
35
- return _load_config().get("host", "localhost:50051")
36
-
37
-
38
- @contextmanager
39
- def _get_client(address: str) -> Generator[QubeClient, None, None]:
40
- client = QubeClient(address=address)
41
- try:
42
- yield client
43
- except grpc.RpcError as e:
44
- if e.code() == grpc.StatusCode.UNAVAILABLE:
45
- typer.echo(f"서버에 연결할 수 없습니다: {address}", err=True)
46
- typer.echo(" 'qubecore config set host <host:port>' 로 서버 주소를 확인하세요.", err=True)
47
- else:
48
- typer.echo(f"gRPC 오류: {e.details()}", err=True)
49
- raise typer.Exit(1)
50
- finally:
51
- client.close()
52
-
53
-
54
- def _priority_label(priority: int) -> str:
55
- return {1: "CRITICAL", 2: "HIGH", 3: "NORMAL", 4: "LOW"}.get(priority, str(priority))
56
-
57
-
58
- @app.callback()
59
- def _callback(
60
- host: Annotated[Optional[str], typer.Option(help="QubeCore 서버 주소 (host:port)")] = None,
61
- ver: Annotated[bool, typer.Option("-v", "--version", help="버전 출력", is_eager=True)] = False,
62
- ) -> None:
63
- global _address
64
- if ver:
65
- typer.echo(f"v{version('qubecli')}")
66
- raise typer.Exit()
67
- _address = _resolve_address(host)
68
-
69
-
70
- @config_app.command("set")
71
- def config_set(
72
- key: Annotated[str, typer.Argument(help="설정 키")],
73
- value: Annotated[str, typer.Argument(help="설정 값")],
74
- ) -> None:
75
- """설정값 저장 (예: qubecore config set host 192.168.1.100:50051)"""
76
- if key != "host":
77
- typer.echo(f"알 수 없는 키: {key}. 사용 가능: host", err=True)
78
- raise typer.Exit(1)
79
- cfg = _load_config()
80
- cfg[key] = value
81
- _save_config(cfg)
82
- typer.echo(f"✓ {key} = {value} 저장 완료 ({_CONFIG_PATH})")
83
-
84
-
85
- @config_app.command("get")
86
- def config_get(
87
- key: Annotated[Optional[str], typer.Argument(help="설정 키 (생략 시 전체 출력)")] = None,
88
- ) -> None:
89
- """설정값 조회"""
90
- cfg = _load_config()
91
- if not cfg:
92
- typer.echo("저장된 설정 없음")
93
- return
94
- if key:
95
- typer.echo(f" {key} : {cfg.get(key, '(없음)')}")
96
- else:
97
- for k, v in cfg.items():
98
- typer.echo(f" {k} : {v}")
99
-
100
-
101
- @config_app.command("unset")
102
- def config_unset(
103
- key: Annotated[str, typer.Argument(help="삭제할 설정 키")],
104
- ) -> None:
105
- """설정값 삭제"""
106
- cfg = _load_config()
107
- if key not in cfg:
108
- typer.echo(f" {key} : 설정된 값 없음")
109
- return
110
- del cfg[key]
111
- _save_config(cfg)
112
- typer.echo(f"✓ {key} 삭제 완료")
113
-
114
-
115
- @app.command()
116
- def submit(
117
- backend: Annotated[str, typer.Option(help="백엔드 이름 (backend1, kreo.sc-20 등)")],
118
- circuit: Annotated[str, typer.Option(help="회로 문자열 (QASM2/QASM3/JSON)")],
119
- shots: Annotated[int, typer.Option(help="실행 횟수")],
120
- priority: Annotated[Optional[int], typer.Option(help="우선순위 1~4 (기본값 3=NORMAL)")] = None,
121
- optimization_level: Annotated[Optional[int], typer.Option(help="최적화 레벨 0~3")] = None,
122
- ) -> None:
123
- """게이트 회로 실행 작업 제출"""
124
- if priority is not None and priority not in (1, 2, 3, 4):
125
- typer.echo("priority는 1~4 사이여야 합니다.", err=True)
126
- raise typer.Exit(1)
127
- if optimization_level is not None and optimization_level not in (0, 1, 2, 3):
128
- typer.echo("optimization-level은 0~3 사이여야 합니다.", err=True)
129
- raise typer.Exit(1)
130
- with _get_client(_address) as client:
131
- job_id = client.submit_gate_job(
132
- backend_name=backend,
133
- gate_circuits=[circuit.replace("\\n", "\n").replace("\\t", "\t")],
134
- shots=shots,
135
- priority=priority,
136
- optimization_level=optimization_level,
137
- )
138
- typer.echo("✓ 작업 제출 완료")
139
- typer.echo(f" job_id : {job_id}")
140
- typer.echo(f" priority : {_priority_label(priority if priority is not None else 3)}")
141
-
142
-
143
- @app.command()
144
- def status(
145
- job_id: Annotated[str, typer.Option(help="작업 ID")],
146
- ) -> None:
147
- """작업 상태 조회"""
148
- with _get_client(_address) as client:
149
- job = client.get_job_status(job_id)
150
- typer.echo(f" job_id : {job['job_id']}")
151
- typer.echo(f" status : {job['status']}")
152
- typer.echo(f" priority : {_priority_label(job['priority'])}")
153
- typer.echo(f" submitted : {job['submitted_at']}")
154
- typer.echo(f" started : {job['started_at'] or '(미시작)'}")
155
- typer.echo(f" finished : {job['finished_at'] or '(미완료)'}")
156
- if job["error"]:
157
- typer.echo(f" error : {job['error']}")
158
-
159
-
160
- @app.command()
161
- def result(
162
- job_id: Annotated[str, typer.Option(help="작업 ID")],
163
- ) -> None:
164
- """작업 결과 조회"""
165
- with _get_client(_address) as client:
166
- job = client.get_job_result(job_id)
167
- typer.echo(f" job_id : {job['job_id']}")
168
- typer.echo(f" status : {job['status']}")
169
- for i, r in enumerate(job["results"]):
170
- typer.echo(f" [회로 {i}] counts : {r['counts_json']}")
171
-
172
-
173
- @app.command()
174
- def cancel(
175
- job_id: Annotated[str, typer.Option(help="작업 ID")],
176
- ) -> None:
177
- """작업 취소 (PENDING 상태일 때만 가능)"""
178
- with _get_client(_address) as client:
179
- message = client.cancel_job(job_id)
180
- typer.echo(f"✓ {message}")
181
-
182
-
183
- def main() -> None:
184
- app()
185
-
186
-
187
- if __name__ == "__main__":
188
- main()
File without changes
File without changes
File without changes
File without changes