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.
- sendrelay-1.7.0/.gitignore +16 -0
- sendrelay-1.7.0/CHANGELOG.md +5 -0
- sendrelay-1.7.0/LICENSE +21 -0
- sendrelay-1.7.0/PKG-INFO +112 -0
- sendrelay-1.7.0/README.md +83 -0
- sendrelay-1.7.0/pyproject.toml +56 -0
- sendrelay-1.7.0/src/sendrelay/__init__.py +24 -0
- sendrelay-1.7.0/src/sendrelay/_client.py +333 -0
- sendrelay-1.7.0/src/sendrelay/_errors.py +44 -0
- sendrelay-1.7.0/src/sendrelay/_http.py +153 -0
- sendrelay-1.7.0/src/sendrelay/_operations.py +101 -0
- sendrelay-1.7.0/src/sendrelay/_version.py +2 -0
- sendrelay-1.7.0/src/sendrelay/_webhooks.py +54 -0
- sendrelay-1.7.0/src/sendrelay/py.typed +0 -0
- sendrelay-1.7.0/src/sendrelay/types.py +203 -0
- sendrelay-1.7.0/tests/test_conformance.py +289 -0
sendrelay-1.7.0/LICENSE
ADDED
|
@@ -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.
|
sendrelay-1.7.0/PKG-INFO
ADDED
|
@@ -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()
|