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/auth.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"Auth",
|
|
3
|
+
"JwtToken",
|
|
4
|
+
]
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import jwt
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Auth:
|
|
15
|
+
"""Authentication details for the ArangoDB instance.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
username (str): Username.
|
|
19
|
+
password (str): Password.
|
|
20
|
+
encoding (str): Encoding for the password (default: utf-8)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
username: str
|
|
24
|
+
password: str
|
|
25
|
+
encoding: str = "utf-8"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class JwtToken:
|
|
29
|
+
"""JWT token.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
token (str): JWT token.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
TypeError: If the token type is not str or bytes.
|
|
36
|
+
jwt.exceptions.ExpiredSignatureError: If the token expired.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, token: str) -> None:
|
|
40
|
+
self._token = token
|
|
41
|
+
self._validate()
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def generate_token(
|
|
45
|
+
secret: str | bytes,
|
|
46
|
+
iat: Optional[int] = None,
|
|
47
|
+
exp: int = 3600,
|
|
48
|
+
iss: str = "arangodb",
|
|
49
|
+
server_id: str = "client",
|
|
50
|
+
) -> "JwtToken":
|
|
51
|
+
"""Generate and return a JWT token.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
secret (str | bytes): JWT secret.
|
|
55
|
+
iat (int): Time the token was issued in seconds. Defaults to current time.
|
|
56
|
+
exp (int): Time to expire in seconds.
|
|
57
|
+
iss (str): Issuer.
|
|
58
|
+
server_id (str): Server ID.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
str: JWT token.
|
|
62
|
+
"""
|
|
63
|
+
iat = iat or int(time.time())
|
|
64
|
+
token = jwt.encode(
|
|
65
|
+
payload={
|
|
66
|
+
"iat": iat,
|
|
67
|
+
"exp": iat + exp,
|
|
68
|
+
"iss": iss,
|
|
69
|
+
"server_id": server_id,
|
|
70
|
+
},
|
|
71
|
+
key=secret,
|
|
72
|
+
)
|
|
73
|
+
return JwtToken(token)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def token(self) -> str:
|
|
77
|
+
"""Get token."""
|
|
78
|
+
return self._token
|
|
79
|
+
|
|
80
|
+
@token.setter
|
|
81
|
+
def token(self, token: str) -> None:
|
|
82
|
+
"""Set token.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
jwt.exceptions.ExpiredSignatureError: If the token expired.
|
|
86
|
+
"""
|
|
87
|
+
self._token = token
|
|
88
|
+
self._validate()
|
|
89
|
+
|
|
90
|
+
def needs_refresh(self, leeway: int = 0) -> bool:
|
|
91
|
+
"""Check if the token needs to be refreshed.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
leeway (int): Leeway in seconds, before official expiration,
|
|
95
|
+
when to consider the token expired.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
bool: True if the token needs to be refreshed, False otherwise.
|
|
99
|
+
"""
|
|
100
|
+
refresh: bool = int(time.time()) > self._token_exp - leeway
|
|
101
|
+
return refresh
|
|
102
|
+
|
|
103
|
+
def _validate(self) -> None:
|
|
104
|
+
"""Validate the token."""
|
|
105
|
+
if type(self._token) is not str:
|
|
106
|
+
raise TypeError("Token must be str")
|
|
107
|
+
|
|
108
|
+
jwt_payload = jwt.decode(
|
|
109
|
+
self._token,
|
|
110
|
+
issuer="arangodb",
|
|
111
|
+
algorithms=["HS256"],
|
|
112
|
+
options={
|
|
113
|
+
"require_exp": True,
|
|
114
|
+
"require_iat": True,
|
|
115
|
+
"verify_iat": True,
|
|
116
|
+
"verify_exp": True,
|
|
117
|
+
"verify_signature": False,
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
self._token_exp = jwt_payload["exp"]
|
arangoasync/client.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
__all__ = ["ArangoClient"]
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Optional, Sequence
|
|
5
|
+
|
|
6
|
+
from arangoasync.auth import Auth, JwtToken
|
|
7
|
+
from arangoasync.compression import CompressionManager
|
|
8
|
+
from arangoasync.connection import (
|
|
9
|
+
BasicConnection,
|
|
10
|
+
Connection,
|
|
11
|
+
JwtConnection,
|
|
12
|
+
JwtSuperuserConnection,
|
|
13
|
+
)
|
|
14
|
+
from arangoasync.database import StandardDatabase
|
|
15
|
+
from arangoasync.http import DefaultHTTPClient, HTTPClient
|
|
16
|
+
from arangoasync.resolver import HostResolver, get_resolver
|
|
17
|
+
from arangoasync.serialization import (
|
|
18
|
+
DefaultDeserializer,
|
|
19
|
+
DefaultSerializer,
|
|
20
|
+
Deserializer,
|
|
21
|
+
Serializer,
|
|
22
|
+
)
|
|
23
|
+
from arangoasync.typings import Json, Jsons
|
|
24
|
+
from arangoasync.version import __version__
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ArangoClient:
|
|
28
|
+
"""ArangoDB client.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
hosts (str | Sequence[str]): Host URL or list of URL's.
|
|
32
|
+
In case of a cluster, this would be the list of coordinators.
|
|
33
|
+
Which coordinator to use is determined by the `host_resolver`.
|
|
34
|
+
host_resolver (str | HostResolver): Host resolver strategy.
|
|
35
|
+
This determines how the client will choose which server to use.
|
|
36
|
+
Passing a string would configure a resolver with the default settings.
|
|
37
|
+
See :class:`DefaultHostResolver <arangoasync.resolver.DefaultHostResolver>`
|
|
38
|
+
and :func:`get_resolver <arangoasync.resolver.get_resolver>`
|
|
39
|
+
for more information.
|
|
40
|
+
If you need more customization, pass a subclass of
|
|
41
|
+
:class:`HostResolver <arangoasync.resolver.HostResolver>`.
|
|
42
|
+
http_client (HTTPClient | None): HTTP client implementation.
|
|
43
|
+
This is the core component that sends requests to the ArangoDB server.
|
|
44
|
+
Defaults to :class:`DefaultHttpClient <arangoasync.http.DefaultHTTPClient>`,
|
|
45
|
+
but you can fully customize its parameters or even use a different
|
|
46
|
+
implementation by subclassing
|
|
47
|
+
:class:`HTTPClient <arangoasync.http.HTTPClient>`.
|
|
48
|
+
compression (CompressionManager | None): Disabled by default.
|
|
49
|
+
Used to compress requests to the server or instruct the server to compress
|
|
50
|
+
responses. Enable it by passing an instance of
|
|
51
|
+
:class:`DefaultCompressionManager
|
|
52
|
+
<arangoasync.compression.DefaultCompressionManager>`
|
|
53
|
+
or a custom subclass of :class:`CompressionManager
|
|
54
|
+
<arangoasync.compression.CompressionManager>`.
|
|
55
|
+
serializer (Serializer | None): Custom JSON serializer implementation.
|
|
56
|
+
Leave as `None` to use the default serializer.
|
|
57
|
+
See :class:`DefaultSerializer
|
|
58
|
+
<arangoasync.serialization.DefaultSerializer>`.
|
|
59
|
+
For custom serialization of collection documents, see :class:`Collection
|
|
60
|
+
<arangoasync.collection.Collection>`.
|
|
61
|
+
deserializer (Deserializer | None): Custom JSON deserializer implementation.
|
|
62
|
+
Leave as `None` to use the default deserializer.
|
|
63
|
+
See :class:`DefaultDeserializer
|
|
64
|
+
<arangoasync.serialization.DefaultDeserializer>`.
|
|
65
|
+
For custom deserialization of collection documents, see :class:`Collection
|
|
66
|
+
<arangoasync.collection.Collection>`.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: If the `host_resolver` is not supported.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
hosts: str | Sequence[str] = "http://127.0.0.1:8529",
|
|
75
|
+
host_resolver: str | HostResolver = "default",
|
|
76
|
+
http_client: Optional[HTTPClient] = None,
|
|
77
|
+
compression: Optional[CompressionManager] = None,
|
|
78
|
+
serializer: Optional[Serializer[Json]] = None,
|
|
79
|
+
deserializer: Optional[Deserializer[Json, Jsons]] = None,
|
|
80
|
+
) -> None:
|
|
81
|
+
self._hosts = [hosts] if isinstance(hosts, str) else hosts
|
|
82
|
+
self._host_resolver = (
|
|
83
|
+
get_resolver(host_resolver, len(self._hosts))
|
|
84
|
+
if isinstance(host_resolver, str)
|
|
85
|
+
else host_resolver
|
|
86
|
+
)
|
|
87
|
+
self._http_client = http_client or DefaultHTTPClient()
|
|
88
|
+
self._sessions = [
|
|
89
|
+
self._http_client.create_session(host) for host in self._hosts
|
|
90
|
+
]
|
|
91
|
+
self._compression = compression
|
|
92
|
+
self._serializer: Serializer[Json] = serializer or DefaultSerializer()
|
|
93
|
+
self._deserializer: Deserializer[Json, Jsons] = (
|
|
94
|
+
deserializer or DefaultDeserializer()
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def __repr__(self) -> str:
|
|
98
|
+
return f"<ArangoClient {','.join(self._hosts)}>"
|
|
99
|
+
|
|
100
|
+
async def __aenter__(self) -> "ArangoClient":
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
104
|
+
await self.close()
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def hosts(self) -> Sequence[str]:
|
|
108
|
+
"""Return the list of hosts."""
|
|
109
|
+
return self._hosts
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def host_resolver(self) -> HostResolver:
|
|
113
|
+
"""Return the host resolver."""
|
|
114
|
+
return self._host_resolver
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def compression(self) -> Optional[CompressionManager]:
|
|
118
|
+
"""Return the compression manager."""
|
|
119
|
+
return self._compression
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def sessions(self) -> Sequence[Any]:
|
|
123
|
+
"""Return the list of sessions.
|
|
124
|
+
|
|
125
|
+
You may use this to customize sessions on the fly (for example,
|
|
126
|
+
adjust the timeout). Not recommended unless you know what you are doing.
|
|
127
|
+
|
|
128
|
+
Warning:
|
|
129
|
+
Modifying only a subset of sessions may lead to unexpected behavior.
|
|
130
|
+
In order to keep the client in a consistent state, you should make sure
|
|
131
|
+
all sessions are configured in the same way.
|
|
132
|
+
"""
|
|
133
|
+
return self._sessions
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def version(self) -> str:
|
|
137
|
+
"""Return the version of the client."""
|
|
138
|
+
return __version__
|
|
139
|
+
|
|
140
|
+
async def close(self) -> None:
|
|
141
|
+
"""Close HTTP sessions."""
|
|
142
|
+
await asyncio.gather(*(session.close() for session in self._sessions))
|
|
143
|
+
|
|
144
|
+
async def db(
|
|
145
|
+
self,
|
|
146
|
+
name: str,
|
|
147
|
+
auth_method: str = "basic",
|
|
148
|
+
auth: Optional[Auth] = None,
|
|
149
|
+
token: Optional[JwtToken] = None,
|
|
150
|
+
verify: bool = False,
|
|
151
|
+
compression: Optional[CompressionManager] = None,
|
|
152
|
+
serializer: Optional[Serializer[Json]] = None,
|
|
153
|
+
deserializer: Optional[Deserializer[Json, Jsons]] = None,
|
|
154
|
+
) -> StandardDatabase:
|
|
155
|
+
"""Connects to a database and returns and API wrapper.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
name (str): Database name.
|
|
159
|
+
auth_method (str): The following methods are supported:
|
|
160
|
+
|
|
161
|
+
- "basic": HTTP authentication.
|
|
162
|
+
Requires the `auth` parameter. The `token` parameter is ignored.
|
|
163
|
+
- "jwt": User JWT authentication.
|
|
164
|
+
At least one of the `auth` or `token` parameters are required.
|
|
165
|
+
If `auth` is provided, but the `token` is not, the token will be
|
|
166
|
+
refreshed automatically. This assumes that the clocks of the server
|
|
167
|
+
and client are synchronized.
|
|
168
|
+
- "superuser": Superuser JWT authentication.
|
|
169
|
+
The `token` parameter is required. The `auth` parameter is ignored.
|
|
170
|
+
auth (Auth | None): Login information.
|
|
171
|
+
token (JwtToken | None): JWT token.
|
|
172
|
+
verify (bool): Verify the connection by sending a test request.
|
|
173
|
+
compression (CompressionManager | None): If set, supersedes the
|
|
174
|
+
client-level compression settings.
|
|
175
|
+
serializer (Serializer | None): If set, supersedes the client-level
|
|
176
|
+
serializer.
|
|
177
|
+
deserializer (Deserializer | None): If set, supersedes the client-level
|
|
178
|
+
deserializer.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
StandardDatabase: Database API wrapper.
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
ValueError: If the authentication is invalid.
|
|
185
|
+
ServerConnectionError: If `verify` is `True` and the connection fails.
|
|
186
|
+
"""
|
|
187
|
+
connection: Connection
|
|
188
|
+
|
|
189
|
+
if auth_method == "basic":
|
|
190
|
+
if auth is None:
|
|
191
|
+
raise ValueError("Basic authentication requires the `auth` parameter")
|
|
192
|
+
connection = BasicConnection(
|
|
193
|
+
sessions=self._sessions,
|
|
194
|
+
host_resolver=self._host_resolver,
|
|
195
|
+
http_client=self._http_client,
|
|
196
|
+
db_name=name,
|
|
197
|
+
compression=compression or self._compression,
|
|
198
|
+
serializer=serializer or self._serializer,
|
|
199
|
+
deserializer=deserializer or self._deserializer,
|
|
200
|
+
auth=auth,
|
|
201
|
+
)
|
|
202
|
+
elif auth_method == "jwt":
|
|
203
|
+
if auth is None and token is None:
|
|
204
|
+
raise ValueError(
|
|
205
|
+
"JWT authentication requires the `auth` or `token` parameter"
|
|
206
|
+
)
|
|
207
|
+
connection = JwtConnection(
|
|
208
|
+
sessions=self._sessions,
|
|
209
|
+
host_resolver=self._host_resolver,
|
|
210
|
+
http_client=self._http_client,
|
|
211
|
+
db_name=name,
|
|
212
|
+
compression=compression or self._compression,
|
|
213
|
+
serializer=serializer or self._serializer,
|
|
214
|
+
deserializer=deserializer or self._deserializer,
|
|
215
|
+
auth=auth,
|
|
216
|
+
token=token,
|
|
217
|
+
)
|
|
218
|
+
elif auth_method == "superuser":
|
|
219
|
+
if token is None:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
"Superuser JWT authentication requires the `token` parameter"
|
|
222
|
+
)
|
|
223
|
+
connection = JwtSuperuserConnection(
|
|
224
|
+
sessions=self._sessions,
|
|
225
|
+
host_resolver=self._host_resolver,
|
|
226
|
+
http_client=self._http_client,
|
|
227
|
+
db_name=name,
|
|
228
|
+
compression=compression or self._compression,
|
|
229
|
+
serializer=serializer or self._serializer,
|
|
230
|
+
deserializer=deserializer or self._deserializer,
|
|
231
|
+
token=token,
|
|
232
|
+
)
|
|
233
|
+
else:
|
|
234
|
+
raise ValueError(f"Invalid authentication method: {auth_method}")
|
|
235
|
+
|
|
236
|
+
if verify:
|
|
237
|
+
await connection.ping()
|
|
238
|
+
|
|
239
|
+
return StandardDatabase(connection)
|