classiq 0.58.1__py3-none-any.whl → 0.60.0__py3-none-any.whl
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.
- classiq/_internals/api_wrapper.py +91 -20
- classiq/_internals/client.py +48 -11
- classiq/_internals/jobs.py +47 -40
- classiq/execution/execution_session.py +62 -22
- classiq/execution/jobs.py +64 -23
- classiq/execution/qaoa.py +17 -15
- classiq/execution/qnn.py +17 -18
- classiq/executor.py +2 -1
- classiq/interface/_version.py +1 -1
- classiq/interface/generator/arith/arithmetic_operations.py +1 -0
- classiq/interface/generator/register_role.py +8 -0
- classiq/interface/model/handle_binding.py +22 -3
- classiq/model_expansions/capturing/captured_vars.py +316 -0
- classiq/model_expansions/capturing/mangling_utils.py +18 -9
- classiq/model_expansions/closure.py +29 -74
- classiq/model_expansions/function_builder.py +51 -66
- classiq/model_expansions/interpreter.py +4 -7
- classiq/model_expansions/quantum_operations/bind.py +1 -3
- classiq/model_expansions/quantum_operations/call_emitter.py +46 -11
- classiq/model_expansions/quantum_operations/classicalif.py +2 -5
- classiq/model_expansions/quantum_operations/control.py +13 -16
- classiq/model_expansions/quantum_operations/emitter.py +36 -8
- classiq/model_expansions/quantum_operations/expression_operation.py +9 -19
- classiq/model_expansions/quantum_operations/inplace_binary_operation.py +4 -6
- classiq/model_expansions/quantum_operations/invert.py +5 -8
- classiq/model_expansions/quantum_operations/power.py +5 -10
- classiq/model_expansions/quantum_operations/quantum_assignment_operation.py +1 -3
- classiq/model_expansions/quantum_operations/quantum_function_call.py +1 -3
- classiq/model_expansions/quantum_operations/repeat.py +3 -3
- classiq/model_expansions/quantum_operations/variable_decleration.py +1 -1
- classiq/model_expansions/quantum_operations/within_apply.py +1 -5
- classiq/model_expansions/scope.py +2 -2
- classiq/model_expansions/transformers/var_splitter.py +32 -19
- classiq/model_expansions/utils/handles_collector.py +33 -0
- classiq/model_expansions/visitors/variable_references.py +18 -2
- classiq/qmod/qfunc.py +9 -13
- classiq/qmod/quantum_expandable.py +1 -21
- classiq/qmod/quantum_function.py +16 -0
- {classiq-0.58.1.dist-info → classiq-0.60.0.dist-info}/METADATA +1 -1
- {classiq-0.58.1.dist-info → classiq-0.60.0.dist-info}/RECORD +41 -42
- classiq/interface/executor/aws_execution_cost.py +0 -90
- classiq/model_expansions/capturing/captured_var_manager.py +0 -48
- classiq/model_expansions/capturing/propagated_var_stack.py +0 -194
- {classiq-0.58.1.dist-info → classiq-0.60.0.dist-info}/WHEEL +0 -0
@@ -1,6 +1,7 @@
|
|
1
1
|
import json
|
2
2
|
from typing import Any, Optional, Protocol, TypeVar
|
3
3
|
|
4
|
+
import httpx
|
4
5
|
import pydantic
|
5
6
|
|
6
7
|
import classiq.interface.executor.execution_result
|
@@ -70,13 +71,18 @@ class ApiWrapper:
|
|
70
71
|
url: str,
|
71
72
|
model: pydantic.BaseModel,
|
72
73
|
use_versioned_url: bool = True,
|
74
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
73
75
|
) -> dict:
|
74
76
|
# TODO: we can't use model.dict() - it doesn't serialize complex class.
|
75
77
|
# This was added because JSON serializer doesn't serialize complex type, and pydantic does.
|
76
78
|
# We should add support for smarter json serialization.
|
77
79
|
body = json.loads(model.model_dump_json())
|
78
80
|
return await cls._call_task(
|
79
|
-
http_method,
|
81
|
+
http_method,
|
82
|
+
url,
|
83
|
+
body,
|
84
|
+
use_versioned_url=use_versioned_url,
|
85
|
+
http_client=http_client,
|
80
86
|
)
|
81
87
|
|
82
88
|
@classmethod
|
@@ -89,6 +95,7 @@ class ApiWrapper:
|
|
89
95
|
use_versioned_url: bool = True,
|
90
96
|
headers: Optional[dict[str, str]] = None,
|
91
97
|
allow_none: bool = False,
|
98
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
92
99
|
) -> dict:
|
93
100
|
res: Any = await client().call_api(
|
94
101
|
http_method=http_method,
|
@@ -97,6 +104,7 @@ class ApiWrapper:
|
|
97
104
|
headers=headers,
|
98
105
|
params=params,
|
99
106
|
use_versioned_url=use_versioned_url,
|
107
|
+
http_client=http_client,
|
100
108
|
)
|
101
109
|
if allow_none and res is None:
|
102
110
|
return {}
|
@@ -106,21 +114,35 @@ class ApiWrapper:
|
|
106
114
|
|
107
115
|
@classmethod
|
108
116
|
async def call_generation_task(
|
109
|
-
cls,
|
117
|
+
cls,
|
118
|
+
model: Model,
|
119
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
110
120
|
) -> generator_result.QuantumProgram:
|
111
121
|
poller = JobPoller(base_url=routes.TASKS_GENERATE_FULL_PATH)
|
112
|
-
result = await poller.run_pydantic(
|
122
|
+
result = await poller.run_pydantic(
|
123
|
+
model, timeout_sec=None, http_client=http_client
|
124
|
+
)
|
113
125
|
return _parse_job_response(result, generator_result.QuantumProgram)
|
114
126
|
|
115
127
|
@classmethod
|
116
|
-
async def
|
117
|
-
cls,
|
118
|
-
|
119
|
-
|
128
|
+
async def call_convert_quantum_program(
|
129
|
+
cls,
|
130
|
+
circuit: generator_result.QuantumProgram,
|
131
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
132
|
+
) -> dict:
|
133
|
+
return await cls._call_task_pydantic(
|
120
134
|
http_method=HTTPMethod.POST,
|
121
135
|
url=routes.CONVERSION_GENERATED_CIRCUIT_TO_EXECUTION_INPUT_FULL,
|
122
136
|
model=circuit,
|
137
|
+
http_client=http_client,
|
123
138
|
)
|
139
|
+
|
140
|
+
@classmethod
|
141
|
+
async def call_execute_execution_input(
|
142
|
+
cls,
|
143
|
+
execution_input: dict,
|
144
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
145
|
+
) -> execution_request.ExecutionJobDetails:
|
124
146
|
headers = {
|
125
147
|
_ACCEPT_HEADER: "v1",
|
126
148
|
_CONTENT_TYPE_HEADER: execution_input["version"],
|
@@ -131,6 +153,7 @@ class ApiWrapper:
|
|
131
153
|
url=routes.EXECUTION_JOBS_NON_VERSIONED_FULL_PATH,
|
132
154
|
body=execution_input,
|
133
155
|
use_versioned_url=False,
|
156
|
+
http_client=http_client,
|
134
157
|
)
|
135
158
|
return execution_request.ExecutionJobDetails.model_validate(data)
|
136
159
|
|
@@ -138,6 +161,7 @@ class ApiWrapper:
|
|
138
161
|
async def call_get_execution_job_details(
|
139
162
|
cls,
|
140
163
|
job_id: JobID,
|
164
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
141
165
|
) -> execution_request.ExecutionJobDetails:
|
142
166
|
headers = {_ACCEPT_HEADER: "v1"}
|
143
167
|
data = await cls._call_task(
|
@@ -145,6 +169,7 @@ class ApiWrapper:
|
|
145
169
|
headers=headers,
|
146
170
|
url=f"{routes.EXECUTION_JOBS_NON_VERSIONED_FULL_PATH}/{job_id.job_id}",
|
147
171
|
use_versioned_url=False,
|
172
|
+
http_client=http_client,
|
148
173
|
)
|
149
174
|
return execution_request.ExecutionJobDetails.model_validate(data)
|
150
175
|
|
@@ -153,12 +178,14 @@ class ApiWrapper:
|
|
153
178
|
cls,
|
154
179
|
job_id: JobID,
|
155
180
|
version: str,
|
181
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
156
182
|
) -> classiq.interface.executor.execution_result.ExecuteGeneratedCircuitResults:
|
157
183
|
data = await cls._call_task(
|
158
184
|
http_method=HTTPMethod.GET,
|
159
185
|
url=f"{routes.EXECUTION_JOBS_NON_VERSIONED_FULL_PATH}/{job_id.job_id}/result",
|
160
186
|
use_versioned_url=False,
|
161
187
|
headers={CLASSIQ_ACCEPT_HEADER: version},
|
188
|
+
http_client=http_client,
|
162
189
|
)
|
163
190
|
return classiq.interface.executor.execution_result.ExecuteGeneratedCircuitResults.model_validate(
|
164
191
|
data
|
@@ -169,6 +196,7 @@ class ApiWrapper:
|
|
169
196
|
cls,
|
170
197
|
job_id: JobID,
|
171
198
|
name: str,
|
199
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
172
200
|
) -> ExecutionJobDetailsV1:
|
173
201
|
data = await cls._call_task(
|
174
202
|
http_method=HTTPMethod.PATCH,
|
@@ -177,6 +205,7 @@ class ApiWrapper:
|
|
177
205
|
"name": name,
|
178
206
|
},
|
179
207
|
use_versioned_url=False,
|
208
|
+
http_client=http_client,
|
180
209
|
)
|
181
210
|
return ExecutionJobDetailsV1.model_validate(data)
|
182
211
|
|
@@ -184,12 +213,14 @@ class ApiWrapper:
|
|
184
213
|
async def call_cancel_execution_job(
|
185
214
|
cls,
|
186
215
|
job_id: JobID,
|
216
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
187
217
|
) -> None:
|
188
218
|
await cls._call_task(
|
189
219
|
http_method=HTTPMethod.PUT,
|
190
220
|
url=f"{routes.EXECUTION_JOBS_NON_VERSIONED_FULL_PATH}/{job_id.job_id}/cancel",
|
191
221
|
use_versioned_url=False,
|
192
222
|
allow_none=True,
|
223
|
+
http_client=http_client,
|
193
224
|
)
|
194
225
|
|
195
226
|
@classmethod
|
@@ -197,6 +228,7 @@ class ApiWrapper:
|
|
197
228
|
cls,
|
198
229
|
offset: int,
|
199
230
|
limit: int,
|
231
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
200
232
|
) -> ExecutionJobsQueryResultsV1:
|
201
233
|
data = await cls._call_task(
|
202
234
|
http_method=HTTPMethod.GET,
|
@@ -206,73 +238,92 @@ class ApiWrapper:
|
|
206
238
|
"limit": limit,
|
207
239
|
},
|
208
240
|
use_versioned_url=False,
|
241
|
+
http_client=http_client,
|
209
242
|
)
|
210
243
|
return ExecutionJobsQueryResultsV1.model_validate(data)
|
211
244
|
|
212
245
|
@classmethod
|
213
246
|
async def call_analysis_task(
|
214
|
-
cls,
|
247
|
+
cls,
|
248
|
+
params: analysis_params.AnalysisParams,
|
249
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
215
250
|
) -> analysis_result.Analysis:
|
216
251
|
data = await cls._call_task_pydantic(
|
217
252
|
http_method=HTTPMethod.POST,
|
218
253
|
url=routes.ANALYZER_FULL_PATH,
|
219
254
|
model=params,
|
255
|
+
http_client=http_client,
|
220
256
|
)
|
221
257
|
|
222
258
|
return analysis_result.Analysis.model_validate(data)
|
223
259
|
|
224
260
|
@classmethod
|
225
261
|
async def call_analyzer_app(
|
226
|
-
cls,
|
262
|
+
cls,
|
263
|
+
params: generator_result.QuantumProgram,
|
264
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
227
265
|
) -> analysis_result.DataID:
|
228
266
|
data = await cls._call_task_pydantic(
|
229
267
|
http_method=HTTPMethod.POST,
|
230
268
|
url=routes.ANALYZER_DATA_FULL_PATH,
|
231
269
|
model=params,
|
270
|
+
http_client=http_client,
|
232
271
|
)
|
233
272
|
return analysis_result.DataID.model_validate(data)
|
234
273
|
|
235
274
|
@classmethod
|
236
275
|
async def get_generated_circuit_from_qasm(
|
237
|
-
cls,
|
276
|
+
cls,
|
277
|
+
params: analysis_result.QasmCode,
|
278
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
238
279
|
) -> generator_result.QuantumProgram:
|
239
280
|
data = await cls._call_task_pydantic(
|
240
281
|
http_method=HTTPMethod.POST,
|
241
282
|
url=routes.IDE_QASM_FULL_PATH,
|
242
283
|
model=params,
|
284
|
+
http_client=http_client,
|
243
285
|
)
|
244
286
|
return generator_result.QuantumProgram.model_validate(data)
|
245
287
|
|
246
288
|
@classmethod
|
247
289
|
async def get_analyzer_app_data(
|
248
|
-
cls,
|
290
|
+
cls,
|
291
|
+
params: analysis_result.DataID,
|
292
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
249
293
|
) -> generator_result.QuantumProgram:
|
250
294
|
data = await cls._call_task(
|
251
295
|
http_method=HTTPMethod.GET,
|
252
296
|
url=f"{routes.ANALYZER_DATA_FULL_PATH}/{params.id}",
|
297
|
+
http_client=http_client,
|
253
298
|
)
|
254
299
|
return generator_result.QuantumProgram.model_validate(data)
|
255
300
|
|
256
301
|
@classmethod
|
257
302
|
async def call_rb_analysis_task(
|
258
|
-
cls,
|
303
|
+
cls,
|
304
|
+
params: AnalysisRBParams,
|
305
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
259
306
|
) -> analysis_result.RbResults:
|
260
307
|
data = await cls._call_task(
|
261
308
|
http_method=HTTPMethod.POST,
|
262
309
|
url=routes.ANALYZER_RB_FULL_PATH,
|
263
310
|
body=params.model_dump(),
|
311
|
+
http_client=http_client,
|
264
312
|
)
|
265
313
|
|
266
314
|
return analysis_result.RbResults.model_validate(data)
|
267
315
|
|
268
316
|
@classmethod
|
269
317
|
async def call_hardware_connectivity_task(
|
270
|
-
cls,
|
318
|
+
cls,
|
319
|
+
params: analysis_params.AnalysisHardwareParams,
|
320
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
271
321
|
) -> analysis_result.GraphResult:
|
272
322
|
data = await cls._call_task_pydantic(
|
273
323
|
http_method=HTTPMethod.POST,
|
274
324
|
url=routes.ANALYZER_HC_GRAPH_FULL_PATH,
|
275
325
|
model=params,
|
326
|
+
http_client=http_client,
|
276
327
|
)
|
277
328
|
return analysis_result.GraphResult.model_validate(data)
|
278
329
|
|
@@ -280,17 +331,21 @@ class ApiWrapper:
|
|
280
331
|
async def call_table_graphs_task(
|
281
332
|
cls,
|
282
333
|
params: analysis_params.AnalysisHardwareListParams,
|
334
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
283
335
|
) -> analysis_result.GraphResult:
|
284
336
|
poller = JobPoller(base_url=routes.ANALYZER_HC_TABLE_GRAPH_FULL_PATH)
|
285
|
-
result = await poller.run_pydantic(
|
337
|
+
result = await poller.run_pydantic(
|
338
|
+
params, timeout_sec=None, http_client=http_client
|
339
|
+
)
|
286
340
|
return _parse_job_response(result, analysis_result.GraphResult)
|
287
341
|
|
288
342
|
@classmethod
|
289
343
|
async def call_available_devices_task(
|
290
344
|
cls,
|
291
345
|
params: analysis_params.AnalysisOptionalDevicesParams,
|
346
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
292
347
|
) -> analysis_result.DevicesResult:
|
293
|
-
hardware_info = await cls.call_get_all_hardware_devices()
|
348
|
+
hardware_info = await cls.call_get_all_hardware_devices(http_client=http_client)
|
294
349
|
return cls._get_devices_from_hardware_info(hardware_info, params)
|
295
350
|
|
296
351
|
@staticmethod
|
@@ -318,11 +373,15 @@ class ApiWrapper:
|
|
318
373
|
)
|
319
374
|
|
320
375
|
@classmethod
|
321
|
-
async def call_get_all_hardware_devices(
|
376
|
+
async def call_get_all_hardware_devices(
|
377
|
+
cls,
|
378
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
379
|
+
) -> list[HardwareInformation]:
|
322
380
|
data = await client().call_api(
|
323
381
|
http_method=HTTPMethod.GET,
|
324
382
|
url="/hardware-catalog/v1/hardwares",
|
325
383
|
use_versioned_url=False,
|
384
|
+
http_client=http_client,
|
326
385
|
)
|
327
386
|
if not isinstance(data, list):
|
328
387
|
raise ClassiqAPIError(f"Unexpected value: {data}")
|
@@ -330,33 +389,45 @@ class ApiWrapper:
|
|
330
389
|
|
331
390
|
@classmethod
|
332
391
|
async def call_generate_hamiltonian_task(
|
333
|
-
cls,
|
392
|
+
cls,
|
393
|
+
problem: ground_state_problem.CHEMISTRY_PROBLEMS_TYPE,
|
394
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
334
395
|
) -> operator.PauliOperator:
|
335
396
|
poller = JobPoller(
|
336
397
|
base_url=routes.GENERATE_HAMILTONIAN_FULL_PATH,
|
337
398
|
use_versioned_url=False,
|
338
399
|
)
|
339
|
-
result = await poller.run_pydantic(
|
400
|
+
result = await poller.run_pydantic(
|
401
|
+
problem, timeout_sec=None, http_client=http_client
|
402
|
+
)
|
340
403
|
return _parse_job_response(result, operator.PauliOperator)
|
341
404
|
|
342
405
|
@classmethod
|
343
|
-
async def call_iqcc_init_auth(
|
406
|
+
async def call_iqcc_init_auth(
|
407
|
+
cls,
|
408
|
+
data: IQCCInitAuthData,
|
409
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
410
|
+
) -> IQCCInitAuthResponse:
|
344
411
|
response = await cls._call_task_pydantic(
|
345
412
|
http_method=HTTPMethod.PUT,
|
346
413
|
url=f"{routes.IQCC_INIT_AUTH_FULL_PATH}",
|
347
414
|
model=data,
|
415
|
+
http_client=http_client,
|
348
416
|
)
|
349
417
|
return IQCCInitAuthResponse.model_validate(response)
|
350
418
|
|
351
419
|
@classmethod
|
352
420
|
async def call_iqcc_probe_auth(
|
353
|
-
cls,
|
421
|
+
cls,
|
422
|
+
data: IQCCProbeAuthData,
|
423
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
354
424
|
) -> Optional[IQCCProbeAuthResponse]:
|
355
425
|
try:
|
356
426
|
response = await cls._call_task_pydantic(
|
357
427
|
http_method=HTTPMethod.PUT,
|
358
428
|
url=f"{routes.IQCC_PROBE_AUTH_FULL_PATH}",
|
359
429
|
model=data,
|
430
|
+
http_client=http_client,
|
360
431
|
)
|
361
432
|
except ClassiqAPIError as ex:
|
362
433
|
if ex.status_code == 418:
|
classiq/_internals/client.py
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
import asyncio
|
2
|
+
import contextlib
|
2
3
|
import functools
|
3
4
|
import inspect
|
4
5
|
import logging
|
5
6
|
import os
|
6
7
|
import platform
|
7
|
-
import ssl
|
8
8
|
import sys
|
9
|
+
import time
|
9
10
|
from collections.abc import Awaitable
|
10
11
|
from typing import (
|
11
12
|
Any,
|
@@ -148,14 +149,11 @@ class Client:
|
|
148
149
|
_SESSION_HEADER = "Classiq-Session"
|
149
150
|
_WARNINGS_HEADER = "X-Classiq-Warnings"
|
150
151
|
_LATEST_VERSION_API_PREFIX = "/api/v1"
|
152
|
+
_HTTP_TIMEOUT_SECONDS = 3600 # Needs to be synced with load-balancer timeout
|
151
153
|
|
152
154
|
def __init__(self, conf: config.Configuration) -> None:
|
153
155
|
self._config = conf
|
154
156
|
self._token_manager = token_manager.TokenManager(config=self._config)
|
155
|
-
self._ssl_context = ssl.create_default_context()
|
156
|
-
self._HTTP_TIMEOUT_SECONDS = (
|
157
|
-
3600 # Needs to be synced with load-balancer timeout
|
158
|
-
)
|
159
157
|
self._api_prefix = self._make_api_prefix()
|
160
158
|
self._session_id: Optional[str] = None
|
161
159
|
|
@@ -203,14 +201,44 @@ class Client:
|
|
203
201
|
pass
|
204
202
|
raise ClassiqAPIError(message, response.status_code)
|
205
203
|
|
204
|
+
@try_again_on_failure
|
205
|
+
async def request(
|
206
|
+
self,
|
207
|
+
http_client: httpx.AsyncClient,
|
208
|
+
method: str,
|
209
|
+
url: str,
|
210
|
+
json: Optional[dict] = None,
|
211
|
+
params: Optional[dict] = None,
|
212
|
+
headers: Optional[dict[str, str]] = None,
|
213
|
+
) -> httpx.Response:
|
214
|
+
http_client.headers.update(self._get_headers())
|
215
|
+
|
216
|
+
_logger.debug("HTTP request: %s %s", method.upper(), url)
|
217
|
+
start_time = time.monotonic()
|
218
|
+
response = await http_client.request(
|
219
|
+
method=method,
|
220
|
+
url=url,
|
221
|
+
json=json,
|
222
|
+
params=params,
|
223
|
+
headers=headers,
|
224
|
+
)
|
225
|
+
_logger.debug(
|
226
|
+
"HTTP response: %s %s %d (%.0fms)",
|
227
|
+
method.upper(),
|
228
|
+
url,
|
229
|
+
response.status_code,
|
230
|
+
(time.monotonic() - start_time) * 1000,
|
231
|
+
)
|
232
|
+
self.handle_response(response)
|
233
|
+
return response
|
234
|
+
|
206
235
|
def _make_client_args(self) -> dict[str, Any]:
|
207
236
|
return {
|
208
237
|
"base_url": str(self._config.host),
|
209
238
|
"timeout": self._HTTP_TIMEOUT_SECONDS,
|
210
|
-
"headers": self.
|
239
|
+
"headers": self._get_headers(),
|
211
240
|
}
|
212
241
|
|
213
|
-
@try_again_on_failure
|
214
242
|
async def call_api(
|
215
243
|
self,
|
216
244
|
http_method: str,
|
@@ -219,20 +247,29 @@ class Client:
|
|
219
247
|
params: Optional[dict] = None,
|
220
248
|
use_versioned_url: bool = True,
|
221
249
|
headers: Optional[dict[str, str]] = None,
|
250
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
222
251
|
) -> Union[dict, list, str]:
|
223
252
|
if use_versioned_url:
|
224
253
|
url = self.make_versioned_url(url)
|
225
|
-
async with self.
|
226
|
-
response = await
|
254
|
+
async with self.use_client_or_create(http_client) as async_client:
|
255
|
+
response = await self.request(
|
256
|
+
http_client=async_client,
|
227
257
|
method=http_method,
|
228
258
|
url=url,
|
229
259
|
json=body,
|
230
260
|
params=params,
|
231
261
|
headers=headers,
|
232
262
|
)
|
233
|
-
self.handle_response(response)
|
234
263
|
return response.json()
|
235
264
|
|
265
|
+
def use_client_or_create(
|
266
|
+
self, http_client: Optional[httpx.AsyncClient]
|
267
|
+
) -> contextlib.AbstractAsyncContextManager[httpx.AsyncClient]:
|
268
|
+
if http_client is None:
|
269
|
+
return self.async_client()
|
270
|
+
else:
|
271
|
+
return contextlib.nullcontext(enter_result=http_client)
|
272
|
+
|
236
273
|
def sync_call_api(
|
237
274
|
self,
|
238
275
|
http_method: str,
|
@@ -253,7 +290,7 @@ class Client:
|
|
253
290
|
def async_client(self) -> httpx.AsyncClient:
|
254
291
|
return httpx.AsyncClient(**self._make_client_args())
|
255
292
|
|
256
|
-
def
|
293
|
+
def _get_headers(self) -> Headers:
|
257
294
|
headers = dict()
|
258
295
|
access_token = self._token_manager.get_access_token()
|
259
296
|
if access_token is not None:
|
classiq/_internals/jobs.py
CHANGED
@@ -10,7 +10,7 @@ from classiq.interface.exceptions import ClassiqAPIError
|
|
10
10
|
from classiq.interface.jobs import JobDescription, JobID, JSONObject
|
11
11
|
|
12
12
|
from classiq._internals.async_utils import poll_for
|
13
|
-
from classiq._internals.client import client
|
13
|
+
from classiq._internals.client import client
|
14
14
|
from classiq._internals.config import SDKMode
|
15
15
|
|
16
16
|
_URL_PATH_SEP = "/"
|
@@ -45,19 +45,17 @@ def _general_job_description_parser(
|
|
45
45
|
|
46
46
|
|
47
47
|
class JobPoller:
|
48
|
-
INITIAL_INTERVAL_SEC = 1
|
49
|
-
INTERVAL_FACTOR =
|
50
|
-
FINAL_INTERVAL_SEC =
|
48
|
+
INITIAL_INTERVAL_SEC = 0.1
|
49
|
+
INTERVAL_FACTOR = 1.5
|
50
|
+
FINAL_INTERVAL_SEC = 25
|
51
51
|
DEV_INTERVAL = 0.05
|
52
52
|
|
53
53
|
def __init__(
|
54
54
|
self,
|
55
55
|
base_url: str,
|
56
|
-
required_headers: Optional[set[str]] = None,
|
57
56
|
use_versioned_url: bool = True,
|
58
57
|
additional_headers: Optional[dict[str, str]] = None,
|
59
58
|
) -> None:
|
60
|
-
self._required_headers = required_headers or set()
|
61
59
|
self._additional_headers = additional_headers
|
62
60
|
client_instance = client()
|
63
61
|
self._base_url = (
|
@@ -65,7 +63,6 @@ class JobPoller:
|
|
65
63
|
if use_versioned_url
|
66
64
|
else base_url
|
67
65
|
)
|
68
|
-
self._async_client = client_instance.async_client()
|
69
66
|
self._mode = client_instance.config.mode
|
70
67
|
|
71
68
|
def _parse_job_id_response(self, response: httpx.Response) -> JobID:
|
@@ -78,37 +75,33 @@ class JobPoller:
|
|
78
75
|
def _make_cancel_url(poll_url: str) -> str:
|
79
76
|
return _join_url_path(poll_url, "cancel")
|
80
77
|
|
81
|
-
def _update_headers(self, response: httpx.Response) -> None:
|
82
|
-
for header in self._required_headers:
|
83
|
-
try:
|
84
|
-
self._async_client.headers[header] = response.headers[header]
|
85
|
-
except KeyError as exc:
|
86
|
-
raise ClassiqAPIError(
|
87
|
-
f"Response to {self._base_url} is missing header {header}"
|
88
|
-
) from exc
|
89
|
-
|
90
|
-
@try_again_on_failure
|
91
78
|
async def _request(
|
92
|
-
self,
|
79
|
+
self,
|
80
|
+
http_client: httpx.AsyncClient,
|
81
|
+
http_method: str,
|
82
|
+
url: str,
|
83
|
+
body: Optional[dict] = None,
|
93
84
|
) -> httpx.Response:
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
85
|
+
return await client().request(
|
86
|
+
http_client=http_client,
|
87
|
+
method=http_method,
|
88
|
+
url=url,
|
89
|
+
json=body,
|
90
|
+
headers=self._additional_headers,
|
98
91
|
)
|
99
|
-
client().handle_response(response)
|
100
|
-
return response
|
101
92
|
|
102
|
-
async def _submit(
|
103
|
-
|
93
|
+
async def _submit(
|
94
|
+
self, http_client: httpx.AsyncClient, body: dict
|
95
|
+
) -> httpx.Response:
|
96
|
+
return await self._request(
|
97
|
+
http_client=http_client, http_method="POST", url=self._base_url, body=body
|
98
|
+
)
|
104
99
|
|
105
100
|
def _interval_sec(self) -> Iterable[float]:
|
106
101
|
if self._mode == SDKMode.DEV:
|
107
102
|
while True:
|
108
103
|
yield self.DEV_INTERVAL
|
109
104
|
else:
|
110
|
-
for _ in range(10):
|
111
|
-
yield self.INITIAL_INTERVAL_SEC
|
112
105
|
interval = self.INITIAL_INTERVAL_SEC
|
113
106
|
while True:
|
114
107
|
yield interval
|
@@ -116,13 +109,16 @@ class JobPoller:
|
|
116
109
|
|
117
110
|
async def _poll(
|
118
111
|
self,
|
112
|
+
http_client: httpx.AsyncClient,
|
119
113
|
poll_url: str,
|
120
114
|
timeout_sec: Optional[float],
|
121
115
|
response_parser: Callable[[JSONObject], Optional[T]] = _general_job_description_parser, # type: ignore[assignment]
|
122
116
|
) -> T:
|
123
117
|
async def poller() -> JSONObject:
|
124
118
|
nonlocal self, poll_url
|
125
|
-
raw_response = await self._request(
|
119
|
+
raw_response = await self._request(
|
120
|
+
http_client=http_client, http_method="GET", url=poll_url
|
121
|
+
)
|
126
122
|
return raw_response.json()
|
127
123
|
|
128
124
|
async for json_response in poll_for(
|
@@ -138,39 +134,50 @@ class JobPoller:
|
|
138
134
|
job_id: JobID,
|
139
135
|
timeout_sec: Optional[float],
|
140
136
|
response_parser: Callable[[JSONObject], Optional[T]] = _general_job_description_parser, # type: ignore[assignment]
|
137
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
141
138
|
) -> T:
|
142
139
|
poll_url = self._make_poll_url(job_id=job_id)
|
143
|
-
async with
|
140
|
+
async with client().use_client_or_create(http_client) as async_client:
|
144
141
|
return await self._poll(
|
142
|
+
http_client=async_client,
|
145
143
|
poll_url=poll_url,
|
146
144
|
response_parser=response_parser,
|
147
145
|
timeout_sec=timeout_sec,
|
148
146
|
)
|
149
147
|
|
150
|
-
async def _cancel(self, poll_url: str) -> None:
|
148
|
+
async def _cancel(self, http_client: httpx.AsyncClient, poll_url: str) -> None:
|
151
149
|
_logger.info("Cancelling job %s", poll_url, exc_info=True)
|
152
150
|
cancel_url = self._make_cancel_url(poll_url)
|
153
|
-
await self._request(http_method="PUT", url=cancel_url)
|
151
|
+
await self._request(http_client=http_client, http_method="PUT", url=cancel_url)
|
154
152
|
|
155
153
|
async def run(
|
156
|
-
self,
|
154
|
+
self,
|
155
|
+
body: dict,
|
156
|
+
timeout_sec: Optional[float],
|
157
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
157
158
|
) -> GeneralJobDescription:
|
158
|
-
async with
|
159
|
-
submit_response = await self._submit(body=body)
|
159
|
+
async with client().use_client_or_create(http_client) as async_client:
|
160
|
+
submit_response = await self._submit(http_client=async_client, body=body)
|
160
161
|
job_id = self._parse_job_id_response(response=submit_response)
|
161
162
|
poll_url = self._make_poll_url(job_id=job_id)
|
162
|
-
self._update_headers(response=submit_response)
|
163
163
|
try:
|
164
|
-
return await self._poll(
|
164
|
+
return await self._poll(
|
165
|
+
http_client=async_client,
|
166
|
+
poll_url=poll_url,
|
167
|
+
timeout_sec=timeout_sec,
|
168
|
+
)
|
165
169
|
except Exception:
|
166
|
-
await self._cancel(poll_url=poll_url)
|
170
|
+
await self._cancel(http_client=async_client, poll_url=poll_url)
|
167
171
|
raise
|
168
172
|
|
169
173
|
async def run_pydantic(
|
170
|
-
self,
|
174
|
+
self,
|
175
|
+
model: pydantic.BaseModel,
|
176
|
+
timeout_sec: Optional[float],
|
177
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
171
178
|
) -> GeneralJobDescription:
|
172
179
|
# TODO: we can't use model.dict() - it doesn't serialize complex class.
|
173
180
|
# This was added because JSON serializer doesn't serialize complex and UUID,
|
174
181
|
# while pydantic does. We should add support for smarter json serialization.
|
175
182
|
body = json.loads(model.model_dump_json())
|
176
|
-
return await self.run(body, timeout_sec)
|
183
|
+
return await self.run(body, timeout_sec, http_client=http_client)
|