quapp-common 0.0.14.dev5__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.dev5/quapp_common.egg-info → quapp_common-0.0.14.dev7}/PKG-INFO +1 -1
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/pyproject.toml +1 -1
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/async_tasks/async_invocation_task.py +5 -4
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/async_tasks/post_processing_task.py +14 -9
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/invocation.py +4 -3
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/job_fetching.py +4 -3
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/component/bridge.py +7 -7
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/component/circuit_adapter.py +32 -29
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/component/device/device_selection.py +5 -5
- {quapp_common-0.0.14.dev5 → 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.dev5 → quapp_common-0.0.14.dev7}/quapp_common/config/logging_config.py +91 -84
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/config/thread_config.py +6 -5
- {quapp_common-0.0.14.dev5 → 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.dev5 → quapp_common-0.0.14.dev7}/quapp_common/handler/handler.py +4 -3
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/model/device/custom_device.py +23 -15
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/model/device/device.py +12 -10
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/model/invocation.py +6 -5
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/util/error_classifier.py +137 -125
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/util/invocation_failure.py +26 -21
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/util/response_utils.py +23 -20
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7/quapp_common.egg-info}/PKG-INFO +1 -1
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common.egg-info/SOURCES.txt +1 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/tests/test_circuit_adapter.py +14 -13
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/tests/test_dispatcher_runner_error.py +9 -9
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/tests/test_error_classifier.py +18 -15
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/tests/test_error_log_dedupe.py +15 -12
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/tests/test_error_report.py +16 -13
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/tests/test_invocation_failure.py +6 -6
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/tests/test_log_secret_leak.py +28 -24
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/tests/test_logging_config.py +29 -26
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/tests/test_no_sdk_dependency.py +19 -19
- quapp_common-0.0.14.dev7/tests/test_phase_coverage.py +197 -0
- quapp_common-0.0.14.dev5/quapp_common/config/log_context.py +0 -186
- quapp_common-0.0.14.dev5/quapp_common/enum/error_category.py +0 -91
- quapp_common-0.0.14.dev5/quapp_common/enum/runtime_phase.py +0 -118
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/LICENSE +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/README.md +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/async_tasks/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/async_tasks/async_task.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/async_tasks/export_circuit_task.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/component/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/job_fetcher.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/job_heartbeat.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/job_manager.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/component/callback/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/component/callback/update_job_metadata.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/component/device/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/component/result_serializer.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/config/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/async_task/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/backend/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/backend/backend_information.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/callback/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/callback/callback_url.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/device/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/device/circuit_running_option.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/promise/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/promise/post_processing_promise.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/promise/promise.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/request/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/request/invocation_request.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/request/job_fetching_request.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/request/request.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/response/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/response/authentication.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/response/custom_header.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/data/response/job_response.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/enum/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/enum/base_enum.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/enum/http_header.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/enum/invocation_step.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/enum/language.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/enum/media_type.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/enum/processing_unit.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/enum/provider_tag.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/enum/sdk.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/enum/status/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/enum/status/job_status.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/enum/status/status_code.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/enum/token_type.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/factory/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/factory/device_factory.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/factory/handler_factory.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/factory/provider_factory.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/handler/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/model/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/model/device/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/model/provider/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/model/provider/provider.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/util/__init__.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/util/file_utils.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/util/http_utils.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common/util/json_parser_utils.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common.egg-info/dependency_links.txt +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common.egg-info/requires.txt +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/quapp_common.egg-info/top_level.txt +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/setup.cfg +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/tests/test_job_heartbeat.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/tests/test_json_parser_utils.py +0 -0
- {quapp_common-0.0.14.dev5 → quapp_common-0.0.14.dev7}/tests/test_language_dispatcher.py +0 -0
- {quapp_common-0.0.14.dev5 → 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
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
from .async_task import AsyncTask
|
|
5
5
|
from ..config.logging_config import job_logger
|
|
6
6
|
from ..data.promise.post_processing_promise import PostProcessingPromise
|
|
7
|
+
from ..config.log_context import phase
|
|
7
8
|
from ..enum.media_type import MediaType
|
|
8
9
|
from ..enum.runtime_phase import RuntimePhase
|
|
9
10
|
from ..util.http_utils import get_job_id_from_url
|
|
@@ -32,15 +33,19 @@ class PostProcessingTask(AsyncTask):
|
|
|
32
33
|
job_response.content_type = MediaType.APPLICATION_JSON
|
|
33
34
|
|
|
34
35
|
try:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
# RuntimePhase.POST_PROCESSING duoc dinh nghia nhung truoc day khong
|
|
37
|
+
# noi nao dung, nen code user chay trong post_processing_fn khong he
|
|
38
|
+
# co phase trong log lan trong ErrorReport.
|
|
39
|
+
with phase(RuntimePhase.POST_PROCESSING):
|
|
40
|
+
self.logger.info("Execute post_processing ...")
|
|
41
|
+
job_result_post_processing = self.post_processing_fn(
|
|
42
|
+
self.promise.job_result)
|
|
43
|
+
|
|
44
|
+
# The old print() went straight to stdout, bypassing the
|
|
45
|
+
# sink, so it was neither redacted nor tagged with a job_id.
|
|
46
|
+
self.logger.debug(
|
|
47
|
+
f"Parsing job result: type={type(job_result_post_processing).__name__}")
|
|
48
|
+
job_response.job_result = parse(job_result_post_processing)
|
|
44
49
|
|
|
45
50
|
update_job_metadata(job_response, self.promise.callback_url.on_done)
|
|
46
51
|
|
{quapp_common-0.0.14.dev5 → 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.dev5 → 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.dev5 → 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":"..."}
|