quapp-common 0.0.12.dev14__tar.gz → 0.0.13.dev1__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_common-0.0.12.dev14/quapp_common.egg-info → quapp_common-0.0.13.dev1}/PKG-INFO +54 -11
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/README.md +51 -10
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/pyproject.toml +7 -2
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/component/bridge.py +21 -16
- quapp_common-0.0.13.dev1/quapp_common/component/circuit_adapter.py +282 -0
- quapp_common-0.0.13.dev1/quapp_common/component/dispatcher.py +119 -0
- quapp_common-0.0.13.dev1/quapp_common/component/result_serializer.py +18 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/enum/language.py +1 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1/quapp_common.egg-info}/PKG-INFO +54 -11
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common.egg-info/SOURCES.txt +4 -1
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common.egg-info/requires.txt +2 -0
- quapp_common-0.0.13.dev1/tests/test_circuit_adapter.py +120 -0
- quapp_common-0.0.13.dev1/tests/test_json_parser_utils.py +96 -0
- quapp_common-0.0.13.dev1/tests/test_result_serializer.py +48 -0
- quapp_common-0.0.12.dev14/quapp_common/component/circuit_adapter.py +0 -308
- quapp_common-0.0.12.dev14/quapp_common/component/dispatcher.py +0 -108
- quapp_common-0.0.12.dev14/quapp_common/component/result_serializer.py +0 -22
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/LICENSE +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/async_tasks/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/async_tasks/async_invocation_task.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/async_tasks/async_task.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/async_tasks/export_circuit_task.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/async_tasks/post_processing_task.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/component/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/component/backend/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/component/backend/invocation.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/component/backend/job_fetcher.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/component/backend/job_fetching.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/component/backend/job_manager.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/component/callback/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/component/callback/update_job_metadata.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/component/device/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/component/device/device_selection.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/config/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/config/logging_config.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/config/thread_config.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/async_task/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/backend/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/backend/backend_information.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/callback/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/callback/callback_url.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/device/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/device/circuit_running_option.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/promise/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/promise/post_processing_promise.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/promise/promise.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/request/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/request/invocation_request.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/request/job_fetching_request.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/request/request.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/response/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/response/authentication.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/response/custom_header.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/data/response/job_response.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/enum/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/enum/base_enum.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/enum/http_header.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/enum/invocation_step.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/enum/media_type.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/enum/processing_unit.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/enum/provider_tag.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/enum/sdk.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/enum/status/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/enum/status/job_status.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/enum/status/status_code.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/enum/token_type.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/factory/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/factory/device_factory.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/factory/handler_factory.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/factory/provider_factory.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/handler/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/handler/handler.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/model/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/model/device/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/model/device/custom_device.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/model/device/device.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/model/invocation.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/model/provider/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/model/provider/provider.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/util/__init__.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/util/file_utils.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/util/http_utils.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/util/json_parser_utils.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common/util/response_utils.py +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common.egg-info/dependency_links.txt +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/quapp_common.egg-info/top_level.txt +0 -0
- {quapp_common-0.0.12.dev14 → quapp_common-0.0.13.dev1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quapp-common
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.13.dev1
|
|
4
4
|
Summary: Quapp common library supporting Quapp Platform for Quantum Computing
|
|
5
5
|
Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -29,6 +29,8 @@ Requires-Dist: qibo
|
|
|
29
29
|
Requires-Dist: dimod
|
|
30
30
|
Requires-Dist: qulacs
|
|
31
31
|
Requires-Dist: qiskit
|
|
32
|
+
Requires-Dist: pytket
|
|
33
|
+
Requires-Dist: pennylane
|
|
32
34
|
Provides-Extra: dev
|
|
33
35
|
Requires-Dist: black; extra == "dev"
|
|
34
36
|
Requires-Dist: bumpver; extra == "dev"
|
|
@@ -75,16 +77,25 @@ responses while work continues in the background.
|
|
|
75
77
|
scheduler.
|
|
76
78
|
- Safety: updates to jobs already DONE/FAILED are ignored to prevent
|
|
77
79
|
post-completion mutations.
|
|
78
|
-
- Multi-language SDK support
|
|
79
|
-
- Language enum for validating and resolving Python
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
- Multi-language SDK support:
|
|
81
|
+
- Language enum for validating and resolving Python, JavaScript, and Q#
|
|
82
|
+
runtimes.
|
|
83
|
+
- Standalone `dispatch()` function (formerly `SubprocessDispatcher` class)
|
|
84
|
+
to delegate tasks to non-Python runtimes via subprocess with timeout
|
|
85
|
+
management and JSON payload processing; supports Node.js and precompiled
|
|
86
|
+
Q# binary (`./handler_runner_bin/Function[.exe]`) runners.
|
|
87
|
+
- Working directory configurable via `QUAPP_WORKING_DIR` environment
|
|
88
|
+
variable; falls back to `os.getcwd()` when unset.
|
|
89
|
+
- `SubprocessBridge` class integrating standalone dispatcher, circuit
|
|
90
|
+
adapter, and result serializer for seamless JS/Q#-Python handler
|
|
91
|
+
interaction.
|
|
92
|
+
- Standalone `adapt()` function (formerly `CircuitAdapter` class) for
|
|
93
|
+
converting subprocess circuit outputs into native SDK formats (OpenQASM,
|
|
94
|
+
QUBO, JSON) across Qiskit, Braket, PennyLane, PyQuil, Qibo, Qulacs, and
|
|
95
|
+
CUDA Quantum.
|
|
96
|
+
- Standalone `serialize()` function (formerly `ResultSerializer` class) for
|
|
97
|
+
converting complex job results into JSON-serializable dictionaries for
|
|
98
|
+
subprocess communication.
|
|
88
99
|
|
|
89
100
|
## Installation
|
|
90
101
|
|
|
@@ -103,6 +114,38 @@ Notes:
|
|
|
103
114
|
|
|
104
115
|
## Recently Changes Highlights
|
|
105
116
|
|
|
117
|
+
### v0.0.13.dev1
|
|
118
|
+
- Refactor: Replace class-based subprocess components with standalone
|
|
119
|
+
module-level functions for simpler imports and testability:
|
|
120
|
+
- `SubprocessDispatcher` class → `dispatch()` and `is_subprocess_language()`
|
|
121
|
+
in `dispatcher.py`.
|
|
122
|
+
- `CircuitAdapter` class → `adapt()` and private helper functions in
|
|
123
|
+
`circuit_adapter.py`.
|
|
124
|
+
- `ResultSerializer` class → `serialize()` in `result_serializer.py`.
|
|
125
|
+
- `SubprocessBridge` retains its class form but delegates internally to the
|
|
126
|
+
new standalone functions.
|
|
127
|
+
- Refactor `dispatcher.py`: `WORKING_DIR` is now read from the
|
|
128
|
+
`QUAPP_WORKING_DIR` environment variable (previously hardcoded `None`);
|
|
129
|
+
falls back to `os.getcwd()` when the variable is unset.
|
|
130
|
+
- Refactor `dispatcher.py`: Q# runner command changed from
|
|
131
|
+
`dotnet run --project HandlerRunner.csproj` to a precompiled
|
|
132
|
+
platform-specific binary — `./handler_runner_bin/Function.exe` on Windows
|
|
133
|
+
and `./handler_runner_bin/Function` on other platforms — eliminating the
|
|
134
|
+
`dotnet` toolchain requirement at runtime.
|
|
135
|
+
- Refactor `circuit_adapter.py`: Extract `_load_qasm_circuit(qasm_str)` private
|
|
136
|
+
utility that centralises OpenQASM 2.0 string parsing with Qiskit version
|
|
137
|
+
compatibility — uses `qiskit.qasm2.loads` (Qiskit >= 1.0) and falls back to
|
|
138
|
+
`QiskitCircuit.from_qasm_str` (Qiskit < 1.0).
|
|
139
|
+
- Fix: `subprocess.run` now passes `check=True` so a non-zero exit code raises
|
|
140
|
+
`CalledProcessError` before the manual return-code branch is reached.
|
|
141
|
+
- Add unit tests: `tests/test_circuit_adapter.py`,
|
|
142
|
+
`tests/test_json_parser_utils.py`, `tests/test_result_serializer.py`.
|
|
143
|
+
|
|
144
|
+
### v0.0.12.dev15
|
|
145
|
+
- Extend multi-language SDK support with Q# (QSharp) runtime:
|
|
146
|
+
- Add `QSHARP` to `Language` enum.
|
|
147
|
+
- Add Q# runner configuration to `SubprocessDispatcher` (`dotnet run --project .`, 300 s timeout).
|
|
148
|
+
|
|
106
149
|
### v0.0.12.dev14
|
|
107
150
|
- Fix: `CircuitAdapter._adapt_openqasm` for `Sdk.PENNYLANE` now catches the
|
|
108
151
|
"Failed to load the qasm plugin" error raised by `qml.from_qasm` on
|
|
@@ -36,16 +36,25 @@ responses while work continues in the background.
|
|
|
36
36
|
scheduler.
|
|
37
37
|
- Safety: updates to jobs already DONE/FAILED are ignored to prevent
|
|
38
38
|
post-completion mutations.
|
|
39
|
-
- Multi-language SDK support
|
|
40
|
-
- Language enum for validating and resolving Python
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
- Multi-language SDK support:
|
|
40
|
+
- Language enum for validating and resolving Python, JavaScript, and Q#
|
|
41
|
+
runtimes.
|
|
42
|
+
- Standalone `dispatch()` function (formerly `SubprocessDispatcher` class)
|
|
43
|
+
to delegate tasks to non-Python runtimes via subprocess with timeout
|
|
44
|
+
management and JSON payload processing; supports Node.js and precompiled
|
|
45
|
+
Q# binary (`./handler_runner_bin/Function[.exe]`) runners.
|
|
46
|
+
- Working directory configurable via `QUAPP_WORKING_DIR` environment
|
|
47
|
+
variable; falls back to `os.getcwd()` when unset.
|
|
48
|
+
- `SubprocessBridge` class integrating standalone dispatcher, circuit
|
|
49
|
+
adapter, and result serializer for seamless JS/Q#-Python handler
|
|
50
|
+
interaction.
|
|
51
|
+
- Standalone `adapt()` function (formerly `CircuitAdapter` class) for
|
|
52
|
+
converting subprocess circuit outputs into native SDK formats (OpenQASM,
|
|
53
|
+
QUBO, JSON) across Qiskit, Braket, PennyLane, PyQuil, Qibo, Qulacs, and
|
|
54
|
+
CUDA Quantum.
|
|
55
|
+
- Standalone `serialize()` function (formerly `ResultSerializer` class) for
|
|
56
|
+
converting complex job results into JSON-serializable dictionaries for
|
|
57
|
+
subprocess communication.
|
|
49
58
|
|
|
50
59
|
## Installation
|
|
51
60
|
|
|
@@ -64,6 +73,38 @@ Notes:
|
|
|
64
73
|
|
|
65
74
|
## Recently Changes Highlights
|
|
66
75
|
|
|
76
|
+
### v0.0.13.dev1
|
|
77
|
+
- Refactor: Replace class-based subprocess components with standalone
|
|
78
|
+
module-level functions for simpler imports and testability:
|
|
79
|
+
- `SubprocessDispatcher` class → `dispatch()` and `is_subprocess_language()`
|
|
80
|
+
in `dispatcher.py`.
|
|
81
|
+
- `CircuitAdapter` class → `adapt()` and private helper functions in
|
|
82
|
+
`circuit_adapter.py`.
|
|
83
|
+
- `ResultSerializer` class → `serialize()` in `result_serializer.py`.
|
|
84
|
+
- `SubprocessBridge` retains its class form but delegates internally to the
|
|
85
|
+
new standalone functions.
|
|
86
|
+
- Refactor `dispatcher.py`: `WORKING_DIR` is now read from the
|
|
87
|
+
`QUAPP_WORKING_DIR` environment variable (previously hardcoded `None`);
|
|
88
|
+
falls back to `os.getcwd()` when the variable is unset.
|
|
89
|
+
- Refactor `dispatcher.py`: Q# runner command changed from
|
|
90
|
+
`dotnet run --project HandlerRunner.csproj` to a precompiled
|
|
91
|
+
platform-specific binary — `./handler_runner_bin/Function.exe` on Windows
|
|
92
|
+
and `./handler_runner_bin/Function` on other platforms — eliminating the
|
|
93
|
+
`dotnet` toolchain requirement at runtime.
|
|
94
|
+
- Refactor `circuit_adapter.py`: Extract `_load_qasm_circuit(qasm_str)` private
|
|
95
|
+
utility that centralises OpenQASM 2.0 string parsing with Qiskit version
|
|
96
|
+
compatibility — uses `qiskit.qasm2.loads` (Qiskit >= 1.0) and falls back to
|
|
97
|
+
`QiskitCircuit.from_qasm_str` (Qiskit < 1.0).
|
|
98
|
+
- Fix: `subprocess.run` now passes `check=True` so a non-zero exit code raises
|
|
99
|
+
`CalledProcessError` before the manual return-code branch is reached.
|
|
100
|
+
- Add unit tests: `tests/test_circuit_adapter.py`,
|
|
101
|
+
`tests/test_json_parser_utils.py`, `tests/test_result_serializer.py`.
|
|
102
|
+
|
|
103
|
+
### v0.0.12.dev15
|
|
104
|
+
- Extend multi-language SDK support with Q# (QSharp) runtime:
|
|
105
|
+
- Add `QSHARP` to `Language` enum.
|
|
106
|
+
- Add Q# runner configuration to `SubprocessDispatcher` (`dotnet run --project .`, 300 s timeout).
|
|
107
|
+
|
|
67
108
|
### v0.0.12.dev14
|
|
68
109
|
- Fix: `CircuitAdapter._adapt_openqasm` for `Sdk.PENNYLANE` now catches the
|
|
69
110
|
"Failed to load the qasm plugin" error raised by `qml.from_qasm` on
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quapp-common"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.13.dev1"
|
|
8
8
|
description = "Quapp common library supporting Quapp Platform for Quantum Computing"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "CITYNOW Co. Ltd. ", email = "corp@citynow.vn" }]
|
|
@@ -25,7 +25,9 @@ dependencies = [
|
|
|
25
25
|
"qibo",
|
|
26
26
|
"dimod",
|
|
27
27
|
"qulacs",
|
|
28
|
-
"qiskit"
|
|
28
|
+
"qiskit",
|
|
29
|
+
"pytket",
|
|
30
|
+
"pennylane"
|
|
29
31
|
]
|
|
30
32
|
requires-python = ">=3.7"
|
|
31
33
|
|
|
@@ -34,3 +36,6 @@ dev = ["black", "bumpver", "isort", "pip-tools", "pytest"]
|
|
|
34
36
|
|
|
35
37
|
[project.urls]
|
|
36
38
|
Homepage = "https://citynow.asia/"
|
|
39
|
+
|
|
40
|
+
[tool.pylint.main]
|
|
41
|
+
source-roots = ["."]
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
"""
|
|
2
|
-
QApp Platform Project
|
|
3
|
-
bridge.py
|
|
4
|
-
Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
5
|
-
"""
|
|
6
1
|
# Quapp Platform Project
|
|
7
2
|
# bridge.py
|
|
8
3
|
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
from typing import Any, Dict
|
|
9
5
|
|
|
10
|
-
from .circuit_adapter import
|
|
11
|
-
from .dispatcher import
|
|
12
|
-
from .result_serializer import
|
|
6
|
+
from .circuit_adapter import adapt
|
|
7
|
+
from .dispatcher import dispatch
|
|
8
|
+
from .result_serializer import serialize
|
|
13
9
|
from ..config.logging_config import logger
|
|
14
10
|
from ..enum.language import Language
|
|
15
11
|
from ..enum.sdk import Sdk
|
|
16
12
|
|
|
13
|
+
# Quapp Platform Project
|
|
14
|
+
# bridge.py
|
|
15
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
16
|
+
|
|
17
17
|
logger = logger.bind(context=__name__)
|
|
18
18
|
|
|
19
19
|
|
|
@@ -32,7 +32,7 @@ class SubprocessBridge:
|
|
|
32
32
|
self.sdk = sdk
|
|
33
33
|
logger.debug(f"SubprocessBridge initialized | language={language.value} | sdk={sdk}")
|
|
34
34
|
|
|
35
|
-
def processing(self, invocation_input:
|
|
35
|
+
def processing(self, invocation_input: Dict[str, Any]):
|
|
36
36
|
"""Call JS handler's processing() and convert a result to a native circuit.
|
|
37
37
|
|
|
38
38
|
Args:
|
|
@@ -47,9 +47,9 @@ class SubprocessBridge:
|
|
|
47
47
|
logger.info(f"SubprocessBridge.processing: dispatching to {self.language.value} | sdk={self.sdk}")
|
|
48
48
|
logger.debug(f"SubprocessBridge.processing: input_keys={list(invocation_input.keys())}")
|
|
49
49
|
|
|
50
|
-
result =
|
|
50
|
+
result = dispatch(self.language, {
|
|
51
51
|
"action": "processing",
|
|
52
|
-
"input"
|
|
52
|
+
"input": invocation_input
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
status = result.get("status")
|
|
@@ -62,11 +62,15 @@ class SubprocessBridge:
|
|
|
62
62
|
payload = result.get("result")
|
|
63
63
|
if payload is None:
|
|
64
64
|
raise RuntimeError("JS processing returned no 'result' field")
|
|
65
|
+
if not isinstance(payload, dict):
|
|
66
|
+
raise RuntimeError(
|
|
67
|
+
f"JS processing returned invalid 'result' type: {type(payload).__name__}"
|
|
68
|
+
)
|
|
65
69
|
|
|
66
70
|
fmt = payload.get("format")
|
|
67
71
|
logger.debug(f"SubprocessBridge.processing: JS returned format={fmt}; adapting for sdk={self.sdk}")
|
|
68
72
|
|
|
69
|
-
circuit =
|
|
73
|
+
circuit = adapt(self.sdk, payload)
|
|
70
74
|
logger.info(f"SubprocessBridge.processing: circuit adapted successfully | type={type(circuit).__name__}")
|
|
71
75
|
return circuit
|
|
72
76
|
|
|
@@ -85,11 +89,12 @@ class SubprocessBridge:
|
|
|
85
89
|
logger.info(f"SubprocessBridge.post_processing: dispatching to {self.language.value}")
|
|
86
90
|
logger.debug(f"SubprocessBridge.post_processing: job_result type={type(job_result).__name__}")
|
|
87
91
|
|
|
88
|
-
serialized =
|
|
89
|
-
logger.debug(
|
|
92
|
+
serialized = serialize(job_result)
|
|
93
|
+
logger.debug(
|
|
94
|
+
f"SubprocessBridge.post_processing: serialized result keys={list(serialized.keys()) if isinstance(serialized, dict) else type(serialized).__name__}")
|
|
90
95
|
|
|
91
|
-
result =
|
|
92
|
-
"action"
|
|
96
|
+
result = dispatch(self.language, {
|
|
97
|
+
"action": "post_processing",
|
|
93
98
|
"job_result": serialized
|
|
94
99
|
})
|
|
95
100
|
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# circuit_adapter.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any, Dict
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pennylane as qml
|
|
11
|
+
import pennylane.measurements as _plm
|
|
12
|
+
try:
|
|
13
|
+
import qiskit.qasm2 as _qasm2
|
|
14
|
+
except ImportError:
|
|
15
|
+
_qasm2 = None
|
|
16
|
+
from dimod import BinaryQuadraticModel
|
|
17
|
+
from pytket.qasm import circuit_from_qasm_str
|
|
18
|
+
from qibo.models import Circuit
|
|
19
|
+
from qiskit import QuantumCircuit as QiskitCircuit
|
|
20
|
+
from qulacs import QuantumCircuit as QulacsCircuit
|
|
21
|
+
from qulacs.gate import DenseMatrix as DenseMatrixGate
|
|
22
|
+
|
|
23
|
+
from ..config.logging_config import logger
|
|
24
|
+
from ..enum.sdk import Sdk
|
|
25
|
+
|
|
26
|
+
logger = logger.bind(context=__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load_qasm_circuit(qasm_str: str) -> QiskitCircuit:
|
|
30
|
+
"""Parse OpenQASM 2.0 string into a Qiskit QuantumCircuit.
|
|
31
|
+
|
|
32
|
+
Compatible with Qiskit >= 1.0 (qiskit.qasm2.loads) and < 1.0 (from_qasm_str).
|
|
33
|
+
"""
|
|
34
|
+
if _qasm2 is not None:
|
|
35
|
+
return _qasm2.loads(qasm_str)
|
|
36
|
+
return QiskitCircuit.from_qasm_str(qasm_str)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def adapt(sdk: Sdk, result: dict):
|
|
40
|
+
"""Route to the appropriate adapter based on the 'format' field."""
|
|
41
|
+
fmt = result.get("format")
|
|
42
|
+
logger.debug(f"adapt() called | sdk={sdk} | format={fmt}")
|
|
43
|
+
if fmt == "openqasm":
|
|
44
|
+
circuit = result.get("circuit")
|
|
45
|
+
if circuit is None:
|
|
46
|
+
raise ValueError("Missing 'circuit' field in openqasm response")
|
|
47
|
+
return _adapt_openqasm(sdk, circuit)
|
|
48
|
+
if fmt == "qubo":
|
|
49
|
+
qubo = result.get("qubo")
|
|
50
|
+
if qubo is None:
|
|
51
|
+
raise ValueError("Missing 'qubo' field in qubo response")
|
|
52
|
+
return _adapt_qubo(sdk, qubo)
|
|
53
|
+
if fmt == "json":
|
|
54
|
+
logger.debug("Returning raw JSON data pass-through")
|
|
55
|
+
return result.get("data")
|
|
56
|
+
raise ValueError(f"Unsupported circuit interchange format: {fmt}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _adapt_openqasm(sdk, qasm_str):
|
|
60
|
+
"""Convert an OpenQASM 2.0 string to a native SDK circuit object."""
|
|
61
|
+
logger.debug(f"Adapting OpenQASM circuit for SDK: {sdk} | qasm_length={len(qasm_str)}")
|
|
62
|
+
_handlers = {
|
|
63
|
+
Sdk.QISKIT: _openqasm_to_qiskit,
|
|
64
|
+
Sdk.BRAKET: _openqasm_to_braket,
|
|
65
|
+
Sdk.PENNYLANE: _openqasm_to_pennylane,
|
|
66
|
+
Sdk.PYQUIL: _openqasm_to_pyquil,
|
|
67
|
+
Sdk.QIBO: _openqasm_to_qibo,
|
|
68
|
+
Sdk.QULACS: _openqasm_to_qulacs,
|
|
69
|
+
Sdk.CUDA_QUANTUM: _openqasm_to_cuda_quantum,
|
|
70
|
+
}
|
|
71
|
+
handler = _handlers.get(sdk)
|
|
72
|
+
if handler is None:
|
|
73
|
+
raise ValueError(f"OpenQASM adaptation not supported for SDK: {sdk}")
|
|
74
|
+
return handler(qasm_str)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _adapt_qubo(sdk, qubo_raw: Dict[str, Any]):
|
|
78
|
+
"""Convert a QUBO dict with string keys ('x0,x1') to a BinaryQuadraticModel.
|
|
79
|
+
|
|
80
|
+
JS returns keys like "x0,x1" as strings. We parse them into tuple keys
|
|
81
|
+
for dimod's from_qubo() method.
|
|
82
|
+
"""
|
|
83
|
+
logger.debug(f"Adapting QUBO for SDK: {sdk} | num_entries={len(qubo_raw)}")
|
|
84
|
+
|
|
85
|
+
qubo_dict = {}
|
|
86
|
+
for key, val in qubo_raw.items():
|
|
87
|
+
if isinstance(key, str) and "," in key:
|
|
88
|
+
parts = tuple(k.strip() for k in key.split(",", 1))
|
|
89
|
+
qubo_dict[parts] = val
|
|
90
|
+
else:
|
|
91
|
+
qubo_dict[(key, key)] = val
|
|
92
|
+
|
|
93
|
+
logger.debug(f"QUBO parsed: {len(qubo_dict)} entries -> BinaryQuadraticModel")
|
|
94
|
+
return BinaryQuadraticModel.from_qubo(qubo_dict)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _count_qasm_qubits(qasm_str):
|
|
98
|
+
"""Sum qubit counts across all `qreg <name>[<n>];` declarations."""
|
|
99
|
+
return sum(int(m) for m in re.findall(r'qreg\s+\w+\[(\d+)]', qasm_str))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _openqasm_to_braket(qasm_str):
|
|
103
|
+
logger.debug("Converting OpenQASM via pytket -> tk_to_braket")
|
|
104
|
+
tk_to_braket = importlib.import_module("pytket.extensions.braket").tk_to_braket
|
|
105
|
+
result = tk_to_braket(circuit_from_qasm_str(qasm_str))
|
|
106
|
+
# tk_to_braket returns (Circuit, n_shots) in pytket-braket >= 0.35
|
|
107
|
+
is_tuple = isinstance(result, tuple)
|
|
108
|
+
logger.debug(
|
|
109
|
+
f"tk_to_braket returned tuple={is_tuple}; unpacking index 0" if is_tuple else "tk_to_braket returned Circuit directly")
|
|
110
|
+
return result[0] if is_tuple else result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _openqasm_to_cuda_quantum(qasm_str):
|
|
114
|
+
logger.debug("CUDA-Q: passing OpenQASM string through directly")
|
|
115
|
+
# CUDA-Q accepts OpenQASM directly via kernel builder
|
|
116
|
+
return qasm_str
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _openqasm_to_pennylane(qasm_str):
|
|
120
|
+
logger.debug("Converting OpenQASM via PennyLane qml.from_qasm wrapper")
|
|
121
|
+
for _compat_name in ("Expectation", "Variance", "Sample", "Probability",
|
|
122
|
+
"State", "MidMeasure"):
|
|
123
|
+
if not hasattr(_plm, _compat_name):
|
|
124
|
+
setattr(_plm, _compat_name, _compat_name)
|
|
125
|
+
|
|
126
|
+
num_wires = _count_qasm_qubits(qasm_str)
|
|
127
|
+
logger.debug(f"PennyLane: detected {num_wires} qubits; wrapping with qml.probs over all wires")
|
|
128
|
+
try:
|
|
129
|
+
base_fn = qml.from_qasm(qasm_str, measurements=[])
|
|
130
|
+
|
|
131
|
+
def _circuit():
|
|
132
|
+
base_fn()
|
|
133
|
+
if num_wires > 0:
|
|
134
|
+
return qml.probs(wires=list(range(num_wires)))
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
return _circuit
|
|
138
|
+
except Exception as _e:
|
|
139
|
+
if "qasm plugin" not in str(_e) and "pennylane-qiskit" not in str(_e):
|
|
140
|
+
raise
|
|
141
|
+
logger.warning(
|
|
142
|
+
f"qml.from_qasm unavailable ({_e}); falling back to Qiskit bridge for PennyLane QASM conversion"
|
|
143
|
+
)
|
|
144
|
+
return _qiskit_to_pennylane(qasm_str, num_wires)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _openqasm_to_pyquil(qasm_str):
|
|
148
|
+
logger.debug("Converting OpenQASM via pytket -> tk_to_pyquil")
|
|
149
|
+
tk_to_pyquil = importlib.import_module("pytket.extensions.pyquil").tk_to_pyquil
|
|
150
|
+
return tk_to_pyquil(circuit_from_qasm_str(qasm_str))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _openqasm_to_qibo(qasm_str):
|
|
154
|
+
logger.debug("Converting OpenQASM via Qibo Circuit.from_qasm")
|
|
155
|
+
return Circuit.from_qasm(qasm_str)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _openqasm_to_qiskit(qasm_str):
|
|
159
|
+
logger.debug("Converting OpenQASM via Qiskit QuantumCircuit")
|
|
160
|
+
return _load_qasm_circuit(qasm_str)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _openqasm_to_qulacs(qasm_str):
|
|
164
|
+
logger.debug("Converting OpenQASM for Qulacs via Qiskit bridge")
|
|
165
|
+
# Try the Qiskit bridge first; fall back to raw pass-through if
|
|
166
|
+
# qiskit is not installed or the conversion fails.
|
|
167
|
+
try:
|
|
168
|
+
qiskit_circ = _load_qasm_circuit(qasm_str)
|
|
169
|
+
logger.debug(
|
|
170
|
+
f"Qiskit parsed circuit: num_qubits={qiskit_circ.num_qubits} num_gates={len(qiskit_circ.data)}")
|
|
171
|
+
return _qiskit_to_qulacs(qiskit_circ)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.warning(f"Qiskit bridge failed for Qulacs ({e}); falling back to raw OpenQASM pass-through")
|
|
174
|
+
return qasm_str
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _qiskit_to_pennylane(qasm_str, num_wires):
|
|
178
|
+
"""Parse an OpenQASM 2.0 string via Qiskit and return a PennyLane callable.
|
|
179
|
+
|
|
180
|
+
Fallback for environments where qml.from_qasm is unavailable because
|
|
181
|
+
pennylane-qiskit is not installed (PennyLane < 0.36).
|
|
182
|
+
"""
|
|
183
|
+
qiskit_circ = _load_qasm_circuit(qasm_str)
|
|
184
|
+
qubit_map = {bit: idx for idx, bit in enumerate(qiskit_circ.qubits)}
|
|
185
|
+
logger.debug(
|
|
186
|
+
f"_qiskit_to_pennylane: num_qubits={qiskit_circ.num_qubits} "
|
|
187
|
+
f"num_instructions={len(qiskit_circ.data)}"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
instructions = [
|
|
191
|
+
(inst.operation.name,
|
|
192
|
+
[qubit_map[q] for q in inst.qubits],
|
|
193
|
+
[float(p) for p in inst.operation.params])
|
|
194
|
+
for inst in qiskit_circ.data
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
_skip = frozenset({'measure', 'barrier', 'reset', 'id'})
|
|
198
|
+
_gate_map = {
|
|
199
|
+
'h': lambda q, p: qml.Hadamard(wires=q[0]),
|
|
200
|
+
'x': lambda q, p: qml.PauliX(wires=q[0]),
|
|
201
|
+
'y': lambda q, p: qml.PauliY(wires=q[0]),
|
|
202
|
+
'z': lambda q, p: qml.PauliZ(wires=q[0]),
|
|
203
|
+
's': lambda q, p: qml.S(wires=q[0]),
|
|
204
|
+
'sdg': lambda q, p: qml.PhaseShift(-np.pi / 2, wires=q[0]),
|
|
205
|
+
't': lambda q, p: qml.T(wires=q[0]),
|
|
206
|
+
'tdg': lambda q, p: qml.PhaseShift(-np.pi / 4, wires=q[0]),
|
|
207
|
+
'cx': lambda q, p: qml.CNOT(wires=q),
|
|
208
|
+
'cz': lambda q, p: qml.CZ(wires=q),
|
|
209
|
+
'swap': lambda q, p: qml.SWAP(wires=q),
|
|
210
|
+
'rx': lambda q, p: qml.RX(p[0], wires=q[0]),
|
|
211
|
+
'ry': lambda q, p: qml.RY(p[0], wires=q[0]),
|
|
212
|
+
'rz': lambda q, p: qml.RZ(p[0], wires=q[0]),
|
|
213
|
+
'p': lambda q, p: qml.PhaseShift(p[0], wires=q[0]),
|
|
214
|
+
'u1': lambda q, p: qml.PhaseShift(p[0], wires=q[0]),
|
|
215
|
+
'u2': lambda q, p: qml.U2(p[0], p[1], wires=q[0]),
|
|
216
|
+
'u3': lambda q, p: qml.U3(p[0], p[1], p[2], wires=q[0]),
|
|
217
|
+
'u': lambda q, p: qml.U3(p[0], p[1], p[2], wires=q[0]),
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
def _circuit():
|
|
221
|
+
for name, qubits, params in instructions:
|
|
222
|
+
if name in _skip:
|
|
223
|
+
continue
|
|
224
|
+
gate_fn = _gate_map.get(name)
|
|
225
|
+
if gate_fn is not None:
|
|
226
|
+
gate_fn(qubits, params)
|
|
227
|
+
else:
|
|
228
|
+
logger.warning(f"PennyLane Qiskit bridge: unrecognized gate '{name}'; skipping")
|
|
229
|
+
if num_wires > 0:
|
|
230
|
+
return qml.probs(wires=list(range(num_wires)))
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
return _circuit
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _qiskit_to_qulacs(qiskit_circ):
|
|
237
|
+
"""Convert a Qiskit QuantumCircuit to a qulacs QuantumCircuit.
|
|
238
|
+
|
|
239
|
+
Fixed gates are mapped via named qulacs methods; parametric and
|
|
240
|
+
unrecognized gates fall back to DenseMatrix (unitary passthrough).
|
|
241
|
+
Measurement, barrier, reset, and identity instructions are skipped.
|
|
242
|
+
"""
|
|
243
|
+
n = qiskit_circ.num_qubits
|
|
244
|
+
logger.debug(f"_qiskit_to_qulacs: num_qubits={n} num_instructions={len(qiskit_circ.data)}")
|
|
245
|
+
qulacs_circ = QulacsCircuit(n)
|
|
246
|
+
qubit_map = {bit: idx for idx, bit in enumerate(qiskit_circ.qubits)}
|
|
247
|
+
|
|
248
|
+
_SINGLE = {
|
|
249
|
+
'h': 'add_H_gate', 'x': 'add_X_gate',
|
|
250
|
+
'y': 'add_Y_gate', 'z': 'add_Z_gate',
|
|
251
|
+
's': 'add_S_gate', 'sdg': 'add_Sdag_gate',
|
|
252
|
+
't': 'add_T_gate', 'tdg': 'add_Tdag_gate',
|
|
253
|
+
}
|
|
254
|
+
_TWO = {
|
|
255
|
+
'cx': 'add_CNOT_gate',
|
|
256
|
+
'cz': 'add_CZ_gate',
|
|
257
|
+
'swap': 'add_SWAP_gate',
|
|
258
|
+
}
|
|
259
|
+
_SKIP = {'measure', 'barrier', 'reset', 'id'}
|
|
260
|
+
|
|
261
|
+
for inst in qiskit_circ.data:
|
|
262
|
+
gate = inst.operation
|
|
263
|
+
qubits = [qubit_map[q] for q in inst.qubits]
|
|
264
|
+
name = gate.name
|
|
265
|
+
|
|
266
|
+
if name in _SKIP:
|
|
267
|
+
continue
|
|
268
|
+
elif name in _SINGLE:
|
|
269
|
+
getattr(qulacs_circ, _SINGLE[name])(qubits[0])
|
|
270
|
+
elif name in _TWO:
|
|
271
|
+
getattr(qulacs_circ, _TWO[name])(qubits[0], qubits[1])
|
|
272
|
+
else:
|
|
273
|
+
# Parametric gates (rx, ry, rz, u*, p, ...) and any
|
|
274
|
+
# unrecognized gate: extract the unitary and apply it
|
|
275
|
+
# as a DenseMatrix. Single-qubit gates have no ordering
|
|
276
|
+
# ambiguity; multi-qubit gates are passed with qulacs'
|
|
277
|
+
# index order matching Qiskit's instruction qubit order.
|
|
278
|
+
logger.debug(f"Gate '{name}' not in named map; converting via DenseMatrix on qubits={qubits}")
|
|
279
|
+
qulacs_circ.add_gate(DenseMatrixGate(qubits, gate.to_matrix()))
|
|
280
|
+
|
|
281
|
+
logger.debug(f"_qiskit_to_qulacs: conversion complete")
|
|
282
|
+
return qulacs_circ
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# dispatcher.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from ..config.logging_config import logger
|
|
11
|
+
from ..enum.language import Language
|
|
12
|
+
|
|
13
|
+
logger = logger.bind(context=__name__)
|
|
14
|
+
|
|
15
|
+
WORKING_DIR: str = os.environ.get('QUAPP_WORKING_DIR')
|
|
16
|
+
|
|
17
|
+
_QSHARP_BINARY = './handler_runner_bin/Function.exe' if sys.platform == 'win32' else './handler_runner_bin/Function'
|
|
18
|
+
|
|
19
|
+
RUNNER_MAP = {
|
|
20
|
+
Language.JAVASCRIPT: {
|
|
21
|
+
'command': ['node', 'handler_runner.js'],
|
|
22
|
+
'timeout': 300,
|
|
23
|
+
},
|
|
24
|
+
Language.QSHARP: {
|
|
25
|
+
'command': [_QSHARP_BINARY],
|
|
26
|
+
'timeout': 300,
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def dispatch(language: Language, request_data: dict,
|
|
32
|
+
timeout: int = None) -> dict:
|
|
33
|
+
"""Dispatch request_data to a subprocess runner.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
language: The target language runtime.
|
|
37
|
+
request_data: Dict to serialize as JSON and pipe to stdin.
|
|
38
|
+
timeout: Optional timeout in seconds. Overrides the runner by default.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Parsed JSON dict from the subprocess stdout.
|
|
42
|
+
"""
|
|
43
|
+
runner = RUNNER_MAP.get(language)
|
|
44
|
+
if runner is None:
|
|
45
|
+
raise ValueError(f"No runner configured for language: {language}")
|
|
46
|
+
|
|
47
|
+
effective_timeout = timeout if timeout is not None else runner[
|
|
48
|
+
'timeout']
|
|
49
|
+
action = request_data.get('action', 'handle')
|
|
50
|
+
logger.info(f"Dispatching to {language.value} runner, action={action}, "
|
|
51
|
+
f"timeout={effective_timeout}s")
|
|
52
|
+
|
|
53
|
+
payload = json.dumps(request_data)
|
|
54
|
+
cwd = WORKING_DIR or os.getcwd()
|
|
55
|
+
result = subprocess.run(runner['command'], input=payload,
|
|
56
|
+
capture_output=True, text=True,
|
|
57
|
+
timeout=effective_timeout, cwd=cwd, check=True)
|
|
58
|
+
|
|
59
|
+
if result.stderr:
|
|
60
|
+
logger.debug(f"Subprocess stderr: {result.stderr[:500]}")
|
|
61
|
+
|
|
62
|
+
if result.returncode != 0:
|
|
63
|
+
error_message = _extract_error_message(result)
|
|
64
|
+
logger.error(
|
|
65
|
+
f"Handler runner failed (exit={result.returncode}): {error_message[:500]}")
|
|
66
|
+
raise RuntimeError(f"Handler runner failed: {error_message}")
|
|
67
|
+
|
|
68
|
+
if not result.stdout:
|
|
69
|
+
raise RuntimeError("Handler runner produced no output")
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
return json.loads(result.stdout)
|
|
73
|
+
except json.JSONDecodeError as exc:
|
|
74
|
+
raise RuntimeError(
|
|
75
|
+
f"Handler runner returned invalid JSON: {exc}") from exc
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def is_subprocess_language(language_str: str) -> bool:
|
|
79
|
+
"""
|
|
80
|
+
Determines if a given language string represents a subprocess-compatible language.
|
|
81
|
+
|
|
82
|
+
This method checks whether the specified language can be executed as a subprocess
|
|
83
|
+
(i.e., it is not equivalent to Python). It resolves the language using the
|
|
84
|
+
`Language.resolve` method and compares it to `Language.PYTHON`. If the language
|
|
85
|
+
is unresolvable or an exception occurs, it assumes the language is not valid for
|
|
86
|
+
subprocess execution.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
language_str (str): The language string to be validated.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
bool: True if the language is a subprocess-compatible language, False otherwise.
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
lang = Language.resolve(language_str)
|
|
96
|
+
return lang != Language.PYTHON
|
|
97
|
+
except Exception:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _extract_error_message(result) -> str:
|
|
102
|
+
"""Extract the most meaningful error message from a failed subprocess.
|
|
103
|
+
|
|
104
|
+
Priority order:
|
|
105
|
+
1. Structured JSON on stdout – {"status":"error","error":"..."}
|
|
106
|
+
2. Raw stderr text
|
|
107
|
+
3. Generic exit-code string
|
|
108
|
+
"""
|
|
109
|
+
if result.stdout:
|
|
110
|
+
try:
|
|
111
|
+
parsed = json.loads(result.stdout)
|
|
112
|
+
if parsed.get('status') == 'error':
|
|
113
|
+
return (parsed.get('error') or parsed.get(
|
|
114
|
+
'stack') or 'Unknown JS error')
|
|
115
|
+
except (json.JSONDecodeError, AttributeError):
|
|
116
|
+
pass
|
|
117
|
+
if result.stderr:
|
|
118
|
+
return result.stderr.strip()
|
|
119
|
+
return f"exit code {result.returncode}"
|