sendrelay 1.7.0__tar.gz

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.
@@ -0,0 +1,16 @@
1
+ # Python
2
+ .venv/
3
+ __pycache__/
4
+ *.egg-info/
5
+ .pytest_cache/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ # Swift
9
+ .build/
10
+ .swiftpm/
11
+ Package.resolved
12
+ # Gradle / Android
13
+ .gradle/
14
+ local.properties
15
+ *.iml
16
+ .idea/
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 1.7.0 — 2026-10-29
4
+
5
+ - First release, versioned with every other Relay SDK (they move together). Matches the shared SDK conformance suite (packages/sdks/conformance).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Relay Delivery
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.5
2
+ Name: sendrelay
3
+ Version: 1.7.0
4
+ Summary: Official Relay server SDK for Python: quotes, deliveries, webhooks and tracking tokens
5
+ Project-URL: Homepage, https://sendrelay.com.ng
6
+ Project-URL: Documentation, https://docs.sendrelay.com.ng/sdks/python-sdk
7
+ Project-URL: Source, https://github.com/SendRelay/relay-sdk-python
8
+ Author: SendRelay
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,delivery,dispatch,logistics,relay,sdk
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.9
19
+ Requires-Dist: httpx<1,>=0.25
20
+ Requires-Dist: typing-extensions>=4.7
21
+ Provides-Extra: dev
22
+ Requires-Dist: build>=1.2; extra == 'dev'
23
+ Requires-Dist: mypy>=1.11; extra == 'dev'
24
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
25
+ Requires-Dist: pytest>=8; extra == 'dev'
26
+ Requires-Dist: ruff>=0.6; extra == 'dev'
27
+ Requires-Dist: twine>=5; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Relay Python SDK
31
+
32
+ The official server SDK for [Relay](https://sendrelay.com.ng), the delivery API. Quote and create deliveries, read them back with the rider's live ETA, manage webhooks, and mint tracking tokens for your apps.
33
+
34
+ ```bash
35
+ pip install sendrelay
36
+ ```
37
+
38
+ Python 3.9 or newer. Use `Relay` in regular code and `AsyncRelay` with asyncio. Both have the same methods.
39
+
40
+ ## Quick start
41
+
42
+ ```python
43
+ import os
44
+ from sendrelay import Relay
45
+
46
+ relay = Relay(api_key=os.environ["RELAY_API_KEY"]) # sk_test_... or sk_live_..., server only
47
+
48
+ stages = [
49
+ {
50
+ "type": "PICKUP",
51
+ "location": {"latitude": 6.5244, "longitude": 3.3792, "address": "12 Marina Road, Lagos"},
52
+ "items": [{"name": "Phone case", "estimatedValue": 500000, "estimatedWeight": "LIGHT", "estimatedSize": "BOX"}],
53
+ },
54
+ {"type": "DROPOFF", "location": {"latitude": 6.4474, "longitude": 3.4723, "address": "22 Admiralty Way, Lekki"}},
55
+ ]
56
+
57
+ quote = relay.tasks.quote({"taskType": "PACKAGE_DELIVERY", "stages": stages})
58
+ print(quote["total"]) # kobo
59
+
60
+ created = relay.tasks.create(
61
+ {"taskType": "PACKAGE_DELIVERY", "stages": stages, "quoteId": quote["quoteId"]},
62
+ idempotency_key="order-1042", # retries with the same key never create a second delivery
63
+ )
64
+
65
+ read = relay.tasks.get(created["task"]["id"])
66
+ if read["liveEta"]:
67
+ print(f"Arriving at stop {read['liveEta']['stageIndex'] + 1} at {read['liveEta']['arrivesAt']}")
68
+ ```
69
+
70
+ Responses are plain dicts with the API's field names (`liveEta`, `totalFee`, ...). `sendrelay.types` has TypedDicts for them.
71
+
72
+ ## Asyncio
73
+
74
+ ```python
75
+ from sendrelay import AsyncRelay
76
+
77
+ async with AsyncRelay(api_key=os.environ["RELAY_API_KEY"]) as relay:
78
+ async for task in relay.tasks.list_all(limit=50):
79
+ print(task["id"], task["status"])
80
+ ```
81
+
82
+ ## Webhooks
83
+
84
+ ```python
85
+ from sendrelay import verify_webhook_signature
86
+
87
+ result = verify_webhook_signature(raw_body, request.headers["X-Relay-Signature"], os.environ["RELAY_WEBHOOK_SECRET"])
88
+ if not result.valid:
89
+ return 400
90
+ ```
91
+
92
+ Pass the raw request body exactly as received, not re-serialized JSON.
93
+
94
+ ## Errors and retries
95
+
96
+ - `ApiError` has `status_code`, `code` and `details` from the API.
97
+ - `NetworkError` means the request never got an answer.
98
+ - `ValidationError` means the SDK rejected the input before sending.
99
+
100
+ Timeouts, 408, 429 and 5xx responses are retried up to 3 times with exponential backoff. Test-mode quota errors are not retried. Change this with `max_retries=` or `retry=RetryConfig(...)`.
101
+
102
+ ## Tracking in your app
103
+
104
+ Mint a short-lived token on your server and hand it to your app:
105
+
106
+ ```python
107
+ token = relay.auth.create_websocket_token({"scope": [f"task:{task_id}"]})["token"]
108
+ ```
109
+
110
+ Your app then tracks the task live with one of the UI SDKs.
111
+
112
+ [Full documentation](https://docs.sendrelay.com.ng/sdks/python-sdk)
@@ -0,0 +1,83 @@
1
+ # Relay Python SDK
2
+
3
+ The official server SDK for [Relay](https://sendrelay.com.ng), the delivery API. Quote and create deliveries, read them back with the rider's live ETA, manage webhooks, and mint tracking tokens for your apps.
4
+
5
+ ```bash
6
+ pip install sendrelay
7
+ ```
8
+
9
+ Python 3.9 or newer. Use `Relay` in regular code and `AsyncRelay` with asyncio. Both have the same methods.
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ import os
15
+ from sendrelay import Relay
16
+
17
+ relay = Relay(api_key=os.environ["RELAY_API_KEY"]) # sk_test_... or sk_live_..., server only
18
+
19
+ stages = [
20
+ {
21
+ "type": "PICKUP",
22
+ "location": {"latitude": 6.5244, "longitude": 3.3792, "address": "12 Marina Road, Lagos"},
23
+ "items": [{"name": "Phone case", "estimatedValue": 500000, "estimatedWeight": "LIGHT", "estimatedSize": "BOX"}],
24
+ },
25
+ {"type": "DROPOFF", "location": {"latitude": 6.4474, "longitude": 3.4723, "address": "22 Admiralty Way, Lekki"}},
26
+ ]
27
+
28
+ quote = relay.tasks.quote({"taskType": "PACKAGE_DELIVERY", "stages": stages})
29
+ print(quote["total"]) # kobo
30
+
31
+ created = relay.tasks.create(
32
+ {"taskType": "PACKAGE_DELIVERY", "stages": stages, "quoteId": quote["quoteId"]},
33
+ idempotency_key="order-1042", # retries with the same key never create a second delivery
34
+ )
35
+
36
+ read = relay.tasks.get(created["task"]["id"])
37
+ if read["liveEta"]:
38
+ print(f"Arriving at stop {read['liveEta']['stageIndex'] + 1} at {read['liveEta']['arrivesAt']}")
39
+ ```
40
+
41
+ Responses are plain dicts with the API's field names (`liveEta`, `totalFee`, ...). `sendrelay.types` has TypedDicts for them.
42
+
43
+ ## Asyncio
44
+
45
+ ```python
46
+ from sendrelay import AsyncRelay
47
+
48
+ async with AsyncRelay(api_key=os.environ["RELAY_API_KEY"]) as relay:
49
+ async for task in relay.tasks.list_all(limit=50):
50
+ print(task["id"], task["status"])
51
+ ```
52
+
53
+ ## Webhooks
54
+
55
+ ```python
56
+ from sendrelay import verify_webhook_signature
57
+
58
+ result = verify_webhook_signature(raw_body, request.headers["X-Relay-Signature"], os.environ["RELAY_WEBHOOK_SECRET"])
59
+ if not result.valid:
60
+ return 400
61
+ ```
62
+
63
+ Pass the raw request body exactly as received, not re-serialized JSON.
64
+
65
+ ## Errors and retries
66
+
67
+ - `ApiError` has `status_code`, `code` and `details` from the API.
68
+ - `NetworkError` means the request never got an answer.
69
+ - `ValidationError` means the SDK rejected the input before sending.
70
+
71
+ Timeouts, 408, 429 and 5xx responses are retried up to 3 times with exponential backoff. Test-mode quota errors are not retried. Change this with `max_retries=` or `retry=RetryConfig(...)`.
72
+
73
+ ## Tracking in your app
74
+
75
+ Mint a short-lived token on your server and hand it to your app:
76
+
77
+ ```python
78
+ token = relay.auth.create_websocket_token({"scope": [f"task:{task_id}"]})["token"]
79
+ ```
80
+
81
+ Your app then tracks the task live with one of the UI SDKs.
82
+
83
+ [Full documentation](https://docs.sendrelay.com.ng/sdks/python-sdk)
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sendrelay"
7
+ version = "1.7.0"
8
+ description = "Official Relay server SDK for Python: quotes, deliveries, webhooks and tracking tokens"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "SendRelay" }]
13
+ requires-python = ">=3.9"
14
+ dependencies = [
15
+ "httpx>=0.25,<1",
16
+ "typing_extensions>=4.7",
17
+ ]
18
+ keywords = ["relay", "delivery", "logistics", "dispatch", "sdk", "api"]
19
+ classifiers = [
20
+ "Development Status :: 5 - Production/Stable",
21
+ "Intended Audience :: Developers",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3 :: Only",
24
+ "Typing :: Typed",
25
+ "Framework :: AsyncIO",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://sendrelay.com.ng"
30
+ Documentation = "https://docs.sendrelay.com.ng/sdks/python-sdk"
31
+ Source = "https://github.com/SendRelay/relay-sdk-python"
32
+
33
+ [project.optional-dependencies]
34
+ dev = ["pytest>=8", "pytest-asyncio>=0.23", "ruff>=0.6", "mypy>=1.11", "build>=1.2", "twine>=5"]
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/sendrelay"]
38
+
39
+ [tool.pytest.ini_options]
40
+ testpaths = ["tests"]
41
+ asyncio_mode = "auto"
42
+ asyncio_default_fixture_loop_scope = "function"
43
+
44
+ [tool.ruff]
45
+ line-length = 120
46
+ target-version = "py39"
47
+
48
+ [tool.ruff.lint]
49
+ select = ["E", "F", "I", "UP", "B", "SIM"]
50
+ # Keep typing.Dict/Optional: annotations must still evaluate on Python 3.9.
51
+ ignore = ["UP006", "UP007", "UP035", "UP045"]
52
+
53
+ [tool.mypy]
54
+ python_version = "3.10"
55
+ strict = true
56
+ files = ["src/sendrelay"]
@@ -0,0 +1,24 @@
1
+ """Official Relay server SDK for Python.
2
+
3
+ Quote and create deliveries, read them back with the rider's live ETA, manage webhooks and mint
4
+ tracking tokens for your apps. Use ``Relay`` in regular code and ``AsyncRelay`` with asyncio.
5
+ """
6
+
7
+ from ._client import AsyncRelay, Relay
8
+ from ._errors import ApiError, NetworkError, RelayError, ValidationError
9
+ from ._http import RetryConfig
10
+ from ._version import __version__
11
+ from ._webhooks import WebhookSignatureVerification, verify_webhook_signature
12
+
13
+ __all__ = [
14
+ "ApiError",
15
+ "AsyncRelay",
16
+ "NetworkError",
17
+ "Relay",
18
+ "RelayError",
19
+ "RetryConfig",
20
+ "ValidationError",
21
+ "WebhookSignatureVerification",
22
+ "__version__",
23
+ "verify_webhook_signature",
24
+ ]
@@ -0,0 +1,333 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, AsyncIterator, Dict, Iterator, Mapping, Optional, cast
4
+
5
+ import httpx
6
+
7
+ from . import _operations as ops
8
+ from ._errors import ValidationError
9
+ from ._http import DEFAULT_API_VERSION, DEFAULT_BASE_URL, Config, Request, RetryConfig, send, send_async
10
+ from ._webhooks import WebhookSignatureVerification, verify_webhook_signature
11
+ from .types import (
12
+ CancelTaskResponse,
13
+ CreateTaskResponse,
14
+ CreateWebhookResponse,
15
+ GetTaskResponse,
16
+ ListTasksResponse,
17
+ ListWebhooksResponse,
18
+ PresenceResponse,
19
+ TaskListItem,
20
+ TaskQuoteResponse,
21
+ Webhook,
22
+ WebSocketToken,
23
+ )
24
+
25
+ Body = Mapping[str, Any]
26
+
27
+
28
+ def _config(
29
+ api_key: str,
30
+ base_url: Optional[str],
31
+ api_version: Optional[str],
32
+ timeout: float,
33
+ max_retries: Optional[int],
34
+ retry: Optional[RetryConfig],
35
+ auto_idempotency: bool,
36
+ headers: Optional[Dict[str, str]],
37
+ ) -> Config:
38
+ if not api_key:
39
+ raise ValidationError("API key is required", "api_key")
40
+ if not (api_key.startswith("sk_live_") or api_key.startswith("sk_test_")):
41
+ raise ValidationError("Invalid API key format. Must start with sk_live_ or sk_test_", "api_key")
42
+ retry = retry or RetryConfig()
43
+ if max_retries is not None:
44
+ retry.max_retries = max_retries
45
+ return Config(
46
+ api_key=api_key,
47
+ base_url=base_url or DEFAULT_BASE_URL,
48
+ api_version=api_version or DEFAULT_API_VERSION,
49
+ timeout=timeout,
50
+ retry=retry,
51
+ auto_idempotency=auto_idempotency,
52
+ headers=dict(headers or {}),
53
+ )
54
+
55
+
56
+ class _Base:
57
+ _config: Config
58
+
59
+ @property
60
+ def environment(self) -> str:
61
+ """``"live"`` for a live key, ``"test"`` for a test key."""
62
+ return "live" if self._config.api_key.startswith("sk_live_") else "test"
63
+
64
+
65
+ # ─── Sync ────────────────────────────────────────────────────────────────────
66
+
67
+
68
+ class Tasks:
69
+ def __init__(self, run: Any) -> None:
70
+ self._run = run
71
+
72
+ def quote(self, body: Body) -> TaskQuoteResponse:
73
+ """Price a delivery without creating it. Pass the ``quoteId`` to ``create`` to lock the price."""
74
+ return cast(TaskQuoteResponse, self._run(ops.tasks_quote(body)))
75
+
76
+ def create(self, body: Body, *, idempotency_key: Optional[str] = None) -> CreateTaskResponse:
77
+ """Create a delivery. Pass your own ``idempotency_key`` (for example your order ID) so retries
78
+ never create a second delivery; otherwise one is generated per call."""
79
+ return cast(CreateTaskResponse, self._run(ops.tasks_create(body, idempotency_key)))
80
+
81
+ def list(self, **query: Any) -> ListTasksResponse:
82
+ """One page of tasks, newest first by default. Filters: ``limit``, ``order``, ``cursor``."""
83
+ return cast(ListTasksResponse, self._run(ops.tasks_list(query)))
84
+
85
+ def list_all(self, **query: Any) -> Iterator[TaskListItem]:
86
+ """Every task, following the cursor page by page."""
87
+ cursor: Optional[str] = None
88
+ while True:
89
+ page = self.list(**{**query, **({"cursor": cursor} if cursor else {})})
90
+ yield from page["data"]
91
+ cursor = page.get("cursor") if page.get("hasMore") else None
92
+ if not cursor:
93
+ return
94
+
95
+ def get(self, task_id: str) -> GetTaskResponse:
96
+ """A task, with your ``rating`` of it and the rider's ``liveEta`` while they are on the way."""
97
+ return cast(GetTaskResponse, self._run(ops.tasks_get(task_id)))
98
+
99
+ def cancel(self, task_id: str, body: Optional[Body] = None) -> CancelTaskResponse:
100
+ return cast(CancelTaskResponse, self._run(ops.tasks_cancel(task_id, body)))
101
+
102
+ def assign(self, task_id: str, body: Body) -> Dict[str, Any]:
103
+ """Offer a manual (``autoAssign: false``) live task to riders from ``available_riders``."""
104
+ return cast(Dict[str, Any], self._run(ops.tasks_assign(task_id, body)))
105
+
106
+ def available_riders(self, task_id: str, **query: Any) -> Dict[str, Any]:
107
+ return cast(Dict[str, Any], self._run(ops.tasks_available_riders(task_id, query)))
108
+
109
+ def rate(self, task_id: str, body: Body) -> Dict[str, Any]:
110
+ """Rate the rider of a completed task, once. Check ``get(...)["rating"]`` first."""
111
+ return cast(Dict[str, Any], self._run(ops.tasks_rate(task_id, body)))
112
+
113
+ def dispute(self, task_id: str, body: Body) -> Dict[str, Any]:
114
+ return cast(Dict[str, Any], self._run(ops.tasks_dispute(task_id, body)))
115
+
116
+
117
+ class Webhooks:
118
+ def __init__(self, run: Any) -> None:
119
+ self._run = run
120
+
121
+ def create(self, body: Body) -> CreateWebhookResponse:
122
+ """Register an endpoint. Store the returned ``secret``: it is shown only once."""
123
+ return cast(CreateWebhookResponse, self._run(ops.webhooks_create(body)))
124
+
125
+ def list(self) -> ListWebhooksResponse:
126
+ return cast(ListWebhooksResponse, self._run(ops.webhooks_list()))
127
+
128
+ def get(self, webhook_id: str) -> Webhook:
129
+ return cast(Webhook, self._run(ops.webhooks_get(webhook_id)))
130
+
131
+ def update(self, webhook_id: str, body: Body) -> Webhook:
132
+ return cast(Webhook, self._run(ops.webhooks_update(webhook_id, body)))
133
+
134
+ def delete(self, webhook_id: str) -> Webhook:
135
+ return cast(Webhook, self._run(ops.webhooks_delete(webhook_id)))
136
+
137
+ @staticmethod
138
+ def verify_signature(
139
+ payload: str | bytes, signature_header: str, secret: str, tolerance: int = 300
140
+ ) -> WebhookSignatureVerification:
141
+ """Check a delivery's ``X-Relay-Signature`` header against the raw body."""
142
+ return verify_webhook_signature(payload, signature_header, secret, tolerance)
143
+
144
+
145
+ class Auth:
146
+ def __init__(self, run: Any) -> None:
147
+ self._run = run
148
+
149
+ def create_websocket_token(self, body: Body) -> WebSocketToken:
150
+ """A short-lived token for an app to track tasks live. ``scope``: ``["task:<id>", ...]``."""
151
+ return cast(WebSocketToken, self._run(ops.auth_create_websocket_token(body)))
152
+
153
+
154
+ class Presence:
155
+ def __init__(self, run: Any) -> None:
156
+ self._run = run
157
+
158
+ def get(self, **query: Any) -> PresenceResponse:
159
+ """Anonymous rider availability around a point: ``latitude``, ``longitude``, ``resolution``."""
160
+ return cast(PresenceResponse, self._run(ops.presence_get(query)))
161
+
162
+
163
+ class Relay(_Base):
164
+ """The Relay server SDK. Keep your API key on your server.
165
+
166
+ Example::
167
+
168
+ from sendrelay import Relay
169
+
170
+ relay = Relay(api_key=os.environ["RELAY_API_KEY"])
171
+ quote = relay.tasks.quote({"taskType": "PACKAGE_DELIVERY", "stages": [...]})
172
+ """
173
+
174
+ def __init__(
175
+ self,
176
+ api_key: str,
177
+ *,
178
+ base_url: Optional[str] = None,
179
+ api_version: Optional[str] = None,
180
+ timeout: float = 30.0,
181
+ max_retries: Optional[int] = None,
182
+ retry: Optional[RetryConfig] = None,
183
+ auto_idempotency: bool = True,
184
+ headers: Optional[Dict[str, str]] = None,
185
+ http_client: Optional[httpx.Client] = None,
186
+ ) -> None:
187
+ self._config = _config(api_key, base_url, api_version, timeout, max_retries, retry, auto_idempotency, headers)
188
+ self._owns_client = http_client is None
189
+ self._http = http_client or httpx.Client(timeout=timeout)
190
+ self.tasks = Tasks(self._request)
191
+ self.webhooks = Webhooks(self._request)
192
+ self.auth = Auth(self._request)
193
+ self.presence = Presence(self._request)
194
+
195
+ def _request(self, request: Request) -> Any:
196
+ return send(self._config, self._http, request)
197
+
198
+ def close(self) -> None:
199
+ if self._owns_client:
200
+ self._http.close()
201
+
202
+ def __enter__(self) -> Relay:
203
+ return self
204
+
205
+ def __exit__(self, *_: object) -> None:
206
+ self.close()
207
+
208
+
209
+ # ─── Async ───────────────────────────────────────────────────────────────────
210
+
211
+
212
+ class AsyncTasks:
213
+ def __init__(self, run: Any) -> None:
214
+ self._run = run
215
+
216
+ async def quote(self, body: Body) -> TaskQuoteResponse:
217
+ return cast(TaskQuoteResponse, await self._run(ops.tasks_quote(body)))
218
+
219
+ async def create(self, body: Body, *, idempotency_key: Optional[str] = None) -> CreateTaskResponse:
220
+ return cast(CreateTaskResponse, await self._run(ops.tasks_create(body, idempotency_key)))
221
+
222
+ async def list(self, **query: Any) -> ListTasksResponse:
223
+ return cast(ListTasksResponse, await self._run(ops.tasks_list(query)))
224
+
225
+ async def list_all(self, **query: Any) -> AsyncIterator[TaskListItem]:
226
+ cursor: Optional[str] = None
227
+ while True:
228
+ page = await self.list(**{**query, **({"cursor": cursor} if cursor else {})})
229
+ for item in page["data"]:
230
+ yield item
231
+ cursor = page.get("cursor") if page.get("hasMore") else None
232
+ if not cursor:
233
+ return
234
+
235
+ async def get(self, task_id: str) -> GetTaskResponse:
236
+ return cast(GetTaskResponse, await self._run(ops.tasks_get(task_id)))
237
+
238
+ async def cancel(self, task_id: str, body: Optional[Body] = None) -> CancelTaskResponse:
239
+ return cast(CancelTaskResponse, await self._run(ops.tasks_cancel(task_id, body)))
240
+
241
+ async def assign(self, task_id: str, body: Body) -> Dict[str, Any]:
242
+ return cast(Dict[str, Any], await self._run(ops.tasks_assign(task_id, body)))
243
+
244
+ async def available_riders(self, task_id: str, **query: Any) -> Dict[str, Any]:
245
+ return cast(Dict[str, Any], await self._run(ops.tasks_available_riders(task_id, query)))
246
+
247
+ async def rate(self, task_id: str, body: Body) -> Dict[str, Any]:
248
+ return cast(Dict[str, Any], await self._run(ops.tasks_rate(task_id, body)))
249
+
250
+ async def dispute(self, task_id: str, body: Body) -> Dict[str, Any]:
251
+ return cast(Dict[str, Any], await self._run(ops.tasks_dispute(task_id, body)))
252
+
253
+
254
+ class AsyncWebhooks:
255
+ def __init__(self, run: Any) -> None:
256
+ self._run = run
257
+
258
+ async def create(self, body: Body) -> CreateWebhookResponse:
259
+ return cast(CreateWebhookResponse, await self._run(ops.webhooks_create(body)))
260
+
261
+ async def list(self) -> ListWebhooksResponse:
262
+ return cast(ListWebhooksResponse, await self._run(ops.webhooks_list()))
263
+
264
+ async def get(self, webhook_id: str) -> Webhook:
265
+ return cast(Webhook, await self._run(ops.webhooks_get(webhook_id)))
266
+
267
+ async def update(self, webhook_id: str, body: Body) -> Webhook:
268
+ return cast(Webhook, await self._run(ops.webhooks_update(webhook_id, body)))
269
+
270
+ async def delete(self, webhook_id: str) -> Webhook:
271
+ return cast(Webhook, await self._run(ops.webhooks_delete(webhook_id)))
272
+
273
+ verify_signature = staticmethod(Webhooks.verify_signature)
274
+
275
+
276
+ class AsyncAuth:
277
+ def __init__(self, run: Any) -> None:
278
+ self._run = run
279
+
280
+ async def create_websocket_token(self, body: Body) -> WebSocketToken:
281
+ return cast(WebSocketToken, await self._run(ops.auth_create_websocket_token(body)))
282
+
283
+
284
+ class AsyncPresence:
285
+ def __init__(self, run: Any) -> None:
286
+ self._run = run
287
+
288
+ async def get(self, **query: Any) -> PresenceResponse:
289
+ return cast(PresenceResponse, await self._run(ops.presence_get(query)))
290
+
291
+
292
+ class AsyncRelay(_Base):
293
+ """The Relay server SDK for asyncio. Same methods as ``Relay``, awaited.
294
+
295
+ Example::
296
+
297
+ async with AsyncRelay(api_key=os.environ["RELAY_API_KEY"]) as relay:
298
+ task = await relay.tasks.get(task_id)
299
+ """
300
+
301
+ def __init__(
302
+ self,
303
+ api_key: str,
304
+ *,
305
+ base_url: Optional[str] = None,
306
+ api_version: Optional[str] = None,
307
+ timeout: float = 30.0,
308
+ max_retries: Optional[int] = None,
309
+ retry: Optional[RetryConfig] = None,
310
+ auto_idempotency: bool = True,
311
+ headers: Optional[Dict[str, str]] = None,
312
+ http_client: Optional[httpx.AsyncClient] = None,
313
+ ) -> None:
314
+ self._config = _config(api_key, base_url, api_version, timeout, max_retries, retry, auto_idempotency, headers)
315
+ self._owns_client = http_client is None
316
+ self._http = http_client or httpx.AsyncClient(timeout=timeout)
317
+ self.tasks = AsyncTasks(self._request)
318
+ self.webhooks = AsyncWebhooks(self._request)
319
+ self.auth = AsyncAuth(self._request)
320
+ self.presence = AsyncPresence(self._request)
321
+
322
+ async def _request(self, request: Request) -> Any:
323
+ return await send_async(self._config, self._http, request)
324
+
325
+ async def close(self) -> None:
326
+ if self._owns_client:
327
+ await self._http.aclose()
328
+
329
+ async def __aenter__(self) -> AsyncRelay:
330
+ return self
331
+
332
+ async def __aexit__(self, *_: object) -> None:
333
+ await self.close()