quapp-common 0.0.14.dev3__tar.gz → 0.0.14.dev5__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.
Files changed (107) hide show
  1. {quapp_common-0.0.14.dev3/quapp_common.egg-info → quapp_common-0.0.14.dev5}/PKG-INFO +59 -7
  2. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/README.md +55 -0
  3. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/pyproject.toml +13 -8
  4. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/component/backend/invocation.py +12 -4
  5. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/component/callback/update_job_metadata.py +11 -1
  6. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/component/circuit_adapter.py +79 -30
  7. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/response/job_response.py +11 -2
  8. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/util/response_utils.py +31 -9
  9. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5/quapp_common.egg-info}/PKG-INFO +59 -7
  10. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common.egg-info/SOURCES.txt +1 -0
  11. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common.egg-info/requires.txt +4 -6
  12. quapp_common-0.0.14.dev5/tests/test_circuit_adapter.py +174 -0
  13. quapp_common-0.0.14.dev5/tests/test_no_sdk_dependency.py +142 -0
  14. quapp_common-0.0.14.dev3/tests/test_circuit_adapter.py +0 -120
  15. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/LICENSE +0 -0
  16. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/__init__.py +0 -0
  17. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/async_tasks/__init__.py +0 -0
  18. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/async_tasks/async_invocation_task.py +0 -0
  19. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/async_tasks/async_task.py +0 -0
  20. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/async_tasks/export_circuit_task.py +0 -0
  21. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/async_tasks/post_processing_task.py +0 -0
  22. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/component/__init__.py +0 -0
  23. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/component/backend/__init__.py +0 -0
  24. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/component/backend/job_fetcher.py +0 -0
  25. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/component/backend/job_fetching.py +0 -0
  26. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/component/backend/job_heartbeat.py +0 -0
  27. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/component/backend/job_manager.py +0 -0
  28. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/component/bridge.py +0 -0
  29. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/component/callback/__init__.py +0 -0
  30. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/component/device/__init__.py +0 -0
  31. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/component/device/device_selection.py +0 -0
  32. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/component/dispatcher.py +0 -0
  33. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/component/result_serializer.py +0 -0
  34. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/config/__init__.py +0 -0
  35. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/config/log_context.py +0 -0
  36. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/config/logging_config.py +0 -0
  37. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/config/thread_config.py +0 -0
  38. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/__init__.py +0 -0
  39. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/async_task/__init__.py +0 -0
  40. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
  41. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
  42. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
  43. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/backend/__init__.py +0 -0
  44. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/backend/backend_information.py +0 -0
  45. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/callback/__init__.py +0 -0
  46. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/callback/callback_url.py +0 -0
  47. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/device/__init__.py +0 -0
  48. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/device/circuit_running_option.py +0 -0
  49. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/promise/__init__.py +0 -0
  50. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/promise/post_processing_promise.py +0 -0
  51. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/promise/promise.py +0 -0
  52. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/request/__init__.py +0 -0
  53. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/request/invocation_request.py +0 -0
  54. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/request/job_fetching_request.py +0 -0
  55. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/request/request.py +0 -0
  56. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/response/__init__.py +0 -0
  57. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/response/authentication.py +0 -0
  58. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/response/custom_header.py +0 -0
  59. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/data/response/error_report.py +0 -0
  60. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/enum/__init__.py +0 -0
  61. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/enum/base_enum.py +0 -0
  62. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/enum/error_category.py +0 -0
  63. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/enum/http_header.py +0 -0
  64. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/enum/invocation_step.py +0 -0
  65. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/enum/language.py +0 -0
  66. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/enum/media_type.py +0 -0
  67. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/enum/processing_unit.py +0 -0
  68. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/enum/provider_tag.py +0 -0
  69. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/enum/runtime_phase.py +0 -0
  70. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/enum/sdk.py +0 -0
  71. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/enum/status/__init__.py +0 -0
  72. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/enum/status/job_status.py +0 -0
  73. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/enum/status/status_code.py +0 -0
  74. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/enum/token_type.py +0 -0
  75. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/factory/__init__.py +0 -0
  76. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/factory/device_factory.py +0 -0
  77. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/factory/handler_factory.py +0 -0
  78. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/factory/provider_factory.py +0 -0
  79. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/handler/__init__.py +0 -0
  80. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/handler/handler.py +0 -0
  81. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/model/__init__.py +0 -0
  82. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/model/device/__init__.py +0 -0
  83. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/model/device/custom_device.py +0 -0
  84. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/model/device/device.py +0 -0
  85. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/model/invocation.py +0 -0
  86. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/model/provider/__init__.py +0 -0
  87. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/model/provider/provider.py +0 -0
  88. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/util/__init__.py +0 -0
  89. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/util/error_classifier.py +0 -0
  90. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/util/file_utils.py +0 -0
  91. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/util/http_utils.py +0 -0
  92. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/util/invocation_failure.py +0 -0
  93. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common/util/json_parser_utils.py +0 -0
  94. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common.egg-info/dependency_links.txt +0 -0
  95. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/quapp_common.egg-info/top_level.txt +0 -0
  96. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/setup.cfg +0 -0
  97. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/tests/test_dispatcher_runner_error.py +0 -0
  98. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/tests/test_error_classifier.py +0 -0
  99. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/tests/test_error_log_dedupe.py +0 -0
  100. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/tests/test_error_report.py +0 -0
  101. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/tests/test_invocation_failure.py +0 -0
  102. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/tests/test_job_heartbeat.py +0 -0
  103. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/tests/test_json_parser_utils.py +0 -0
  104. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/tests/test_language_dispatcher.py +0 -0
  105. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/tests/test_log_secret_leak.py +0 -0
  106. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/tests/test_logging_config.py +0 -0
  107. {quapp_common-0.0.14.dev3 → quapp_common-0.0.14.dev5}/tests/test_result_serializer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quapp-common
3
- Version: 0.0.14.dev3
3
+ Version: 0.0.14.dev5
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)
@@ -25,18 +25,15 @@ Requires-Dist: numpy
25
25
  Requires-Dist: matplotlib
26
26
  Requires-Dist: pylatexenc
27
27
  Requires-Dist: starlette
28
- Requires-Dist: qibo
29
- Requires-Dist: dimod
30
- Requires-Dist: qulacs
31
- Requires-Dist: qiskit
32
- Requires-Dist: pytket
33
- Requires-Dist: pennylane
34
28
  Provides-Extra: dev
35
29
  Requires-Dist: black; extra == "dev"
36
30
  Requires-Dist: bumpver; extra == "dev"
37
31
  Requires-Dist: isort; extra == "dev"
38
32
  Requires-Dist: pip-tools; extra == "dev"
39
33
  Requires-Dist: pytest; extra == "dev"
34
+ Provides-Extra: test
35
+ Requires-Dist: pytest; extra == "test"
36
+ Requires-Dist: qiskit; extra == "test"
40
37
  Dynamic: license-file
41
38
 
42
39
  # quapp-common
@@ -114,6 +111,61 @@ Notes:
114
111
 
115
112
  ## Recently Changes Highlights
116
113
 
114
+ ### v0.0.14.dev5 — Gỡ toàn bộ dependency SDK lượng tử
115
+
116
+ `quapp-common` không còn phụ thuộc SDK nào. SDK thuộc về lib con.
117
+
118
+ **Gỡ khỏi `dependencies`** — 6 package, chỉ được dùng trong `circuit_adapter.py`:
119
+ `qiskit`, `pennylane`, `qibo`, `qulacs`, `dimod`, `pytket`.
120
+
121
+ Còn lại: `loguru`, `requests`, `numpy`, `matplotlib`, `pylatexenc`, `starlette`
122
+ — đều được chính code của `quapp-common` dùng (`matplotlib`/`pylatexenc` cần cho
123
+ `CircuitExportTask.__convert()`, nơi gọi `circuit.draw(output='mpl')` rồi
124
+ `savefig()`).
125
+
126
+ **`circuit_adapter.py`: import SDK lazy.** Mỗi handler tự import SDK của nó ngay
127
+ trước khi dùng, qua helper `_require()`. Trước đây 7 SDK được import ở module
128
+ level, nên:
129
+
130
+ - `from quapp_common.component.bridge import SubprocessBridge` đòi có **đủ cả 6
131
+ SDK cùng lúc**, kể cả khi function chỉ chạy Qiskit. Điều này vô hiệu hoá thiết
132
+ kế lazy-import của `js-*/index.py` trong `quapp-sdk-templates` — comment trong
133
+ đó ghi rõ ý định *"so the server can start even if optional dependencies are
134
+ missing"*;
135
+ - buộc mọi function image tải cả 6 SDK, và cài chung chúng gây xung đột version
136
+ thật (`pytket`/`lark`, `pennylane-rigetti`/`pyquil` — xem
137
+ `revert/pennylane-to-0.0.4.dev6` trong `qapp-pennylane`).
138
+
139
+ `_require()` raise `ModuleNotFoundError` với thông điệp nêu tên package cần thêm
140
+ vào `requirements.txt`. Classifier map nó thành `DEPENDENCY_MISSING` nên user
141
+ nhận được hướng dẫn cụ thể thay vì traceback import.
142
+
143
+ **`tests/test_circuit_adapter.py`**: SDK được import trong từng test qua
144
+ `pytest.importorskip`. Trước đây file này import `qiskit` + `qibo` + `qulacs` ở
145
+ module level nên **cả file không collect được** nếu thiếu một SDK. Test
146
+ `test_qulacs_falls_back_to_string_on_parse_error` đổi target patch từ
147
+ `circuit_adapter.QiskitCircuit` (tên module-level đã bỏ) sang `_load_qasm_circuit`.
148
+
149
+ **`tests/test_no_sdk_dependency.py`** (mới) — khoá hồi quy bằng AST: quét từng
150
+ file, fail nếu có SDK nào được import ở module level; và đọc `pyproject.toml`,
151
+ fail nếu `dependencies` khai báo SDK.
152
+
153
+ Đã verify trong **venv sạch không có SDK nào**: 82/82 module import được, suite
154
+ 300 passed / 7 skipped. Với venv chỉ có `qiskit`: 302 passed / 5 skipped.
155
+
156
+ > **Lib con cần bổ sung khai báo.** Hai lib đang dựa vào `quapp-common` kéo hộ và
157
+ > sẽ vỡ đường OpenQASM sau bản này:
158
+ > - `qapp-pyquil` — thiếu `pytket` (cần cho `_openqasm_to_pyquil`);
159
+ > - `quapp-qulacs` — thiếu `qiskit` (cầu nối OpenQASM → Qulacs đi qua Qiskit).
160
+ >
161
+ > Ngoài ra `qapp-dwave-ocean` import `matplotlib` trực tiếp nhưng không khai báo;
162
+ > hiện vẫn chạy vì `quapp-common` còn giữ `matplotlib`, nên nên khai báo tường
163
+ > minh để không phụ thuộc may mắn.
164
+ >
165
+ > `pytket-braket` và `pytket-pyquil` chưa từng được khai báo ở đâu — code dùng
166
+ > `importlib.import_module()` cho chúng từ trước. Cần thêm vào `qapp-braket` và
167
+ > `qapp-pyquil`.
168
+
117
169
  ### v0.0.14.dev2 — Fix: log lỗi bị nhân đôi
118
170
 
119
171
  - **Fix**: mỗi lần lỗi phát **hai** dòng ERROR cùng `event=job_failed`, mỗi dòng
@@ -73,6 +73,61 @@ Notes:
73
73
 
74
74
  ## Recently Changes Highlights
75
75
 
76
+ ### v0.0.14.dev5 — Gỡ toàn bộ dependency SDK lượng tử
77
+
78
+ `quapp-common` không còn phụ thuộc SDK nào. SDK thuộc về lib con.
79
+
80
+ **Gỡ khỏi `dependencies`** — 6 package, chỉ được dùng trong `circuit_adapter.py`:
81
+ `qiskit`, `pennylane`, `qibo`, `qulacs`, `dimod`, `pytket`.
82
+
83
+ Còn lại: `loguru`, `requests`, `numpy`, `matplotlib`, `pylatexenc`, `starlette`
84
+ — đều được chính code của `quapp-common` dùng (`matplotlib`/`pylatexenc` cần cho
85
+ `CircuitExportTask.__convert()`, nơi gọi `circuit.draw(output='mpl')` rồi
86
+ `savefig()`).
87
+
88
+ **`circuit_adapter.py`: import SDK lazy.** Mỗi handler tự import SDK của nó ngay
89
+ trước khi dùng, qua helper `_require()`. Trước đây 7 SDK được import ở module
90
+ level, nên:
91
+
92
+ - `from quapp_common.component.bridge import SubprocessBridge` đòi có **đủ cả 6
93
+ SDK cùng lúc**, kể cả khi function chỉ chạy Qiskit. Điều này vô hiệu hoá thiết
94
+ kế lazy-import của `js-*/index.py` trong `quapp-sdk-templates` — comment trong
95
+ đó ghi rõ ý định *"so the server can start even if optional dependencies are
96
+ missing"*;
97
+ - buộc mọi function image tải cả 6 SDK, và cài chung chúng gây xung đột version
98
+ thật (`pytket`/`lark`, `pennylane-rigetti`/`pyquil` — xem
99
+ `revert/pennylane-to-0.0.4.dev6` trong `qapp-pennylane`).
100
+
101
+ `_require()` raise `ModuleNotFoundError` với thông điệp nêu tên package cần thêm
102
+ vào `requirements.txt`. Classifier map nó thành `DEPENDENCY_MISSING` nên user
103
+ nhận được hướng dẫn cụ thể thay vì traceback import.
104
+
105
+ **`tests/test_circuit_adapter.py`**: SDK được import trong từng test qua
106
+ `pytest.importorskip`. Trước đây file này import `qiskit` + `qibo` + `qulacs` ở
107
+ module level nên **cả file không collect được** nếu thiếu một SDK. Test
108
+ `test_qulacs_falls_back_to_string_on_parse_error` đổi target patch từ
109
+ `circuit_adapter.QiskitCircuit` (tên module-level đã bỏ) sang `_load_qasm_circuit`.
110
+
111
+ **`tests/test_no_sdk_dependency.py`** (mới) — khoá hồi quy bằng AST: quét từng
112
+ file, fail nếu có SDK nào được import ở module level; và đọc `pyproject.toml`,
113
+ fail nếu `dependencies` khai báo SDK.
114
+
115
+ Đã verify trong **venv sạch không có SDK nào**: 82/82 module import được, suite
116
+ 300 passed / 7 skipped. Với venv chỉ có `qiskit`: 302 passed / 5 skipped.
117
+
118
+ > **Lib con cần bổ sung khai báo.** Hai lib đang dựa vào `quapp-common` kéo hộ và
119
+ > sẽ vỡ đường OpenQASM sau bản này:
120
+ > - `qapp-pyquil` — thiếu `pytket` (cần cho `_openqasm_to_pyquil`);
121
+ > - `quapp-qulacs` — thiếu `qiskit` (cầu nối OpenQASM → Qulacs đi qua Qiskit).
122
+ >
123
+ > Ngoài ra `qapp-dwave-ocean` import `matplotlib` trực tiếp nhưng không khai báo;
124
+ > hiện vẫn chạy vì `quapp-common` còn giữ `matplotlib`, nên nên khai báo tường
125
+ > minh để không phụ thuộc may mắn.
126
+ >
127
+ > `pytket-braket` và `pytket-pyquil` chưa từng được khai báo ở đâu — code dùng
128
+ > `importlib.import_module()` cho chúng từ trước. Cần thêm vào `qapp-braket` và
129
+ > `qapp-pyquil`.
130
+
76
131
  ### v0.0.14.dev2 — Fix: log lỗi bị nhân đôi
77
132
 
78
133
  - **Fix**: mỗi lần lỗi phát **hai** dòng ERROR cùng `event=job_failed`, mỗi dòng
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quapp-common"
7
- version = "0.0.14.dev3"
7
+ version = "0.0.14.dev5"
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" }]
@@ -15,24 +15,29 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3",
16
16
  ]
17
17
  keywords = ["quapp", "quapp-common", "quantum"]
18
+ # KHÔNG khai báo SDK lượng tử ở đây. Mỗi lib con (quapp-qiskit, qapp-pennylane,
19
+ # qapp-braket, ...) tự khai báo SDK của nó, và một function image chỉ cài đúng
20
+ # SDK nó dùng. circuit_adapter.py import SDK lazy nên quapp-common không cần
21
+ # chúng để import được.
22
+ #
23
+ # Lý do: cài chung qiskit + pennylane + qibo + qulacs + dimod + pytket gây xung
24
+ # đột version thật (pytket/lark, pennylane-rigetti/pyquil), và bắt mọi function
25
+ # tải cả 6 SDK dù chỉ dùng một.
18
26
  dependencies = [
19
27
  "loguru",
20
28
  "requests",
21
29
  "numpy",
22
30
  "matplotlib",
23
31
  "pylatexenc",
24
- "starlette",
25
- "qibo",
26
- "dimod",
27
- "qulacs",
28
- "qiskit",
29
- "pytket",
30
- "pennylane"
32
+ "starlette"
31
33
  ]
32
34
  requires-python = ">=3.7"
33
35
 
34
36
  [project.optional-dependencies]
35
37
  dev = ["black", "bumpver", "isort", "pip-tools", "pytest"]
38
+ # SDK chỉ để chạy test của chính quapp-common (tests/test_circuit_adapter.py
39
+ # cần qiskit). KHÔNG phải runtime dependency.
40
+ test = ["pytest", "qiskit"]
36
41
 
37
42
  [project.urls]
38
43
  Homepage = "https://citynow.asia/"
@@ -97,8 +97,10 @@ class Invocation(ABC):
97
97
  job_response = build_error_job_response(exception,
98
98
  message='Error when prepare backend data',
99
99
  stage='while preparing the backend',
100
- phase=RuntimePhase.DEVICE_SELECTION)
101
- job_response.authentication = self.authentication
100
+ phase=RuntimePhase.DEVICE_SELECTION,
101
+ authentication=self.authentication,
102
+ project_header=self.project_header,
103
+ workspace_header=self.workspace_header)
102
104
  update_job_metadata(job_response, self.callback_dict.get(
103
105
  InvocationStep.PREPARATION).on_error)
104
106
 
@@ -142,7 +144,10 @@ class Invocation(ABC):
142
144
  job_response = build_error_job_response(exception,
143
145
  message='Error when create provider or device',
144
146
  stage='while running the circuit on the device',
145
- phase=RuntimePhase.PROVIDER_INVOCATION)
147
+ phase=RuntimePhase.PROVIDER_INVOCATION,
148
+ authentication=self.authentication,
149
+ project_header=self.project_header,
150
+ workspace_header=self.workspace_header)
146
151
  update_job_metadata(job_response, self.callback_dict.get(
147
152
  InvocationStep.EXECUTION).on_error, )
148
153
 
@@ -194,7 +199,10 @@ class Invocation(ABC):
194
199
  job_response = build_error_job_response(exception, job_response,
195
200
  message='Error when prepare circuit',
196
201
  stage='while preparing the circuit',
197
- phase=RuntimePhase.PREPROCESSING)
202
+ phase=RuntimePhase.PREPROCESSING,
203
+ authentication=self.authentication,
204
+ project_header=self.project_header,
205
+ workspace_header=self.workspace_header)
198
206
 
199
207
  update_job_metadata(job_response, self.callback_dict.get(
200
208
  InvocationStep.PREPARATION).on_error)
@@ -17,7 +17,12 @@ def update_job_metadata(job_response: JobResponse, callback_url: str):
17
17
  def _log_http_result(tag: str, resp):
18
18
  logger.info(
19
19
  f"Request to {tag} completed with status code: {resp.status_code}")
20
- if not resp.ok:
20
+ if resp.status_code in (401, 403):
21
+ logger.error(
22
+ f"Request to {tag} was rejected with status code {resp.status_code}. "
23
+ f"The job update is lost, so the platform will not see this state change. "
24
+ f"Check that the response carries the user token and the project header.")
25
+ elif not resp.ok:
21
26
  logger.warning(
22
27
  f"Unexpected status code received from {tag}: {resp.status_code}, response content: {resp.content}")
23
28
 
@@ -60,6 +65,11 @@ def update_job_metadata(job_response: JobResponse, callback_url: str):
60
65
  logger.info(
61
66
  f'Calling backend to update job metadata at URL: {callback_url}')
62
67
 
68
+ if not getattr(job_response, 'user_token', None):
69
+ logger.error(
70
+ 'Job update is being sent without a user token, the backend will reject it. '
71
+ 'The response was built without the invocation credentials.')
72
+
63
73
  request_body = generate_response(job_response)
64
74
 
65
75
  try:
@@ -2,38 +2,73 @@
2
2
  # circuit_adapter.py
3
3
  # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
4
 
5
+ """Chuyển circuit từ định dạng trung gian (OpenQASM / QUBO / JSON) sang object
6
+ native của từng SDK.
7
+
8
+ Mọi SDK được import LAZY, ngay bên trong handler cần nó. Lý do:
9
+
10
+ ``quapp-common`` không được phụ thuộc SDK nào. Mỗi lib con
11
+ (``quapp-qiskit``, ``qapp-pennylane``, ...) tự khai báo SDK của nó, và một
12
+ function image chỉ cài đúng SDK nó dùng. Nếu file này import ở module level thì:
13
+
14
+ - ``import quapp_common.component.bridge`` đòi có ĐỦ 6 SDK cùng lúc, kể cả khi
15
+ function chỉ chạy Qiskit -- điều này vô hiệu hoá thiết kế lazy-import của
16
+ ``js-*/index.py`` trong quapp-sdk-templates (comment trong đó ghi rõ ý định
17
+ "so the server can start even if optional dependencies are missing");
18
+ - cài chung 6 SDK gây xung đột version thật, đã xảy ra với ``pytket``/``lark``
19
+ và ``pennylane-rigetti``/``pyquil``.
20
+
21
+ Khi SDK thiếu, handler raise ``ModuleNotFoundError`` -- classifier map thành
22
+ ``DEPENDENCY_MISSING`` và user nhận được hướng dẫn thêm package vào
23
+ requirements.txt.
24
+ """
25
+
5
26
  import importlib
6
27
  import re
7
28
  from typing import Any, Dict
8
29
 
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
30
  from ..config.logging_config import logger
24
31
  from ..enum.sdk import Sdk
25
32
 
26
33
  logger = logger.bind(context=__name__)
27
34
 
28
35
 
29
- def _load_qasm_circuit(qasm_str: str) -> QiskitCircuit:
30
- """Parse OpenQASM 2.0 string into a Qiskit QuantumCircuit.
36
+ def _require(module_name: str, package_hint: str = None):
37
+ """Import module của SDK, báo lỗi rõ ràng nếu thiếu.
38
+
39
+ @param module_name: tên module cần import, ví dụ 'qiskit.qasm2'.
40
+ @param package_hint: tên package trên PyPI để nhắc user, nếu khác module.
41
+ @raise ModuleNotFoundError: giữ đúng kiểu này để classifier phân loại thành
42
+ DEPENDENCY_MISSING.
43
+ """
44
+ try:
45
+ return importlib.import_module(module_name)
46
+ except ImportError as exception:
47
+ package = package_hint or module_name.split('.')[0]
48
+ raise ModuleNotFoundError(
49
+ f"SDK '{package}' chưa được cài trong function image nhưng "
50
+ f"circuit cần nó. Thêm '{package}' vào requirements.txt của "
51
+ f"function rồi deploy lại.") from exception
52
+
53
+
54
+ def _qiskit_quantum_circuit():
55
+ return _require('qiskit').QuantumCircuit
56
+
31
57
 
32
- Compatible with Qiskit >= 1.0 (qiskit.qasm2.loads) and < 1.0 (from_qasm_str).
58
+ def _load_qasm_circuit(qasm_str: str):
59
+ """Parse OpenQASM 2.0 string thành Qiskit QuantumCircuit.
60
+
61
+ Tương thích Qiskit >= 1.0 (``qiskit.qasm2.loads``) và < 1.0
62
+ (``QuantumCircuit.from_qasm_str``).
33
63
  """
34
- if _qasm2 is not None:
35
- return _qasm2.loads(qasm_str)
36
- return QiskitCircuit.from_qasm_str(qasm_str)
64
+ try:
65
+ qasm2 = importlib.import_module('qiskit.qasm2')
66
+ except ImportError:
67
+ qasm2 = None
68
+
69
+ if qasm2 is not None:
70
+ return qasm2.loads(qasm_str)
71
+ return _qiskit_quantum_circuit().from_qasm_str(qasm_str)
37
72
 
38
73
 
39
74
  def adapt(sdk: Sdk, result: dict):
@@ -91,7 +126,8 @@ def _adapt_qubo(sdk, qubo_raw: Dict[str, Any]):
91
126
  qubo_dict[(key, key)] = val
92
127
 
93
128
  logger.debug(f"QUBO parsed: {len(qubo_dict)} entries -> BinaryQuadraticModel")
94
- return BinaryQuadraticModel.from_qubo(qubo_dict)
129
+ binary_quadratic_model = _require('dimod').BinaryQuadraticModel
130
+ return binary_quadratic_model.from_qubo(qubo_dict)
95
131
 
96
132
 
97
133
  def _count_qasm_qubits(qasm_str):
@@ -101,7 +137,9 @@ def _count_qasm_qubits(qasm_str):
101
137
 
102
138
  def _openqasm_to_braket(qasm_str):
103
139
  logger.debug("Converting OpenQASM via pytket -> tk_to_braket")
104
- tk_to_braket = importlib.import_module("pytket.extensions.braket").tk_to_braket
140
+ circuit_from_qasm_str = _require('pytket.qasm', 'pytket').circuit_from_qasm_str
141
+ tk_to_braket = _require('pytket.extensions.braket',
142
+ 'pytket-braket').tk_to_braket
105
143
  result = tk_to_braket(circuit_from_qasm_str(qasm_str))
106
144
  # tk_to_braket returns (Circuit, n_shots) in pytket-braket >= 0.35
107
145
  is_tuple = isinstance(result, tuple)
@@ -122,10 +160,13 @@ def _openqasm_to_cuda_quantum(qasm_str):
122
160
 
123
161
  def _openqasm_to_pennylane(qasm_str):
124
162
  logger.debug("Converting OpenQASM via PennyLane qml.from_qasm wrapper")
163
+ qml = _require('pennylane')
164
+ measurements = _require('pennylane.measurements', 'pennylane')
165
+
125
166
  for _compat_name in ("Expectation", "Variance", "Sample", "Probability",
126
167
  "State", "MidMeasure"):
127
- if not hasattr(_plm, _compat_name):
128
- setattr(_plm, _compat_name, _compat_name)
168
+ if not hasattr(measurements, _compat_name):
169
+ setattr(measurements, _compat_name, _compat_name)
129
170
 
130
171
  num_wires = _count_qasm_qubits(qasm_str)
131
172
  logger.debug(f"PennyLane: detected {num_wires} qubits; wrapping with qml.probs over all wires")
@@ -150,13 +191,15 @@ def _openqasm_to_pennylane(qasm_str):
150
191
 
151
192
  def _openqasm_to_pyquil(qasm_str):
152
193
  logger.debug("Converting OpenQASM via pytket -> tk_to_pyquil")
153
- tk_to_pyquil = importlib.import_module("pytket.extensions.pyquil").tk_to_pyquil
194
+ circuit_from_qasm_str = _require('pytket.qasm', 'pytket').circuit_from_qasm_str
195
+ tk_to_pyquil = _require('pytket.extensions.pyquil',
196
+ 'pytket-pyquil').tk_to_pyquil
154
197
  return tk_to_pyquil(circuit_from_qasm_str(qasm_str))
155
198
 
156
199
 
157
200
  def _openqasm_to_qibo(qasm_str):
158
201
  logger.debug("Converting OpenQASM via Qibo Circuit.from_qasm")
159
- return Circuit.from_qasm(qasm_str)
202
+ return _require('qibo.models', 'qibo').Circuit.from_qasm(qasm_str)
160
203
 
161
204
 
162
205
  def _openqasm_to_qiskit(qasm_str):
@@ -184,6 +227,9 @@ def _qiskit_to_pennylane(qasm_str, num_wires):
184
227
  Fallback for environments where qml.from_qasm is unavailable because
185
228
  pennylane-qiskit is not installed (PennyLane < 0.36).
186
229
  """
230
+ qml = _require('pennylane')
231
+ numpy = _require('numpy')
232
+
187
233
  qiskit_circ = _load_qasm_circuit(qasm_str)
188
234
  qubit_map = {bit: idx for idx, bit in enumerate(qiskit_circ.qubits)}
189
235
  logger.debug(
@@ -205,9 +251,9 @@ def _qiskit_to_pennylane(qasm_str, num_wires):
205
251
  'y': lambda q, p: qml.PauliY(wires=q[0]),
206
252
  'z': lambda q, p: qml.PauliZ(wires=q[0]),
207
253
  's': lambda q, p: qml.S(wires=q[0]),
208
- 'sdg': lambda q, p: qml.PhaseShift(-np.pi / 2, wires=q[0]),
254
+ 'sdg': lambda q, p: qml.PhaseShift(-numpy.pi / 2, wires=q[0]),
209
255
  't': lambda q, p: qml.T(wires=q[0]),
210
- 'tdg': lambda q, p: qml.PhaseShift(-np.pi / 4, wires=q[0]),
256
+ 'tdg': lambda q, p: qml.PhaseShift(-numpy.pi / 4, wires=q[0]),
211
257
  'cx': lambda q, p: qml.CNOT(wires=q),
212
258
  'cz': lambda q, p: qml.CZ(wires=q),
213
259
  'swap': lambda q, p: qml.SWAP(wires=q),
@@ -244,9 +290,12 @@ def _qiskit_to_qulacs(qiskit_circ):
244
290
  unrecognized gates fall back to DenseMatrix (unitary passthrough).
245
291
  Measurement, barrier, reset, and identity instructions are skipped.
246
292
  """
293
+ qulacs_quantum_circuit = _require('qulacs').QuantumCircuit
294
+ dense_matrix_gate = _require('qulacs.gate', 'qulacs').DenseMatrix
295
+
247
296
  n = qiskit_circ.num_qubits
248
297
  logger.debug(f"_qiskit_to_qulacs: num_qubits={n} num_instructions={len(qiskit_circ.data)}")
249
- qulacs_circ = QulacsCircuit(n)
298
+ qulacs_circ = qulacs_quantum_circuit(n)
250
299
  qubit_map = {bit: idx for idx, bit in enumerate(qiskit_circ.qubits)}
251
300
 
252
301
  _SINGLE = {
@@ -280,7 +329,7 @@ def _qiskit_to_qulacs(qiskit_circ):
280
329
  # ambiguity; multi-qubit gates are passed with qulacs'
281
330
  # index order matching Qiskit's instruction qubit order.
282
331
  logger.debug(f"Gate '{name}' not in named map; converting via DenseMatrix on qubits={qubits}")
283
- qulacs_circ.add_gate(DenseMatrixGate(qubits, gate.to_matrix()))
332
+ qulacs_circ.add_gate(dense_matrix_gate(qubits, gate.to_matrix()))
284
333
 
285
334
  logger.debug(f"_qiskit_to_qulacs: conversion complete")
286
335
  return qulacs_circ
@@ -23,9 +23,18 @@ class JobResponse(object):
23
23
  self.job_result = job_result
24
24
  self.content_type = content_type
25
25
  self.job_histogram = job_histogram
26
- self.user_identity = getattr(authentication, 'user_identity', None)
27
- self.user_token = getattr(authentication, 'user_token', None)
26
+ self.authentication = authentication
28
27
  self.execution_time = execution_time
29
28
  self.status_code = status_code
30
29
  self.project_header = project_header
31
30
  self.workspace_header = workspace_header
31
+
32
+ @property
33
+ def authentication(self) -> Authentication:
34
+ return self._authentication
35
+
36
+ @authentication.setter
37
+ def authentication(self, authentication: Authentication) -> None:
38
+ self._authentication = authentication
39
+ self.user_identity = getattr(authentication, 'user_identity', None)
40
+ self.user_token = getattr(authentication, 'user_token', None)
@@ -11,6 +11,8 @@ from .http_utils import CUSTOM_HEADER_VALUE
11
11
  from ..component.backend.job_fetcher import JobFetcher
12
12
  from ..config.logging_config import job_logger, redact
13
13
  from ..data.promise.post_processing_promise import PostProcessingPromise
14
+ from ..data.response.authentication import Authentication
15
+ from ..data.response.custom_header import CustomHeader
14
16
  from ..data.response.job_response import JobResponse
15
17
  from ..enum.runtime_phase import en_phrase_of
16
18
  from ..enum.status.job_status import JobStatus
@@ -195,7 +197,10 @@ def build_error_job_response(exception: Exception,
195
197
  message: str = None,
196
198
  stage: str = None,
197
199
  phase=None,
198
- context: dict = None) -> JobResponse:
200
+ context: dict = None,
201
+ authentication: Authentication = None,
202
+ project_header: CustomHeader = None,
203
+ workspace_header: CustomHeader = None) -> JobResponse:
199
204
  """
200
205
  Builds and returns a JobResponse object populated with error-related information.
201
206
 
@@ -232,14 +237,18 @@ def build_error_job_response(exception: Exception,
232
237
  """
233
238
  if job_response is None:
234
239
  job_response = JobResponse()
235
- job_response.provider_job_id = __extract_value(PROVIDER_JOB_ID, fetcher,
236
- post_promise)
237
- job_response.authentication = __extract_authentication(fetcher,
238
- post_promise)
239
- job_response.project_header = __extract_value(PROJECT_HEADER, fetcher,
240
- post_promise)
241
- job_response.tenant_header = __extract_value(WORKSPACE_HEADER, fetcher,
242
- post_promise)
240
+
241
+ __apply_if_present(job_response, 'provider_job_id',
242
+ __extract_value(PROVIDER_JOB_ID, fetcher, post_promise))
243
+ __apply_if_present(job_response, 'authentication',
244
+ authentication
245
+ or __extract_authentication(fetcher, post_promise))
246
+ __apply_if_present(job_response, 'project_header',
247
+ project_header
248
+ or __extract_value(PROJECT_HEADER, fetcher, post_promise))
249
+ __apply_if_present(job_response, 'workspace_header',
250
+ workspace_header
251
+ or __extract_value(WORKSPACE_HEADER, fetcher, post_promise))
243
252
  job_response.status_code = StatusCode.ERROR
244
253
  job_response.job_status = JobStatus.ERROR.value
245
254
 
@@ -281,6 +290,19 @@ def build_error_job_response(exception: Exception,
281
290
  return job_response
282
291
 
283
292
 
293
+
294
+ def __apply_if_present(job_response: JobResponse, property_name: str, value):
295
+ """Only overwrite when a value is actually available.
296
+
297
+ The error path is often called without a fetcher or a post promise, so the
298
+ extracted value is None. Overwriting blindly used to wipe the credentials
299
+ already carried by the response, which made the resulting event reach the
300
+ backend without a token and get rejected with 401.
301
+ """
302
+ if value is not None:
303
+ setattr(job_response, property_name, value)
304
+
305
+
284
306
  def __extract_authentication(fetcher: JobFetcher,
285
307
  post_promise: PostProcessingPromise):
286
308
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quapp-common
3
- Version: 0.0.14.dev3
3
+ Version: 0.0.14.dev5
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)
@@ -25,18 +25,15 @@ Requires-Dist: numpy
25
25
  Requires-Dist: matplotlib
26
26
  Requires-Dist: pylatexenc
27
27
  Requires-Dist: starlette
28
- Requires-Dist: qibo
29
- Requires-Dist: dimod
30
- Requires-Dist: qulacs
31
- Requires-Dist: qiskit
32
- Requires-Dist: pytket
33
- Requires-Dist: pennylane
34
28
  Provides-Extra: dev
35
29
  Requires-Dist: black; extra == "dev"
36
30
  Requires-Dist: bumpver; extra == "dev"
37
31
  Requires-Dist: isort; extra == "dev"
38
32
  Requires-Dist: pip-tools; extra == "dev"
39
33
  Requires-Dist: pytest; extra == "dev"
34
+ Provides-Extra: test
35
+ Requires-Dist: pytest; extra == "test"
36
+ Requires-Dist: qiskit; extra == "test"
40
37
  Dynamic: license-file
41
38
 
42
39
  # quapp-common
@@ -114,6 +111,61 @@ Notes:
114
111
 
115
112
  ## Recently Changes Highlights
116
113
 
114
+ ### v0.0.14.dev5 — Gỡ toàn bộ dependency SDK lượng tử
115
+
116
+ `quapp-common` không còn phụ thuộc SDK nào. SDK thuộc về lib con.
117
+
118
+ **Gỡ khỏi `dependencies`** — 6 package, chỉ được dùng trong `circuit_adapter.py`:
119
+ `qiskit`, `pennylane`, `qibo`, `qulacs`, `dimod`, `pytket`.
120
+
121
+ Còn lại: `loguru`, `requests`, `numpy`, `matplotlib`, `pylatexenc`, `starlette`
122
+ — đều được chính code của `quapp-common` dùng (`matplotlib`/`pylatexenc` cần cho
123
+ `CircuitExportTask.__convert()`, nơi gọi `circuit.draw(output='mpl')` rồi
124
+ `savefig()`).
125
+
126
+ **`circuit_adapter.py`: import SDK lazy.** Mỗi handler tự import SDK của nó ngay
127
+ trước khi dùng, qua helper `_require()`. Trước đây 7 SDK được import ở module
128
+ level, nên:
129
+
130
+ - `from quapp_common.component.bridge import SubprocessBridge` đòi có **đủ cả 6
131
+ SDK cùng lúc**, kể cả khi function chỉ chạy Qiskit. Điều này vô hiệu hoá thiết
132
+ kế lazy-import của `js-*/index.py` trong `quapp-sdk-templates` — comment trong
133
+ đó ghi rõ ý định *"so the server can start even if optional dependencies are
134
+ missing"*;
135
+ - buộc mọi function image tải cả 6 SDK, và cài chung chúng gây xung đột version
136
+ thật (`pytket`/`lark`, `pennylane-rigetti`/`pyquil` — xem
137
+ `revert/pennylane-to-0.0.4.dev6` trong `qapp-pennylane`).
138
+
139
+ `_require()` raise `ModuleNotFoundError` với thông điệp nêu tên package cần thêm
140
+ vào `requirements.txt`. Classifier map nó thành `DEPENDENCY_MISSING` nên user
141
+ nhận được hướng dẫn cụ thể thay vì traceback import.
142
+
143
+ **`tests/test_circuit_adapter.py`**: SDK được import trong từng test qua
144
+ `pytest.importorskip`. Trước đây file này import `qiskit` + `qibo` + `qulacs` ở
145
+ module level nên **cả file không collect được** nếu thiếu một SDK. Test
146
+ `test_qulacs_falls_back_to_string_on_parse_error` đổi target patch từ
147
+ `circuit_adapter.QiskitCircuit` (tên module-level đã bỏ) sang `_load_qasm_circuit`.
148
+
149
+ **`tests/test_no_sdk_dependency.py`** (mới) — khoá hồi quy bằng AST: quét từng
150
+ file, fail nếu có SDK nào được import ở module level; và đọc `pyproject.toml`,
151
+ fail nếu `dependencies` khai báo SDK.
152
+
153
+ Đã verify trong **venv sạch không có SDK nào**: 82/82 module import được, suite
154
+ 300 passed / 7 skipped. Với venv chỉ có `qiskit`: 302 passed / 5 skipped.
155
+
156
+ > **Lib con cần bổ sung khai báo.** Hai lib đang dựa vào `quapp-common` kéo hộ và
157
+ > sẽ vỡ đường OpenQASM sau bản này:
158
+ > - `qapp-pyquil` — thiếu `pytket` (cần cho `_openqasm_to_pyquil`);
159
+ > - `quapp-qulacs` — thiếu `qiskit` (cầu nối OpenQASM → Qulacs đi qua Qiskit).
160
+ >
161
+ > Ngoài ra `qapp-dwave-ocean` import `matplotlib` trực tiếp nhưng không khai báo;
162
+ > hiện vẫn chạy vì `quapp-common` còn giữ `matplotlib`, nên nên khai báo tường
163
+ > minh để không phụ thuộc may mắn.
164
+ >
165
+ > `pytket-braket` và `pytket-pyquil` chưa từng được khai báo ở đâu — code dùng
166
+ > `importlib.import_module()` cho chúng từ trước. Cần thêm vào `qapp-braket` và
167
+ > `qapp-pyquil`.
168
+
117
169
  ### v0.0.14.dev2 — Fix: log lỗi bị nhân đôi
118
170
 
119
171
  - **Fix**: mỗi lần lỗi phát **hai** dòng ERROR cùng `event=job_failed`, mỗi dòng
@@ -100,4 +100,5 @@ tests/test_json_parser_utils.py
100
100
  tests/test_language_dispatcher.py
101
101
  tests/test_log_secret_leak.py
102
102
  tests/test_logging_config.py
103
+ tests/test_no_sdk_dependency.py
103
104
  tests/test_result_serializer.py
@@ -4,12 +4,6 @@ numpy
4
4
  matplotlib
5
5
  pylatexenc
6
6
  starlette
7
- qibo
8
- dimod
9
- qulacs
10
- qiskit
11
- pytket
12
- pennylane
13
7
 
14
8
  [dev]
15
9
  black
@@ -17,3 +11,7 @@ bumpver
17
11
  isort
18
12
  pip-tools
19
13
  pytest
14
+
15
+ [test]
16
+ pytest
17
+ qiskit