quapp-common 0.0.13.dev7__tar.gz → 0.0.13.dev9__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.13.dev7/quapp_common.egg-info → quapp_common-0.0.13.dev9}/PKG-INFO +73 -1
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/README.md +72 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/pyproject.toml +1 -1
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/async_tasks/async_invocation_task.py +11 -7
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/async_tasks/post_processing_task.py +11 -4
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/backend/job_fetcher.py +9 -5
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/backend/job_fetching.py +17 -12
- quapp_common-0.0.13.dev9/quapp_common/component/backend/job_heartbeat.py +83 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/backend/job_manager.py +6 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/bridge.py +18 -20
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/device/device_selection.py +15 -5
- quapp_common-0.0.13.dev9/quapp_common/component/dispatcher.py +272 -0
- quapp_common-0.0.13.dev9/quapp_common/config/log_context.py +102 -0
- quapp_common-0.0.13.dev9/quapp_common/config/logging_config.py +276 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/handler/handler.py +6 -1
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/model/device/custom_device.py +4 -2
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/model/device/device.py +16 -8
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/util/response_utils.py +58 -17
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9/quapp_common.egg-info}/PKG-INFO +73 -1
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common.egg-info/SOURCES.txt +5 -0
- quapp_common-0.0.13.dev9/tests/test_dispatcher_runner_error.py +196 -0
- quapp_common-0.0.13.dev9/tests/test_job_heartbeat.py +115 -0
- quapp_common-0.0.13.dev9/tests/test_logging_config.py +262 -0
- quapp_common-0.0.13.dev7/quapp_common/component/dispatcher.py +0 -123
- quapp_common-0.0.13.dev7/quapp_common/config/logging_config.py +0 -33
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/LICENSE +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/async_tasks/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/async_tasks/async_task.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/async_tasks/export_circuit_task.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/backend/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/backend/invocation.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/callback/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/callback/update_job_metadata.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/circuit_adapter.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/device/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/result_serializer.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/config/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/config/thread_config.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/async_task/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/backend/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/backend/backend_information.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/callback/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/callback/callback_url.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/device/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/device/circuit_running_option.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/promise/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/promise/post_processing_promise.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/promise/promise.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/request/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/request/invocation_request.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/request/job_fetching_request.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/request/request.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/response/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/response/authentication.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/response/custom_header.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/data/response/job_response.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/base_enum.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/http_header.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/invocation_step.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/language.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/media_type.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/processing_unit.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/provider_tag.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/sdk.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/status/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/status/job_status.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/status/status_code.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/enum/token_type.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/factory/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/factory/device_factory.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/factory/handler_factory.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/factory/provider_factory.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/handler/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/model/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/model/device/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/model/invocation.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/model/provider/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/model/provider/provider.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/util/__init__.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/util/file_utils.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/util/http_utils.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/util/json_parser_utils.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common.egg-info/dependency_links.txt +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common.egg-info/requires.txt +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common.egg-info/top_level.txt +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/setup.cfg +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/tests/test_circuit_adapter.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/tests/test_json_parser_utils.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/tests/test_language_dispatcher.py +0 -0
- {quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/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.13.
|
|
3
|
+
Version: 0.0.13.dev9
|
|
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)
|
|
@@ -114,6 +114,78 @@ Notes:
|
|
|
114
114
|
|
|
115
115
|
## Recently Changes Highlights
|
|
116
116
|
|
|
117
|
+
### v0.0.14.dev1 — Error logging & observability (Phase 0)
|
|
118
|
+
|
|
119
|
+
Mục tiêu: Function Log và Job Log phải chỉ ra được nguyên nhân lỗi. Toàn bộ thay
|
|
120
|
+
đổi trong bản này là **additive hoặc bug fix** — không đổi contract callback,
|
|
121
|
+
không đổi schema `job_result`, không cần frontend/backend sửa gì.
|
|
122
|
+
|
|
123
|
+
**Logging (`config/`)**
|
|
124
|
+
- Thêm `config/log_context.py`: `bind_job()` / `enrich_job_context()` đặt
|
|
125
|
+
`job_id`, `trace_id`, `sdk`, `provider`, `device` vào `contextvars`. Mọi log
|
|
126
|
+
sau đó tự có đủ field, kể cả log của `Provider` / `DeviceSelection` /
|
|
127
|
+
`*Factory` — những chỗ trước đây rơi vào context `"QuappLibs"` nên không gắn
|
|
128
|
+
được vào job nào.
|
|
129
|
+
- `configure_logging()` **idempotent**. Trước đây `AsyncInvocationTask.do()` gọi
|
|
130
|
+
`logger.add(sink=sys.stderr, ...)` cho mỗi request, nên sau N job có N+1 sink
|
|
131
|
+
và mỗi dòng log in N+1 lần.
|
|
132
|
+
- Thêm `InterceptHandler` + `install_stdlib_intercept()`: đẩy mọi record của
|
|
133
|
+
stdlib `logging` sang loguru. Cần thiết vì 24/34 template trong
|
|
134
|
+
`quapp-sdk-templates` dùng `logging.getLogger('index')` mà không cấu hình
|
|
135
|
+
handler — `debug`/`info` của chúng bị drop im lặng và `exception` in ra không
|
|
136
|
+
có prefix `[ConsoleJobLog]` lẫn `job_id`. Log của uvicorn/qiskit/botocore cũng
|
|
137
|
+
về cùng một format.
|
|
138
|
+
- Thêm JSON sink cho structured logging, bật bằng `QUAPP_LOG_FORMAT=json|both`
|
|
139
|
+
(default `text`, giữ nguyên hành vi cũ). Field: `jobId`, `traceId`, `phase`,
|
|
140
|
+
`category`, `errorCode`, `sdk`, `provider`, `device`, `event`, `durationMs`.
|
|
141
|
+
- Thêm redaction filter: mask `token` / `apiKey` / `authentication` /
|
|
142
|
+
`clientSecret` / … trước khi ghi log, áp dụng cả cho stack trace.
|
|
143
|
+
- Fix: text formatter thiếu `{exception}`. Với formatter dạng callable, loguru
|
|
144
|
+
**không** tự append traceback — nên trước đây mọi `logger.exception()` trong
|
|
145
|
+
codebase không in ra stack trace nào.
|
|
146
|
+
- Fix: `depth` trong intercept khiến mọi record hiện là
|
|
147
|
+
`logging:callHandlers:<line>` thay vì file:line của nơi gọi log.
|
|
148
|
+
- Fix: với `enqueue=True`, traceback object không picklable nên bị thay bằng
|
|
149
|
+
`None` sau khi qua queue. Traceback nay được format sẵn ở filter.
|
|
150
|
+
|
|
151
|
+
**Error từ runner không phải Python (`component/dispatcher.py`, `bridge.py`)**
|
|
152
|
+
- Bỏ `subprocess.run(..., check=True)`. Nó raise `CalledProcessError` **trước
|
|
153
|
+
khi** đọc được output, làm nhánh `_extract_error_message()` thành dead code và
|
|
154
|
+
user chỉ thấy `Command '[...]' returned non-zero exit status 1`.
|
|
155
|
+
- Thêm `HandlerRuntimeError` mang `language`, `action`, `error_type`, `stack`
|
|
156
|
+
(stack của runtime gốc — JS/.NET), `exit_code`, `signal_name`. Sáu tình huống
|
|
157
|
+
được phân biệt: runner không có trong image, timeout, bị signal (handler.c
|
|
158
|
+
segfault → returncode âm, stdout rỗng), structured error trên stdout, exit
|
|
159
|
+
code khác 0, và output không phải JSON hợp lệ.
|
|
160
|
+
- Đọc được **cả contract cũ** (`error` dạng string, stack trên stderr) nên không
|
|
161
|
+
cần deploy đồng thời với template.
|
|
162
|
+
- `_format_exception()` đưa `handlerStackTrace` / `language` / `action` /
|
|
163
|
+
`exitCode` / `signal` lên `job_result` cho lỗi handler JS/Q#/C.
|
|
164
|
+
|
|
165
|
+
**Đường lỗi (`model/device/device.py`, `util/response_utils.py`, …)**
|
|
166
|
+
- `Device._on_execution` dùng `build_error_job_response()` thay cho
|
|
167
|
+
`job_result = {"error": str(exception)}` và log ở `ERROR` thay vì `DEBUG`.
|
|
168
|
+
Đây là đường lỗi quan trọng nhất (submit circuit lên device) và trước đây là
|
|
169
|
+
đường mất thông tin nhiều nhất.
|
|
170
|
+
- `build_error_job_response()` nay tự phát một log `ERROR` duy nhất cho mọi lỗi,
|
|
171
|
+
nên Job Log và Function Log không còn lệch nhau.
|
|
172
|
+
- Truyền `stage` cho 8 call-site còn thiếu (trước đây 10/13 call-site rơi vào
|
|
173
|
+
default `'while running your job'`).
|
|
174
|
+
- Fix `generate_response()`: nhánh `else` truy cập `job_response.status_code`
|
|
175
|
+
khi `job_response` là falsy → `AttributeError`, thay lỗi gốc bằng lỗi khác.
|
|
176
|
+
- Fix `job_fetching.py`: `__produce_histogram_data` / `__get_execution_time`
|
|
177
|
+
khai báo `@staticmethod` nhưng dùng `self.logger` → `NameError` khi được gọi.
|
|
178
|
+
- `DeviceSelection`: không log nguyên response (chứa token provider) và không
|
|
179
|
+
nhét `response.content` thô vào exception (nội dung này chảy vào `job_result`
|
|
180
|
+
rồi hiện lên UI).
|
|
181
|
+
- `PostProcessingTask`: bỏ `print()` ghi thẳng ra stdout (không qua sink nên
|
|
182
|
+
không được redact và không có `job_id`).
|
|
183
|
+
|
|
184
|
+
**Test**
|
|
185
|
+
- `tests/test_logging_config.py` — redaction, idempotent sink, job context,
|
|
186
|
+
intercept stdlib, exception payload.
|
|
187
|
+
- `tests/test_dispatcher_runner_error.py` — 6 nhánh phân loại lỗi runner.
|
|
188
|
+
|
|
117
189
|
### v0.0.13.dev2
|
|
118
190
|
- Fix: `_openqasm_to_braket` in `circuit_adapter.py` now injects a default
|
|
119
191
|
`Probability` result type when the Braket circuit produced by `tk_to_braket`
|
|
@@ -73,6 +73,78 @@ Notes:
|
|
|
73
73
|
|
|
74
74
|
## Recently Changes Highlights
|
|
75
75
|
|
|
76
|
+
### v0.0.14.dev1 — Error logging & observability (Phase 0)
|
|
77
|
+
|
|
78
|
+
Mục tiêu: Function Log và Job Log phải chỉ ra được nguyên nhân lỗi. Toàn bộ thay
|
|
79
|
+
đổi trong bản này là **additive hoặc bug fix** — không đổi contract callback,
|
|
80
|
+
không đổi schema `job_result`, không cần frontend/backend sửa gì.
|
|
81
|
+
|
|
82
|
+
**Logging (`config/`)**
|
|
83
|
+
- Thêm `config/log_context.py`: `bind_job()` / `enrich_job_context()` đặt
|
|
84
|
+
`job_id`, `trace_id`, `sdk`, `provider`, `device` vào `contextvars`. Mọi log
|
|
85
|
+
sau đó tự có đủ field, kể cả log của `Provider` / `DeviceSelection` /
|
|
86
|
+
`*Factory` — những chỗ trước đây rơi vào context `"QuappLibs"` nên không gắn
|
|
87
|
+
được vào job nào.
|
|
88
|
+
- `configure_logging()` **idempotent**. Trước đây `AsyncInvocationTask.do()` gọi
|
|
89
|
+
`logger.add(sink=sys.stderr, ...)` cho mỗi request, nên sau N job có N+1 sink
|
|
90
|
+
và mỗi dòng log in N+1 lần.
|
|
91
|
+
- Thêm `InterceptHandler` + `install_stdlib_intercept()`: đẩy mọi record của
|
|
92
|
+
stdlib `logging` sang loguru. Cần thiết vì 24/34 template trong
|
|
93
|
+
`quapp-sdk-templates` dùng `logging.getLogger('index')` mà không cấu hình
|
|
94
|
+
handler — `debug`/`info` của chúng bị drop im lặng và `exception` in ra không
|
|
95
|
+
có prefix `[ConsoleJobLog]` lẫn `job_id`. Log của uvicorn/qiskit/botocore cũng
|
|
96
|
+
về cùng một format.
|
|
97
|
+
- Thêm JSON sink cho structured logging, bật bằng `QUAPP_LOG_FORMAT=json|both`
|
|
98
|
+
(default `text`, giữ nguyên hành vi cũ). Field: `jobId`, `traceId`, `phase`,
|
|
99
|
+
`category`, `errorCode`, `sdk`, `provider`, `device`, `event`, `durationMs`.
|
|
100
|
+
- Thêm redaction filter: mask `token` / `apiKey` / `authentication` /
|
|
101
|
+
`clientSecret` / … trước khi ghi log, áp dụng cả cho stack trace.
|
|
102
|
+
- Fix: text formatter thiếu `{exception}`. Với formatter dạng callable, loguru
|
|
103
|
+
**không** tự append traceback — nên trước đây mọi `logger.exception()` trong
|
|
104
|
+
codebase không in ra stack trace nào.
|
|
105
|
+
- Fix: `depth` trong intercept khiến mọi record hiện là
|
|
106
|
+
`logging:callHandlers:<line>` thay vì file:line của nơi gọi log.
|
|
107
|
+
- Fix: với `enqueue=True`, traceback object không picklable nên bị thay bằng
|
|
108
|
+
`None` sau khi qua queue. Traceback nay được format sẵn ở filter.
|
|
109
|
+
|
|
110
|
+
**Error từ runner không phải Python (`component/dispatcher.py`, `bridge.py`)**
|
|
111
|
+
- Bỏ `subprocess.run(..., check=True)`. Nó raise `CalledProcessError` **trước
|
|
112
|
+
khi** đọc được output, làm nhánh `_extract_error_message()` thành dead code và
|
|
113
|
+
user chỉ thấy `Command '[...]' returned non-zero exit status 1`.
|
|
114
|
+
- Thêm `HandlerRuntimeError` mang `language`, `action`, `error_type`, `stack`
|
|
115
|
+
(stack của runtime gốc — JS/.NET), `exit_code`, `signal_name`. Sáu tình huống
|
|
116
|
+
được phân biệt: runner không có trong image, timeout, bị signal (handler.c
|
|
117
|
+
segfault → returncode âm, stdout rỗng), structured error trên stdout, exit
|
|
118
|
+
code khác 0, và output không phải JSON hợp lệ.
|
|
119
|
+
- Đọc được **cả contract cũ** (`error` dạng string, stack trên stderr) nên không
|
|
120
|
+
cần deploy đồng thời với template.
|
|
121
|
+
- `_format_exception()` đưa `handlerStackTrace` / `language` / `action` /
|
|
122
|
+
`exitCode` / `signal` lên `job_result` cho lỗi handler JS/Q#/C.
|
|
123
|
+
|
|
124
|
+
**Đường lỗi (`model/device/device.py`, `util/response_utils.py`, …)**
|
|
125
|
+
- `Device._on_execution` dùng `build_error_job_response()` thay cho
|
|
126
|
+
`job_result = {"error": str(exception)}` và log ở `ERROR` thay vì `DEBUG`.
|
|
127
|
+
Đây là đường lỗi quan trọng nhất (submit circuit lên device) và trước đây là
|
|
128
|
+
đường mất thông tin nhiều nhất.
|
|
129
|
+
- `build_error_job_response()` nay tự phát một log `ERROR` duy nhất cho mọi lỗi,
|
|
130
|
+
nên Job Log và Function Log không còn lệch nhau.
|
|
131
|
+
- Truyền `stage` cho 8 call-site còn thiếu (trước đây 10/13 call-site rơi vào
|
|
132
|
+
default `'while running your job'`).
|
|
133
|
+
- Fix `generate_response()`: nhánh `else` truy cập `job_response.status_code`
|
|
134
|
+
khi `job_response` là falsy → `AttributeError`, thay lỗi gốc bằng lỗi khác.
|
|
135
|
+
- Fix `job_fetching.py`: `__produce_histogram_data` / `__get_execution_time`
|
|
136
|
+
khai báo `@staticmethod` nhưng dùng `self.logger` → `NameError` khi được gọi.
|
|
137
|
+
- `DeviceSelection`: không log nguyên response (chứa token provider) và không
|
|
138
|
+
nhét `response.content` thô vào exception (nội dung này chảy vào `job_result`
|
|
139
|
+
rồi hiện lên UI).
|
|
140
|
+
- `PostProcessingTask`: bỏ `print()` ghi thẳng ra stdout (không qua sink nên
|
|
141
|
+
không được redact và không có `job_id`).
|
|
142
|
+
|
|
143
|
+
**Test**
|
|
144
|
+
- `tests/test_logging_config.py` — redaction, idempotent sink, job context,
|
|
145
|
+
intercept stdlib, exception payload.
|
|
146
|
+
- `tests/test_dispatcher_runner_error.py` — 6 nhánh phân loại lỗi runner.
|
|
147
|
+
|
|
76
148
|
### v0.0.13.dev2
|
|
77
149
|
- Fix: `_openqasm_to_braket` in `circuit_adapter.py` now injects a default
|
|
78
150
|
`Probability` result type when the Braket circuit produced by `tk_to_braket`
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quapp-common"
|
|
7
|
-
version = "0.0.13.
|
|
7
|
+
version = "0.0.13.dev9"
|
|
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" }]
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# async_invocation_task.py
|
|
3
3
|
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
4
|
import asyncio
|
|
5
|
-
import sys
|
|
6
5
|
|
|
7
6
|
from starlette.concurrency import run_in_threadpool
|
|
8
7
|
from starlette.responses import JSONResponse
|
|
@@ -10,6 +9,7 @@ from starlette.responses import JSONResponse
|
|
|
10
9
|
from ..async_tasks.async_task import AsyncTask
|
|
11
10
|
from ..component.backend.job_manager import JobManager
|
|
12
11
|
from ..component.callback.update_job_metadata import update_job_metadata
|
|
12
|
+
from ..config.log_context import bind_job
|
|
13
13
|
from ..config.logging_config import logger
|
|
14
14
|
from ..factory.handler_factory import HandlerFactory
|
|
15
15
|
from ..model.invocation import Event, Error, Success
|
|
@@ -61,9 +61,12 @@ class AsyncInvocationTask(AsyncTask):
|
|
|
61
61
|
job_id = self.event.json().get('jobId')
|
|
62
62
|
if job_id is None:
|
|
63
63
|
raise Exception("Job ID is missing from event")
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
|
|
65
|
+
# Trước đây chỗ này gọi logger.add(sink=sys.stderr, ...) cho MỖI
|
|
66
|
+
# request: sau N job có N+1 sink và mỗi dòng log in N+1 lần. Sink đã
|
|
67
|
+
# được cấu hình một lần trong logging_config; ở đây chỉ cần bind
|
|
68
|
+
# context để mọi log sau đó tự có job_id/trace_id.
|
|
69
|
+
job_log = bind_job(job_id)
|
|
67
70
|
|
|
68
71
|
# Define the blocking work
|
|
69
72
|
def _run_handle():
|
|
@@ -78,7 +81,7 @@ class AsyncInvocationTask(AsyncTask):
|
|
|
78
81
|
if t.cancelled() or t.exception() is None:
|
|
79
82
|
return
|
|
80
83
|
exc = t.exception()
|
|
81
|
-
|
|
84
|
+
job_log.bind(event='background_task_failed').exception(
|
|
82
85
|
f"Background job task failed for job {job_id}: {exc}")
|
|
83
86
|
try:
|
|
84
87
|
event_data = self.event.json()
|
|
@@ -87,7 +90,8 @@ class AsyncInvocationTask(AsyncTask):
|
|
|
87
90
|
if on_error_url:
|
|
88
91
|
error_response = build_error_job_response(
|
|
89
92
|
exc,
|
|
90
|
-
message=f"Unhandled background task error: {exc}"
|
|
93
|
+
message=f"Unhandled background task error: {exc}",
|
|
94
|
+
stage='while running your job in the background')
|
|
91
95
|
error_response.user_token = event_data.get('userToken')
|
|
92
96
|
error_response.user_identity = event_data.get(
|
|
93
97
|
'userIdentity')
|
|
@@ -97,7 +101,7 @@ class AsyncInvocationTask(AsyncTask):
|
|
|
97
101
|
event_data, 'workspaceHeader')
|
|
98
102
|
update_job_metadata(error_response, on_error_url)
|
|
99
103
|
except Exception as fallback_exc:
|
|
100
|
-
|
|
104
|
+
job_log.bind(event='callback_failed').exception(
|
|
101
105
|
f"Failed to notify BE of error for job {job_id}: {fallback_exc}")
|
|
102
106
|
|
|
103
107
|
task.add_done_callback(_on_task_done)
|
|
@@ -35,15 +35,22 @@ class PostProcessingTask(AsyncTask):
|
|
|
35
35
|
job_result_post_processing = self.post_processing_fn(
|
|
36
36
|
self.promise.job_result)
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
# print() cũ ghi thẳng ra stdout, không qua sink nên không được
|
|
39
|
+
# redact và không có job_id.
|
|
40
|
+
self.logger.debug(
|
|
41
|
+
f"Parsing job result: type={type(job_result_post_processing).__name__}")
|
|
40
42
|
job_response.job_result = parse(job_result_post_processing)
|
|
41
43
|
|
|
42
44
|
update_job_metadata(job_response, self.promise.callback_url.on_done)
|
|
43
45
|
|
|
44
46
|
except Exception as exception:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
self.logger.bind(event='job_failed').exception(
|
|
48
|
+
f"Post-processing failed: {exception}")
|
|
49
|
+
|
|
50
|
+
job_response = build_error_job_response(
|
|
51
|
+
exception, job_response,
|
|
52
|
+
message='Error when post processing job result',
|
|
53
|
+
stage='while post-processing the job result')
|
|
47
54
|
|
|
48
55
|
update_job_metadata(job_response,
|
|
49
56
|
self.promise.callback_url.on_error)
|
{quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/backend/job_fetcher.py
RENAMED
|
@@ -131,9 +131,11 @@ class JobFetcher(ABC):
|
|
|
131
131
|
f"Exception during job fetch for provider_job_id {self.provider_job_id}: {exception}",
|
|
132
132
|
exc_info=True)
|
|
133
133
|
|
|
134
|
-
job_response = build_error_job_response(
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
job_response = build_error_job_response(
|
|
135
|
+
exception, job_response,
|
|
136
|
+
message='Error when fetching job with provider_job_id {0}'.format(
|
|
137
|
+
self.provider_job_id),
|
|
138
|
+
stage='while fetching the job result from the provider')
|
|
137
139
|
update_job_metadata(job_response, self.callback_urls[
|
|
138
140
|
InvocationStep.EXECUTION].on_error)
|
|
139
141
|
|
|
@@ -208,8 +210,10 @@ class JobFetcher(ABC):
|
|
|
208
210
|
self.logger.exception(f"Exception during analysis: {exception}",
|
|
209
211
|
exc_info=True)
|
|
210
212
|
from ...util.response_utils import build_error_job_response
|
|
211
|
-
job_response = build_error_job_response(
|
|
212
|
-
|
|
213
|
+
job_response = build_error_job_response(
|
|
214
|
+
exception, job_response,
|
|
215
|
+
message='Error when analyzing job result',
|
|
216
|
+
stage='while analysing the job result')
|
|
213
217
|
|
|
214
218
|
update_job_metadata(job_response, callback_url.on_error)
|
|
215
219
|
return None
|
{quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/backend/job_fetching.py
RENAMED
|
@@ -89,13 +89,15 @@ class JobFetching(ABC):
|
|
|
89
89
|
job_response.status_code = StatusCode.POLLING
|
|
90
90
|
|
|
91
91
|
except Exception as exception:
|
|
92
|
-
self.logger.
|
|
92
|
+
self.logger.bind(event='job_failed').exception(
|
|
93
93
|
"Exception when fetch job with provider_job_id {0}: {1}".format(
|
|
94
94
|
self.provider_job_id, str(exception)))
|
|
95
95
|
|
|
96
|
-
job_response = build_error_job_response(
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
job_response = build_error_job_response(
|
|
97
|
+
exception, job_response,
|
|
98
|
+
message='Exception when fetch job with provider_job_id {0}'.format(
|
|
99
|
+
self.provider_job_id),
|
|
100
|
+
stage='while fetching the job result from the provider')
|
|
99
101
|
|
|
100
102
|
update_job_metadata(job_response, self.callback_dict.get(
|
|
101
103
|
InvocationStep.EXECUTION).on_error)
|
|
@@ -156,9 +158,11 @@ class JobFetching(ABC):
|
|
|
156
158
|
"Exception when analyst job result with provider_job_id {0}: {1}".format(
|
|
157
159
|
self.provider_job_id, str(exception)))
|
|
158
160
|
|
|
159
|
-
job_response = build_error_job_response(
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
job_response = build_error_job_response(
|
|
162
|
+
exception, job_response,
|
|
163
|
+
message='Exception when analyst job result with provider_job_id {0}'.format(
|
|
164
|
+
self.provider_job_id),
|
|
165
|
+
stage='while analysing the job result')
|
|
162
166
|
|
|
163
167
|
update_job_metadata(job_response, callback_url.on_error)
|
|
164
168
|
|
|
@@ -183,8 +187,10 @@ class JobFetching(ABC):
|
|
|
183
187
|
PostProcessingTask(post_processing_fn=post_processing_fn,
|
|
184
188
|
promise=promise).do()
|
|
185
189
|
|
|
186
|
-
@staticmethod
|
|
187
|
-
|
|
190
|
+
# Trước đây hai method dưới là @staticmethod nhưng vẫn dùng `self.logger`,
|
|
191
|
+
# nên chúng raise NameError ngay khi được gọi -- biến lỗi analysis thành một
|
|
192
|
+
# lỗi vô nghĩa hoàn toàn khác. Bỏ @staticmethod để `self` hợp lệ.
|
|
193
|
+
def __produce_histogram_data(self, job_result) -> Any | None:
|
|
188
194
|
"""
|
|
189
195
|
|
|
190
196
|
@param job_result:
|
|
@@ -196,14 +202,13 @@ class JobFetching(ABC):
|
|
|
196
202
|
histogram_data = job_result.result()[0].data.meas.get_counts()
|
|
197
203
|
|
|
198
204
|
except Exception as e:
|
|
199
|
-
self.logger.
|
|
205
|
+
self.logger.warning(
|
|
200
206
|
"Can't produce histogram with error: {0}".format(str(e)))
|
|
201
207
|
histogram_data = None
|
|
202
208
|
|
|
203
209
|
return histogram_data
|
|
204
210
|
|
|
205
|
-
|
|
206
|
-
def __get_execution_time(job_result):
|
|
211
|
+
def __get_execution_time(self, job_result):
|
|
207
212
|
"""
|
|
208
213
|
|
|
209
214
|
@param job_result:
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# job_heartbeat.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from ...config.logging_config import logger
|
|
11
|
+
|
|
12
|
+
DEFAULT_INTERVAL_SEC = 30.0
|
|
13
|
+
REQUEST_TIMEOUT_SEC = 5.0
|
|
14
|
+
TERMINAL_STATUSES = ('DONE', 'FAILED')
|
|
15
|
+
|
|
16
|
+
_lock = threading.Lock()
|
|
17
|
+
_thread = None
|
|
18
|
+
_stop_event = threading.Event()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def heartbeat_interval_sec() -> float:
|
|
22
|
+
raw = os.getenv('HEARTBEAT_INTERVAL_SEC', '')
|
|
23
|
+
try:
|
|
24
|
+
value = float(raw)
|
|
25
|
+
if value > 0:
|
|
26
|
+
return value
|
|
27
|
+
except ValueError:
|
|
28
|
+
pass
|
|
29
|
+
return DEFAULT_INTERVAL_SEC
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def beat_once() -> int:
|
|
33
|
+
from .job_manager import JobManager, META, STATUS, SCHEDULER_CALLBACK_URL
|
|
34
|
+
|
|
35
|
+
sent = 0
|
|
36
|
+
for job_id, data in JobManager.list_jobs().items():
|
|
37
|
+
if data.get(STATUS) in TERMINAL_STATUSES:
|
|
38
|
+
continue
|
|
39
|
+
url = (data.get(META) or {}).get(SCHEDULER_CALLBACK_URL)
|
|
40
|
+
if not url:
|
|
41
|
+
continue
|
|
42
|
+
try:
|
|
43
|
+
response = requests.patch(
|
|
44
|
+
url, json={'status': 'RUNNING', 'heartbeat': True},
|
|
45
|
+
timeout=REQUEST_TIMEOUT_SEC)
|
|
46
|
+
logger.debug(
|
|
47
|
+
f'Heartbeat for job {job_id} returned {response.status_code}')
|
|
48
|
+
sent += 1
|
|
49
|
+
except Exception as exception:
|
|
50
|
+
logger.debug(f'Heartbeat for job {job_id} failed: {exception}')
|
|
51
|
+
return sent
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _run() -> None:
|
|
55
|
+
while True:
|
|
56
|
+
try:
|
|
57
|
+
beat_once()
|
|
58
|
+
except Exception as exception:
|
|
59
|
+
logger.exception(f'Heartbeat loop error: {exception}')
|
|
60
|
+
if _stop_event.wait(heartbeat_interval_sec()):
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def ensure_started() -> None:
|
|
65
|
+
global _thread
|
|
66
|
+
with _lock:
|
|
67
|
+
if _thread is not None and _thread.is_alive():
|
|
68
|
+
return
|
|
69
|
+
_stop_event.clear()
|
|
70
|
+
_thread = threading.Thread(target=_run, name='job-heartbeat',
|
|
71
|
+
daemon=True)
|
|
72
|
+
_thread.start()
|
|
73
|
+
logger.info(
|
|
74
|
+
f'Job heartbeat started with interval {heartbeat_interval_sec()}s')
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def stop() -> None:
|
|
78
|
+
global _thread
|
|
79
|
+
with _lock:
|
|
80
|
+
_stop_event.set()
|
|
81
|
+
if _thread is not None:
|
|
82
|
+
_thread.join(timeout=1.0)
|
|
83
|
+
_thread = None
|
{quapp_common-0.0.13.dev7 → quapp_common-0.0.13.dev9}/quapp_common/component/backend/job_manager.py
RENAMED
|
@@ -66,6 +66,9 @@ class JobManager:
|
|
|
66
66
|
"""
|
|
67
67
|
Stop any listener threads in this process and shutdown Manager.
|
|
68
68
|
"""
|
|
69
|
+
from .job_heartbeat import stop
|
|
70
|
+
stop()
|
|
71
|
+
|
|
69
72
|
# stop listeners
|
|
70
73
|
for key, (t, stop_ev) in list(cls._listener_threads.items()):
|
|
71
74
|
stop_ev.set()
|
|
@@ -100,6 +103,9 @@ class JobManager:
|
|
|
100
103
|
cls._jobs[job_id] = data # proxy is process-safe
|
|
101
104
|
cls._publish(job_id, {"event": "added", **data})
|
|
102
105
|
|
|
106
|
+
from .job_heartbeat import ensure_started
|
|
107
|
+
ensure_started()
|
|
108
|
+
|
|
103
109
|
@classmethod
|
|
104
110
|
def update_status(cls, job_id: str, status: str,
|
|
105
111
|
meta_update: Optional[Dict[str, Any]] = None) -> None:
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
from typing import Any, Dict
|
|
5
5
|
|
|
6
6
|
from .circuit_adapter import adapt
|
|
7
|
-
from .dispatcher import dispatch
|
|
7
|
+
from .dispatcher import HandlerRuntimeError, dispatch
|
|
8
8
|
from .result_serializer import serialize
|
|
9
9
|
from ..config.logging_config import logger
|
|
10
10
|
from ..enum.language import Language
|
|
@@ -47,25 +47,27 @@ 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
|
+
# dispatch() đã raise HandlerRuntimeError cho mọi trường hợp lỗi
|
|
51
|
+
# (structured error, exit code khác 0, signal, output không hợp lệ) với
|
|
52
|
+
# đầy đủ error_type / stack của runner, nên ở đây không cần kiểm
|
|
53
|
+
# status == "error" nữa.
|
|
50
54
|
result = dispatch(self.language, {
|
|
51
55
|
"action": "processing",
|
|
52
56
|
"input": invocation_input
|
|
53
57
|
})
|
|
54
58
|
|
|
55
|
-
status = result.get("status")
|
|
56
|
-
logger.debug(f"SubprocessBridge.processing: JS status={status}")
|
|
57
|
-
|
|
58
|
-
if status == "error":
|
|
59
|
-
logger.error(f"SubprocessBridge.processing: JS error={result.get('error')}")
|
|
60
|
-
raise RuntimeError(f"JS processing failed: {result.get('error')}")
|
|
61
|
-
|
|
62
59
|
payload = result.get("result")
|
|
63
60
|
if payload is None:
|
|
64
|
-
raise
|
|
61
|
+
raise HandlerRuntimeError(
|
|
62
|
+
language=self.language.value, action='processing',
|
|
63
|
+
error_type='InvalidOutput',
|
|
64
|
+
message="Handler không trả về field 'result'.")
|
|
65
65
|
if not isinstance(payload, dict):
|
|
66
|
-
raise
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
raise HandlerRuntimeError(
|
|
67
|
+
language=self.language.value, action='processing',
|
|
68
|
+
error_type='InvalidOutput',
|
|
69
|
+
message=f"Field 'result' phải là object, nhận được "
|
|
70
|
+
f"{type(payload).__name__}.")
|
|
69
71
|
|
|
70
72
|
fmt = payload.get("format")
|
|
71
73
|
logger.debug(f"SubprocessBridge.processing: JS returned format={fmt}; adapting for sdk={self.sdk}")
|
|
@@ -98,16 +100,12 @@ class SubprocessBridge:
|
|
|
98
100
|
"job_result": serialized
|
|
99
101
|
})
|
|
100
102
|
|
|
101
|
-
status = result.get("status")
|
|
102
|
-
logger.debug(f"SubprocessBridge.post_processing: JS status={status}")
|
|
103
|
-
|
|
104
|
-
if status == "error":
|
|
105
|
-
logger.error(f"SubprocessBridge.post_processing: JS error={result.get('error')}")
|
|
106
|
-
raise RuntimeError(f"JS post_processing failed: {result.get('error')}")
|
|
107
|
-
|
|
108
103
|
payload = result.get("result")
|
|
109
104
|
if payload is None:
|
|
110
|
-
raise
|
|
105
|
+
raise HandlerRuntimeError(
|
|
106
|
+
language=self.language.value, action='post_processing',
|
|
107
|
+
error_type='InvalidOutput',
|
|
108
|
+
message="Handler không trả về field 'result'.")
|
|
111
109
|
|
|
112
110
|
logger.info(f"SubprocessBridge.post_processing: completed successfully | result type={type(payload).__name__}")
|
|
113
111
|
return payload
|
|
@@ -60,21 +60,31 @@ class DeviceSelection:
|
|
|
60
60
|
response = requests.get(self.url, params=request, headers=header)
|
|
61
61
|
|
|
62
62
|
if response.status_code != 200:
|
|
63
|
-
logger.
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
logger.bind(event='device_selection_failed',
|
|
64
|
+
status_code=response.status_code).error(
|
|
65
|
+
'Select device fail with status code: {0}'.format(
|
|
66
|
+
response.status_code))
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
# Không đưa response.content thô vào exception: nội dung này chảy
|
|
69
|
+
# vào job_result rồi hiện lên UI, và có thể mang theo credential.
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f'Select device failed with status code '
|
|
72
|
+
f'{response.status_code}. Xem Function Log để biết chi tiết.')
|
|
68
73
|
|
|
69
74
|
try:
|
|
70
75
|
response_dict = response.json().get("data")
|
|
71
76
|
|
|
77
|
+
# response_dict chứa cả `authentication` = token của provider, nên
|
|
78
|
+
# chỉ log các field không phải secret.
|
|
72
79
|
logger.debug(
|
|
73
|
-
'Select device successfully: {0}'.format(
|
|
80
|
+
'Select device successfully: providerTag={0}, deviceName={1}'.format(
|
|
81
|
+
response_dict.get("providerTag"),
|
|
82
|
+
response_dict.get("deviceName")))
|
|
74
83
|
|
|
75
84
|
return BackendInformation(provider_tag=ProviderTag.resolve(
|
|
76
85
|
response_dict.get("providerTag")),
|
|
77
86
|
device_name=response_dict.get("deviceName"),
|
|
78
87
|
authentication=response_dict.get("authentication"))
|
|
79
88
|
except JSONDecodeError:
|
|
89
|
+
logger.warning('Select device response is not valid JSON')
|
|
80
90
|
return None
|