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/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)