avee 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.
avee/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ from . import models
2
+ from ._client import DEFAULT_BASE_URL, PREVIEW_BASE_URL, VERSION, AsyncAveeClient, AveeClient
3
+ from ._operations import OPERATIONS
4
+ from ._request import RequestOptions, apaginate, paginate
5
+ from ._types import PaymentReceipt, RateLimit, ResponseInfo
6
+ from .errors import AveeAPIError, AveeConnectionError, AveeError, AveePaymentError, AveeTimeoutError, AveeValidationError
7
+ from .x402 import Payer, PaymentContext, PaymentSignature
8
+
9
+ __version__ = VERSION
10
+
11
+ __all__ = [
12
+ "DEFAULT_BASE_URL",
13
+ "OPERATIONS",
14
+ "PREVIEW_BASE_URL",
15
+ "AsyncAveeClient",
16
+ "AveeAPIError",
17
+ "AveeClient",
18
+ "AveeConnectionError",
19
+ "AveeError",
20
+ "AveePaymentError",
21
+ "AveeTimeoutError",
22
+ "AveeValidationError",
23
+ "Payer",
24
+ "PaymentContext",
25
+ "PaymentReceipt",
26
+ "PaymentSignature",
27
+ "RateLimit",
28
+ "RequestOptions",
29
+ "ResponseInfo",
30
+ "apaginate",
31
+ "models",
32
+ "paginate",
33
+ ]
avee/_client.py ADDED
@@ -0,0 +1,214 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ import json
6
+ import time
7
+ from collections.abc import Callable, Mapping
8
+ from types import TracebackType
9
+ from typing import Any, TypeVar
10
+
11
+ import httpx
12
+
13
+ from . import _transport as tr
14
+ from ._operations import AsyncOperations, SyncOperations
15
+ from ._request import Call, RequestOptions
16
+ from ._types import ResponseInfo
17
+ from .errors import AveeError, AveePaymentError, AveeValidationError
18
+ from .x402 import Payer, decode_required, finish, prepare
19
+
20
+ T = TypeVar("T")
21
+
22
+ DEFAULT_BASE_URL = "https://api.preview.avee.tech/api/v1"
23
+ PREVIEW_BASE_URL = "https://api.preview.avee.tech/api/v1"
24
+ VERSION = "0.1.0"
25
+ _USER_AGENT = f"avee-python/{VERSION}"
26
+
27
+
28
+ class _Base:
29
+ def __init__(
30
+ self,
31
+ base_url: str,
32
+ api_key: str | None,
33
+ timeout: float,
34
+ max_retries: int,
35
+ max_retry_delay: float,
36
+ max_response_bytes: int,
37
+ headers: Mapping[str, str] | None,
38
+ payer: Payer | None,
39
+ ) -> None:
40
+ self._config = tr.validate_config(base_url, api_key, timeout, max_retries, max_retry_delay, max_response_bytes, headers, payer)
41
+ self.last_response: ResponseInfo | None = None
42
+ """Status, request id, rate-limit headers and x402 receipt of the most recent response."""
43
+
44
+ def _prepare(self, call: Call) -> tuple[str, bytes | None]:
45
+ body = None if call.body is None else json.dumps(call.body, separators=(",", ":")).encode()
46
+ return self._config.base_url + call.path(), body
47
+
48
+ def _challenge(self, call: Call, headers: httpx.Headers, data: bytes) -> dict[str, Any]:
49
+ raw = decode_required(headers.get("payment-required"), data)
50
+ if raw is None:
51
+ raise AveePaymentError("the 402 challenge is unreadable", call.operation)
52
+ return raw
53
+
54
+ def _record(self, call: Call, resp: httpx.Response, data: bytes | None, url: str) -> bytes:
55
+ self.last_response = tr.response_info(call.operation, resp.status_code, resp.headers)
56
+ if data is None:
57
+ raise AveeValidationError(f"{call.operation}: response from {url} exceeds {self._config.max_response_bytes} bytes")
58
+ return data
59
+
60
+
61
+ class AveeClient(_Base, SyncOperations):
62
+ """Synchronous client for the avee DEX data API; every /api/v1 operation is a method."""
63
+
64
+ def __init__(
65
+ self,
66
+ base_url: str = DEFAULT_BASE_URL,
67
+ *,
68
+ api_key: str | None = None,
69
+ timeout: float = 30.0,
70
+ max_retries: int = 2,
71
+ max_retry_delay: float = 30.0,
72
+ max_response_bytes: int = 16 * 1024 * 1024,
73
+ headers: Mapping[str, str] | None = None,
74
+ payer: Payer | None = None,
75
+ http_client: httpx.Client | None = None,
76
+ ) -> None:
77
+ super().__init__(base_url, api_key, timeout, max_retries, max_retry_delay, max_response_bytes, headers, payer)
78
+ self._owns = http_client is None
79
+ self._http = http_client if http_client is not None else httpx.Client(follow_redirects=False)
80
+
81
+ def close(self) -> None:
82
+ if self._owns:
83
+ self._http.close()
84
+
85
+ def __enter__(self) -> AveeClient:
86
+ return self
87
+
88
+ def __exit__(self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None:
89
+ self.close()
90
+
91
+ def _attempt(self, call: Call, url: str, body: bytes | None, signature: str | None) -> tuple[int, httpx.Headers, bytes]:
92
+ headers = tr.request_headers(self._config, body is not None, signature, _USER_AGENT)
93
+ deadline = time.monotonic() + self._config.timeout
94
+ try:
95
+ with self._http.stream(call.method, url, params=tuple(call.query), content=body, headers=headers, timeout=self._config.timeout, follow_redirects=False) as resp:
96
+ data = tr.read_sync(resp.iter_bytes(), self._config.max_response_bytes, deadline)
97
+ return resp.status_code, resp.headers, self._record(call, resp, data, url)
98
+ except httpx.HTTPError as err:
99
+ raise tr.transport_error(err, call.method, url, self._config.timeout) from err
100
+
101
+ def _sign(self, payer: Payer, call: Call, url: str, raw: dict[str, Any]) -> str:
102
+ context, choice = prepare(payer, call.operation, call.method, url, raw)
103
+ try:
104
+ signature = payer.sign(context)
105
+ except Exception as err:
106
+ raise AveePaymentError(f"the payer did not sign: {err}", call.operation, raw) from err
107
+ if inspect.isawaitable(signature):
108
+ if inspect.iscoroutine(signature):
109
+ signature.close()
110
+ raise AveePaymentError("an async payer needs AsyncAveeClient", call.operation, raw)
111
+ return finish(signature, context, choice, raw)
112
+
113
+ def _send(self, call: Call, decode: Callable[[Any], T], options: RequestOptions | None) -> T:
114
+ url, body = self._prepare(call)
115
+ signature: str | None = None
116
+ attempt = 0
117
+ while True:
118
+ try:
119
+ status, headers, data = self._attempt(call, url, body, signature)
120
+ except AveeError as err:
121
+ delay = tr.retry_delay(err, call.method, attempt, signature is not None, self._config)
122
+ if delay is None:
123
+ raise
124
+ time.sleep(delay)
125
+ attempt += 1
126
+ continue
127
+ if 200 <= status < 300:
128
+ return decode(tr.decode_json(data, call.operation, url))
129
+ if status == 402 and self._config.payer is not None and signature is None:
130
+ signature = self._sign(self._config.payer, call, url, self._challenge(call, headers, data))
131
+ continue
132
+ api_err = tr.api_error(call.operation, call.method, url, status, headers, data, signature is not None)
133
+ delay = tr.retry_delay(api_err, call.method, attempt, signature is not None, self._config)
134
+ if delay is None:
135
+ raise api_err
136
+ time.sleep(delay)
137
+ attempt += 1
138
+
139
+
140
+ class AsyncAveeClient(_Base, AsyncOperations):
141
+ """Asyncio client for the avee DEX data API; the same methods as coroutines."""
142
+
143
+ def __init__(
144
+ self,
145
+ base_url: str = DEFAULT_BASE_URL,
146
+ *,
147
+ api_key: str | None = None,
148
+ timeout: float = 30.0,
149
+ max_retries: int = 2,
150
+ max_retry_delay: float = 30.0,
151
+ max_response_bytes: int = 16 * 1024 * 1024,
152
+ headers: Mapping[str, str] | None = None,
153
+ payer: Payer | None = None,
154
+ http_client: httpx.AsyncClient | None = None,
155
+ ) -> None:
156
+ super().__init__(base_url, api_key, timeout, max_retries, max_retry_delay, max_response_bytes, headers, payer)
157
+ self._owns = http_client is None
158
+ self._http = http_client if http_client is not None else httpx.AsyncClient(follow_redirects=False)
159
+
160
+ async def aclose(self) -> None:
161
+ if self._owns:
162
+ await self._http.aclose()
163
+
164
+ async def __aenter__(self) -> AsyncAveeClient:
165
+ return self
166
+
167
+ async def __aexit__(self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None:
168
+ await self.aclose()
169
+
170
+ async def _attempt(self, call: Call, url: str, body: bytes | None, signature: str | None) -> tuple[int, httpx.Headers, bytes]:
171
+ headers = tr.request_headers(self._config, body is not None, signature, _USER_AGENT)
172
+ deadline = time.monotonic() + self._config.timeout
173
+ try:
174
+ async with self._http.stream(call.method, url, params=tuple(call.query), content=body, headers=headers, timeout=self._config.timeout, follow_redirects=False) as resp:
175
+ data = await tr.read_async(resp.aiter_bytes(), self._config.max_response_bytes, deadline)
176
+ return resp.status_code, resp.headers, self._record(call, resp, data, url)
177
+ except httpx.HTTPError as err:
178
+ raise tr.transport_error(err, call.method, url, self._config.timeout) from err
179
+
180
+ async def _sign(self, payer: Payer, call: Call, url: str, raw: dict[str, Any]) -> str:
181
+ context, choice = prepare(payer, call.operation, call.method, url, raw)
182
+ try:
183
+ signature = payer.sign(context)
184
+ if inspect.isawaitable(signature):
185
+ signature = await signature
186
+ except Exception as err:
187
+ raise AveePaymentError(f"the payer did not sign: {err}", call.operation, raw) from err
188
+ return finish(signature, context, choice, raw)
189
+
190
+ async def _send(self, call: Call, decode: Callable[[Any], T], options: RequestOptions | None) -> T:
191
+ url, body = self._prepare(call)
192
+ signature: str | None = None
193
+ attempt = 0
194
+ while True:
195
+ try:
196
+ status, headers, data = await self._attempt(call, url, body, signature)
197
+ except AveeError as err:
198
+ delay = tr.retry_delay(err, call.method, attempt, signature is not None, self._config)
199
+ if delay is None:
200
+ raise
201
+ await asyncio.sleep(delay)
202
+ attempt += 1
203
+ continue
204
+ if 200 <= status < 300:
205
+ return decode(tr.decode_json(data, call.operation, url))
206
+ if status == 402 and self._config.payer is not None and signature is None:
207
+ signature = await self._sign(self._config.payer, call, url, self._challenge(call, headers, data))
208
+ continue
209
+ api_err = tr.api_error(call.operation, call.method, url, status, headers, data, signature is not None)
210
+ delay = tr.retry_delay(api_err, call.method, attempt, signature is not None, self._config)
211
+ if delay is None:
212
+ raise api_err
213
+ await asyncio.sleep(delay)
214
+ attempt += 1
avee/_decode.py ADDED
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Mapping
4
+ from typing import Any, TypeVar
5
+
6
+ from .errors import AveeValidationError
7
+
8
+ T = TypeVar("T")
9
+ Decoder = Callable[[Any, str], T]
10
+
11
+
12
+ def _obj(value: Any, where: str) -> Mapping[str, Any]:
13
+ if not isinstance(value, dict):
14
+ raise AveeValidationError(f"{where} must be an object, got {type(value).__name__}")
15
+ return value
16
+
17
+
18
+ def _req(o: Mapping[str, Any], key: str, fn: Decoder[T], where: str) -> T:
19
+ value = o.get(key)
20
+ if value is None:
21
+ raise AveeValidationError(f"{where} is missing")
22
+ return fn(value, where)
23
+
24
+
25
+ def _opt(o: Mapping[str, Any], key: str, fn: Decoder[T], where: str) -> T | None:
26
+ value = o.get(key)
27
+ return None if value is None else fn(value, where)
28
+
29
+
30
+ def _str(value: Any, where: str) -> str:
31
+ if not isinstance(value, str):
32
+ raise AveeValidationError(f"{where} must be a string, got {type(value).__name__}")
33
+ return value
34
+
35
+
36
+ def _int(value: Any, where: str) -> int:
37
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or (isinstance(value, float) and not value.is_integer()):
38
+ raise AveeValidationError(f"{where} must be an integer, got {value!r}")
39
+ return int(value)
40
+
41
+
42
+ def _float(value: Any, where: str) -> float:
43
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
44
+ raise AveeValidationError(f"{where} must be a number, got {type(value).__name__}")
45
+ return float(value)
46
+
47
+
48
+ def _bool(value: Any, where: str) -> bool:
49
+ if not isinstance(value, bool):
50
+ raise AveeValidationError(f"{where} must be a boolean, got {type(value).__name__}")
51
+ return value
52
+
53
+
54
+ def _any(value: Any, where: str) -> Any:
55
+ return value
56
+
57
+
58
+ def _list_of(fn: Decoder[T]) -> Decoder[list[T]]:
59
+ def decode(value: Any, where: str) -> list[T]:
60
+ if not isinstance(value, list):
61
+ raise AveeValidationError(f"{where} must be an array, got {type(value).__name__}")
62
+ return [fn(v, f"{where}[{i}]") for i, v in enumerate(value)]
63
+
64
+ return decode
65
+
66
+
67
+ def _map_of(fn: Decoder[T]) -> Decoder[dict[str, T]]:
68
+ def decode(value: Any, where: str) -> dict[str, T]:
69
+ return {k: fn(v, f"{where}.{k}") for k, v in _obj(value, where).items()}
70
+
71
+ return decode
72
+
73
+
74
+ def _one_of(value: Any, where: str, branches: tuple[tuple[Decoder[Any], tuple[str, ...]], ...]) -> Any:
75
+ o = _obj(value, where)
76
+ for fn, required in branches:
77
+ if all(k in o for k in required):
78
+ return fn(value, where)
79
+ return dict(o)