vatly-api-python 0.1.0a2__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.
- vatly/__init__.py +43 -0
- vatly/_model.py +13 -0
- vatly/_transport.py +284 -0
- vatly/_version.py +1 -0
- vatly/client.py +100 -0
- vatly/errors.py +86 -0
- vatly/models.py +3009 -0
- vatly/pagination.py +29 -0
- vatly/py.typed +1 -0
- vatly/resources.py +2003 -0
- vatly/webhooks.py +146 -0
- vatly_api_python-0.1.0a2.dist-info/METADATA +144 -0
- vatly_api_python-0.1.0a2.dist-info/RECORD +15 -0
- vatly_api_python-0.1.0a2.dist-info/WHEEL +4 -0
- vatly_api_python-0.1.0a2.dist-info/licenses/LICENSE +21 -0
vatly/webhooks.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Raw-body HMAC verification and typed, forward-compatible webhook deliveries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
import re
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any, Literal, get_args
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
16
|
+
|
|
17
|
+
from .errors import VatlyInvalidSignatureError
|
|
18
|
+
from .models import WebhookDelivery
|
|
19
|
+
|
|
20
|
+
VATLY_SIGNATURE_HEADER = "Vatly-Signature"
|
|
21
|
+
VATLY_EVENT_ID_HEADER = "Vatly-Event-Id"
|
|
22
|
+
WEBHOOK_EVENT_NAMES: tuple[str, ...] = get_args(
|
|
23
|
+
WebhookDelivery.model_fields["event_name"].annotation
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class KnownWebhookEvent(WebhookDelivery):
|
|
28
|
+
known: Literal[True] = True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UnsupportedWebhookEvent(BaseModel):
|
|
32
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
33
|
+
known: Literal[False] = False
|
|
34
|
+
id: str
|
|
35
|
+
resource: Literal["webhook_event"]
|
|
36
|
+
event_name: str = Field(alias="eventName")
|
|
37
|
+
reason: str | None
|
|
38
|
+
entity_type: str = Field(alias="entityType")
|
|
39
|
+
entity_id: str = Field(alias="entityId")
|
|
40
|
+
object: Any
|
|
41
|
+
created_at: datetime = Field(alias="createdAt")
|
|
42
|
+
testmode: bool
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
VatlyWebhookEvent = KnownWebhookEvent | UnsupportedWebhookEvent
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def compute_signature_v1(raw_body: bytes | str, secret: str, timestamp: int) -> str:
|
|
49
|
+
if isinstance(timestamp, bool) or not isinstance(timestamp, int) or timestamp < 0:
|
|
50
|
+
raise ValueError("timestamp must be a non-negative integer")
|
|
51
|
+
body = raw_body.encode("utf-8") if isinstance(raw_body, str) else raw_body
|
|
52
|
+
return hmac.new(
|
|
53
|
+
secret.encode("utf-8"), str(timestamp).encode() + b"." + body, hashlib.sha256
|
|
54
|
+
).hexdigest()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def verify_webhook_signature(
|
|
58
|
+
raw_body: bytes | str,
|
|
59
|
+
signature_header: str | None,
|
|
60
|
+
secret: str | Sequence[str],
|
|
61
|
+
*,
|
|
62
|
+
tolerance: float = 300,
|
|
63
|
+
now: float | None = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Verify exact bytes. Zero tolerance disables freshness, not deduplication."""
|
|
66
|
+
if not math.isfinite(tolerance) or tolerance < 0:
|
|
67
|
+
raise ValueError("tolerance must be finite and non-negative")
|
|
68
|
+
secrets = [secret] if isinstance(secret, str) else list(secret)
|
|
69
|
+
if not secrets or any(not s for s in secrets):
|
|
70
|
+
raise VatlyInvalidSignatureError("At least one non-empty webhook secret is required")
|
|
71
|
+
if not signature_header:
|
|
72
|
+
raise VatlyInvalidSignatureError("Missing webhook signature")
|
|
73
|
+
parts = {}
|
|
74
|
+
for piece in signature_header.split(","):
|
|
75
|
+
key, sep, value = piece.strip().partition("=")
|
|
76
|
+
if sep:
|
|
77
|
+
parts[key.strip()] = value.strip()
|
|
78
|
+
stamp = parts.get("t", "")
|
|
79
|
+
signature = parts.get("v1", "")
|
|
80
|
+
if not re.fullmatch(r"[0-9]{1,15}", stamp) or not re.fullmatch(r"[0-9a-fA-F]{64}", signature):
|
|
81
|
+
raise VatlyInvalidSignatureError("Malformed timestamp or v1 signature")
|
|
82
|
+
timestamp = int(stamp)
|
|
83
|
+
current = time.time() if now is None else now
|
|
84
|
+
if not math.isfinite(current):
|
|
85
|
+
raise ValueError("now must be finite")
|
|
86
|
+
if tolerance and abs(current - timestamp) > tolerance:
|
|
87
|
+
raise VatlyInvalidSignatureError("Webhook timestamp outside tolerance")
|
|
88
|
+
valid = False
|
|
89
|
+
for candidate in secrets:
|
|
90
|
+
valid |= hmac.compare_digest(
|
|
91
|
+
compute_signature_v1(raw_body, candidate, timestamp), signature.lower()
|
|
92
|
+
)
|
|
93
|
+
if not valid:
|
|
94
|
+
raise VatlyInvalidSignatureError("Invalid webhook signature")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_valid_webhook_signature(
|
|
98
|
+
raw_body: bytes | str,
|
|
99
|
+
signature_header: str | None,
|
|
100
|
+
secret: str | Sequence[str],
|
|
101
|
+
*,
|
|
102
|
+
tolerance: float = 300,
|
|
103
|
+
now: float | None = None,
|
|
104
|
+
) -> bool:
|
|
105
|
+
try:
|
|
106
|
+
verify_webhook_signature(raw_body, signature_header, secret, tolerance=tolerance, now=now)
|
|
107
|
+
return True
|
|
108
|
+
except VatlyInvalidSignatureError:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def construct_event(
|
|
113
|
+
raw_body: bytes | str,
|
|
114
|
+
signature_header: str | None,
|
|
115
|
+
secret: str | Sequence[str],
|
|
116
|
+
*,
|
|
117
|
+
tolerance: float = 300,
|
|
118
|
+
now: float | None = None,
|
|
119
|
+
) -> WebhookDelivery:
|
|
120
|
+
verify_webhook_signature(raw_body, signature_header, secret, tolerance=tolerance, now=now)
|
|
121
|
+
try:
|
|
122
|
+
return WebhookDelivery.model_validate_json(raw_body)
|
|
123
|
+
except ValidationError as exc:
|
|
124
|
+
raise VatlyInvalidSignatureError("Invalid webhook JSON or envelope") from exc
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def construct_webhook_event(
|
|
128
|
+
raw_body: bytes | str,
|
|
129
|
+
signature_header: str | None,
|
|
130
|
+
secret: str | Sequence[str],
|
|
131
|
+
*,
|
|
132
|
+
tolerance: float = 300,
|
|
133
|
+
now: float | None = None,
|
|
134
|
+
) -> VatlyWebhookEvent:
|
|
135
|
+
verify_webhook_signature(raw_body, signature_header, secret, tolerance=tolerance, now=now)
|
|
136
|
+
try:
|
|
137
|
+
payload = json.loads(raw_body)
|
|
138
|
+
if not isinstance(payload, dict):
|
|
139
|
+
raise ValueError("Envelope must be an object")
|
|
140
|
+
# `known` is SDK-owned; never trust a supplied discriminator.
|
|
141
|
+
payload.pop("known", None)
|
|
142
|
+
if payload.get("eventName") in WEBHOOK_EVENT_NAMES:
|
|
143
|
+
return KnownWebhookEvent.model_validate(payload)
|
|
144
|
+
return UnsupportedWebhookEvent.model_validate(payload)
|
|
145
|
+
except (ValueError, UnicodeDecodeError) as exc:
|
|
146
|
+
raise VatlyInvalidSignatureError("Invalid webhook JSON or envelope") from exc
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vatly-api-python
|
|
3
|
+
Version: 0.1.0a2
|
|
4
|
+
Summary: Python SDK for Vatly, the merchant of record for AI, software and digital products
|
|
5
|
+
Project-URL: Homepage, https://github.com/vatly/vatly-api-python
|
|
6
|
+
Project-URL: Documentation, https://docs.vatly.com
|
|
7
|
+
Project-URL: Issues, https://github.com/vatly/vatly-api-python/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: httpx<1,>=0.27
|
|
15
|
+
Requires-Dist: pydantic<3,>=2.11
|
|
16
|
+
Requires-Dist: typing-extensions<5,>=4.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Vatly Python SDK
|
|
20
|
+
|
|
21
|
+
> **Alpha — under active development. Every alpha release may include breaking changes. Pin exact versions during alpha.**
|
|
22
|
+
|
|
23
|
+
Python SDK for Vatly, the merchant of record for AI, software and digital products.
|
|
24
|
+
Python 3.10+, Pydantic v2 models, and pooled httpx sync/async clients. All 50 production
|
|
25
|
+
OpenAPI operations are covered. No package has been published yet.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Once published:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install vatly-api-python==0.1.0a2
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For local development: `uv sync --locked`.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
Get a test or live API key from [the dashboard](https://my.vatly.com), under Settings > API.
|
|
40
|
+
Keep keys server-side. The client reads `VATLY_API_KEY` by default.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from vatly import VatlyClient
|
|
44
|
+
from vatly.models import CreateCheckoutRequest
|
|
45
|
+
|
|
46
|
+
with VatlyClient() as vatly:
|
|
47
|
+
checkout = vatly.checkouts.create(
|
|
48
|
+
CreateCheckoutRequest(
|
|
49
|
+
redirect_url_success="https://example.com/success",
|
|
50
|
+
redirect_url_canceled="https://example.com/canceled",
|
|
51
|
+
products=[{"id": "subscription_plan_abc123", "quantity": 1}],
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
print(checkout.links.checkout_url.href)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from vatly import AsyncVatlyClient
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def read_orders():
|
|
62
|
+
async with AsyncVatlyClient() as vatly:
|
|
63
|
+
async for order in vatly.orders.list_all({"limit": 100}):
|
|
64
|
+
print(order.id, order.total)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Pydantic models use snake_case attributes and accept Vatly's camelCase aliases.
|
|
68
|
+
Dictionary inputs use **wire-format camelCase keys**. Model requests serialize with
|
|
69
|
+
aliases and omit unset fields; explicitly supplied `None` is sent as JSON null.
|
|
70
|
+
Money values remain decimal **strings**, never floats.
|
|
71
|
+
|
|
72
|
+
## Configuration and reliability
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from vatly import VatlyClient, RequestOptions
|
|
76
|
+
|
|
77
|
+
with VatlyClient(timeout=30, max_retries=2) as vatly:
|
|
78
|
+
page = vatly.customers.list({"limit": 25}, options=RequestOptions(timeout=10))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Timeouts are **seconds per httpx network phase per attempt**, not a total request deadline;
|
|
82
|
+
zero disables timeouts. Default base URL: `https://api.vatly.com/v1`.
|
|
83
|
+
`base_url`, `default_headers`, and `transport` (an httpx transport) can be overridden.
|
|
84
|
+
`mode` exposes `test` or `live`. Test helpers reject live keys before sending.
|
|
85
|
+
|
|
86
|
+
POST/PATCH/PUT/DELETE receive a UUID idempotency key, reused across retries.
|
|
87
|
+
Set `RequestOptions(idempotency_key="your-operation-id")` to reuse a business-operation key
|
|
88
|
+
across separate SDK calls. Set `idempotency_key_generator=None` to disable auto-keys.
|
|
89
|
+
GETs and keyed mutations retry 429, 5xx, timeouts and connection errors with exponential
|
|
90
|
+
backoff (250ms to 8s plus jitter), honoring numeric or HTTP-date `Retry-After`.
|
|
91
|
+
Keyed 409 retries **only** `details.idempotency_outcome == "in_progress"`.
|
|
92
|
+
Never blindly retry an `unknown` idempotency outcome: read/reconcile the resource first.
|
|
93
|
+
Unkeyed mutations do not retry. Redirects are not followed.
|
|
94
|
+
|
|
95
|
+
`Vatly-Client-Info` identifies SDK/runtime/OS/architecture. Pass
|
|
96
|
+
`client_info={"app": "MyApp/1.0", "framework": "Django/5"}` to add sanitized identifiers,
|
|
97
|
+
or `client_info=False` to suppress the SDK-generated header.
|
|
98
|
+
|
|
99
|
+
## Errors
|
|
100
|
+
|
|
101
|
+
All API/transport errors extend `VatlyError`, with `status`, `body`, `url`, `method`,
|
|
102
|
+
`details`, `errors` and `retry_after`. Network errors retain the httpx exception as `__cause__`.
|
|
103
|
+
|
|
104
|
+
| Status | Error |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| 401 | `VatlyAuthError` |
|
|
107
|
+
| 403 | `VatlyForbiddenError` |
|
|
108
|
+
| 404 | `VatlyNotFoundError` |
|
|
109
|
+
| 409 | `VatlyConflictError` |
|
|
110
|
+
| 422 | `VatlyValidationError` |
|
|
111
|
+
| 429 | `VatlyRateLimitError` |
|
|
112
|
+
| Other / invalid response | `VatlyApiError` |
|
|
113
|
+
| Network / timeout | `VatlyConnectionError` / `VatlyTimeoutError` |
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from vatly import VatlyValidationError
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
# Your API call here
|
|
120
|
+
pass
|
|
121
|
+
except VatlyValidationError as error:
|
|
122
|
+
print(error.errors)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
See [getting started and resource guides](docs/README.md), the generated
|
|
126
|
+
[method inventory](docs/API.md), [webhook guide](docs/Webhooks.md), and
|
|
127
|
+
[production API reference](https://docs.vatly.com/api-reference).
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
|
|
131
|
+
Run `.agents/setup` in a fresh orb. It installs uv if missing, provisions Python 3.12,
|
|
132
|
+
syncs locked dependencies and runs an import sanity check. Warm runs reuse uv's caches.
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
uv run python scripts/generate.py --check
|
|
136
|
+
uv run ruff check .
|
|
137
|
+
uv run ruff format --check .
|
|
138
|
+
uv run mypy
|
|
139
|
+
uv run pytest
|
|
140
|
+
uv build
|
|
141
|
+
uv run twine check dist/*
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
See [maintainer notes](docs/Development.md) for spec regeneration, docs sync, and trusted publishing.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
vatly/__init__.py,sha256=9TDFCpOAM591UZ2dxrrOVkRDwRUo0eKiOzc7qRZEtBA,1003
|
|
2
|
+
vatly/_model.py,sha256=JBRyh3t064-3zgYd_MWqWkzt8tT50AYz6aI0MFU939s,424
|
|
3
|
+
vatly/_transport.py,sha256=DQeNGu3ygN6Y-PF0Gz_w0fsfPlthswQiztd9B1tf6RM,10743
|
|
4
|
+
vatly/_version.py,sha256=MVC3xG9U8KXXTiZ2FMZ9ARASAJV2A-Qu7UcliE-N_hI,24
|
|
5
|
+
vatly/client.py,sha256=eIc25KLNh5o2l98JcXgnbKKoMtgxbrHp3kka0HEWGaU,2947
|
|
6
|
+
vatly/errors.py,sha256=5jt0xjLYjfpGN6IbSSWgZfRVf7aKuiwjrlrYx4C6XT4,1881
|
|
7
|
+
vatly/models.py,sha256=Nou8Ome0kMXbFb2hniHXs_5qZuT9V77GDgO9jjtUcxw,118964
|
|
8
|
+
vatly/pagination.py,sha256=QNzrFd9a3umT6xx1Rcc7YoGIwF9VOP5n0vLZYbbu1CM,853
|
|
9
|
+
vatly/py.typed,sha256=-lBwBtQZPKsgcrt4OLCCZA8mJQAMAnBGxQhahf5SL88,58
|
|
10
|
+
vatly/resources.py,sha256=w9cQXnmESU6it1JJjIQKcDJSewAOGsJbBN9tq8lZ_gg,64541
|
|
11
|
+
vatly/webhooks.py,sha256=C082LOgJ-Dc_Bd5ZRNRhwjJNkHFFbI-sXZpUeaNbaCI,5165
|
|
12
|
+
vatly_api_python-0.1.0a2.dist-info/METADATA,sha256=G0uAhhURWadTZ7PTZ4ae63vdWk7Hd-YwLmFN0XLQPRg,5140
|
|
13
|
+
vatly_api_python-0.1.0a2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
+
vatly_api_python-0.1.0a2.dist-info/licenses/LICENSE,sha256=CkjDnWn3u8SlJD-QfG6Bd_2O7ZtuzM0nzfpy9E-AQsw,1062
|
|
15
|
+
vatly_api_python-0.1.0a2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vatly
|
|
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.
|