quapp-common 0.0.14.dev8__tar.gz → 0.0.14.dev10__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.dev8/quapp_common.egg-info → quapp_common-0.0.14.dev10}/PKG-INFO +1 -1
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/pyproject.toml +1 -1
- quapp_common-0.0.14.dev10/quapp_common/component/callback/update_job_metadata.py +157 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/dispatcher.py +0 -4
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/util/error_classifier.py +17 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10/quapp_common.egg-info}/PKG-INFO +1 -1
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common.egg-info/SOURCES.txt +1 -0
- quapp_common-0.0.14.dev10/tests/test_callback_payload.py +173 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/tests/test_dispatcher_runner_error.py +3 -3
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/tests/test_language_dispatcher.py +2 -8
- quapp_common-0.0.14.dev8/quapp_common/component/callback/update_job_metadata.py +0 -95
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/LICENSE +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/README.md +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/async_tasks/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/async_tasks/async_invocation_task.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/async_tasks/async_task.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/async_tasks/export_circuit_task.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/async_tasks/post_processing_task.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/backend/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/backend/invocation.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/backend/job_fetcher.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/backend/job_fetching.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/backend/job_heartbeat.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/backend/job_manager.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/bridge.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/callback/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/circuit_adapter.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/device/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/device/device_selection.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/result_serializer.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/config/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/config/log_context.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/config/logging_config.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/config/thread_config.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/async_task/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/backend/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/backend/backend_information.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/callback/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/callback/callback_url.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/device/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/device/circuit_running_option.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/promise/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/promise/post_processing_promise.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/promise/promise.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/request/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/request/invocation_request.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/request/job_fetching_request.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/request/request.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/response/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/response/authentication.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/response/custom_header.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/response/error_report.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/response/job_response.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/base_enum.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/error_category.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/http_header.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/invocation_step.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/language.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/media_type.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/processing_unit.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/provider_tag.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/runtime_phase.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/sdk.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/status/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/status/job_status.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/status/status_code.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/token_type.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/factory/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/factory/device_factory.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/factory/handler_factory.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/factory/provider_factory.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/handler/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/handler/handler.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/model/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/model/device/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/model/device/custom_device.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/model/device/device.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/model/invocation.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/model/provider/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/model/provider/provider.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/util/__init__.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/util/file_utils.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/util/http_utils.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/util/invocation_failure.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/util/json_parser_utils.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/util/response_utils.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common.egg-info/dependency_links.txt +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common.egg-info/requires.txt +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common.egg-info/top_level.txt +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/setup.cfg +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/tests/test_circuit_adapter.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/tests/test_error_classifier.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/tests/test_error_log_dedupe.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/tests/test_error_report.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/tests/test_invocation_failure.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/tests/test_job_heartbeat.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/tests/test_json_parser_utils.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/tests/test_log_secret_leak.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/tests/test_logging_config.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/tests/test_no_sdk_dependency.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/tests/test_phase_coverage.py +0 -0
- {quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/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.dev10"
|
|
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" }]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# update_job_metadata.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from ..backend.job_manager import JobManager
|
|
11
|
+
from ...config.log_context import phase
|
|
12
|
+
from ...config.logging_config import job_logger
|
|
13
|
+
from ...data.response.job_response import JobResponse
|
|
14
|
+
from ...enum.runtime_phase import RuntimePhase
|
|
15
|
+
from ...util.http_utils import create_application_json_header, \
|
|
16
|
+
get_job_id_from_url
|
|
17
|
+
from ...util.response_utils import generate_response
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _sanitize(value):
|
|
21
|
+
"""Replace what strict JSON cannot carry, recursively.
|
|
22
|
+
|
|
23
|
+
Only used on the fallback path. NaN/Infinity become None because
|
|
24
|
+
``allow_nan=False`` rejects them -- which is what requests did too, so the
|
|
25
|
+
fallback still produces valid JSON instead of a bare ``NaN`` literal the
|
|
26
|
+
backend cannot parse.
|
|
27
|
+
"""
|
|
28
|
+
if isinstance(value, dict):
|
|
29
|
+
return {key: _sanitize(item) for key, item in value.items()}
|
|
30
|
+
if isinstance(value, (list, tuple)):
|
|
31
|
+
return [_sanitize(item) for item in value]
|
|
32
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
33
|
+
return None
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _json_payload(payload: dict, logger) -> str:
|
|
38
|
+
"""Serialize the callback body, degrading what json cannot encode to text.
|
|
39
|
+
|
|
40
|
+
``requests.patch(json=...)`` calls ``json.dumps(..., allow_nan=False)`` with
|
|
41
|
+
no ``default=``, so ONE object json cannot encode anywhere inside
|
|
42
|
+
job_response raised TypeError and the request never left the process. Worse,
|
|
43
|
+
the ``except`` path below then sends the SAME body to the on_error callback,
|
|
44
|
+
which fails identically -- the job is left RUNNING with nothing reported.
|
|
45
|
+
|
|
46
|
+
That really happened: quapp-pennylane put PennyLane's ``Shots`` object into
|
|
47
|
+
job_result and "Object of type Shots is not JSON serializable" took down both
|
|
48
|
+
callbacks of job 2b96f552-787d-40bd-8cfb-b5fe40983fd2.
|
|
49
|
+
|
|
50
|
+
The strict pass runs first, so a well-formed body is serialized exactly as
|
|
51
|
+
requests would have; only a body that would otherwise raise falls back.
|
|
52
|
+
Coercing to text is right here: these values are for display, and losing the
|
|
53
|
+
job is far worse than showing ``Shots(total_shots=1024)``.
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
return json.dumps(payload, allow_nan=False)
|
|
57
|
+
except (TypeError, ValueError) as exception:
|
|
58
|
+
logger.warning(
|
|
59
|
+
f'Callback body is not strictly JSON serializable ({exception}); '
|
|
60
|
+
f'sending it with those values coerced to text. The value comes '
|
|
61
|
+
f'from a provider lib and should be converted to a plain type '
|
|
62
|
+
f'before it reaches job_response.')
|
|
63
|
+
return json.dumps(_sanitize(payload), allow_nan=False, default=str)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def update_job_metadata(job_response: JobResponse, callback_url: str):
|
|
67
|
+
# Helpers to improve readability and remove duplication
|
|
68
|
+
def _log_http_result(tag: str, resp):
|
|
69
|
+
logger.info(
|
|
70
|
+
f"Request to {tag} completed with status code: {resp.status_code}")
|
|
71
|
+
if resp.status_code in (401, 403):
|
|
72
|
+
logger.error(
|
|
73
|
+
f"Request to {tag} was rejected with status code {resp.status_code}. "
|
|
74
|
+
f"The job update is lost, so the platform will not see this state change. "
|
|
75
|
+
f"Check that the response carries the user token and the project header.")
|
|
76
|
+
elif not resp.ok:
|
|
77
|
+
logger.warning(
|
|
78
|
+
f"Unexpected status code received from {tag}: {resp.status_code}, response content: {resp.content}")
|
|
79
|
+
|
|
80
|
+
def _update_local_status() -> None:
|
|
81
|
+
"""
|
|
82
|
+
Update local JobManager status based on callback URL semantics:
|
|
83
|
+
- contains 'FAILED' => FAILED
|
|
84
|
+
- contains 'FINALIZATION_PASSED' => DONE
|
|
85
|
+
- otherwise => RUNNING
|
|
86
|
+
"""
|
|
87
|
+
url_upper = (callback_url or "").upper()
|
|
88
|
+
if "FAILED" in url_upper:
|
|
89
|
+
JobManager.set_failed(job_id=job_id)
|
|
90
|
+
elif "FINALIZATION_PASSED" in url_upper:
|
|
91
|
+
JobManager.set_done(job_id=job_id)
|
|
92
|
+
else:
|
|
93
|
+
JobManager.set_running(job_id=job_id)
|
|
94
|
+
|
|
95
|
+
def _patch_scheduler() -> None:
|
|
96
|
+
job_details = JobManager.get_job(job_id)
|
|
97
|
+
if job_details is None:
|
|
98
|
+
logger.warning(
|
|
99
|
+
f"Job {job_id} not found in JobManager, skipping scheduler patch")
|
|
100
|
+
return
|
|
101
|
+
scheduler_callback_url = job_details.get('meta', {}).get(
|
|
102
|
+
'scheduler_callback_url')
|
|
103
|
+
if not scheduler_callback_url:
|
|
104
|
+
logger.debug(
|
|
105
|
+
f"No scheduler callback URL for job {job_id}, skipping")
|
|
106
|
+
return
|
|
107
|
+
scheduler_payload = {'job_id' : job_id,
|
|
108
|
+
'status' : job_details.get('status'),
|
|
109
|
+
'updated_at': job_details.get('updated_at')}
|
|
110
|
+
scheduler_response = requests.patch(url=scheduler_callback_url,
|
|
111
|
+
json=scheduler_payload)
|
|
112
|
+
_log_http_result('scheduler', scheduler_response)
|
|
113
|
+
|
|
114
|
+
job_id = get_job_id_from_url(callback_url)
|
|
115
|
+
logger = job_logger(job_id)
|
|
116
|
+
logger.info(
|
|
117
|
+
f'Calling backend to update job metadata at URL: {callback_url}')
|
|
118
|
+
|
|
119
|
+
if not getattr(job_response, 'user_token', None):
|
|
120
|
+
logger.error(
|
|
121
|
+
'Job update is being sent without a user token, the backend will reject it. '
|
|
122
|
+
'The response was built without the invocation credentials.')
|
|
123
|
+
|
|
124
|
+
request_body = generate_response(job_response)
|
|
125
|
+
|
|
126
|
+
# The callback runs after the caller's `with phase(...)` has closed, so a
|
|
127
|
+
# failure here was reported with phase=None -- what happened to job
|
|
128
|
+
# 2b96f552-787d-40bd-8cfb-b5fe40983fd2. RuntimePhase.CALLBACK was declared
|
|
129
|
+
# for exactly this and had no place that opened it.
|
|
130
|
+
with phase(RuntimePhase.CALLBACK):
|
|
131
|
+
try:
|
|
132
|
+
request_headers = create_application_json_header(
|
|
133
|
+
token=job_response.user_token,
|
|
134
|
+
project_header=job_response.project_header,
|
|
135
|
+
workspace_header=job_response.workspace_header)
|
|
136
|
+
|
|
137
|
+
# data= rather than json=: the body goes through _json_payload so no
|
|
138
|
+
# provider object can kill this callback. Content-Type is already
|
|
139
|
+
# application/json in the headers above.
|
|
140
|
+
response = requests.patch(
|
|
141
|
+
url=callback_url,
|
|
142
|
+
data=_json_payload({'data': request_body}, logger),
|
|
143
|
+
headers=request_headers)
|
|
144
|
+
|
|
145
|
+
# Log backend response
|
|
146
|
+
_log_http_result('backend', response)
|
|
147
|
+
|
|
148
|
+
# Update local job status and notify scheduler only after backend confirms
|
|
149
|
+
_update_local_status()
|
|
150
|
+
_patch_scheduler()
|
|
151
|
+
|
|
152
|
+
except Exception as exception:
|
|
153
|
+
logger.exception(
|
|
154
|
+
f'Error occurred while calling backend: {exception}',
|
|
155
|
+
exc_info=True)
|
|
156
|
+
|
|
157
|
+
raise exception
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/util/error_classifier.py
RENAMED
|
@@ -329,6 +329,23 @@ register_rules(
|
|
|
329
329
|
code='INPUT_TOO_FEW_ITEMS',
|
|
330
330
|
resolution='A field in the input has fewer items than expected.'),
|
|
331
331
|
|
|
332
|
+
# --- serialization -------------------------------------------------
|
|
333
|
+
# A provider lib left a vendor object in job_response and json could not
|
|
334
|
+
# encode it, so the callback body never went out. This is a lib bug, not
|
|
335
|
+
# a user one: it landed as category UNKNOWN before this rule, which told
|
|
336
|
+
# nobody anything (job 2b96f552-787d-40bd-8cfb-b5fe40983fd2, PennyLane's
|
|
337
|
+
# Shots object). Placed above the vaguer rules because the message is
|
|
338
|
+
# unmistakable.
|
|
339
|
+
Rule(match=lambda e: isinstance(e, TypeError)
|
|
340
|
+
and 'is not json serializable' in str(e).lower(),
|
|
341
|
+
category=ErrorCategory.PLATFORM_INTERNAL,
|
|
342
|
+
code='PLATFORM_RESULT_NOT_SERIALIZABLE',
|
|
343
|
+
resolution='An object in the job result cannot be converted to '
|
|
344
|
+
'JSON, so the platform could not report the result.',
|
|
345
|
+
steps=('Report this to the Quapp team with the job id',
|
|
346
|
+
'The provider lib has to convert the value to a plain type '
|
|
347
|
+
'before it goes into job_result')),
|
|
348
|
+
|
|
332
349
|
# --- dependency ----------------------------------------------------
|
|
333
350
|
Rule(match=lambda e: isinstance(e, ModuleNotFoundError),
|
|
334
351
|
category=ErrorCategory.DEPENDENCY,
|
|
@@ -89,6 +89,7 @@ quapp_common/util/http_utils.py
|
|
|
89
89
|
quapp_common/util/invocation_failure.py
|
|
90
90
|
quapp_common/util/json_parser_utils.py
|
|
91
91
|
quapp_common/util/response_utils.py
|
|
92
|
+
tests/test_callback_payload.py
|
|
92
93
|
tests/test_circuit_adapter.py
|
|
93
94
|
tests/test_dispatcher_runner_error.py
|
|
94
95
|
tests/test_error_classifier.py
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# test_callback_payload.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
"""One unserializable value must not cost us the whole job.
|
|
6
|
+
|
|
7
|
+
`requests.patch(json=...)` calls `json.dumps(..., allow_nan=False)` with no
|
|
8
|
+
`default=`, so a single object json cannot encode anywhere inside `job_response`
|
|
9
|
+
raised TypeError before the request ever left the process. The `except` path then
|
|
10
|
+
sent the SAME body to the on_error callback, which failed identically -- so the
|
|
11
|
+
backend heard nothing at all and the job stayed RUNNING forever.
|
|
12
|
+
|
|
13
|
+
Seen in production on job 2b96f552-787d-40bd-8cfb-b5fe40983fd2: quapp-pennylane
|
|
14
|
+
put PennyLane's `Shots` object into job_result and both callbacks died with
|
|
15
|
+
"Object of type Shots is not JSON serializable".
|
|
16
|
+
|
|
17
|
+
The provider lib is fixed separately (shots is coerced to int). These tests lock
|
|
18
|
+
the platform-side guarantee: the callback goes out regardless.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
|
|
23
|
+
import pytest
|
|
24
|
+
|
|
25
|
+
from quapp_common.component.callback import update_job_metadata as module
|
|
26
|
+
from quapp_common.component.callback.update_job_metadata import (
|
|
27
|
+
_json_payload, update_job_metadata,
|
|
28
|
+
)
|
|
29
|
+
from quapp_common.config.logging_config import job_logger
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Shots:
|
|
33
|
+
"""Stands in for pennylane.measurements.Shots: json cannot encode it."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, total):
|
|
36
|
+
self.total_shots = total
|
|
37
|
+
|
|
38
|
+
def __str__(self):
|
|
39
|
+
return f'Shots(total_shots={self.total_shots})'
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture
|
|
43
|
+
def logger():
|
|
44
|
+
return job_logger('job_payload')
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------- the payload itself
|
|
48
|
+
|
|
49
|
+
def test_serializable_body_is_untouched(logger):
|
|
50
|
+
body = {'data': {'jobResult': {'counts': {'00': 512}}, 'shots': 1024}}
|
|
51
|
+
|
|
52
|
+
assert _json_payload(body, logger) == json.dumps(body, allow_nan=False)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_unserializable_value_becomes_text(logger):
|
|
56
|
+
body = {'data': {'shots': Shots(1024)}}
|
|
57
|
+
|
|
58
|
+
payload = json.loads(_json_payload(body, logger))
|
|
59
|
+
|
|
60
|
+
assert payload['data']['shots'] == 'Shots(total_shots=1024)'
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_nan_does_not_produce_invalid_json(logger):
|
|
64
|
+
"""NaN is not JSON. requests rejected it too (allow_nan=False), so the
|
|
65
|
+
fallback has to drop it rather than emit a bare NaN literal the backend
|
|
66
|
+
cannot parse."""
|
|
67
|
+
body = {'data': {'probability': float('nan'), 'shots': Shots(8)}}
|
|
68
|
+
|
|
69
|
+
text = _json_payload(body, logger)
|
|
70
|
+
|
|
71
|
+
assert 'NaN' not in text
|
|
72
|
+
assert json.loads(text)['data']['probability'] is None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_nested_unserializable_value_is_reached(logger):
|
|
76
|
+
body = {'data': {'jobResult': {'meta': [{'shots': Shots(2048)}]}}}
|
|
77
|
+
|
|
78
|
+
payload = json.loads(_json_payload(body, logger))
|
|
79
|
+
|
|
80
|
+
assert payload['data']['jobResult']['meta'][0]['shots'] == \
|
|
81
|
+
'Shots(total_shots=2048)'
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ------------------------------------ the callback actually goes out
|
|
85
|
+
|
|
86
|
+
class FakeResponse:
|
|
87
|
+
status_code = 200
|
|
88
|
+
ok = True
|
|
89
|
+
content = b'{}'
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.fixture
|
|
93
|
+
def sent(monkeypatch):
|
|
94
|
+
"""Capture the outgoing PATCH instead of making one."""
|
|
95
|
+
calls = []
|
|
96
|
+
|
|
97
|
+
def _patch(url, data=None, json=None, headers=None, **kwargs):
|
|
98
|
+
calls.append({'url': url, 'data': data, 'json': json})
|
|
99
|
+
return FakeResponse()
|
|
100
|
+
|
|
101
|
+
monkeypatch.setattr(module.requests, 'patch', _patch)
|
|
102
|
+
monkeypatch.setattr(module.JobManager, 'set_running', lambda job_id: None)
|
|
103
|
+
monkeypatch.setattr(module.JobManager, 'set_done', lambda job_id: None)
|
|
104
|
+
monkeypatch.setattr(module.JobManager, 'set_failed', lambda job_id: None)
|
|
105
|
+
monkeypatch.setattr(module.JobManager, 'get_job', lambda job_id: None)
|
|
106
|
+
return calls
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_callback_is_sent_even_with_a_provider_object(monkeypatch, sent):
|
|
110
|
+
"""The regression itself: this used to raise TypeError instead of sending."""
|
|
111
|
+
monkeypatch.setattr(module, 'generate_response',
|
|
112
|
+
lambda job_response: {'shots': Shots(1024)})
|
|
113
|
+
|
|
114
|
+
class Response:
|
|
115
|
+
user_token = 'token'
|
|
116
|
+
project_header = None
|
|
117
|
+
workspace_header = None
|
|
118
|
+
|
|
119
|
+
update_job_metadata(Response(),
|
|
120
|
+
'http://backend/jobs/job_1/ANALYSIS_PASSED')
|
|
121
|
+
|
|
122
|
+
assert len(sent) == 1, 'the callback never went out'
|
|
123
|
+
assert sent[0]['json'] is None, 'body must be pre-serialized, not json='
|
|
124
|
+
assert 'Shots(total_shots=1024)' in sent[0]['data']
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ------------------------------- the error is classified, not "UNKNOWN"
|
|
128
|
+
|
|
129
|
+
def test_serialization_failure_gets_a_real_category():
|
|
130
|
+
"""It arrived as category=UNKNOWN, owner=UNKNOWN, which told nobody anything.
|
|
131
|
+
|
|
132
|
+
A vendor object in job_result is a provider-lib bug, so it belongs to the
|
|
133
|
+
platform team, not the user.
|
|
134
|
+
"""
|
|
135
|
+
from quapp_common.enum.error_category import ErrorCategory
|
|
136
|
+
from quapp_common.enum.runtime_phase import RuntimePhase
|
|
137
|
+
from quapp_common.util.error_classifier import classify
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
json.dumps({'shots': Shots(1024)})
|
|
141
|
+
except TypeError as caught:
|
|
142
|
+
error = caught
|
|
143
|
+
|
|
144
|
+
rule, _ = classify(error, RuntimePhase.ANALYSIS)
|
|
145
|
+
|
|
146
|
+
assert rule.code == 'PLATFORM_RESULT_NOT_SERIALIZABLE'
|
|
147
|
+
assert rule.category is ErrorCategory.PLATFORM_INTERNAL
|
|
148
|
+
assert rule.steps
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_callback_failure_carries_the_callback_phase(monkeypatch, sent):
|
|
152
|
+
"""The caller's `with phase(...)` has already closed by callback time, so the
|
|
153
|
+
report said phase=None. RuntimePhase.CALLBACK was declared for this."""
|
|
154
|
+
from quapp_common.config.log_context import bind_job, effective_phase
|
|
155
|
+
from quapp_common.enum.runtime_phase import RuntimePhase
|
|
156
|
+
|
|
157
|
+
def _boom(url, **kwargs):
|
|
158
|
+
raise ConnectionError('backend unreachable')
|
|
159
|
+
|
|
160
|
+
monkeypatch.setattr(module.requests, 'patch', _boom)
|
|
161
|
+
monkeypatch.setattr(module, 'generate_response', lambda job_response: {})
|
|
162
|
+
|
|
163
|
+
class Response:
|
|
164
|
+
user_token = 'token'
|
|
165
|
+
project_header = None
|
|
166
|
+
workspace_header = None
|
|
167
|
+
|
|
168
|
+
bind_job('job_phase')
|
|
169
|
+
with pytest.raises(ConnectionError):
|
|
170
|
+
update_job_metadata(Response(),
|
|
171
|
+
'http://backend/jobs/job_phase/ANALYSIS_PASSED')
|
|
172
|
+
|
|
173
|
+
assert effective_phase() is RuntimePhase.CALLBACK
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/tests/test_dispatcher_runner_error.py
RENAMED
|
@@ -89,7 +89,7 @@ def test_structured_error_legacy_string_form(fake_run):
|
|
|
89
89
|
'status': 'error', 'error': 'invalid JSON on stdin'})))
|
|
90
90
|
|
|
91
91
|
with pytest.raises(HandlerRuntimeError) as info:
|
|
92
|
-
dispatch(Language.
|
|
92
|
+
dispatch(Language.QSHARP, REQUEST)
|
|
93
93
|
|
|
94
94
|
assert 'invalid JSON on stdin' in str(info.value)
|
|
95
95
|
|
|
@@ -97,11 +97,11 @@ def test_structured_error_legacy_string_form(fake_run):
|
|
|
97
97
|
# ------------------------------------------------------------- (3) killed by signal
|
|
98
98
|
|
|
99
99
|
def test_signal_is_detected(fake_run):
|
|
100
|
-
"""handler
|
|
100
|
+
"""A subprocess handler is killed by a signal: returncode = -11, stdout empty."""
|
|
101
101
|
fake_run(FakeCompletedProcess(returncode=-11, stdout=''))
|
|
102
102
|
|
|
103
103
|
with pytest.raises(HandlerRuntimeError) as info:
|
|
104
|
-
dispatch(Language.
|
|
104
|
+
dispatch(Language.QSHARP, REQUEST)
|
|
105
105
|
|
|
106
106
|
error = info.value
|
|
107
107
|
assert error.error_type == 'Signal'
|
|
@@ -10,14 +10,8 @@ class TestLanguageCEnum(unittest.TestCase):
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class TestRunnerMapLanguageC(unittest.TestCase):
|
|
13
|
-
def
|
|
14
|
-
self.
|
|
15
|
-
|
|
16
|
-
def test_command_is_compiled_binary(self):
|
|
17
|
-
self.assertEqual(RUNNER_MAP[Language.C]['command'], ['./handler_compiled'])
|
|
18
|
-
|
|
19
|
-
def test_timeout(self):
|
|
20
|
-
self.assertEqual(RUNNER_MAP[Language.C]['timeout'], 300)
|
|
13
|
+
def test_language_c_not_registered(self):
|
|
14
|
+
self.assertNotIn(Language.C, RUNNER_MAP)
|
|
21
15
|
|
|
22
16
|
|
|
23
17
|
class TestLanguageCppEnum(unittest.TestCase):
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
# Quapp Platform Project
|
|
2
|
-
# update_job_metadata.py
|
|
3
|
-
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
-
|
|
5
|
-
import requests
|
|
6
|
-
|
|
7
|
-
from ..backend.job_manager import JobManager
|
|
8
|
-
from ...config.logging_config import job_logger
|
|
9
|
-
from ...data.response.job_response import JobResponse
|
|
10
|
-
from ...util.http_utils import create_application_json_header, \
|
|
11
|
-
get_job_id_from_url
|
|
12
|
-
from ...util.response_utils import generate_response
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def update_job_metadata(job_response: JobResponse, callback_url: str):
|
|
16
|
-
# Helpers to improve readability and remove duplication
|
|
17
|
-
def _log_http_result(tag: str, resp):
|
|
18
|
-
logger.info(
|
|
19
|
-
f"Request to {tag} completed with status code: {resp.status_code}")
|
|
20
|
-
if resp.status_code in (401, 403):
|
|
21
|
-
logger.error(
|
|
22
|
-
f"Request to {tag} was rejected with status code {resp.status_code}. "
|
|
23
|
-
f"The job update is lost, so the platform will not see this state change. "
|
|
24
|
-
f"Check that the response carries the user token and the project header.")
|
|
25
|
-
elif not resp.ok:
|
|
26
|
-
logger.warning(
|
|
27
|
-
f"Unexpected status code received from {tag}: {resp.status_code}, response content: {resp.content}")
|
|
28
|
-
|
|
29
|
-
def _update_local_status() -> None:
|
|
30
|
-
"""
|
|
31
|
-
Update local JobManager status based on callback URL semantics:
|
|
32
|
-
- contains 'FAILED' => FAILED
|
|
33
|
-
- contains 'FINALIZATION_PASSED' => DONE
|
|
34
|
-
- otherwise => RUNNING
|
|
35
|
-
"""
|
|
36
|
-
url_upper = (callback_url or "").upper()
|
|
37
|
-
if "FAILED" in url_upper:
|
|
38
|
-
JobManager.set_failed(job_id=job_id)
|
|
39
|
-
elif "FINALIZATION_PASSED" in url_upper:
|
|
40
|
-
JobManager.set_done(job_id=job_id)
|
|
41
|
-
else:
|
|
42
|
-
JobManager.set_running(job_id=job_id)
|
|
43
|
-
|
|
44
|
-
def _patch_scheduler() -> None:
|
|
45
|
-
job_details = JobManager.get_job(job_id)
|
|
46
|
-
if job_details is None:
|
|
47
|
-
logger.warning(
|
|
48
|
-
f"Job {job_id} not found in JobManager, skipping scheduler patch")
|
|
49
|
-
return
|
|
50
|
-
scheduler_callback_url = job_details.get('meta', {}).get(
|
|
51
|
-
'scheduler_callback_url')
|
|
52
|
-
if not scheduler_callback_url:
|
|
53
|
-
logger.debug(
|
|
54
|
-
f"No scheduler callback URL for job {job_id}, skipping")
|
|
55
|
-
return
|
|
56
|
-
scheduler_payload = {'job_id' : job_id,
|
|
57
|
-
'status' : job_details.get('status'),
|
|
58
|
-
'updated_at': job_details.get('updated_at')}
|
|
59
|
-
scheduler_response = requests.patch(url=scheduler_callback_url,
|
|
60
|
-
json=scheduler_payload)
|
|
61
|
-
_log_http_result('scheduler', scheduler_response)
|
|
62
|
-
|
|
63
|
-
job_id = get_job_id_from_url(callback_url)
|
|
64
|
-
logger = job_logger(job_id)
|
|
65
|
-
logger.info(
|
|
66
|
-
f'Calling backend to update job metadata at URL: {callback_url}')
|
|
67
|
-
|
|
68
|
-
if not getattr(job_response, 'user_token', None):
|
|
69
|
-
logger.error(
|
|
70
|
-
'Job update is being sent without a user token, the backend will reject it. '
|
|
71
|
-
'The response was built without the invocation credentials.')
|
|
72
|
-
|
|
73
|
-
request_body = generate_response(job_response)
|
|
74
|
-
|
|
75
|
-
try:
|
|
76
|
-
request_headers = create_application_json_header(
|
|
77
|
-
token=job_response.user_token,
|
|
78
|
-
project_header=job_response.project_header,
|
|
79
|
-
workspace_header=job_response.workspace_header)
|
|
80
|
-
|
|
81
|
-
response = requests.patch(url=callback_url, json={'data': request_body},
|
|
82
|
-
headers=request_headers)
|
|
83
|
-
|
|
84
|
-
# Log backend response
|
|
85
|
-
_log_http_result('backend', response)
|
|
86
|
-
|
|
87
|
-
# Update local job status and notify scheduler only after backend confirms
|
|
88
|
-
_update_local_status()
|
|
89
|
-
_patch_scheduler()
|
|
90
|
-
|
|
91
|
-
except Exception as exception:
|
|
92
|
-
logger.exception(f'Error occurred while calling backend: {exception}',
|
|
93
|
-
exc_info=True)
|
|
94
|
-
|
|
95
|
-
raise exception
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/async_tasks/async_task.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/backend/__init__.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/backend/invocation.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/backend/job_fetcher.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/backend/job_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/callback/__init__.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/circuit_adapter.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/device/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/component/result_serializer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/config/logging_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/async_task/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/backend/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/callback/__init__.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/callback/callback_url.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/promise/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/request/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/response/__init__.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/response/authentication.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/response/custom_header.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/response/error_report.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/data/response/job_response.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/status/job_status.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/enum/status/status_code.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/factory/device_factory.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/factory/handler_factory.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/factory/provider_factory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/model/device/__init__.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/model/device/custom_device.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/model/provider/__init__.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/model/provider/provider.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/util/invocation_failure.py
RENAMED
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common/util/json_parser_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{quapp_common-0.0.14.dev8 → quapp_common-0.0.14.dev10}/quapp_common.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|