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.
Files changed (44) hide show
  1. classiq/_internals/api_wrapper.py +91 -20
  2. classiq/_internals/client.py +48 -11
  3. classiq/_internals/jobs.py +47 -40
  4. classiq/execution/execution_session.py +62 -22
  5. classiq/execution/jobs.py +64 -23
  6. classiq/execution/qaoa.py +17 -15
  7. classiq/execution/qnn.py +17 -18
  8. classiq/executor.py +2 -1
  9. classiq/interface/_version.py +1 -1
  10. classiq/interface/generator/arith/arithmetic_operations.py +1 -0
  11. classiq/interface/generator/register_role.py +8 -0
  12. classiq/interface/model/handle_binding.py +22 -3
  13. classiq/model_expansions/capturing/captured_vars.py +316 -0
  14. classiq/model_expansions/capturing/mangling_utils.py +18 -9
  15. classiq/model_expansions/closure.py +29 -74
  16. classiq/model_expansions/function_builder.py +51 -66
  17. classiq/model_expansions/interpreter.py +4 -7
  18. classiq/model_expansions/quantum_operations/bind.py +1 -3
  19. classiq/model_expansions/quantum_operations/call_emitter.py +46 -11
  20. classiq/model_expansions/quantum_operations/classicalif.py +2 -5
  21. classiq/model_expansions/quantum_operations/control.py +13 -16
  22. classiq/model_expansions/quantum_operations/emitter.py +36 -8
  23. classiq/model_expansions/quantum_operations/expression_operation.py +9 -19
  24. classiq/model_expansions/quantum_operations/inplace_binary_operation.py +4 -6
  25. classiq/model_expansions/quantum_operations/invert.py +5 -8
  26. classiq/model_expansions/quantum_operations/power.py +5 -10
  27. classiq/model_expansions/quantum_operations/quantum_assignment_operation.py +1 -3
  28. classiq/model_expansions/quantum_operations/quantum_function_call.py +1 -3
  29. classiq/model_expansions/quantum_operations/repeat.py +3 -3
  30. classiq/model_expansions/quantum_operations/variable_decleration.py +1 -1
  31. classiq/model_expansions/quantum_operations/within_apply.py +1 -5
  32. classiq/model_expansions/scope.py +2 -2
  33. classiq/model_expansions/transformers/var_splitter.py +32 -19
  34. classiq/model_expansions/utils/handles_collector.py +33 -0
  35. classiq/model_expansions/visitors/variable_references.py +18 -2
  36. classiq/qmod/qfunc.py +9 -13
  37. classiq/qmod/quantum_expandable.py +1 -21
  38. classiq/qmod/quantum_function.py +16 -0
  39. {classiq-0.58.1.dist-info → classiq-0.60.0.dist-info}/METADATA +1 -1
  40. {classiq-0.58.1.dist-info → classiq-0.60.0.dist-info}/RECORD +41 -42
  41. classiq/interface/executor/aws_execution_cost.py +0 -90
  42. classiq/model_expansions/capturing/captured_var_manager.py +0 -48
  43. classiq/model_expansions/capturing/propagated_var_stack.py +0 -194
  44. {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, url, body, use_versioned_url=use_versioned_url
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, model: Model
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(model, timeout_sec=None)
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 call_execute_generated_circuit(
117
- cls, circuit: generator_result.QuantumProgram
118
- ) -> execution_request.ExecutionJobDetails:
119
- execution_input = await cls._call_task_pydantic(
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, params: analysis_params.AnalysisParams
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, params: generator_result.QuantumProgram
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, params: analysis_result.QasmCode
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, params: analysis_result.DataID
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, params: AnalysisRBParams
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, params: analysis_params.AnalysisHardwareParams
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(params, timeout_sec=None)
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(cls) -> list[HardwareInformation]:
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, problem: ground_state_problem.CHEMISTRY_PROBLEMS_TYPE
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(problem, timeout_sec=None)
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(cls, data: IQCCInitAuthData) -> IQCCInitAuthResponse:
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, data: IQCCProbeAuthData
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:
@@ -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.get_headers(),
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.async_client() as async_client:
226
- response = await async_client.request(
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 get_headers(self) -> Headers:
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:
@@ -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, try_again_on_failure
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 = 2
50
- FINAL_INTERVAL_SEC = INITIAL_INTERVAL_SEC * INTERVAL_FACTOR**5 # 32 secs
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, http_method: str, url: str, body: Optional[dict] = None
79
+ self,
80
+ http_client: httpx.AsyncClient,
81
+ http_method: str,
82
+ url: str,
83
+ body: Optional[dict] = None,
93
84
  ) -> httpx.Response:
94
- # Update headers in case they change
95
- self._async_client.headers.update(client().get_headers())
96
- response = await self._async_client.request(
97
- method=http_method, url=url, json=body, headers=self._additional_headers
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(self, body: dict) -> httpx.Response:
103
- return await self._request(http_method="POST", url=self._base_url, body=body)
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(http_method="GET", url=poll_url)
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 self._async_client:
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, body: dict, timeout_sec: Optional[float]
154
+ self,
155
+ body: dict,
156
+ timeout_sec: Optional[float],
157
+ http_client: Optional[httpx.AsyncClient] = None,
157
158
  ) -> GeneralJobDescription:
158
- async with self._async_client:
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(poll_url=poll_url, timeout_sec=timeout_sec)
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, model: pydantic.BaseModel, timeout_sec: Optional[float]
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)