sursolver 0.1.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.
- sursolver/__init__.py +86 -0
- sursolver/_async_client.py +191 -0
- sursolver/_client.py +194 -0
- sursolver/_core.py +362 -0
- sursolver/_errors.py +163 -0
- sursolver/_types.py +105 -0
- sursolver/_version.py +2 -0
- sursolver/py.typed +0 -0
- sursolver/task_types.py +75 -0
- sursolver-0.1.0.dist-info/METADATA +235 -0
- sursolver-0.1.0.dist-info/RECORD +13 -0
- sursolver-0.1.0.dist-info/WHEEL +4 -0
- sursolver-0.1.0.dist-info/licenses/LICENSE +21 -0
sursolver/__init__.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Official Python client for the SurSolver captcha-solving API."""
|
|
2
|
+
|
|
3
|
+
from ._async_client import AsyncSurSolver
|
|
4
|
+
from ._client import SurSolver
|
|
5
|
+
from ._core import (
|
|
6
|
+
DEFAULT_BASE_URL,
|
|
7
|
+
DEFAULT_MAX_SUBMIT_RETRIES,
|
|
8
|
+
DEFAULT_POLL_INTERVAL,
|
|
9
|
+
DEFAULT_REQUEST_TIMEOUT,
|
|
10
|
+
DEFAULT_SOLVE_TIMEOUT,
|
|
11
|
+
SUBMIT_RETRY_JITTER_MS,
|
|
12
|
+
)
|
|
13
|
+
from ._errors import (
|
|
14
|
+
ApiError,
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
ConfigurationError,
|
|
17
|
+
InsufficientBalanceError,
|
|
18
|
+
InvalidTaskError,
|
|
19
|
+
NoSlotAvailableError,
|
|
20
|
+
Operation,
|
|
21
|
+
ProtocolError,
|
|
22
|
+
SolveTimeoutError,
|
|
23
|
+
SurSolverError,
|
|
24
|
+
TaskFailedError,
|
|
25
|
+
TaskNotFoundError,
|
|
26
|
+
TransportError,
|
|
27
|
+
)
|
|
28
|
+
from ._types import (
|
|
29
|
+
AsyncClock,
|
|
30
|
+
AsyncTransport,
|
|
31
|
+
Clock,
|
|
32
|
+
HttpRequest,
|
|
33
|
+
HttpResponse,
|
|
34
|
+
ServiceInfo,
|
|
35
|
+
Solution,
|
|
36
|
+
SolveResult,
|
|
37
|
+
Task,
|
|
38
|
+
TaskResult,
|
|
39
|
+
TaskTypeCatalog,
|
|
40
|
+
TaskTypeInfo,
|
|
41
|
+
Transport,
|
|
42
|
+
)
|
|
43
|
+
from ._version import VERSION
|
|
44
|
+
from .task_types import ErrorCode, TaskType
|
|
45
|
+
|
|
46
|
+
__version__ = VERSION
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"DEFAULT_BASE_URL",
|
|
50
|
+
"DEFAULT_MAX_SUBMIT_RETRIES",
|
|
51
|
+
"DEFAULT_POLL_INTERVAL",
|
|
52
|
+
"DEFAULT_REQUEST_TIMEOUT",
|
|
53
|
+
"DEFAULT_SOLVE_TIMEOUT",
|
|
54
|
+
"SUBMIT_RETRY_JITTER_MS",
|
|
55
|
+
"VERSION",
|
|
56
|
+
"ApiError",
|
|
57
|
+
"AsyncClock",
|
|
58
|
+
"AsyncSurSolver",
|
|
59
|
+
"AsyncTransport",
|
|
60
|
+
"AuthenticationError",
|
|
61
|
+
"Clock",
|
|
62
|
+
"ConfigurationError",
|
|
63
|
+
"ErrorCode",
|
|
64
|
+
"HttpRequest",
|
|
65
|
+
"HttpResponse",
|
|
66
|
+
"InsufficientBalanceError",
|
|
67
|
+
"InvalidTaskError",
|
|
68
|
+
"NoSlotAvailableError",
|
|
69
|
+
"Operation",
|
|
70
|
+
"ProtocolError",
|
|
71
|
+
"ServiceInfo",
|
|
72
|
+
"Solution",
|
|
73
|
+
"SolveResult",
|
|
74
|
+
"SolveTimeoutError",
|
|
75
|
+
"SurSolver",
|
|
76
|
+
"SurSolverError",
|
|
77
|
+
"Task",
|
|
78
|
+
"TaskFailedError",
|
|
79
|
+
"TaskNotFoundError",
|
|
80
|
+
"TaskResult",
|
|
81
|
+
"TaskType",
|
|
82
|
+
"TaskTypeCatalog",
|
|
83
|
+
"TaskTypeInfo",
|
|
84
|
+
"Transport",
|
|
85
|
+
"TransportError",
|
|
86
|
+
]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Asynchronous client (asyncio)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import random as _random
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from types import TracebackType
|
|
9
|
+
from typing import Any, Self
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from . import _core
|
|
14
|
+
from ._errors import NoSlotAvailableError, Operation, SolveTimeoutError, SurSolverError
|
|
15
|
+
from ._types import (
|
|
16
|
+
AsyncClock,
|
|
17
|
+
AsyncTransport,
|
|
18
|
+
HttpRequest,
|
|
19
|
+
HttpResponse,
|
|
20
|
+
ServiceInfo,
|
|
21
|
+
SolveResult,
|
|
22
|
+
Task,
|
|
23
|
+
TaskResult,
|
|
24
|
+
TaskTypeCatalog,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _AsyncSystemClock:
|
|
29
|
+
def now(self) -> float:
|
|
30
|
+
return asyncio.get_running_loop().time()
|
|
31
|
+
|
|
32
|
+
async def sleep(self, seconds: float) -> None:
|
|
33
|
+
await asyncio.sleep(seconds)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AsyncSurSolver:
|
|
37
|
+
"""asyncio client for the SurSolver solver API; same behaviour as ``SurSolver``.
|
|
38
|
+
|
|
39
|
+
Cancel a call the asyncio way (``task.cancel()``, ``asyncio.timeout``): the call raises
|
|
40
|
+
``asyncio.CancelledError`` and never claims the task failed. Use as an async context
|
|
41
|
+
manager (or call ``aclose()``) to release the default transport's connection pool.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
api_key: str,
|
|
47
|
+
*,
|
|
48
|
+
base_url: str = _core.DEFAULT_BASE_URL,
|
|
49
|
+
request_timeout: float | None = None,
|
|
50
|
+
poll_interval: float | None = None,
|
|
51
|
+
solve_timeout: float | None = None,
|
|
52
|
+
max_submit_retries: int | None = None,
|
|
53
|
+
app_id: str | None = None,
|
|
54
|
+
user_agent_suffix: str | None = None,
|
|
55
|
+
transport: AsyncTransport | None = None,
|
|
56
|
+
clock: AsyncClock | None = None,
|
|
57
|
+
random: Callable[[], float] | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
self._settings = _core.build_settings(
|
|
60
|
+
api_key,
|
|
61
|
+
base_url=base_url,
|
|
62
|
+
request_timeout=request_timeout,
|
|
63
|
+
poll_interval=poll_interval,
|
|
64
|
+
solve_timeout=solve_timeout,
|
|
65
|
+
max_submit_retries=max_submit_retries,
|
|
66
|
+
app_id=app_id,
|
|
67
|
+
user_agent_suffix=user_agent_suffix,
|
|
68
|
+
)
|
|
69
|
+
self._http: httpx.AsyncClient | None = None
|
|
70
|
+
if transport is None:
|
|
71
|
+
self._http = httpx.AsyncClient(follow_redirects=False)
|
|
72
|
+
transport = self._httpx_send
|
|
73
|
+
self._transport = transport
|
|
74
|
+
self._clock: AsyncClock = clock if clock is not None else _AsyncSystemClock()
|
|
75
|
+
self._random = random if random is not None else _random.random
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
return f"AsyncSurSolver(base_url={self._settings.base_url!r})"
|
|
79
|
+
|
|
80
|
+
async def __aenter__(self) -> Self:
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
async def __aexit__(
|
|
84
|
+
self,
|
|
85
|
+
exc_type: type[BaseException] | None,
|
|
86
|
+
exc: BaseException | None,
|
|
87
|
+
tb: TracebackType | None,
|
|
88
|
+
) -> None:
|
|
89
|
+
await self.aclose()
|
|
90
|
+
|
|
91
|
+
async def aclose(self) -> None:
|
|
92
|
+
if self._http is not None:
|
|
93
|
+
await self._http.aclose()
|
|
94
|
+
|
|
95
|
+
async def create_task(self, task: Task) -> str:
|
|
96
|
+
"""Submit a task and return its id. The task is charged from this point. Never retried."""
|
|
97
|
+
payload = _core.create_task_payload(self._settings, task)
|
|
98
|
+
body = await self._solver_call("createTask", "/createTask", payload)
|
|
99
|
+
return _core.task_id_from(body)
|
|
100
|
+
|
|
101
|
+
async def get_task_result(self, task_id: str) -> TaskResult:
|
|
102
|
+
"""Read a task's state once. A terminal failure raises ``TaskFailedError``."""
|
|
103
|
+
payload = {"clientKey": self._settings.api_key, "taskId": task_id}
|
|
104
|
+
body = await self._solver_call("getTaskResult", "/getTaskResult", payload, task_id)
|
|
105
|
+
return _core.task_result_from(body)
|
|
106
|
+
|
|
107
|
+
async def get_balance(self) -> float:
|
|
108
|
+
"""Cash balance in USD as the JSON number the API sends (display only)."""
|
|
109
|
+
body = await self._solver_call("getBalance", "/getBalance", {"clientKey": self._settings.api_key})
|
|
110
|
+
return _core.balance_from(body)
|
|
111
|
+
|
|
112
|
+
async def solve(
|
|
113
|
+
self,
|
|
114
|
+
task: Task,
|
|
115
|
+
*,
|
|
116
|
+
poll_interval: float | None = None,
|
|
117
|
+
solve_timeout: float | None = None,
|
|
118
|
+
on_task_created: Callable[[str], None] | None = None,
|
|
119
|
+
) -> SolveResult:
|
|
120
|
+
"""Create a task and poll it until it is ready, failed, or the deadline passes.
|
|
121
|
+
|
|
122
|
+
Same rules as ``SurSolver.solve``. Pass ``on_task_created`` to keep the task id when
|
|
123
|
+
the call is cancelled.
|
|
124
|
+
"""
|
|
125
|
+
interval = _core.positive_seconds("poll_interval", poll_interval, self._settings.poll_interval)
|
|
126
|
+
timeout = _core.positive_seconds("solve_timeout", solve_timeout, self._settings.solve_timeout)
|
|
127
|
+
deadline = self._clock.now() + timeout
|
|
128
|
+
|
|
129
|
+
retries = 0
|
|
130
|
+
while True:
|
|
131
|
+
try:
|
|
132
|
+
task_id = await self.create_task(task)
|
|
133
|
+
break
|
|
134
|
+
except NoSlotAvailableError:
|
|
135
|
+
if retries >= self._settings.max_submit_retries:
|
|
136
|
+
raise
|
|
137
|
+
retries += 1
|
|
138
|
+
await self._clock.sleep(_core.submit_retry_delay(interval, self._random()))
|
|
139
|
+
if on_task_created is not None:
|
|
140
|
+
on_task_created(task_id)
|
|
141
|
+
|
|
142
|
+
while True:
|
|
143
|
+
last_error: SurSolverError | None = None
|
|
144
|
+
try:
|
|
145
|
+
result = await self.get_task_result(task_id)
|
|
146
|
+
if result.status == "ready" and result.solution is not None:
|
|
147
|
+
return SolveResult(task_id=task_id, solution=result.solution)
|
|
148
|
+
except SurSolverError as error:
|
|
149
|
+
if not _core.is_transient_poll_error(error):
|
|
150
|
+
raise
|
|
151
|
+
last_error = error
|
|
152
|
+
await self._clock.sleep(interval)
|
|
153
|
+
if self._clock.now() >= deadline:
|
|
154
|
+
raise SolveTimeoutError(task_id, timeout, last_error)
|
|
155
|
+
|
|
156
|
+
async def list_task_types(self) -> TaskTypeCatalog:
|
|
157
|
+
"""The public task-type catalog: accepted names, fields, proxy modes and result keys."""
|
|
158
|
+
return _core.task_types_from(await self._catalog_call("listTaskTypes", "/v1/task-types"))
|
|
159
|
+
|
|
160
|
+
async def list_services(self) -> list[ServiceInfo]:
|
|
161
|
+
"""Active services with their current USD price per task (decimal strings)."""
|
|
162
|
+
return _core.services_from(await self._catalog_call("listServices", "/v1/services"))
|
|
163
|
+
|
|
164
|
+
async def _httpx_send(self, request: HttpRequest) -> HttpResponse:
|
|
165
|
+
assert self._http is not None
|
|
166
|
+
response = await self._http.request(
|
|
167
|
+
request.method,
|
|
168
|
+
request.url,
|
|
169
|
+
headers=dict(request.headers),
|
|
170
|
+
content=request.body,
|
|
171
|
+
timeout=request.timeout,
|
|
172
|
+
)
|
|
173
|
+
return HttpResponse(
|
|
174
|
+
status=response.status_code, body=response.content, headers=dict(response.headers)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
async def _send(self, operation: Operation, request: HttpRequest) -> HttpResponse:
|
|
178
|
+
try:
|
|
179
|
+
return await self._transport(request)
|
|
180
|
+
except Exception as error:
|
|
181
|
+
raise _core.transport_failure(operation, error, request.timeout) from error
|
|
182
|
+
|
|
183
|
+
async def _solver_call(
|
|
184
|
+
self, operation: Operation, path: str, payload: dict[str, Any], task_id: str | None = None
|
|
185
|
+
) -> dict[str, Any]:
|
|
186
|
+
response = await self._send(operation, _core.solver_request(self._settings, path, payload))
|
|
187
|
+
return _core.parse_solver(operation, response, task_id)
|
|
188
|
+
|
|
189
|
+
async def _catalog_call(self, operation: Operation, path: str) -> dict[str, Any]:
|
|
190
|
+
response = await self._send(operation, _core.catalog_request(self._settings, path))
|
|
191
|
+
return _core.parse_catalog(operation, response)
|
sursolver/_client.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Synchronous client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import random as _random
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from types import TracebackType
|
|
9
|
+
from typing import Any, Self
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from . import _core
|
|
14
|
+
from ._errors import NoSlotAvailableError, Operation, SolveTimeoutError, SurSolverError
|
|
15
|
+
from ._types import (
|
|
16
|
+
Clock,
|
|
17
|
+
HttpRequest,
|
|
18
|
+
HttpResponse,
|
|
19
|
+
ServiceInfo,
|
|
20
|
+
SolveResult,
|
|
21
|
+
Task,
|
|
22
|
+
TaskResult,
|
|
23
|
+
TaskTypeCatalog,
|
|
24
|
+
Transport,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _SystemClock:
|
|
29
|
+
def now(self) -> float:
|
|
30
|
+
return time.monotonic()
|
|
31
|
+
|
|
32
|
+
def sleep(self, seconds: float) -> None:
|
|
33
|
+
time.sleep(seconds)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SurSolver:
|
|
37
|
+
"""Synchronous client for the SurSolver solver API.
|
|
38
|
+
|
|
39
|
+
The API key travels only in request bodies and never appears in errors or ``repr``.
|
|
40
|
+
Durations are seconds. Use as a context manager (or call ``close()``) to release the
|
|
41
|
+
connection pool of the default transport.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
api_key: str,
|
|
47
|
+
*,
|
|
48
|
+
base_url: str = _core.DEFAULT_BASE_URL,
|
|
49
|
+
request_timeout: float | None = None,
|
|
50
|
+
poll_interval: float | None = None,
|
|
51
|
+
solve_timeout: float | None = None,
|
|
52
|
+
max_submit_retries: int | None = None,
|
|
53
|
+
app_id: str | None = None,
|
|
54
|
+
user_agent_suffix: str | None = None,
|
|
55
|
+
transport: Transport | None = None,
|
|
56
|
+
clock: Clock | None = None,
|
|
57
|
+
random: Callable[[], float] | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
self._settings = _core.build_settings(
|
|
60
|
+
api_key,
|
|
61
|
+
base_url=base_url,
|
|
62
|
+
request_timeout=request_timeout,
|
|
63
|
+
poll_interval=poll_interval,
|
|
64
|
+
solve_timeout=solve_timeout,
|
|
65
|
+
max_submit_retries=max_submit_retries,
|
|
66
|
+
app_id=app_id,
|
|
67
|
+
user_agent_suffix=user_agent_suffix,
|
|
68
|
+
)
|
|
69
|
+
self._http: httpx.Client | None = None
|
|
70
|
+
if transport is None:
|
|
71
|
+
self._http = httpx.Client(follow_redirects=False)
|
|
72
|
+
transport = self._httpx_send
|
|
73
|
+
self._transport = transport
|
|
74
|
+
self._clock: Clock = clock if clock is not None else _SystemClock()
|
|
75
|
+
self._random = random if random is not None else _random.random
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
return f"SurSolver(base_url={self._settings.base_url!r})"
|
|
79
|
+
|
|
80
|
+
def __enter__(self) -> Self:
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def __exit__(
|
|
84
|
+
self,
|
|
85
|
+
exc_type: type[BaseException] | None,
|
|
86
|
+
exc: BaseException | None,
|
|
87
|
+
tb: TracebackType | None,
|
|
88
|
+
) -> None:
|
|
89
|
+
self.close()
|
|
90
|
+
|
|
91
|
+
def close(self) -> None:
|
|
92
|
+
if self._http is not None:
|
|
93
|
+
self._http.close()
|
|
94
|
+
|
|
95
|
+
def create_task(self, task: Task) -> str:
|
|
96
|
+
"""Submit a task and return its id. The task is charged from this point. Never retried."""
|
|
97
|
+
payload = _core.create_task_payload(self._settings, task)
|
|
98
|
+
body = self._solver_call("createTask", "/createTask", payload)
|
|
99
|
+
return _core.task_id_from(body)
|
|
100
|
+
|
|
101
|
+
def get_task_result(self, task_id: str) -> TaskResult:
|
|
102
|
+
"""Read a task's state once. A terminal failure raises ``TaskFailedError``."""
|
|
103
|
+
payload = {"clientKey": self._settings.api_key, "taskId": task_id}
|
|
104
|
+
return _core.task_result_from(self._solver_call("getTaskResult", "/getTaskResult", payload, task_id))
|
|
105
|
+
|
|
106
|
+
def get_balance(self) -> float:
|
|
107
|
+
"""Cash balance in USD as the JSON number the API sends: display it, do not do money
|
|
108
|
+
arithmetic on it (the dashboard's ``GET /v1/balance`` has the exact decimal)."""
|
|
109
|
+
body = self._solver_call("getBalance", "/getBalance", {"clientKey": self._settings.api_key})
|
|
110
|
+
return _core.balance_from(body)
|
|
111
|
+
|
|
112
|
+
def solve(
|
|
113
|
+
self,
|
|
114
|
+
task: Task,
|
|
115
|
+
*,
|
|
116
|
+
poll_interval: float | None = None,
|
|
117
|
+
solve_timeout: float | None = None,
|
|
118
|
+
on_task_created: Callable[[str], None] | None = None,
|
|
119
|
+
) -> SolveResult:
|
|
120
|
+
"""Create a task and poll it until it is ready, failed, or the deadline passes.
|
|
121
|
+
|
|
122
|
+
``create_task`` is resubmitted only after ``ERROR_NO_SLOT_AVAILABLE`` (nothing was
|
|
123
|
+
created), at most ``max_submit_retries`` times; any other submit failure is raised at
|
|
124
|
+
once. Polls failing at the transport level or with ``ERROR_INTERNAL_ERROR`` are retried
|
|
125
|
+
until the deadline; any other error ends the call and carries the task id. On the
|
|
126
|
+
deadline ``SolveTimeoutError`` is raised; the task may still finish.
|
|
127
|
+
"""
|
|
128
|
+
interval = _core.positive_seconds("poll_interval", poll_interval, self._settings.poll_interval)
|
|
129
|
+
timeout = _core.positive_seconds("solve_timeout", solve_timeout, self._settings.solve_timeout)
|
|
130
|
+
deadline = self._clock.now() + timeout
|
|
131
|
+
|
|
132
|
+
retries = 0
|
|
133
|
+
while True:
|
|
134
|
+
try:
|
|
135
|
+
task_id = self.create_task(task)
|
|
136
|
+
break
|
|
137
|
+
except NoSlotAvailableError:
|
|
138
|
+
if retries >= self._settings.max_submit_retries:
|
|
139
|
+
raise
|
|
140
|
+
retries += 1
|
|
141
|
+
self._clock.sleep(_core.submit_retry_delay(interval, self._random()))
|
|
142
|
+
if on_task_created is not None:
|
|
143
|
+
on_task_created(task_id)
|
|
144
|
+
|
|
145
|
+
while True:
|
|
146
|
+
last_error: SurSolverError | None = None
|
|
147
|
+
try:
|
|
148
|
+
result = self.get_task_result(task_id)
|
|
149
|
+
if result.status == "ready" and result.solution is not None:
|
|
150
|
+
return SolveResult(task_id=task_id, solution=result.solution)
|
|
151
|
+
except SurSolverError as error:
|
|
152
|
+
if not _core.is_transient_poll_error(error):
|
|
153
|
+
raise
|
|
154
|
+
last_error = error
|
|
155
|
+
self._clock.sleep(interval)
|
|
156
|
+
if self._clock.now() >= deadline:
|
|
157
|
+
raise SolveTimeoutError(task_id, timeout, last_error)
|
|
158
|
+
|
|
159
|
+
def list_task_types(self) -> TaskTypeCatalog:
|
|
160
|
+
"""The public task-type catalog: accepted names, fields, proxy modes and result keys."""
|
|
161
|
+
return _core.task_types_from(self._catalog_call("listTaskTypes", "/v1/task-types"))
|
|
162
|
+
|
|
163
|
+
def list_services(self) -> list[ServiceInfo]:
|
|
164
|
+
"""Active services with their current USD price per task (decimal strings)."""
|
|
165
|
+
return _core.services_from(self._catalog_call("listServices", "/v1/services"))
|
|
166
|
+
|
|
167
|
+
def _httpx_send(self, request: HttpRequest) -> HttpResponse:
|
|
168
|
+
assert self._http is not None
|
|
169
|
+
response = self._http.request(
|
|
170
|
+
request.method,
|
|
171
|
+
request.url,
|
|
172
|
+
headers=dict(request.headers),
|
|
173
|
+
content=request.body,
|
|
174
|
+
timeout=request.timeout,
|
|
175
|
+
)
|
|
176
|
+
return HttpResponse(
|
|
177
|
+
status=response.status_code, body=response.content, headers=dict(response.headers)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def _send(self, operation: Operation, request: HttpRequest) -> HttpResponse:
|
|
181
|
+
try:
|
|
182
|
+
return self._transport(request)
|
|
183
|
+
except Exception as error:
|
|
184
|
+
raise _core.transport_failure(operation, error, request.timeout) from error
|
|
185
|
+
|
|
186
|
+
def _solver_call(
|
|
187
|
+
self, operation: Operation, path: str, payload: dict[str, Any], task_id: str | None = None
|
|
188
|
+
) -> dict[str, Any]:
|
|
189
|
+
response = self._send(operation, _core.solver_request(self._settings, path, payload))
|
|
190
|
+
return _core.parse_solver(operation, response, task_id)
|
|
191
|
+
|
|
192
|
+
def _catalog_call(self, operation: Operation, path: str) -> dict[str, Any]:
|
|
193
|
+
response = self._send(operation, _core.catalog_request(self._settings, path))
|
|
194
|
+
return _core.parse_catalog(operation, response)
|