quapp-common 0.0.14.dev6__tar.gz → 0.0.14.dev7__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.14.dev6/quapp_common.egg-info → quapp_common-0.0.14.dev7}/PKG-INFO +1 -1
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/pyproject.toml +1 -1
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/async_tasks/async_invocation_task.py +5 -4
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/async_tasks/post_processing_task.py +2 -2
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/invocation.py +4 -3
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/job_fetching.py +4 -3
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/bridge.py +7 -7
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/circuit_adapter.py +32 -29
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/device/device_selection.py +5 -5
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/dispatcher.py +41 -37
- quapp_common-0.0.14.dev7/quapp_common/config/log_context.py +201 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/config/logging_config.py +91 -84
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/config/thread_config.py +6 -5
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/response/error_report.py +24 -19
- quapp_common-0.0.14.dev7/quapp_common/enum/error_category.py +92 -0
- quapp_common-0.0.14.dev7/quapp_common/enum/runtime_phase.py +105 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/handler/handler.py +4 -3
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/model/device/device.py +12 -10
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/model/invocation.py +6 -5
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/util/error_classifier.py +137 -125
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/util/invocation_failure.py +26 -21
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/util/response_utils.py +23 -20
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7/quapp_common.egg-info}/PKG-INFO +1 -1
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_circuit_adapter.py +14 -13
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_dispatcher_runner_error.py +9 -9
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_error_classifier.py +18 -15
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_error_log_dedupe.py +15 -12
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_error_report.py +16 -13
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_invocation_failure.py +6 -6
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_log_secret_leak.py +28 -24
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_logging_config.py +29 -26
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_no_sdk_dependency.py +19 -19
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_phase_coverage.py +14 -13
- quapp_common-0.0.14.dev6/quapp_common/config/log_context.py +0 -194
- quapp_common-0.0.14.dev6/quapp_common/enum/error_category.py +0 -91
- quapp_common-0.0.14.dev6/quapp_common/enum/runtime_phase.py +0 -118
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/LICENSE +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/README.md +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/async_tasks/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/async_tasks/async_task.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/async_tasks/export_circuit_task.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/job_fetcher.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/job_heartbeat.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/job_manager.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/callback/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/callback/update_job_metadata.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/device/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/result_serializer.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/config/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/async_task/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/backend/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/backend/backend_information.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/callback/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/callback/callback_url.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/device/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/device/circuit_running_option.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/promise/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/promise/post_processing_promise.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/promise/promise.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/request/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/request/invocation_request.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/request/job_fetching_request.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/request/request.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/response/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/response/authentication.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/response/custom_header.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/response/job_response.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/base_enum.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/http_header.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/invocation_step.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/language.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/media_type.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/processing_unit.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/provider_tag.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/sdk.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/status/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/status/job_status.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/status/status_code.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/token_type.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/factory/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/factory/device_factory.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/factory/handler_factory.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/factory/provider_factory.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/handler/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/model/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/model/device/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/model/device/custom_device.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/model/provider/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/model/provider/provider.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/util/__init__.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/util/file_utils.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/util/http_utils.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/util/json_parser_utils.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common.egg-info/SOURCES.txt +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common.egg-info/dependency_links.txt +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common.egg-info/requires.txt +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common.egg-info/top_level.txt +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/setup.cfg +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_job_heartbeat.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_json_parser_utils.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_language_dispatcher.py +0 -0
- {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_result_serializer.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quapp-common"
|
|
7
|
-
version = "0.0.14.
|
|
7
|
+
version = "0.0.14.dev7"
|
|
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" }]
|
|
@@ -62,10 +62,11 @@ class AsyncInvocationTask(AsyncTask):
|
|
|
62
62
|
if job_id is None:
|
|
63
63
|
raise Exception("Job ID is missing from event")
|
|
64
64
|
|
|
65
|
-
#
|
|
66
|
-
# request:
|
|
67
|
-
#
|
|
68
|
-
#
|
|
65
|
+
# This used to call logger.add(sink=sys.stderr, ...) for EVERY
|
|
66
|
+
# request: after N jobs there were N+1 sinks and every line printed
|
|
67
|
+
# N+1 times. The sinks are configured once in logging_config; all
|
|
68
|
+
# that is needed here is binding the context so every later line
|
|
69
|
+
# carries job_id/trace_id on its own.
|
|
69
70
|
job_log = bind_job(job_id)
|
|
70
71
|
|
|
71
72
|
# Define the blocking work
|
|
@@ -41,8 +41,8 @@ class PostProcessingTask(AsyncTask):
|
|
|
41
41
|
job_result_post_processing = self.post_processing_fn(
|
|
42
42
|
self.promise.job_result)
|
|
43
43
|
|
|
44
|
-
# print()
|
|
45
|
-
#
|
|
44
|
+
# The old print() went straight to stdout, bypassing the
|
|
45
|
+
# sink, so it was neither redacted nor tagged with a job_id.
|
|
46
46
|
self.logger.debug(
|
|
47
47
|
f"Parsing job result: type={type(job_result_post_processing).__name__}")
|
|
48
48
|
job_response.job_result = parse(job_result_post_processing)
|
{quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/invocation.py
RENAMED
|
@@ -130,9 +130,10 @@ class Invocation(ABC):
|
|
|
130
130
|
self.logger.debug(
|
|
131
131
|
f"Executing job with provider tag: {provider_tag.value}")
|
|
132
132
|
|
|
133
|
-
# PROVIDER_INVOCATION
|
|
134
|
-
#
|
|
135
|
-
#
|
|
133
|
+
# PROVIDER_INVOCATION is kept apart from EXECUTION: failing to
|
|
134
|
+
# authenticate with the provider and the provider rejecting the
|
|
135
|
+
# circuit are two different things, and they used to share the
|
|
136
|
+
# EXECUTION label.
|
|
136
137
|
with phase(RuntimePhase.PROVIDER_INVOCATION):
|
|
137
138
|
provider = self._create_provider()
|
|
138
139
|
|
{quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/job_fetching.py
RENAMED
|
@@ -182,9 +182,10 @@ class JobFetching(ABC):
|
|
|
182
182
|
PostProcessingTask(post_processing_fn=post_processing_fn,
|
|
183
183
|
promise=promise).do()
|
|
184
184
|
|
|
185
|
-
#
|
|
186
|
-
#
|
|
187
|
-
#
|
|
185
|
+
# The two methods below used to be @staticmethod while still using
|
|
186
|
+
# `self.logger`, so they raised NameError the moment they were called --
|
|
187
|
+
# turning an analysis error into a completely unrelated, meaningless one.
|
|
188
|
+
# Dropping @staticmethod makes `self` valid.
|
|
188
189
|
def __produce_histogram_data(self, job_result) -> Any | None:
|
|
189
190
|
"""
|
|
190
191
|
|
|
@@ -47,10 +47,10 @@ 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()
|
|
51
|
-
# (structured error, exit code
|
|
52
|
-
#
|
|
53
|
-
# status == "error"
|
|
50
|
+
# dispatch() already raises HandlerRuntimeError for every failure
|
|
51
|
+
# mode (structured error, non-zero exit code, signal, unusable output)
|
|
52
|
+
# carrying the runner's error_type and stack, so there is no need to
|
|
53
|
+
# check status == "error" here any more.
|
|
54
54
|
result = dispatch(self.language, {
|
|
55
55
|
"action": "processing",
|
|
56
56
|
"input": invocation_input
|
|
@@ -61,12 +61,12 @@ class SubprocessBridge:
|
|
|
61
61
|
raise HandlerRuntimeError(
|
|
62
62
|
language=self.language.value, action='processing',
|
|
63
63
|
error_type='InvalidOutput',
|
|
64
|
-
message="
|
|
64
|
+
message="The handler returned no 'result' field.")
|
|
65
65
|
if not isinstance(payload, dict):
|
|
66
66
|
raise HandlerRuntimeError(
|
|
67
67
|
language=self.language.value, action='processing',
|
|
68
68
|
error_type='InvalidOutput',
|
|
69
|
-
message=f"
|
|
69
|
+
message=f"The 'result' field must be an object, got "
|
|
70
70
|
f"{type(payload).__name__}.")
|
|
71
71
|
|
|
72
72
|
fmt = payload.get("format")
|
|
@@ -105,7 +105,7 @@ class SubprocessBridge:
|
|
|
105
105
|
raise HandlerRuntimeError(
|
|
106
106
|
language=self.language.value, action='post_processing',
|
|
107
107
|
error_type='InvalidOutput',
|
|
108
|
-
message="
|
|
108
|
+
message="The handler returned no 'result' field.")
|
|
109
109
|
|
|
110
110
|
logger.info(f"SubprocessBridge.post_processing: completed successfully | result type={type(payload).__name__}")
|
|
111
111
|
return payload
|
{quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/circuit_adapter.py
RENAMED
|
@@ -2,25 +2,27 @@
|
|
|
2
2
|
# circuit_adapter.py
|
|
3
3
|
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
4
|
|
|
5
|
-
"""
|
|
6
|
-
native
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
``quapp-common``
|
|
11
|
-
(``quapp-qiskit``, ``qapp-pennylane``, ...)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
5
|
+
"""Convert a circuit from an intermediate format (OpenQASM / QUBO / JSON) into
|
|
6
|
+
the native object of each SDK.
|
|
7
|
+
|
|
8
|
+
Every SDK is imported LAZILY, inside the handler that needs it. The reason:
|
|
9
|
+
|
|
10
|
+
``quapp-common`` must not depend on any SDK. Each child lib
|
|
11
|
+
(``quapp-qiskit``, ``qapp-pennylane``, ...) declares its own SDK, and a function
|
|
12
|
+
image installs only the SDK it actually uses. Importing at module level here
|
|
13
|
+
would mean:
|
|
14
|
+
|
|
15
|
+
- ``import quapp_common.component.bridge`` demanding ALL 6 SDKs at once, even
|
|
16
|
+
when the function only runs Qiskit -- which defeats the lazy-import design of
|
|
17
|
+
``js-*/index.py`` in quapp-sdk-templates (whose comment states the intent
|
|
18
|
+
outright: "so the server can start even if optional dependencies are
|
|
19
|
+
missing");
|
|
20
|
+
- installing all 6 SDKs together causes real version conflicts; it already
|
|
21
|
+
happened with ``pytket``/``lark`` and ``pennylane-rigetti``/``pyquil``.
|
|
22
|
+
|
|
23
|
+
When an SDK is missing, the handler raises ``ModuleNotFoundError`` -- the
|
|
24
|
+
classifier maps that to ``DEPENDENCY_MISSING`` and the user is told to add the
|
|
25
|
+
package to requirements.txt.
|
|
24
26
|
"""
|
|
25
27
|
|
|
26
28
|
import importlib
|
|
@@ -34,21 +36,22 @@ logger = logger.bind(context=__name__)
|
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
def _require(module_name: str, package_hint: str = None):
|
|
37
|
-
"""Import
|
|
39
|
+
"""Import an SDK module, failing with a clear message when it is absent.
|
|
38
40
|
|
|
39
|
-
@param module_name:
|
|
40
|
-
@param package_hint:
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
@param module_name: the module to import, e.g. 'qiskit.qasm2'.
|
|
42
|
+
@param package_hint: the PyPI package name to mention to the user, when it
|
|
43
|
+
differs from the module name.
|
|
44
|
+
@raise ModuleNotFoundError: deliberately this exact type, so the classifier
|
|
45
|
+
maps it to DEPENDENCY_MISSING.
|
|
43
46
|
"""
|
|
44
47
|
try:
|
|
45
48
|
return importlib.import_module(module_name)
|
|
46
49
|
except ImportError as exception:
|
|
47
50
|
package = package_hint or module_name.split('.')[0]
|
|
48
51
|
raise ModuleNotFoundError(
|
|
49
|
-
f"SDK '{package}'
|
|
50
|
-
f"circuit
|
|
51
|
-
f"
|
|
52
|
+
f"SDK '{package}' is not installed in the function image but "
|
|
53
|
+
f"the circuit needs it. Add '{package}' to the function's "
|
|
54
|
+
f"requirements.txt and deploy again.") from exception
|
|
52
55
|
|
|
53
56
|
|
|
54
57
|
def _qiskit_quantum_circuit():
|
|
@@ -56,9 +59,9 @@ def _qiskit_quantum_circuit():
|
|
|
56
59
|
|
|
57
60
|
|
|
58
61
|
def _load_qasm_circuit(qasm_str: str):
|
|
59
|
-
"""Parse OpenQASM 2.0 string
|
|
62
|
+
"""Parse an OpenQASM 2.0 string into a Qiskit QuantumCircuit.
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
Compatible with Qiskit >= 1.0 (``qiskit.qasm2.loads``) and < 1.0
|
|
62
65
|
(``QuantumCircuit.from_qasm_str``).
|
|
63
66
|
"""
|
|
64
67
|
try:
|
|
@@ -65,17 +65,17 @@ class DeviceSelection:
|
|
|
65
65
|
'Select device fail with status code: {0}'.format(
|
|
66
66
|
response.status_code))
|
|
67
67
|
|
|
68
|
-
#
|
|
69
|
-
#
|
|
68
|
+
# Do not put raw response.content on the exception: it flows
|
|
69
|
+
# into job_result and onto the UI, and may carry a credential.
|
|
70
70
|
raise ValueError(
|
|
71
71
|
f'Select device failed with status code '
|
|
72
|
-
f'{response.status_code}.
|
|
72
|
+
f'{response.status_code}. See the Function Log for details.')
|
|
73
73
|
|
|
74
74
|
try:
|
|
75
75
|
response_dict = response.json().get("data")
|
|
76
76
|
|
|
77
|
-
# response_dict
|
|
78
|
-
#
|
|
77
|
+
# response_dict also holds `authentication`, the provider token,
|
|
78
|
+
# so only the non-secret fields are logged.
|
|
79
79
|
logger.debug(
|
|
80
80
|
'Select device successfully: providerTag={0}, deviceName={1}'.format(
|
|
81
81
|
response_dict.get("providerTag"),
|
|
@@ -2,24 +2,26 @@
|
|
|
2
2
|
# dispatcher.py
|
|
3
3
|
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
4
|
|
|
5
|
-
"""Dispatch request
|
|
5
|
+
"""Dispatch a request to the runner of a non-Python runtime (JS / Q# / C).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
``CalledProcessError``
|
|
9
|
-
|
|
10
|
-
``handler_runner.c``
|
|
11
|
-
|
|
7
|
+
On error handling: ``subprocess.run`` used to be called with ``check=True``, so
|
|
8
|
+
``CalledProcessError`` was raised BEFORE the runner's output could be read. As a
|
|
9
|
+
result the ``_extract_error_message()`` branch -- written to read the
|
|
10
|
+
``{"status":"error",...}`` that ``handler_runner.c`` already emits correctly --
|
|
11
|
+
was dead code, and all the user ever saw was
|
|
12
|
+
``Command '[...]' returned non-zero exit status 1``.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
``check=True`` is now gone and the output is classified into a
|
|
15
|
+
``HandlerRuntimeError`` carrying the full context: error kind, message, the
|
|
16
|
+
runner's stack, exit code, signal. Six situations are told apart:
|
|
16
17
|
|
|
17
|
-
1. runner
|
|
18
|
+
1. the runner is absent from the image (a build problem, not the user's)
|
|
18
19
|
2. timeout
|
|
19
|
-
3. process
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
3. the process was signalled (returncode < 0) -- e.g. handler.c segfaults and
|
|
21
|
+
stdout is empty
|
|
22
|
+
4. the runner emitted a structured error on stdout per the contract
|
|
23
|
+
5. a non-zero exit code with no structured error -- fall back to stderr
|
|
24
|
+
6. exit code 0 but stdout is not valid JSON
|
|
23
25
|
"""
|
|
24
26
|
|
|
25
27
|
import json
|
|
@@ -50,15 +52,15 @@ RUNNER_MAP = {
|
|
|
50
52
|
},
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
#
|
|
55
|
+
# Cap the stack/stderr length carried on the exception so job_result stays small.
|
|
54
56
|
_MAX_DETAIL_LENGTH = 8000
|
|
55
57
|
|
|
56
58
|
|
|
57
59
|
class HandlerRuntimeError(RuntimeError):
|
|
58
|
-
"""
|
|
60
|
+
"""An error raised inside the user-written handler (JS / Q# / C).
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
user
|
|
62
|
+
Keeps the original runtime's stack trace (JS stack, .NET stack) -- that is
|
|
63
|
+
what the user needs in order to fix the handler, not a Python traceback.
|
|
62
64
|
"""
|
|
63
65
|
|
|
64
66
|
def __init__(self, language: str, action: str = None,
|
|
@@ -96,8 +98,9 @@ def dispatch(language: Language, request_data: dict,
|
|
|
96
98
|
Parsed JSON dict from the subprocess stdout.
|
|
97
99
|
|
|
98
100
|
Raises:
|
|
99
|
-
ValueError:
|
|
100
|
-
HandlerRuntimeError:
|
|
101
|
+
ValueError: when the language has no runner configured.
|
|
102
|
+
HandlerRuntimeError: when the handler errors, crashes, times out or
|
|
103
|
+
returns unusable output.
|
|
101
104
|
"""
|
|
102
105
|
runner = RUNNER_MAP.get(language)
|
|
103
106
|
if runner is None:
|
|
@@ -123,16 +126,17 @@ def dispatch(language: Language, request_data: dict,
|
|
|
123
126
|
f"Handler runner timed out after {effective_timeout}s")
|
|
124
127
|
raise HandlerRuntimeError(
|
|
125
128
|
language=language.value, action=action, error_type='Timeout',
|
|
126
|
-
message=f'
|
|
129
|
+
message=f'The handler exceeded its {effective_timeout}s time limit.',
|
|
127
130
|
stack=_truncate(exception.stderr)) from exception
|
|
128
131
|
except FileNotFoundError as exception:
|
|
129
|
-
#
|
|
132
|
+
# The runner is absent from the image -- a build/deploy problem, not the
|
|
133
|
+
# user's code.
|
|
130
134
|
logger.bind(event='runner_missing').error(
|
|
131
135
|
f"Handler runner not found: {runner['command']}")
|
|
132
136
|
raise HandlerRuntimeError(
|
|
133
137
|
language=language.value, action=action,
|
|
134
138
|
error_type='RunnerNotFound',
|
|
135
|
-
message=f"
|
|
139
|
+
message=f"Runner '{runner['command'][0]}' was not found in the "
|
|
136
140
|
f"function image.") from exception
|
|
137
141
|
|
|
138
142
|
if result.stderr:
|
|
@@ -140,16 +144,16 @@ def dispatch(language: Language, request_data: dict,
|
|
|
140
144
|
|
|
141
145
|
output = _safe_json(result.stdout)
|
|
142
146
|
|
|
143
|
-
# (3)
|
|
144
|
-
#
|
|
147
|
+
# (3) Killed by a signal: negative returncode. The most common case is
|
|
148
|
+
# handler.c touching invalid memory -- the process dies with empty stdout.
|
|
145
149
|
if result.returncode is not None and result.returncode < 0:
|
|
146
150
|
name = _signal_name(-result.returncode)
|
|
147
151
|
logger.bind(event='runner_signal', signal_name=name).error(
|
|
148
152
|
f"Handler runner killed by {name}")
|
|
149
153
|
raise HandlerRuntimeError(
|
|
150
154
|
language=language.value, action=action, error_type='Signal',
|
|
151
|
-
message=f'
|
|
152
|
-
f'
|
|
155
|
+
message=f'The handler was killed by signal {name}, usually an '
|
|
156
|
+
f'invalid memory access inside the handler.',
|
|
153
157
|
stack=_truncate(result.stderr), exit_code=result.returncode,
|
|
154
158
|
signal_name=name)
|
|
155
159
|
|
|
@@ -157,7 +161,7 @@ def dispatch(language: Language, request_data: dict,
|
|
|
157
161
|
if isinstance(output, dict) and output.get('status') == 'error':
|
|
158
162
|
detail = output.get('error')
|
|
159
163
|
if isinstance(detail, str):
|
|
160
|
-
#
|
|
164
|
+
# Older contract: `error` is a plain string. Still has to be read.
|
|
161
165
|
detail = {'message': detail}
|
|
162
166
|
detail = detail or {}
|
|
163
167
|
message = (detail.get('message') or output.get('stack')
|
|
@@ -172,9 +176,9 @@ def dispatch(language: Language, request_data: dict,
|
|
|
172
176
|
stack=_truncate(detail.get('stack') or result.stderr),
|
|
173
177
|
exit_code=result.returncode)
|
|
174
178
|
|
|
175
|
-
# (5)
|
|
176
|
-
#
|
|
177
|
-
#
|
|
179
|
+
# (5) Non-zero exit with no structured error -- use stderr as the message.
|
|
180
|
+
# This is the path handler_runner.js and handler_runner.cs currently take:
|
|
181
|
+
# they write the stack to stderr and exit 1.
|
|
178
182
|
if result.returncode != 0:
|
|
179
183
|
message = (result.stderr or '').strip() \
|
|
180
184
|
or f'Handler exited with code {result.returncode}'
|
|
@@ -187,15 +191,15 @@ def dispatch(language: Language, request_data: dict,
|
|
|
187
191
|
error_type='NonZeroExit', message=message,
|
|
188
192
|
stack=_truncate(result.stderr), exit_code=result.returncode)
|
|
189
193
|
|
|
190
|
-
# (6) Exit 0
|
|
194
|
+
# (6) Exit 0 but the output is unusable.
|
|
191
195
|
if output is None:
|
|
192
196
|
logger.bind(event='runner_invalid_output').error(
|
|
193
197
|
'Handler runner produced no valid JSON on stdout')
|
|
194
198
|
raise HandlerRuntimeError(
|
|
195
199
|
language=language.value, action=action,
|
|
196
200
|
error_type='InvalidOutput',
|
|
197
|
-
message='
|
|
198
|
-
'
|
|
201
|
+
message='The handler did not return valid JSON on stdout. Check '
|
|
202
|
+
'for stray print/console.log mixed into the output.',
|
|
199
203
|
stack=_truncate(result.stdout), exit_code=result.returncode)
|
|
200
204
|
|
|
201
205
|
return output
|
|
@@ -225,7 +229,7 @@ def is_subprocess_language(language_str: str) -> bool:
|
|
|
225
229
|
|
|
226
230
|
|
|
227
231
|
def _safe_json(text):
|
|
228
|
-
"""Parse JSON,
|
|
232
|
+
"""Parse JSON, returning None when it will not parse (instead of raising)."""
|
|
229
233
|
if not text:
|
|
230
234
|
return None
|
|
231
235
|
try:
|
|
@@ -251,8 +255,8 @@ def _truncate(text, limit: int = _MAX_DETAIL_LENGTH):
|
|
|
251
255
|
def _extract_error_message(result) -> str:
|
|
252
256
|
"""Extract the most meaningful error message from a failed subprocess.
|
|
253
257
|
|
|
254
|
-
|
|
255
|
-
|
|
258
|
+
Kept for backward compatibility with outside callers; the main classification
|
|
259
|
+
logic has moved into `dispatch()`.
|
|
256
260
|
|
|
257
261
|
Priority order:
|
|
258
262
|
1. Structured JSON on stdout – {"status":"error","error":"..."}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# log_context.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
"""Per-job logging context.
|
|
6
|
+
|
|
7
|
+
Why it exists: previously every call site had to pass ``job_id`` into
|
|
8
|
+
``job_logger()`` by hand, and many of them forgot -- ``Device.__init__``,
|
|
9
|
+
``Provider``, ``DeviceSelection`` and the ``*Factory`` classes all used
|
|
10
|
+
``logger.bind(context='...')`` or called ``job_logger()`` with no argument, so
|
|
11
|
+
their lines landed in the ``"QuappLibs"`` context and could not be attached to
|
|
12
|
+
any job in the Job Log UI.
|
|
13
|
+
|
|
14
|
+
The fix: bind the context ONCE at the start of the request into ``contextvars``.
|
|
15
|
+
Every line logged afterwards -- including third-party library lines coming
|
|
16
|
+
through ``InterceptHandler`` -- then carries ``job_id`` / ``trace_id`` / ``sdk``
|
|
17
|
+
/ ``provider`` / ``device`` on its own.
|
|
18
|
+
|
|
19
|
+
Threading note: ``contextvars`` does NOT propagate through
|
|
20
|
+
``ThreadPoolExecutor.submit()``. ``circuit_running_pool`` is used for analysis
|
|
21
|
+
and finalization, so those two phases lose the context unless the pool copies it
|
|
22
|
+
-- see ``thread_config.ContextAwareThreadPoolExecutor`` (Phase 1).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import contextvars
|
|
26
|
+
import time
|
|
27
|
+
import uuid
|
|
28
|
+
from contextlib import contextmanager
|
|
29
|
+
|
|
30
|
+
from loguru import logger
|
|
31
|
+
|
|
32
|
+
from ..enum.runtime_phase import RuntimePhase
|
|
33
|
+
|
|
34
|
+
JOB_ID = 'job_id'
|
|
35
|
+
TRACE_ID = 'trace_id'
|
|
36
|
+
|
|
37
|
+
_CONTEXT_KEYS = (JOB_ID, TRACE_ID, 'sdk', 'provider', 'device',
|
|
38
|
+
'provider_job_id')
|
|
39
|
+
|
|
40
|
+
_JOB_CONTEXT: contextvars.ContextVar[dict] = contextvars.ContextVar(
|
|
41
|
+
'quapp_job_context', default={})
|
|
42
|
+
|
|
43
|
+
_PHASE: contextvars.ContextVar['RuntimePhase | None'] = contextvars.ContextVar(
|
|
44
|
+
'quapp_phase', default=None)
|
|
45
|
+
|
|
46
|
+
# The phase an exception most recently escaped from.
|
|
47
|
+
#
|
|
48
|
+
# This needs its own variable because `phase()` resets _PHASE in its `finally`,
|
|
49
|
+
# while the `except` block that handles the error almost always sits OUTSIDE the
|
|
50
|
+
# `with phase(...)` block:
|
|
51
|
+
#
|
|
52
|
+
# try:
|
|
53
|
+
# with phase(RuntimePhase.COMPILATION):
|
|
54
|
+
# transpile(...)
|
|
55
|
+
# except Exception: # <- _PHASE is already None here
|
|
56
|
+
# build_error_job_response(...)
|
|
57
|
+
#
|
|
58
|
+
# Without this variable every ErrorReport would carry phase=None and the
|
|
59
|
+
# phase-based fallback would never run.
|
|
60
|
+
_FAILED_PHASE: contextvars.ContextVar[
|
|
61
|
+
'RuntimePhase | None'] = contextvars.ContextVar(
|
|
62
|
+
'quapp_failed_phase', default=None)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def bind_job(job_id: str = None, *, sdk=None, provider=None, device=None,
|
|
66
|
+
provider_job_id=None, trace_id=None):
|
|
67
|
+
"""Initialise the context for one job and return the bound logger.
|
|
68
|
+
|
|
69
|
+
Call it once, as early as possible -- ``Handler.__init__`` or ``index.py``.
|
|
70
|
+
|
|
71
|
+
@param job_id: the job id on the Quapp backend side
|
|
72
|
+
@param trace_id: generated when not supplied; used to join Function Log to
|
|
73
|
+
Job Log
|
|
74
|
+
@return: a logger with the whole context bound
|
|
75
|
+
"""
|
|
76
|
+
# A job gets bound twice (AsyncInvocationTask, then Handler.__init__).
|
|
77
|
+
# Previously each call generated a fresh trace_id, so one job ended up with
|
|
78
|
+
# two different traceIds and filtering by traceId lost the lines at the
|
|
79
|
+
# entry point. Keep the existing trace while it is still the same job; a
|
|
80
|
+
# different job must get a new trace, never a mixed one.
|
|
81
|
+
current = _JOB_CONTEXT.get()
|
|
82
|
+
inherited = (current.get(TRACE_ID)
|
|
83
|
+
if current.get(JOB_ID) in (None, job_id) else None)
|
|
84
|
+
|
|
85
|
+
context = {JOB_ID : job_id,
|
|
86
|
+
TRACE_ID : trace_id or inherited or uuid.uuid4().hex[:16],
|
|
87
|
+
'sdk' : sdk,
|
|
88
|
+
'provider' : provider,
|
|
89
|
+
'device' : device,
|
|
90
|
+
'provider_job_id': provider_job_id}
|
|
91
|
+
_JOB_CONTEXT.set(context)
|
|
92
|
+
_FAILED_PHASE.set(None)
|
|
93
|
+
return logger.bind(**context)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def enrich_job_context(**fields):
|
|
97
|
+
"""Add fields once more is known (device_name after device selection,
|
|
98
|
+
provider_job_id after submit...). Only keys in _CONTEXT_KEYS are accepted.
|
|
99
|
+
|
|
100
|
+
@return: a logger bound to the updated context
|
|
101
|
+
"""
|
|
102
|
+
context = dict(_JOB_CONTEXT.get())
|
|
103
|
+
context.update(
|
|
104
|
+
{key: value for key, value in fields.items() if key in _CONTEXT_KEYS})
|
|
105
|
+
_JOB_CONTEXT.set(context)
|
|
106
|
+
return logger.bind(**context)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def job_context() -> dict:
|
|
110
|
+
"""Snapshot of the current context. Empty dict when nothing is bound yet."""
|
|
111
|
+
return dict(_JOB_CONTEXT.get())
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def current_job_id() -> str | None:
|
|
115
|
+
return _JOB_CONTEXT.get().get(JOB_ID)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def trace_id() -> str | None:
|
|
119
|
+
return _JOB_CONTEXT.get().get(TRACE_ID)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def current_phase():
|
|
123
|
+
"""The RuntimePhase currently open, or None."""
|
|
124
|
+
return _PHASE.get()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def failed_phase():
|
|
128
|
+
"""The phase an exception most recently escaped from in this job, or None."""
|
|
129
|
+
return _FAILED_PHASE.get()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def effective_phase():
|
|
133
|
+
"""The phase to classify an error with: the open one, else the failed one.
|
|
134
|
+
|
|
135
|
+
This is the function the error-reporting path should call -- an `except`
|
|
136
|
+
block is nearly always outside `with phase(...)`, so `current_phase()` on
|
|
137
|
+
its own would return None.
|
|
138
|
+
"""
|
|
139
|
+
return _PHASE.get() or _FAILED_PHASE.get()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def current_phase_value():
|
|
143
|
+
"""The str value of the open phase, for binding onto log lines."""
|
|
144
|
+
phase = _PHASE.get()
|
|
145
|
+
return phase.value if phase is not None else None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@contextmanager
|
|
149
|
+
def phase(runtime_phase: RuntimePhase):
|
|
150
|
+
"""Mark a runtime phase.
|
|
151
|
+
|
|
152
|
+
It does three things at once:
|
|
153
|
+
|
|
154
|
+
1. stamps ``phase`` onto every line logged inside, including third-party
|
|
155
|
+
library lines coming through ``InterceptHandler``;
|
|
156
|
+
2. emits ``phase_start`` / ``phase_end`` / ``phase_failed`` events with
|
|
157
|
+
``duration_ms`` -- exactly the data the UI needs to draw a timeline and
|
|
158
|
+
point at the step where the job died;
|
|
159
|
+
3. supplies the phase to the classifier when an exception flies out, with no
|
|
160
|
+
call site having to pass it by hand.
|
|
161
|
+
|
|
162
|
+
Phases may nest: a child phase restores its parent on exit.
|
|
163
|
+
|
|
164
|
+
@return: a logger bound to the context plus the phase
|
|
165
|
+
"""
|
|
166
|
+
token = _PHASE.set(runtime_phase)
|
|
167
|
+
bound = logger.bind(**_JOB_CONTEXT.get(), phase=runtime_phase.value)
|
|
168
|
+
started = time.perf_counter()
|
|
169
|
+
bound.bind(event='phase_start').info(f'{runtime_phase.value} started')
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
yield bound
|
|
173
|
+
except BaseException:
|
|
174
|
+
# Record it so the outer except block -- where _PHASE has already been
|
|
175
|
+
# reset -- still knows which phase the error escaped from.
|
|
176
|
+
_FAILED_PHASE.set(runtime_phase)
|
|
177
|
+
elapsed = _elapsed_ms(started)
|
|
178
|
+
# WARNING rather than DEBUG: this is a timeline marker and has to be
|
|
179
|
+
# visible at LOG_LEVEL=INFO. No traceback attached, because
|
|
180
|
+
# build_error_job_response logs the full exception right after -- that
|
|
181
|
+
# avoids printing the stack twice.
|
|
182
|
+
bound.bind(event='phase_failed', duration_ms=elapsed).warning(
|
|
183
|
+
f'{runtime_phase.value} failed after {elapsed} ms')
|
|
184
|
+
raise
|
|
185
|
+
else:
|
|
186
|
+
elapsed = _elapsed_ms(started)
|
|
187
|
+
bound.bind(event='phase_end', duration_ms=elapsed).info(
|
|
188
|
+
f'{runtime_phase.value} completed in {elapsed} ms')
|
|
189
|
+
finally:
|
|
190
|
+
_PHASE.reset(token)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _elapsed_ms(started: float) -> float:
|
|
194
|
+
return round((time.perf_counter() - started) * 1000, 2)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def reset() -> None:
|
|
198
|
+
"""Clear the context. Mainly used by tests."""
|
|
199
|
+
_JOB_CONTEXT.set({})
|
|
200
|
+
_PHASE.set(None)
|
|
201
|
+
_FAILED_PHASE.set(None)
|