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.
Files changed (110) hide show
  1. {quapp_common-0.0.14.dev6/quapp_common.egg-info → quapp_common-0.0.14.dev7}/PKG-INFO +1 -1
  2. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/pyproject.toml +1 -1
  3. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/async_tasks/async_invocation_task.py +5 -4
  4. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/async_tasks/post_processing_task.py +2 -2
  5. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/invocation.py +4 -3
  6. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/job_fetching.py +4 -3
  7. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/bridge.py +7 -7
  8. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/circuit_adapter.py +32 -29
  9. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/device/device_selection.py +5 -5
  10. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/dispatcher.py +41 -37
  11. quapp_common-0.0.14.dev7/quapp_common/config/log_context.py +201 -0
  12. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/config/logging_config.py +91 -84
  13. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/config/thread_config.py +6 -5
  14. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/response/error_report.py +24 -19
  15. quapp_common-0.0.14.dev7/quapp_common/enum/error_category.py +92 -0
  16. quapp_common-0.0.14.dev7/quapp_common/enum/runtime_phase.py +105 -0
  17. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/handler/handler.py +4 -3
  18. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/model/device/device.py +12 -10
  19. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/model/invocation.py +6 -5
  20. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/util/error_classifier.py +137 -125
  21. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/util/invocation_failure.py +26 -21
  22. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/util/response_utils.py +23 -20
  23. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7/quapp_common.egg-info}/PKG-INFO +1 -1
  24. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_circuit_adapter.py +14 -13
  25. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_dispatcher_runner_error.py +9 -9
  26. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_error_classifier.py +18 -15
  27. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_error_log_dedupe.py +15 -12
  28. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_error_report.py +16 -13
  29. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_invocation_failure.py +6 -6
  30. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_log_secret_leak.py +28 -24
  31. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_logging_config.py +29 -26
  32. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_no_sdk_dependency.py +19 -19
  33. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_phase_coverage.py +14 -13
  34. quapp_common-0.0.14.dev6/quapp_common/config/log_context.py +0 -194
  35. quapp_common-0.0.14.dev6/quapp_common/enum/error_category.py +0 -91
  36. quapp_common-0.0.14.dev6/quapp_common/enum/runtime_phase.py +0 -118
  37. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/LICENSE +0 -0
  38. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/README.md +0 -0
  39. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/__init__.py +0 -0
  40. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/async_tasks/__init__.py +0 -0
  41. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/async_tasks/async_task.py +0 -0
  42. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/async_tasks/export_circuit_task.py +0 -0
  43. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/__init__.py +0 -0
  44. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/__init__.py +0 -0
  45. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/job_fetcher.py +0 -0
  46. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/job_heartbeat.py +0 -0
  47. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/backend/job_manager.py +0 -0
  48. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/callback/__init__.py +0 -0
  49. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/callback/update_job_metadata.py +0 -0
  50. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/device/__init__.py +0 -0
  51. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/component/result_serializer.py +0 -0
  52. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/config/__init__.py +0 -0
  53. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/__init__.py +0 -0
  54. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/async_task/__init__.py +0 -0
  55. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
  56. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
  57. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
  58. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/backend/__init__.py +0 -0
  59. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/backend/backend_information.py +0 -0
  60. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/callback/__init__.py +0 -0
  61. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/callback/callback_url.py +0 -0
  62. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/device/__init__.py +0 -0
  63. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/device/circuit_running_option.py +0 -0
  64. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/promise/__init__.py +0 -0
  65. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/promise/post_processing_promise.py +0 -0
  66. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/promise/promise.py +0 -0
  67. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/request/__init__.py +0 -0
  68. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/request/invocation_request.py +0 -0
  69. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/request/job_fetching_request.py +0 -0
  70. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/request/request.py +0 -0
  71. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/response/__init__.py +0 -0
  72. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/response/authentication.py +0 -0
  73. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/response/custom_header.py +0 -0
  74. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/data/response/job_response.py +0 -0
  75. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/__init__.py +0 -0
  76. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/base_enum.py +0 -0
  77. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/http_header.py +0 -0
  78. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/invocation_step.py +0 -0
  79. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/language.py +0 -0
  80. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/media_type.py +0 -0
  81. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/processing_unit.py +0 -0
  82. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/provider_tag.py +0 -0
  83. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/sdk.py +0 -0
  84. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/status/__init__.py +0 -0
  85. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/status/job_status.py +0 -0
  86. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/status/status_code.py +0 -0
  87. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/enum/token_type.py +0 -0
  88. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/factory/__init__.py +0 -0
  89. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/factory/device_factory.py +0 -0
  90. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/factory/handler_factory.py +0 -0
  91. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/factory/provider_factory.py +0 -0
  92. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/handler/__init__.py +0 -0
  93. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/model/__init__.py +0 -0
  94. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/model/device/__init__.py +0 -0
  95. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/model/device/custom_device.py +0 -0
  96. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/model/provider/__init__.py +0 -0
  97. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/model/provider/provider.py +0 -0
  98. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/util/__init__.py +0 -0
  99. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/util/file_utils.py +0 -0
  100. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/util/http_utils.py +0 -0
  101. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common/util/json_parser_utils.py +0 -0
  102. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common.egg-info/SOURCES.txt +0 -0
  103. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common.egg-info/dependency_links.txt +0 -0
  104. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common.egg-info/requires.txt +0 -0
  105. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/quapp_common.egg-info/top_level.txt +0 -0
  106. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/setup.cfg +0 -0
  107. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_job_heartbeat.py +0 -0
  108. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_json_parser_utils.py +0 -0
  109. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_language_dispatcher.py +0 -0
  110. {quapp_common-0.0.14.dev6 → quapp_common-0.0.14.dev7}/tests/test_result_serializer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quapp-common
3
- Version: 0.0.14.dev6
3
+ Version: 0.0.14.dev7
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)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quapp-common"
7
- version = "0.0.14.dev6"
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
- # 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.
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() cũ ghi thẳng ra stdout, không qua sink nên không được
45
- # redact và không có job_id.
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)
@@ -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 tách riêng khỏi EXECUTION: lỗi authenticate
134
- # với provider và lỗi provider từ chối circuit là hai chuyện khác
135
- # nhau, trước đây cùng ra nhãn EXECUTION.
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
 
@@ -182,9 +182,10 @@ class JobFetching(ABC):
182
182
  PostProcessingTask(post_processing_fn=post_processing_fn,
183
183
  promise=promise).do()
184
184
 
185
- # Trước đây hai method dưới là @staticmethod nhưng vẫn dùng `self.logger`,
186
- # nên chúng raise NameError ngay khi được gọi -- biến lỗi analysis thành một
187
- # lỗi vô nghĩa hoàn toàn khác. Bỏ @staticmethod để `self` hợp lệ.
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() đã 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
+ # 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="Handler không trả về field 'result'.")
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"Field 'result' phải là object, nhận được "
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="Handler không trả về field 'result'.")
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
@@ -2,25 +2,27 @@
2
2
  # circuit_adapter.py
3
3
  # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
4
 
5
- """Chuyển circuit từ định dạng trung gian (OpenQASM / QUBO / JSON) sang object
6
- native của từng SDK.
7
-
8
- Mọi SDK được import LAZY, ngay bên trong handler cần nó. Lý do:
9
-
10
- ``quapp-common`` không được phụ thuộc SDK nào. Mỗi lib con
11
- (``quapp-qiskit``, ``qapp-pennylane``, ...) tự khai báo SDK của nó, và một
12
- function image chỉ cài đúng SDK nó dùng. Nếu file này import ở module level thì:
13
-
14
- - ``import quapp_common.component.bridge`` đòi có ĐỦ 6 SDK cùng lúc, kể cả khi
15
- function chỉ chạy Qiskit -- điều này vô hiệu hoá thiết kế lazy-import của
16
- ``js-*/index.py`` trong quapp-sdk-templates (comment trong đó ghi rõ ý định
17
- "so the server can start even if optional dependencies are missing");
18
- - cài chung 6 SDK gây xung đột version thật, đã xảy ra với ``pytket``/``lark``
19
- và ``pennylane-rigetti``/``pyquil``.
20
-
21
- Khi SDK thiếu, handler raise ``ModuleNotFoundError`` -- classifier map thành
22
- ``DEPENDENCY_MISSING`` và user nhận được hướng dẫn thêm package vào
23
- requirements.txt.
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 module của SDK, báo lỗi rõ ràng nếu thiếu.
39
+ """Import an SDK module, failing with a clear message when it is absent.
38
40
 
39
- @param module_name: tên module cần import, ví dụ 'qiskit.qasm2'.
40
- @param package_hint: tên package trên PyPI để nhắc user, nếu khác module.
41
- @raise ModuleNotFoundError: giữ đúng kiểu này để classifier phân loại thành
42
- DEPENDENCY_MISSING.
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}' chưa được cài trong function image nhưng "
50
- f"circuit cần nó. Thêm '{package}' vào requirements.txt của "
51
- f"function rồi deploy lại.") from exception
52
+ 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 thành Qiskit QuantumCircuit.
62
+ """Parse an OpenQASM 2.0 string into a Qiskit QuantumCircuit.
60
63
 
61
- Tương thích Qiskit >= 1.0 (``qiskit.qasm2.loads``) và < 1.0
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
- # 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.
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}. Xem Function Log để biết chi tiết.')
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 chứa cả `authentication` = token của provider, nên
78
- # chỉ log các field không phải secret.
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 tới runner của các runtime không phải Python (JS / Q# / C).
5
+ """Dispatch a request to the runner of a non-Python runtime (JS / Q# / C).
6
6
 
7
- Về error handling: trước đây ``subprocess.run`` được gọi với ``check=True``, nên
8
- ``CalledProcessError`` được raise TRƯỚC khi kịp đọc output của runner. Hệ quả là
9
- nhánh ``_extract_error_message()`` -- vốn để đọc ``{"status":"error",...}`` mà
10
- ``handler_runner.c`` đã phát ra đúng chuẩn -- thành dead code, và user chỉ nhận
11
- được ``Command '[...]' returned non-zero exit status 1``.
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
- Nay ``check=True`` được bỏ và output được phân loại thành ``HandlerRuntimeError``
14
- mang đủ ngữ cảnh: loại lỗi, message, stack của runner, exit code, signal. Sáu
15
- tình huống được phân biệt:
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 không tồn tại trong image (lỗi build, không phải lỗi user)
18
+ 1. the runner is absent from the image (a build problem, not the user's)
18
19
  2. timeout
19
- 3. process bị signal (returncode < 0) -- ví dụ handler.c segfault, stdout rỗng
20
- 4. runner phát structured error trên stdout theo contract
21
- 5. exit code khác 0 nhưng không có structured error -- lấy stderr
22
- 6. exit code 0 nhưng stdout không phải JSON hợp lệ
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
- # Giới hạn độ dài stack/stderr đưa vào exception để không làm phình job_result.
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
- """Lỗi phát sinh trong handler do user viết (JS / Q# / C).
60
+ """An error raised inside the user-written handler (JS / Q# / C).
59
61
 
60
- Giữ lại stack trace của runtime gốc (JS stack, .NET stack) -- đây mới là thứ
61
- user cần để sửa handler, không phải traceback Python.
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: khi language chưa có runner.
100
- HandlerRuntimeError: khi handler lỗi, crash, timeout hoặc trả output sai.
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'Handler vượt giới hạn thời gian {effective_timeout}s.',
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
- # Runner không có trong image -- lỗi build/deploy, không phải lỗi user.
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"Không tìm thấy runner '{runner['command'][0]}' trong "
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) Bị kết thúc bởi signal: returncode âm. Thường gặp nhất là handler.c
144
- # truy cập bộ nhớ không hợp lệ -- process chết, stdout rỗng.
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'Handler bị kết thúc bởi signal {name}. Thường do truy '
152
- f'cập bộ nhớ không hợp lệ trong handler.',
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
- # Contract cũ: `error` là một string. Vẫn phải đọc được.
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) Exit khác 0 nhưng không có structured error -- lấy stderr làm message.
176
- # Đây là đường đi hiện tại của handler_runner.js và handler_runner.cs, vốn
177
- # ghi stack ra stderr rồi exit 1.
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 nhưng output không dùng được.
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='Handler không trả về JSON hợp lệ trên stdout. Kiểm tra '
198
- 'xem có print/console.log lẫn vào output không.',
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, trả None nếu không parse được (thay vì raise)."""
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
- Giữ lại cho tương thích ngược với code gọi từ ngoài; logic phân loại chính
255
- đã chuyển vào `dispatch()`.
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)