python-arango-async 0.0.1__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.
- arangoasync/__init__.py +5 -0
- arangoasync/aql.py +760 -0
- arangoasync/auth.py +121 -0
- arangoasync/client.py +239 -0
- arangoasync/collection.py +1688 -0
- arangoasync/compression.py +139 -0
- arangoasync/connection.py +515 -0
- arangoasync/cursor.py +262 -0
- arangoasync/database.py +1533 -0
- arangoasync/errno.py +1168 -0
- arangoasync/exceptions.py +379 -0
- arangoasync/executor.py +168 -0
- arangoasync/http.py +182 -0
- arangoasync/job.py +214 -0
- arangoasync/logger.py +3 -0
- arangoasync/request.py +107 -0
- arangoasync/resolver.py +119 -0
- arangoasync/response.py +65 -0
- arangoasync/result.py +9 -0
- arangoasync/serialization.py +111 -0
- arangoasync/typings.py +1646 -0
- arangoasync/version.py +1 -0
- python_arango_async-0.0.1.dist-info/METADATA +142 -0
- python_arango_async-0.0.1.dist-info/RECORD +27 -0
- python_arango_async-0.0.1.dist-info/WHEEL +5 -0
- python_arango_async-0.0.1.dist-info/licenses/LICENSE +21 -0
- python_arango_async-0.0.1.dist-info/top_level.txt +1 -0
arangoasync/http.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"HTTPClient",
|
|
3
|
+
"AioHTTPClient",
|
|
4
|
+
"DefaultHTTPClient",
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from ssl import SSLContext, create_default_context
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from aiohttp import (
|
|
12
|
+
BaseConnector,
|
|
13
|
+
BasicAuth,
|
|
14
|
+
ClientSession,
|
|
15
|
+
ClientTimeout,
|
|
16
|
+
TCPConnector,
|
|
17
|
+
client_exceptions,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from arangoasync.exceptions import ClientConnectionError
|
|
21
|
+
from arangoasync.request import Request
|
|
22
|
+
from arangoasync.response import Response
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HTTPClient(ABC): # pragma: no cover
|
|
26
|
+
"""Abstract base class for HTTP clients.
|
|
27
|
+
|
|
28
|
+
Custom HTTP clients should inherit from this class.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
.. code-block:: python
|
|
32
|
+
|
|
33
|
+
class MyCustomHTTPClient(HTTPClient):
|
|
34
|
+
def create_session(self, host):
|
|
35
|
+
pass
|
|
36
|
+
async def send_request(self, session, request):
|
|
37
|
+
pass
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def create_session(self, host: str) -> Any:
|
|
42
|
+
"""Return a new session given the base host URL.
|
|
43
|
+
|
|
44
|
+
Note:
|
|
45
|
+
This method must be overridden by the user.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
host (str): ArangoDB host URL.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Requests session object.
|
|
52
|
+
"""
|
|
53
|
+
raise NotImplementedError
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
async def send_request(
|
|
57
|
+
self,
|
|
58
|
+
session: Any,
|
|
59
|
+
request: Request,
|
|
60
|
+
) -> Response:
|
|
61
|
+
"""Send an HTTP request.
|
|
62
|
+
|
|
63
|
+
Note:
|
|
64
|
+
This method must be overridden by the user.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
session (Any): Client session object.
|
|
68
|
+
request (Request): HTTP request.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Response: HTTP response.
|
|
72
|
+
"""
|
|
73
|
+
raise NotImplementedError
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class AioHTTPClient(HTTPClient):
|
|
77
|
+
"""HTTP client implemented on top of aiohttp_.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
connector (aiohttp.BaseConnector | None): Supports connection pooling.
|
|
81
|
+
By default, 100 simultaneous connections are supported, with a 60-second
|
|
82
|
+
timeout for connection reusing after release.
|
|
83
|
+
timeout (aiohttp.ClientTimeout | None): Client timeout settings.
|
|
84
|
+
300s total timeout by default for a complete request/response operation.
|
|
85
|
+
read_bufsize (int): Size of read buffer (64KB default).
|
|
86
|
+
ssl_context (ssl.SSLContext | bool): SSL validation mode.
|
|
87
|
+
`True` for default SSL checks (see :func:`ssl.create_default_context`).
|
|
88
|
+
`False` disables SSL checks.
|
|
89
|
+
Additionally, you can pass a custom :class:`ssl.SSLContext`.
|
|
90
|
+
|
|
91
|
+
.. _aiohttp:
|
|
92
|
+
https://docs.aiohttp.org/en/stable/
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
connector: Optional[BaseConnector] = None,
|
|
98
|
+
timeout: Optional[ClientTimeout] = None,
|
|
99
|
+
read_bufsize: int = 2**16,
|
|
100
|
+
ssl_context: bool | SSLContext = True,
|
|
101
|
+
) -> None:
|
|
102
|
+
self._connector = connector or TCPConnector(
|
|
103
|
+
keepalive_timeout=60, # timeout for connection reusing after releasing
|
|
104
|
+
limit=100, # total number simultaneous connections
|
|
105
|
+
)
|
|
106
|
+
self._timeout = timeout or ClientTimeout(
|
|
107
|
+
total=300, # total number of seconds for the whole request
|
|
108
|
+
connect=60, # max number of seconds for acquiring a pool connection
|
|
109
|
+
)
|
|
110
|
+
self._read_bufsize = read_bufsize
|
|
111
|
+
self._ssl_context = (
|
|
112
|
+
ssl_context if ssl_context is not True else create_default_context()
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def create_session(self, host: str) -> ClientSession:
|
|
116
|
+
"""Return a new session given the base host URL.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
host (str): ArangoDB host URL. Must not include any paths. Typically, this
|
|
120
|
+
is the address and port of a coordinator (e.g. "http://127.0.0.1:8529").
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
aiohttp.ClientSession: Session object, used to send future requests.
|
|
124
|
+
"""
|
|
125
|
+
return ClientSession(
|
|
126
|
+
base_url=host,
|
|
127
|
+
connector=self._connector,
|
|
128
|
+
timeout=self._timeout,
|
|
129
|
+
read_bufsize=self._read_bufsize,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
async def send_request(
|
|
133
|
+
self,
|
|
134
|
+
session: ClientSession,
|
|
135
|
+
request: Request,
|
|
136
|
+
) -> Response:
|
|
137
|
+
"""Send an HTTP request.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
session (aiohttp.ClientSession): Session object used to make the request.
|
|
141
|
+
request (Request): HTTP request.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Response: HTTP response.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
ClientConnectionError: If the request fails.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
if request.auth is not None:
|
|
151
|
+
auth = BasicAuth(
|
|
152
|
+
login=request.auth.username,
|
|
153
|
+
password=request.auth.password,
|
|
154
|
+
encoding=request.auth.encoding,
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
auth = None
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
async with session.request(
|
|
161
|
+
request.method.name,
|
|
162
|
+
request.endpoint,
|
|
163
|
+
headers=request.normalized_headers(),
|
|
164
|
+
params=request.normalized_params(),
|
|
165
|
+
data=request.data,
|
|
166
|
+
auth=auth,
|
|
167
|
+
ssl=self._ssl_context,
|
|
168
|
+
) as response:
|
|
169
|
+
raw_body = await response.read()
|
|
170
|
+
return Response(
|
|
171
|
+
method=request.method,
|
|
172
|
+
url=str(response.real_url),
|
|
173
|
+
headers=response.headers,
|
|
174
|
+
status_code=response.status,
|
|
175
|
+
status_text=str(response.reason),
|
|
176
|
+
raw_body=raw_body,
|
|
177
|
+
)
|
|
178
|
+
except client_exceptions.ClientConnectionError as e:
|
|
179
|
+
raise ClientConnectionError(str(e)) from e
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
DefaultHTTPClient = AioHTTPClient
|
arangoasync/job.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
__all__ = ["AsyncJob"]
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import Callable, Generic, Optional, TypeVar
|
|
6
|
+
|
|
7
|
+
from arangoasync.connection import Connection
|
|
8
|
+
from arangoasync.errno import HTTP_NOT_FOUND
|
|
9
|
+
from arangoasync.exceptions import (
|
|
10
|
+
AsyncJobCancelError,
|
|
11
|
+
AsyncJobClearError,
|
|
12
|
+
AsyncJobResultError,
|
|
13
|
+
AsyncJobStatusError,
|
|
14
|
+
)
|
|
15
|
+
from arangoasync.request import Method, Request
|
|
16
|
+
from arangoasync.response import Response
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AsyncJob(Generic[T]):
|
|
22
|
+
"""Job for tracking and retrieving result of an async API execution.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
conn: HTTP connection.
|
|
26
|
+
job_id: Async job ID.
|
|
27
|
+
response_handler: HTTP response handler
|
|
28
|
+
|
|
29
|
+
References:
|
|
30
|
+
- `jobs <https://docs.arangodb.com/stable/develop/http-api/jobs/>`__
|
|
31
|
+
""" # noqa: E501
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
conn: Connection,
|
|
36
|
+
job_id: str,
|
|
37
|
+
response_handler: Callable[[Response], T],
|
|
38
|
+
) -> None:
|
|
39
|
+
self._conn = conn
|
|
40
|
+
self._id = job_id
|
|
41
|
+
self._response_handler = response_handler
|
|
42
|
+
|
|
43
|
+
def __repr__(self) -> str:
|
|
44
|
+
return f"<AsyncJob {self._id}>"
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def id(self) -> str:
|
|
48
|
+
"""Return the async job ID.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
str: Async job ID.
|
|
52
|
+
"""
|
|
53
|
+
return self._id
|
|
54
|
+
|
|
55
|
+
async def status(self) -> str:
|
|
56
|
+
"""Return the async job status from server.
|
|
57
|
+
|
|
58
|
+
Once a job result is retrieved via func:`arangoasync.job.AsyncJob.result`
|
|
59
|
+
method, it is deleted from server and subsequent status queries will
|
|
60
|
+
fail.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
str: Async job status. Possible values are "pending" (job is still
|
|
64
|
+
in queue), "done" (job finished or raised an error).
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ArangoError: If there is a problem with the request.
|
|
68
|
+
AsyncJobStatusError: If retrieval fails or the job is not found.
|
|
69
|
+
|
|
70
|
+
References:
|
|
71
|
+
- `list-async-jobs-by-status-or-get-the-status-of-specific-job <https://docs.arangodb.com/stable/develop/http-api/jobs/#list-async-jobs-by-status-or-get-the-status-of-specific-job>`__
|
|
72
|
+
""" # noqa: E501
|
|
73
|
+
request = Request(method=Method.GET, endpoint=f"/_api/job/{self._id}")
|
|
74
|
+
response = await self._conn.send_request(request)
|
|
75
|
+
|
|
76
|
+
if response.is_success:
|
|
77
|
+
if response.status_code == 204:
|
|
78
|
+
return "pending"
|
|
79
|
+
else:
|
|
80
|
+
return "done"
|
|
81
|
+
if response.error_code == HTTP_NOT_FOUND:
|
|
82
|
+
error_message = f"job {self._id} not found"
|
|
83
|
+
raise AsyncJobStatusError(response, request, error_message)
|
|
84
|
+
raise AsyncJobStatusError(response, request)
|
|
85
|
+
|
|
86
|
+
async def result(self) -> T:
|
|
87
|
+
"""Fetch the async job result from server.
|
|
88
|
+
|
|
89
|
+
If the job raised an exception, it is propagated up at this point.
|
|
90
|
+
|
|
91
|
+
Once job result is retrieved, it is deleted from server and subsequent
|
|
92
|
+
queries for result will fail.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Async job result.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ArangoError: If the job raised an exception or there was a problem with
|
|
99
|
+
the request.
|
|
100
|
+
AsyncJobResultError: If retrieval fails, because job no longer exists or
|
|
101
|
+
is still pending.
|
|
102
|
+
|
|
103
|
+
References:
|
|
104
|
+
- `get-the-results-of-an-async-job <https://docs.arangodb.com/stable/develop/http-api/jobs/#get-the-results-of-an-async-job>`__
|
|
105
|
+
""" # noqa: E501
|
|
106
|
+
request = Request(method=Method.PUT, endpoint=f"/_api/job/{self._id}")
|
|
107
|
+
response = await self._conn.send_request(request)
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
"x-arango-async-id" in response.headers
|
|
111
|
+
or "X-Arango-Async-Id" in response.headers
|
|
112
|
+
):
|
|
113
|
+
# The job result is available on the server
|
|
114
|
+
return self._response_handler(response)
|
|
115
|
+
|
|
116
|
+
if response.status_code == 204:
|
|
117
|
+
# The job is still in the pending queue or not yet finished.
|
|
118
|
+
raise AsyncJobResultError(response, request, self._not_done())
|
|
119
|
+
# The job is not known (anymore).
|
|
120
|
+
# We can tell the status from the HTTP status code.
|
|
121
|
+
if response.error_code == HTTP_NOT_FOUND:
|
|
122
|
+
raise AsyncJobResultError(response, request, self._not_found())
|
|
123
|
+
raise AsyncJobResultError(response, request)
|
|
124
|
+
|
|
125
|
+
async def cancel(self, ignore_missing: bool = False) -> bool:
|
|
126
|
+
"""Cancel the async job.
|
|
127
|
+
|
|
128
|
+
An async job cannot be cancelled once it is taken out of the queue.
|
|
129
|
+
|
|
130
|
+
Note:
|
|
131
|
+
It still might take some time to actually cancel the running async job.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
ignore_missing: Do not raise an exception if the job is not found.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
`True` if job was cancelled successfully, `False` if the job was not found
|
|
138
|
+
but **ignore_missing** was set to `True`.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ArangoError: If there was a problem with the request.
|
|
142
|
+
AsyncJobCancelError: If cancellation fails.
|
|
143
|
+
|
|
144
|
+
References:
|
|
145
|
+
- `cancel-an-async-job <https://docs.arangodb.com/stable/develop/http-api/jobs/#cancel-an-async-job>`__
|
|
146
|
+
""" # noqa: E501
|
|
147
|
+
request = Request(method=Method.PUT, endpoint=f"/_api/job/{self._id}/cancel")
|
|
148
|
+
response = await self._conn.send_request(request)
|
|
149
|
+
|
|
150
|
+
if response.is_success:
|
|
151
|
+
return True
|
|
152
|
+
if response.error_code == HTTP_NOT_FOUND:
|
|
153
|
+
if ignore_missing:
|
|
154
|
+
return False
|
|
155
|
+
raise AsyncJobCancelError(response, request, self._not_found())
|
|
156
|
+
raise AsyncJobCancelError(response, request)
|
|
157
|
+
|
|
158
|
+
async def clear(
|
|
159
|
+
self,
|
|
160
|
+
ignore_missing: bool = False,
|
|
161
|
+
) -> bool:
|
|
162
|
+
"""Delete the job result from the server.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
ignore_missing: Do not raise an exception if the job is not found.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
`True` if result was deleted successfully, `False` if the job was
|
|
169
|
+
not found but **ignore_missing** was set to `True`.
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
ArangoError: If there was a problem with the request.
|
|
173
|
+
AsyncJobClearError: If deletion fails.
|
|
174
|
+
|
|
175
|
+
References:
|
|
176
|
+
- `delete-async-job-results <https://docs.arangodb.com/stable/develop/http-api/jobs/#delete-async-job-results>`__
|
|
177
|
+
""" # noqa: E501
|
|
178
|
+
request = Request(method=Method.DELETE, endpoint=f"/_api/job/{self._id}")
|
|
179
|
+
resp = await self._conn.send_request(request)
|
|
180
|
+
|
|
181
|
+
if resp.is_success:
|
|
182
|
+
return True
|
|
183
|
+
if resp.error_code == HTTP_NOT_FOUND:
|
|
184
|
+
if ignore_missing:
|
|
185
|
+
return False
|
|
186
|
+
raise AsyncJobClearError(resp, request, self._not_found())
|
|
187
|
+
raise AsyncJobClearError(resp, request)
|
|
188
|
+
|
|
189
|
+
async def wait(self, seconds: Optional[float] = None) -> bool:
|
|
190
|
+
"""Wait for the async job to finish.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
seconds: Number of seconds to wait between status checks. If not
|
|
194
|
+
provided, the method will wait indefinitely.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
`True` if the job is done, `False` if the job is still pending.
|
|
198
|
+
"""
|
|
199
|
+
while True:
|
|
200
|
+
if await self.status() == "done":
|
|
201
|
+
return True
|
|
202
|
+
if seconds is None:
|
|
203
|
+
await asyncio.sleep(1)
|
|
204
|
+
else:
|
|
205
|
+
seconds -= 1
|
|
206
|
+
if seconds < 0:
|
|
207
|
+
return False
|
|
208
|
+
await asyncio.sleep(1)
|
|
209
|
+
|
|
210
|
+
def _not_found(self) -> str:
|
|
211
|
+
return f"job {self._id} not found"
|
|
212
|
+
|
|
213
|
+
def _not_done(self) -> str:
|
|
214
|
+
return f"job {self._id} not done"
|
arangoasync/logger.py
ADDED
arangoasync/request.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"Method",
|
|
3
|
+
"Request",
|
|
4
|
+
]
|
|
5
|
+
|
|
6
|
+
from enum import Enum, auto
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from arangoasync.auth import Auth
|
|
10
|
+
from arangoasync.typings import Params, RequestHeaders
|
|
11
|
+
from arangoasync.version import __version__
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Method(Enum):
|
|
15
|
+
"""HTTP methods enum: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"""
|
|
16
|
+
|
|
17
|
+
GET = auto()
|
|
18
|
+
POST = auto()
|
|
19
|
+
PUT = auto()
|
|
20
|
+
PATCH = auto()
|
|
21
|
+
DELETE = auto()
|
|
22
|
+
HEAD = auto()
|
|
23
|
+
OPTIONS = auto()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Request:
|
|
27
|
+
"""HTTP request.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
method (Method): HTTP method.
|
|
31
|
+
endpoint (str): API endpoint.
|
|
32
|
+
headers (dict | None): Request headers.
|
|
33
|
+
params (dict | None): URL parameters.
|
|
34
|
+
data (bytes | None): Request payload.
|
|
35
|
+
auth (Auth | None): Authentication.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
method (Method): HTTP method.
|
|
39
|
+
endpoint (str): API endpoint.
|
|
40
|
+
headers (dict | None): Request headers.
|
|
41
|
+
params (dict | None): URL parameters.
|
|
42
|
+
data (bytes | None): Request payload.
|
|
43
|
+
auth (Auth | None): Authentication.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
__slots__ = (
|
|
47
|
+
"method",
|
|
48
|
+
"endpoint",
|
|
49
|
+
"headers",
|
|
50
|
+
"params",
|
|
51
|
+
"data",
|
|
52
|
+
"auth",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
method: Method,
|
|
58
|
+
endpoint: str,
|
|
59
|
+
headers: Optional[RequestHeaders] = None,
|
|
60
|
+
params: Optional[Params] = None,
|
|
61
|
+
data: Optional[bytes | str] = None,
|
|
62
|
+
auth: Optional[Auth] = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
self.method: Method = method
|
|
65
|
+
self.endpoint: str = endpoint
|
|
66
|
+
self.headers: RequestHeaders = headers or dict()
|
|
67
|
+
self.params: Params = params or dict()
|
|
68
|
+
self.data: Optional[bytes | str] = data
|
|
69
|
+
self.auth: Optional[Auth] = auth
|
|
70
|
+
|
|
71
|
+
def normalized_headers(self) -> RequestHeaders:
|
|
72
|
+
"""Normalize request headers.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
dict: Normalized request headers.
|
|
76
|
+
"""
|
|
77
|
+
driver_header = f"arangoasync/{__version__}"
|
|
78
|
+
normalized_headers: RequestHeaders = {
|
|
79
|
+
"charset": "utf-8",
|
|
80
|
+
"content-type": "application/json",
|
|
81
|
+
"x-arango-driver": driver_header,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if self.headers is not None:
|
|
85
|
+
for key, value in self.headers.items():
|
|
86
|
+
normalized_headers[key.lower()] = value
|
|
87
|
+
|
|
88
|
+
return normalized_headers
|
|
89
|
+
|
|
90
|
+
def normalized_params(self) -> Params:
|
|
91
|
+
"""Normalize URL parameters.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
dict: Normalized URL parameters.
|
|
95
|
+
"""
|
|
96
|
+
normalized_params: Params = {}
|
|
97
|
+
|
|
98
|
+
if self.params is not None:
|
|
99
|
+
for key, value in self.params.items():
|
|
100
|
+
if isinstance(value, bool):
|
|
101
|
+
value = int(value)
|
|
102
|
+
normalized_params[key] = str(value)
|
|
103
|
+
|
|
104
|
+
return normalized_params
|
|
105
|
+
|
|
106
|
+
def __repr__(self) -> str:
|
|
107
|
+
return f"<{self.method.name} {self.endpoint}>"
|
arangoasync/resolver.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"HostResolver",
|
|
3
|
+
"SingleHostResolver",
|
|
4
|
+
"RoundRobinHostResolver",
|
|
5
|
+
"DefaultHostResolver",
|
|
6
|
+
"get_resolver",
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HostResolver(ABC):
|
|
14
|
+
"""Abstract base class for host resolvers.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
host_count (int): Number of hosts.
|
|
18
|
+
max_tries (int | None): Maximum number of attempts to try a host.
|
|
19
|
+
Will default to 3 times the number of hosts if not provided.
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
ValueError: If max_tries is less than host_count.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, host_count: int = 1, max_tries: Optional[int] = None) -> None:
|
|
26
|
+
max_tries = max_tries or host_count * 3
|
|
27
|
+
if max_tries < host_count:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
"The maximum number of attempts cannot be "
|
|
30
|
+
"lower than the number of hosts."
|
|
31
|
+
)
|
|
32
|
+
self._host_count = host_count
|
|
33
|
+
self._max_tries = max_tries
|
|
34
|
+
self._index = 0
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def get_host_index(self) -> int: # pragma: no cover
|
|
38
|
+
"""Return the index of the host to use.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
int: Index of the host.
|
|
42
|
+
"""
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
|
|
45
|
+
def change_host(self) -> None:
|
|
46
|
+
"""If there are multiple hosts available, switch to the next one."""
|
|
47
|
+
self._index = (self._index + 1) % self.host_count
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def host_count(self) -> int:
|
|
51
|
+
"""Return the number of hosts."""
|
|
52
|
+
return self._host_count
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def max_tries(self) -> int:
|
|
56
|
+
"""Return the maximum number of attempts."""
|
|
57
|
+
return self._max_tries
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SingleHostResolver(HostResolver):
|
|
61
|
+
"""Single host resolver.
|
|
62
|
+
|
|
63
|
+
Always returns the same host index, unless prompted to change.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, host_count: int, max_tries: Optional[int] = None) -> None:
|
|
67
|
+
super().__init__(host_count, max_tries)
|
|
68
|
+
|
|
69
|
+
def get_host_index(self) -> int:
|
|
70
|
+
return self._index
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class RoundRobinHostResolver(HostResolver):
|
|
74
|
+
"""Round-robin host resolver. Changes host every time.
|
|
75
|
+
|
|
76
|
+
Useful for bulk inserts or updates.
|
|
77
|
+
|
|
78
|
+
Note:
|
|
79
|
+
Do not use this resolver for stream transactions.
|
|
80
|
+
Transaction IDs cannot be shared across different coordinators.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, host_count: int, max_tries: Optional[int] = None) -> None:
|
|
84
|
+
super().__init__(host_count, max_tries)
|
|
85
|
+
self._index = -1
|
|
86
|
+
|
|
87
|
+
def get_host_index(self, indexes_to_filter: Optional[List[int]] = None) -> int:
|
|
88
|
+
self.change_host()
|
|
89
|
+
return self._index
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
DefaultHostResolver = SingleHostResolver
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_resolver(
|
|
96
|
+
strategy: str,
|
|
97
|
+
host_count: int,
|
|
98
|
+
max_tries: Optional[int] = None,
|
|
99
|
+
) -> HostResolver:
|
|
100
|
+
"""Return a host resolver based on the strategy.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
strategy (str): Resolver strategy.
|
|
104
|
+
host_count (int): Number of hosts.
|
|
105
|
+
max_tries (int): Maximum number of attempts to try a host.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
HostResolver: Host resolver.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
ValueError: If the strategy is not supported.
|
|
112
|
+
"""
|
|
113
|
+
if strategy == "roundrobin":
|
|
114
|
+
return RoundRobinHostResolver(host_count, max_tries)
|
|
115
|
+
if strategy == "single":
|
|
116
|
+
return SingleHostResolver(host_count, max_tries)
|
|
117
|
+
if strategy == "default":
|
|
118
|
+
return DefaultHostResolver(host_count, max_tries)
|
|
119
|
+
raise ValueError(f"Unsupported host resolver strategy: {strategy}")
|
arangoasync/response.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"Response",
|
|
3
|
+
]
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from arangoasync.request import Method
|
|
8
|
+
from arangoasync.typings import ResponseHeaders
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Response:
|
|
12
|
+
"""HTTP response.
|
|
13
|
+
|
|
14
|
+
Parameters:
|
|
15
|
+
method (Method): HTTP method.
|
|
16
|
+
url (str): API URL.
|
|
17
|
+
headers (dict): Response headers.
|
|
18
|
+
status_code (int): Response status code.
|
|
19
|
+
status_text (str): Response status text.
|
|
20
|
+
raw_body (bytes): Raw response body.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
method (Method): HTTP method.
|
|
24
|
+
url (str): API URL.
|
|
25
|
+
headers (dict): Response headers.
|
|
26
|
+
status_code (int): Response status code.
|
|
27
|
+
status_text (str): Response status text.
|
|
28
|
+
raw_body (bytes): Raw response body.
|
|
29
|
+
error_code (int | None): Error code from ArangoDB server.
|
|
30
|
+
error_message (str | None): Error message from ArangoDB server.
|
|
31
|
+
is_success (bool | None): True if response status code was 2XX.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
__slots__ = (
|
|
35
|
+
"method",
|
|
36
|
+
"url",
|
|
37
|
+
"headers",
|
|
38
|
+
"status_code",
|
|
39
|
+
"status_text",
|
|
40
|
+
"raw_body",
|
|
41
|
+
"error_code",
|
|
42
|
+
"error_message",
|
|
43
|
+
"is_success",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
method: Method,
|
|
49
|
+
url: str,
|
|
50
|
+
headers: ResponseHeaders,
|
|
51
|
+
status_code: int,
|
|
52
|
+
status_text: str,
|
|
53
|
+
raw_body: bytes,
|
|
54
|
+
) -> None:
|
|
55
|
+
self.method: Method = method
|
|
56
|
+
self.url: str = url
|
|
57
|
+
self.headers: ResponseHeaders = headers
|
|
58
|
+
self.status_code: int = status_code
|
|
59
|
+
self.status_text: str = status_text
|
|
60
|
+
self.raw_body: bytes = raw_body
|
|
61
|
+
|
|
62
|
+
# Populated later
|
|
63
|
+
self.error_code: Optional[int] = None
|
|
64
|
+
self.error_message: Optional[str] = None
|
|
65
|
+
self.is_success: Optional[bool] = None
|
arangoasync/result.py
ADDED