zazu-sdk 0.2.1__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.
- zazu_sdk-0.2.1/.gitignore +18 -0
- zazu_sdk-0.2.1/CHANGELOG.md +24 -0
- zazu_sdk-0.2.1/LICENSE +21 -0
- zazu_sdk-0.2.1/PKG-INFO +120 -0
- zazu_sdk-0.2.1/README.md +87 -0
- zazu_sdk-0.2.1/pyproject.toml +91 -0
- zazu_sdk-0.2.1/src/zazu_sdk/__init__.py +57 -0
- zazu_sdk-0.2.1/src/zazu_sdk/_version.py +1 -0
- zazu_sdk-0.2.1/src/zazu_sdk/client.py +200 -0
- zazu_sdk-0.2.1/src/zazu_sdk/errors.py +69 -0
- zazu_sdk-0.2.1/src/zazu_sdk/page.py +54 -0
- zazu_sdk-0.2.1/src/zazu_sdk/py.typed +0 -0
- zazu_sdk-0.2.1/src/zazu_sdk/resources/__init__.py +0 -0
- zazu_sdk-0.2.1/src/zazu_sdk/resources/accounts.py +64 -0
- zazu_sdk-0.2.1/src/zazu_sdk/resources/base.py +89 -0
- zazu_sdk-0.2.1/src/zazu_sdk/resources/beneficiaries.py +23 -0
- zazu_sdk-0.2.1/src/zazu_sdk/resources/checkout_sessions.py +16 -0
- zazu_sdk-0.2.1/src/zazu_sdk/resources/customers.py +32 -0
- zazu_sdk-0.2.1/src/zazu_sdk/resources/entity.py +11 -0
- zazu_sdk-0.2.1/src/zazu_sdk/resources/invoices.py +56 -0
- zazu_sdk-0.2.1/src/zazu_sdk/resources/payment_links.py +35 -0
- zazu_sdk-0.2.1/src/zazu_sdk/resources/transfer_drafts.py +22 -0
- zazu_sdk-0.2.1/src/zazu_sdk/resources/webhook_endpoints.py +53 -0
- zazu_sdk-0.2.1/src/zazu_sdk/response.py +26 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.mypy_cache/
|
|
7
|
+
.ruff_cache/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
dist/
|
|
11
|
+
build/
|
|
12
|
+
.coverage
|
|
13
|
+
htmlcov/
|
|
14
|
+
.env
|
|
15
|
+
tests/fixtures/cassettes/
|
|
16
|
+
cassettes-v*.tar.gz
|
|
17
|
+
.tox/
|
|
18
|
+
.python-version.local
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `zazu-sdk` are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.2.1]
|
|
11
|
+
|
|
12
|
+
Version alignment: the whole SDK family now releases in lockstep with zazu-ruby. No functional changes since [0.1.0].
|
|
13
|
+
|
|
14
|
+
## [0.1.0]
|
|
15
|
+
|
|
16
|
+
Initial release.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Sync `Zazu` client built on `httpx`
|
|
21
|
+
- Resource modules: `accounts`, `beneficiaries`, `checkout_sessions`, `customers`, `entity`, `invoices`, `payment_links`, `transfer_drafts`, `webhook_endpoints`
|
|
22
|
+
- Cursor-based `Page` with `auto_paging_iter()`
|
|
23
|
+
- Nine-class error hierarchy mirroring `zazu-ruby`
|
|
24
|
+
- Cassette-replay test harness driven by the Ruby SDK's release tarball
|
zazu_sdk-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zazu
|
|
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.
|
zazu_sdk-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zazu-sdk
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Python SDK for the Zazu API.
|
|
5
|
+
Project-URL: Homepage, https://github.com/getzazu/zazu-python
|
|
6
|
+
Project-URL: Documentation, https://github.com/getzazu/zazu-python#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/getzazu/zazu-python
|
|
8
|
+
Project-URL: Issues, https://github.com/getzazu/zazu-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/getzazu/zazu-python/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Zazu <engineering@zazu.ma>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api,fintech,morocco,sdk,zazu
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-vcr>=1.0.2; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.7; extra == 'dev'
|
|
31
|
+
Requires-Dist: vcrpy>=6; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# zazu-sdk
|
|
35
|
+
|
|
36
|
+
Python SDK for the [Zazu API](https://zazu.ma).
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install zazu-sdk
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Requires Python 3.11+.
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from zazu_sdk import Zazu
|
|
50
|
+
|
|
51
|
+
client = Zazu(api_key="sk_live_...")
|
|
52
|
+
|
|
53
|
+
client.entity.get()
|
|
54
|
+
client.accounts.list(currency_code="MAD")
|
|
55
|
+
client.customers.list(q="Acme")
|
|
56
|
+
client.customers.create(
|
|
57
|
+
customer_type="business",
|
|
58
|
+
company_name="Acme Corp",
|
|
59
|
+
email="billing@acme.com",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
client.invoices.list()
|
|
63
|
+
client.payment_links.cancel(payment_link_id)
|
|
64
|
+
client.webhook_endpoints.list()
|
|
65
|
+
|
|
66
|
+
client.checkout_sessions.create(
|
|
67
|
+
account_id=account_id,
|
|
68
|
+
amount="100.00",
|
|
69
|
+
success_url="https://example.com/ok",
|
|
70
|
+
cancel_url="https://example.com/cancel",
|
|
71
|
+
)
|
|
72
|
+
client.checkout_sessions.get(session_id)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The client picks up `ZAZU_API_KEY`, `ZAZU_BASE_URL`, `ZAZU_API_VERSION`, and
|
|
76
|
+
`ZAZU_TIMEOUT` from the environment if you don't pass them.
|
|
77
|
+
|
|
78
|
+
## Pagination
|
|
79
|
+
|
|
80
|
+
List endpoints return a `Page`. Iterate one page or chase cursors:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
page = client.invoices.list(limit=25)
|
|
84
|
+
for invoice in page:
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
# All items, automatic cursor chasing:
|
|
88
|
+
for invoice in client.invoices.list().auto_paging_iter():
|
|
89
|
+
...
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Errors
|
|
93
|
+
|
|
94
|
+
Nine concrete subclasses; discriminate with `isinstance`:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from zazu_sdk import (
|
|
98
|
+
ZazuValidationError,
|
|
99
|
+
ZazuRateLimitError,
|
|
100
|
+
ZazuNotFoundError,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
client.invoices.get("nope")
|
|
105
|
+
except ZazuNotFoundError as err:
|
|
106
|
+
print(err.status, err.request_id, err.body)
|
|
107
|
+
except ZazuRateLimitError as err:
|
|
108
|
+
print("retry after", err.retry_after, "seconds")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Cross-SDK contract
|
|
112
|
+
|
|
113
|
+
`zazu-sdk` is one of several Zazu SDKs that all replay cassettes recorded by
|
|
114
|
+
the canonical [`zazu-ruby`](https://github.com/getzazu/zazu-ruby) SDK against
|
|
115
|
+
staging. The wire format is snake_case JSON; request and response shapes match
|
|
116
|
+
across Ruby, TypeScript, Python, Go, and Rust.
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT.
|
zazu_sdk-0.2.1/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# zazu-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK for the [Zazu API](https://zazu.ma).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install zazu-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.11+.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from zazu_sdk import Zazu
|
|
17
|
+
|
|
18
|
+
client = Zazu(api_key="sk_live_...")
|
|
19
|
+
|
|
20
|
+
client.entity.get()
|
|
21
|
+
client.accounts.list(currency_code="MAD")
|
|
22
|
+
client.customers.list(q="Acme")
|
|
23
|
+
client.customers.create(
|
|
24
|
+
customer_type="business",
|
|
25
|
+
company_name="Acme Corp",
|
|
26
|
+
email="billing@acme.com",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
client.invoices.list()
|
|
30
|
+
client.payment_links.cancel(payment_link_id)
|
|
31
|
+
client.webhook_endpoints.list()
|
|
32
|
+
|
|
33
|
+
client.checkout_sessions.create(
|
|
34
|
+
account_id=account_id,
|
|
35
|
+
amount="100.00",
|
|
36
|
+
success_url="https://example.com/ok",
|
|
37
|
+
cancel_url="https://example.com/cancel",
|
|
38
|
+
)
|
|
39
|
+
client.checkout_sessions.get(session_id)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The client picks up `ZAZU_API_KEY`, `ZAZU_BASE_URL`, `ZAZU_API_VERSION`, and
|
|
43
|
+
`ZAZU_TIMEOUT` from the environment if you don't pass them.
|
|
44
|
+
|
|
45
|
+
## Pagination
|
|
46
|
+
|
|
47
|
+
List endpoints return a `Page`. Iterate one page or chase cursors:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
page = client.invoices.list(limit=25)
|
|
51
|
+
for invoice in page:
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
# All items, automatic cursor chasing:
|
|
55
|
+
for invoice in client.invoices.list().auto_paging_iter():
|
|
56
|
+
...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Errors
|
|
60
|
+
|
|
61
|
+
Nine concrete subclasses; discriminate with `isinstance`:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from zazu_sdk import (
|
|
65
|
+
ZazuValidationError,
|
|
66
|
+
ZazuRateLimitError,
|
|
67
|
+
ZazuNotFoundError,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
client.invoices.get("nope")
|
|
72
|
+
except ZazuNotFoundError as err:
|
|
73
|
+
print(err.status, err.request_id, err.body)
|
|
74
|
+
except ZazuRateLimitError as err:
|
|
75
|
+
print("retry after", err.retry_after, "seconds")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Cross-SDK contract
|
|
79
|
+
|
|
80
|
+
`zazu-sdk` is one of several Zazu SDKs that all replay cassettes recorded by
|
|
81
|
+
the canonical [`zazu-ruby`](https://github.com/getzazu/zazu-ruby) SDK against
|
|
82
|
+
staging. The wire format is snake_case JSON; request and response shapes match
|
|
83
|
+
across Ruby, TypeScript, Python, Go, and Rust.
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zazu-sdk"
|
|
7
|
+
description = "Python SDK for the Zazu API."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
license-files = ["LICENSE"]
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "Zazu", email = "engineering@zazu.ma" }]
|
|
13
|
+
keywords = ["zazu", "sdk", "api", "fintech", "morocco"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dynamic = ["version"]
|
|
27
|
+
dependencies = ["httpx>=0.27"]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=8",
|
|
32
|
+
"pytest-vcr>=1.0.2",
|
|
33
|
+
"vcrpy>=6",
|
|
34
|
+
"ruff>=0.7",
|
|
35
|
+
"mypy>=1.11",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/getzazu/zazu-python"
|
|
40
|
+
Documentation = "https://github.com/getzazu/zazu-python#readme"
|
|
41
|
+
Repository = "https://github.com/getzazu/zazu-python"
|
|
42
|
+
Issues = "https://github.com/getzazu/zazu-python/issues"
|
|
43
|
+
Changelog = "https://github.com/getzazu/zazu-python/blob/main/CHANGELOG.md"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.version]
|
|
46
|
+
path = "src/zazu_sdk/_version.py"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/zazu_sdk"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
include = ["src/zazu_sdk", "README.md", "CHANGELOG.md", "LICENSE"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
addopts = "-ra"
|
|
57
|
+
filterwarnings = ["error"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
line-length = 100
|
|
61
|
+
target-version = "py311"
|
|
62
|
+
src = ["src", "tests"]
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = [
|
|
66
|
+
"E", "F", "W", # pycodestyle / pyflakes
|
|
67
|
+
"I", # isort
|
|
68
|
+
"B", # bugbear
|
|
69
|
+
"UP", # pyupgrade
|
|
70
|
+
"PL", # pylint subset
|
|
71
|
+
"RUF", # ruff-specific
|
|
72
|
+
"SIM", # simplify
|
|
73
|
+
"TCH", # type-checking imports
|
|
74
|
+
]
|
|
75
|
+
ignore = [
|
|
76
|
+
"PLR0913", # too many args — SDK methods accept many kwargs by design
|
|
77
|
+
"PLR0911", # too many returns — error-status dispatch is naturally a switch
|
|
78
|
+
"PLR2004", # magic-value comparison — fine for HTTP status codes
|
|
79
|
+
"TC001", # move application imports under TYPE_CHECKING — many are used at runtime
|
|
80
|
+
"TC002", # move third-party imports under TYPE_CHECKING — same reason
|
|
81
|
+
"TC003", # move stdlib imports under TYPE_CHECKING — same reason
|
|
82
|
+
"UP037", # remove quotes from annotations — needed for `Self`-style forward refs
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
[tool.ruff.lint.per-file-ignores]
|
|
86
|
+
"tests/**" = ["PLR2004"]
|
|
87
|
+
|
|
88
|
+
[tool.mypy]
|
|
89
|
+
python_version = "3.11"
|
|
90
|
+
strict = true
|
|
91
|
+
files = ["src/zazu_sdk"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Zazu Python SDK. Mirrors lib/zazu.rb."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .client import Zazu
|
|
5
|
+
from .errors import (
|
|
6
|
+
ZazuArgumentError,
|
|
7
|
+
ZazuAuthenticationError,
|
|
8
|
+
ZazuConfigurationError,
|
|
9
|
+
ZazuConnectionError,
|
|
10
|
+
ZazuError,
|
|
11
|
+
ZazuForbiddenError,
|
|
12
|
+
ZazuNotFoundError,
|
|
13
|
+
ZazuRateLimitError,
|
|
14
|
+
ZazuServerError,
|
|
15
|
+
ZazuValidationError,
|
|
16
|
+
)
|
|
17
|
+
from .page import MAX_PER_PAGE, Page
|
|
18
|
+
from .resources.accounts import Accounts
|
|
19
|
+
from .resources.beneficiaries import Beneficiaries
|
|
20
|
+
from .resources.checkout_sessions import CheckoutSessions
|
|
21
|
+
from .resources.customers import Customers
|
|
22
|
+
from .resources.entity import Entity
|
|
23
|
+
from .resources.invoices import Invoices
|
|
24
|
+
from .resources.payment_links import PaymentLinks
|
|
25
|
+
from .resources.transfer_drafts import TransferDrafts
|
|
26
|
+
from .resources.webhook_endpoints import WebhookEndpoints
|
|
27
|
+
from .response import ZazuResponse
|
|
28
|
+
|
|
29
|
+
VERSION = __version__
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"MAX_PER_PAGE",
|
|
33
|
+
"VERSION",
|
|
34
|
+
"Accounts",
|
|
35
|
+
"Beneficiaries",
|
|
36
|
+
"CheckoutSessions",
|
|
37
|
+
"Customers",
|
|
38
|
+
"Entity",
|
|
39
|
+
"Invoices",
|
|
40
|
+
"Page",
|
|
41
|
+
"PaymentLinks",
|
|
42
|
+
"TransferDrafts",
|
|
43
|
+
"WebhookEndpoints",
|
|
44
|
+
"Zazu",
|
|
45
|
+
"ZazuArgumentError",
|
|
46
|
+
"ZazuAuthenticationError",
|
|
47
|
+
"ZazuConfigurationError",
|
|
48
|
+
"ZazuConnectionError",
|
|
49
|
+
"ZazuError",
|
|
50
|
+
"ZazuForbiddenError",
|
|
51
|
+
"ZazuNotFoundError",
|
|
52
|
+
"ZazuRateLimitError",
|
|
53
|
+
"ZazuResponse",
|
|
54
|
+
"ZazuServerError",
|
|
55
|
+
"ZazuValidationError",
|
|
56
|
+
"__version__",
|
|
57
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.1"
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Mirrors lib/zazu/client.rb. Sync HTTP entry point built on httpx."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import urlencode
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from ._version import __version__
|
|
13
|
+
from .errors import (
|
|
14
|
+
ZazuAuthenticationError,
|
|
15
|
+
ZazuConfigurationError,
|
|
16
|
+
ZazuConnectionError,
|
|
17
|
+
ZazuError,
|
|
18
|
+
ZazuForbiddenError,
|
|
19
|
+
ZazuNotFoundError,
|
|
20
|
+
ZazuRateLimitError,
|
|
21
|
+
ZazuServerError,
|
|
22
|
+
ZazuValidationError,
|
|
23
|
+
)
|
|
24
|
+
from .page import MAX_PER_PAGE, Page
|
|
25
|
+
from .resources.accounts import Accounts
|
|
26
|
+
from .resources.beneficiaries import Beneficiaries
|
|
27
|
+
from .resources.checkout_sessions import CheckoutSessions
|
|
28
|
+
from .resources.customers import Customers
|
|
29
|
+
from .resources.entity import Entity
|
|
30
|
+
from .resources.invoices import Invoices
|
|
31
|
+
from .resources.payment_links import PaymentLinks
|
|
32
|
+
from .resources.transfer_drafts import TransferDrafts
|
|
33
|
+
from .resources.webhook_endpoints import WebhookEndpoints
|
|
34
|
+
from .response import ZazuResponse
|
|
35
|
+
|
|
36
|
+
DEFAULT_BASE_URL = "https://zazu.ma"
|
|
37
|
+
DEFAULT_TIMEOUT = 30.0
|
|
38
|
+
USER_AGENT = f"zazu-sdk/{__version__}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Zazu:
|
|
42
|
+
"""Top-level Zazu client. Resources hang off it as attributes."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
api_key: str | None = None,
|
|
47
|
+
*,
|
|
48
|
+
base_url: str | None = None,
|
|
49
|
+
api_version: str | None = None,
|
|
50
|
+
timeout: float | None = None,
|
|
51
|
+
http_client: httpx.Client | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
api_key = api_key or os.getenv("ZAZU_API_KEY")
|
|
54
|
+
if not api_key:
|
|
55
|
+
raise ZazuConfigurationError(
|
|
56
|
+
"Missing api_key. Pass api_key= or set ZAZU_API_KEY."
|
|
57
|
+
)
|
|
58
|
+
self.api_key = api_key
|
|
59
|
+
self.base_url = (base_url or os.getenv("ZAZU_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
60
|
+
self.api_version = api_version or os.getenv("ZAZU_API_VERSION")
|
|
61
|
+
self.timeout = (
|
|
62
|
+
timeout if timeout is not None else _env_float("ZAZU_TIMEOUT", DEFAULT_TIMEOUT)
|
|
63
|
+
)
|
|
64
|
+
self._owns_client = http_client is None
|
|
65
|
+
self._http = http_client or httpx.Client(timeout=self.timeout)
|
|
66
|
+
|
|
67
|
+
self.accounts = Accounts(self)
|
|
68
|
+
self.beneficiaries = Beneficiaries(self)
|
|
69
|
+
self.checkout_sessions = CheckoutSessions(self)
|
|
70
|
+
self.customers = Customers(self)
|
|
71
|
+
self.entity = Entity(self)
|
|
72
|
+
self.invoices = Invoices(self)
|
|
73
|
+
self.payment_links = PaymentLinks(self)
|
|
74
|
+
self.transfer_drafts = TransferDrafts(self)
|
|
75
|
+
self.webhook_endpoints = WebhookEndpoints(self)
|
|
76
|
+
|
|
77
|
+
def __enter__(self) -> Zazu:
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def __exit__(self, *exc: Any) -> None:
|
|
81
|
+
self.close()
|
|
82
|
+
|
|
83
|
+
def close(self) -> None:
|
|
84
|
+
if self._owns_client:
|
|
85
|
+
self._http.close()
|
|
86
|
+
|
|
87
|
+
def request(
|
|
88
|
+
self,
|
|
89
|
+
method: str,
|
|
90
|
+
path: str,
|
|
91
|
+
*,
|
|
92
|
+
params: dict[str, Any] | None = None,
|
|
93
|
+
body: Any = None,
|
|
94
|
+
headers: dict[str, str] | None = None,
|
|
95
|
+
) -> ZazuResponse:
|
|
96
|
+
url = self._build_url(path, params)
|
|
97
|
+
request_headers = {
|
|
98
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
99
|
+
"User-Agent": USER_AGENT,
|
|
100
|
+
"Accept": "application/json",
|
|
101
|
+
}
|
|
102
|
+
if self.api_version:
|
|
103
|
+
request_headers["Zazu-Version"] = self.api_version
|
|
104
|
+
if headers:
|
|
105
|
+
request_headers.update(headers)
|
|
106
|
+
|
|
107
|
+
request_kwargs: dict[str, Any] = {"headers": request_headers}
|
|
108
|
+
if body is not None:
|
|
109
|
+
request_headers["Content-Type"] = "application/json"
|
|
110
|
+
request_kwargs["content"] = json.dumps(body)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
raw = self._http.request(method.upper(), url, **request_kwargs)
|
|
114
|
+
except httpx.TimeoutException as err:
|
|
115
|
+
raise ZazuConnectionError(f"Request timed out after {self.timeout}s") from err
|
|
116
|
+
except httpx.HTTPError as err:
|
|
117
|
+
raise ZazuConnectionError(f"Connection failed: {err}") from err
|
|
118
|
+
|
|
119
|
+
parsed = _parse_body(raw)
|
|
120
|
+
response = ZazuResponse(raw, parsed)
|
|
121
|
+
if response.success:
|
|
122
|
+
return response
|
|
123
|
+
raise _build_error(response)
|
|
124
|
+
|
|
125
|
+
def _build_url(self, path: str, params: dict[str, Any] | None) -> str:
|
|
126
|
+
trimmed = path.lstrip("/")
|
|
127
|
+
url = f"{self.base_url}/{trimmed}"
|
|
128
|
+
if params:
|
|
129
|
+
cleaned = {k: v for k, v in params.items() if v is not None}
|
|
130
|
+
if cleaned:
|
|
131
|
+
url = f"{url}?{urlencode(cleaned, doseq=True)}"
|
|
132
|
+
return url
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _env_float(name: str, default: float) -> float:
|
|
136
|
+
raw = os.getenv(name)
|
|
137
|
+
if not raw:
|
|
138
|
+
return default
|
|
139
|
+
try:
|
|
140
|
+
return float(raw)
|
|
141
|
+
except ValueError:
|
|
142
|
+
return default
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _parse_body(raw: httpx.Response) -> Any:
|
|
146
|
+
content_type = raw.headers.get("content-type", "")
|
|
147
|
+
if "json" not in content_type:
|
|
148
|
+
return None
|
|
149
|
+
text = raw.text
|
|
150
|
+
if not text:
|
|
151
|
+
return None
|
|
152
|
+
try:
|
|
153
|
+
return json.loads(text)
|
|
154
|
+
except json.JSONDecodeError:
|
|
155
|
+
return text
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _error_payload(body: Any) -> dict[str, Any]:
|
|
159
|
+
if not isinstance(body, dict):
|
|
160
|
+
return {}
|
|
161
|
+
err = body.get("error")
|
|
162
|
+
return err if isinstance(err, dict) else {}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _build_error(response: ZazuResponse) -> ZazuError:
|
|
166
|
+
payload = _error_payload(response.body)
|
|
167
|
+
message = payload.get("message")
|
|
168
|
+
opts: dict[str, Any] = {
|
|
169
|
+
"status": response.status,
|
|
170
|
+
"request_id": response.request_id,
|
|
171
|
+
"type": payload.get("type"),
|
|
172
|
+
"param": payload.get("param"),
|
|
173
|
+
"body": response.body,
|
|
174
|
+
"headers": response.headers,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
status = response.status
|
|
178
|
+
if status == 401:
|
|
179
|
+
return ZazuAuthenticationError(message or "Authentication failed", **opts)
|
|
180
|
+
if status == 403:
|
|
181
|
+
return ZazuForbiddenError(message or "Forbidden", **opts)
|
|
182
|
+
if status == 404:
|
|
183
|
+
return ZazuNotFoundError(message or "Not found", **opts)
|
|
184
|
+
if status == 422:
|
|
185
|
+
return ZazuValidationError(message or "Validation failed", **opts)
|
|
186
|
+
if status == 429:
|
|
187
|
+
retry_after = response.headers.get("retry-after")
|
|
188
|
+
try:
|
|
189
|
+
retry_after_int = int(retry_after) if retry_after else None
|
|
190
|
+
except ValueError:
|
|
191
|
+
retry_after_int = None
|
|
192
|
+
return ZazuRateLimitError(
|
|
193
|
+
message or "Rate limited", retry_after=retry_after_int, **opts
|
|
194
|
+
)
|
|
195
|
+
if 500 <= status < 600:
|
|
196
|
+
return ZazuServerError(message or f"Server error ({status})", **opts)
|
|
197
|
+
return ZazuError(message or f"Unexpected status {status}", **opts)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
__all__ = ["MAX_PER_PAGE", "Page", "Zazu"]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Mirrors lib/zazu/errors.rb. Nine-class hierarchy shared across SDKs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ZazuError(Exception):
|
|
9
|
+
"""Base class for every error raised by the SDK."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
message: str,
|
|
14
|
+
*,
|
|
15
|
+
status: int | None = None,
|
|
16
|
+
request_id: str | None = None,
|
|
17
|
+
type: str | None = None,
|
|
18
|
+
param: str | None = None,
|
|
19
|
+
body: Any = None,
|
|
20
|
+
headers: Any = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.message = message
|
|
24
|
+
self.status = status
|
|
25
|
+
self.request_id = request_id
|
|
26
|
+
self.type = type
|
|
27
|
+
self.param = param
|
|
28
|
+
self.body = body
|
|
29
|
+
self.headers = headers
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ZazuArgumentError(ZazuError):
|
|
33
|
+
"""Caller passed bad arguments before any HTTP request was made."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ZazuConfigurationError(ZazuError):
|
|
37
|
+
"""Missing or invalid client configuration (e.g. no API key)."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ZazuConnectionError(ZazuError):
|
|
41
|
+
"""The HTTP request failed before getting a response (timeout, DNS, refused)."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ZazuAuthenticationError(ZazuError):
|
|
45
|
+
"""HTTP 401."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ZazuForbiddenError(ZazuError):
|
|
49
|
+
"""HTTP 403."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ZazuNotFoundError(ZazuError):
|
|
53
|
+
"""HTTP 404."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ZazuValidationError(ZazuError):
|
|
57
|
+
"""HTTP 422."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ZazuRateLimitError(ZazuError):
|
|
61
|
+
"""HTTP 429. `retry_after` is the value of the Retry-After header in seconds, if present."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, message: str, *, retry_after: int | None = None, **kwargs: Any) -> None:
|
|
64
|
+
super().__init__(message, **kwargs)
|
|
65
|
+
self.retry_after = retry_after
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ZazuServerError(ZazuError):
|
|
69
|
+
"""HTTP 5xx."""
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Mirrors lib/zazu/page.rb. Cursor-based pagination, hard cap of 100 items per page."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Iterator
|
|
6
|
+
from typing import Any, Generic, TypeVar
|
|
7
|
+
|
|
8
|
+
from .response import ZazuResponse
|
|
9
|
+
|
|
10
|
+
MAX_PER_PAGE = 100
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Page(Generic[T]):
|
|
16
|
+
"""A single page of results plus a fetcher to load the next page on demand."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
response: ZazuResponse,
|
|
21
|
+
fetcher: Callable[[str | None], "Page[T]"],
|
|
22
|
+
) -> None:
|
|
23
|
+
self.response = response
|
|
24
|
+
self._fetcher = fetcher
|
|
25
|
+
body = response.body if isinstance(response.body, dict) else {}
|
|
26
|
+
self.data: list[T] = list(body.get("data") or [])
|
|
27
|
+
self.has_more: bool = bool(body.get("has_more"))
|
|
28
|
+
self.next_cursor: str | None = body.get("next_cursor")
|
|
29
|
+
|
|
30
|
+
def next(self) -> Page[T] | None:
|
|
31
|
+
"""Fetch the next page, or None if there are no more results."""
|
|
32
|
+
if not self.has_more or not self.next_cursor:
|
|
33
|
+
return None
|
|
34
|
+
return self._fetcher(self.next_cursor)
|
|
35
|
+
|
|
36
|
+
def auto_paging_iter(self) -> Iterator[T]:
|
|
37
|
+
"""Yield every item across all pages by chasing cursors."""
|
|
38
|
+
page: Page[T] | None = self
|
|
39
|
+
while page is not None:
|
|
40
|
+
yield from page.data
|
|
41
|
+
page = page.next()
|
|
42
|
+
|
|
43
|
+
def __iter__(self) -> Iterator[T]:
|
|
44
|
+
return iter(self.data)
|
|
45
|
+
|
|
46
|
+
def __len__(self) -> int:
|
|
47
|
+
return len(self.data)
|
|
48
|
+
|
|
49
|
+
def __repr__(self) -> str:
|
|
50
|
+
return f"Page(items={len(self.data)}, has_more={self.has_more})"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
PageBody = dict[str, Any]
|
|
54
|
+
PageFetcher = Callable[[str | None], Page[Any]]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Mirrors lib/zazu/resources/accounts.rb."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..page import MAX_PER_PAGE, Page
|
|
9
|
+
from ..response import ZazuResponse
|
|
10
|
+
from .base import ResourceBase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Accounts(ResourceBase):
|
|
14
|
+
def list(
|
|
15
|
+
self,
|
|
16
|
+
*,
|
|
17
|
+
status: str | None = None,
|
|
18
|
+
currency_code: str | None = None,
|
|
19
|
+
limit: int = MAX_PER_PAGE,
|
|
20
|
+
cursor: str | None = None,
|
|
21
|
+
) -> Page[Any]:
|
|
22
|
+
return self.list_page(
|
|
23
|
+
"api/accounts",
|
|
24
|
+
{"status": status, "currency_code": currency_code},
|
|
25
|
+
limit=limit,
|
|
26
|
+
cursor=cursor,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def get(self, id: str) -> ZazuResponse:
|
|
30
|
+
return self.http_get(self.encode_path("api/accounts", id))
|
|
31
|
+
|
|
32
|
+
def list_transactions(
|
|
33
|
+
self,
|
|
34
|
+
account_id: str,
|
|
35
|
+
*,
|
|
36
|
+
operation: str | None = None,
|
|
37
|
+
posted_after: str | datetime | None = None,
|
|
38
|
+
posted_before: str | datetime | None = None,
|
|
39
|
+
limit: int = MAX_PER_PAGE,
|
|
40
|
+
cursor: str | None = None,
|
|
41
|
+
) -> Page[Any]:
|
|
42
|
+
return self.list_page(
|
|
43
|
+
self.encode_path("api/accounts", account_id, "transactions"),
|
|
44
|
+
{
|
|
45
|
+
"operation": operation,
|
|
46
|
+
"posted_after": _serialize_time(posted_after),
|
|
47
|
+
"posted_before": _serialize_time(posted_before),
|
|
48
|
+
},
|
|
49
|
+
limit=limit,
|
|
50
|
+
cursor=cursor,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def get_transaction(self, account_id: str, transaction_id: str) -> ZazuResponse:
|
|
54
|
+
return self.http_get(
|
|
55
|
+
self.encode_path("api/accounts", account_id, "transactions", transaction_id)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _serialize_time(value: str | datetime | None) -> str | None:
|
|
60
|
+
if value is None:
|
|
61
|
+
return None
|
|
62
|
+
if isinstance(value, str):
|
|
63
|
+
return value
|
|
64
|
+
return value.isoformat()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Mirrors lib/zazu/resources/base.rb."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
from ..errors import ZazuArgumentError
|
|
9
|
+
from ..page import MAX_PER_PAGE, Page
|
|
10
|
+
from ..response import ZazuResponse
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..client import Zazu
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ResourceBase:
|
|
17
|
+
def __init__(self, client: Zazu) -> None:
|
|
18
|
+
self._client = client
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def encode_path(base: str, *segments: str) -> str:
|
|
22
|
+
"""Build a request path by joining a literal base path with dynamic
|
|
23
|
+
segments. The base is appended verbatim; each dynamic segment is
|
|
24
|
+
percent-encoded so an ID containing `/` or other special characters
|
|
25
|
+
cannot escape the intended path.
|
|
26
|
+
|
|
27
|
+
encode_path("api/accounts", "acc_xyz")
|
|
28
|
+
# => "api/accounts/acc_xyz"
|
|
29
|
+
encode_path("api/accounts", "acc 1", "transactions", "tx 1")
|
|
30
|
+
# => "api/accounts/acc%201/transactions/tx%201"
|
|
31
|
+
"""
|
|
32
|
+
encoded = []
|
|
33
|
+
for seg in segments:
|
|
34
|
+
text = str(seg)
|
|
35
|
+
# An empty segment would silently turn `/things/:id` into
|
|
36
|
+
# `/things/`, which on most APIs redispatches to the list
|
|
37
|
+
# endpoint. Surface it loudly.
|
|
38
|
+
if not text:
|
|
39
|
+
raise ZazuArgumentError("path segment cannot be blank")
|
|
40
|
+
encoded.append(quote(text, safe=""))
|
|
41
|
+
return "/".join([base, *encoded])
|
|
42
|
+
|
|
43
|
+
def http_get(
|
|
44
|
+
self,
|
|
45
|
+
path: str,
|
|
46
|
+
*,
|
|
47
|
+
params: dict[str, Any] | None = None,
|
|
48
|
+
) -> ZazuResponse:
|
|
49
|
+
return self._client.request("GET", path, params=params)
|
|
50
|
+
|
|
51
|
+
def http_post(
|
|
52
|
+
self,
|
|
53
|
+
path: str,
|
|
54
|
+
*,
|
|
55
|
+
body: Any = None,
|
|
56
|
+
params: dict[str, Any] | None = None,
|
|
57
|
+
) -> ZazuResponse:
|
|
58
|
+
return self._client.request("POST", path, body=body, params=params)
|
|
59
|
+
|
|
60
|
+
def http_patch(
|
|
61
|
+
self,
|
|
62
|
+
path: str,
|
|
63
|
+
*,
|
|
64
|
+
body: Any = None,
|
|
65
|
+
) -> ZazuResponse:
|
|
66
|
+
return self._client.request("PATCH", path, body=body)
|
|
67
|
+
|
|
68
|
+
def http_delete(self, path: str) -> ZazuResponse:
|
|
69
|
+
return self._client.request("DELETE", path)
|
|
70
|
+
|
|
71
|
+
def list_page(
|
|
72
|
+
self,
|
|
73
|
+
path: str,
|
|
74
|
+
params: dict[str, Any] | None = None,
|
|
75
|
+
*,
|
|
76
|
+
limit: int | None = None,
|
|
77
|
+
cursor: str | None = None,
|
|
78
|
+
) -> Page[Any]:
|
|
79
|
+
merged: dict[str, Any] = dict(params or {})
|
|
80
|
+
if limit is not None:
|
|
81
|
+
merged["limit"] = min(limit, MAX_PER_PAGE)
|
|
82
|
+
if cursor is not None:
|
|
83
|
+
merged["cursor"] = cursor
|
|
84
|
+
|
|
85
|
+
def fetch(next_cursor: str | None) -> Page[Any]:
|
|
86
|
+
return self.list_page(path, params, limit=limit, cursor=next_cursor)
|
|
87
|
+
|
|
88
|
+
response = self.http_get(path, params=merged)
|
|
89
|
+
return Page(response, fetch)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Mirrors lib/zazu/resources/beneficiaries.rb.
|
|
2
|
+
|
|
3
|
+
Read-only directory of saved transfer recipients. Each beneficiary
|
|
4
|
+
embeds its bank accounts; the one flagged ``default`` is used when a
|
|
5
|
+
transfer names only the beneficiary_id. Beneficiaries are created and
|
|
6
|
+
managed in the Zazu dashboard.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ..page import MAX_PER_PAGE, Page
|
|
14
|
+
from ..response import ZazuResponse
|
|
15
|
+
from .base import ResourceBase
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Beneficiaries(ResourceBase):
|
|
19
|
+
def list(self, *, limit: int = MAX_PER_PAGE, cursor: str | None = None) -> Page[Any]:
|
|
20
|
+
return self.list_page("api/beneficiaries", {}, limit=limit, cursor=cursor)
|
|
21
|
+
|
|
22
|
+
def get(self, id: str) -> ZazuResponse:
|
|
23
|
+
return self.http_get(self.encode_path("api/beneficiaries", id))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Mirrors lib/zazu/resources/checkout_sessions.rb."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..response import ZazuResponse
|
|
8
|
+
from .base import ResourceBase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CheckoutSessions(ResourceBase):
|
|
12
|
+
def get(self, id: str) -> ZazuResponse:
|
|
13
|
+
return self.http_get(self.encode_path("api/checkout_sessions", id))
|
|
14
|
+
|
|
15
|
+
def create(self, **attributes: Any) -> ZazuResponse:
|
|
16
|
+
return self.http_post("api/checkout_sessions", body=attributes)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Mirrors lib/zazu/resources/customers.rb."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..page import MAX_PER_PAGE, Page
|
|
8
|
+
from ..response import ZazuResponse
|
|
9
|
+
from .base import ResourceBase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Customers(ResourceBase):
|
|
13
|
+
def list(
|
|
14
|
+
self,
|
|
15
|
+
*,
|
|
16
|
+
q: str | None = None,
|
|
17
|
+
limit: int = MAX_PER_PAGE,
|
|
18
|
+
cursor: str | None = None,
|
|
19
|
+
) -> Page[Any]:
|
|
20
|
+
return self.list_page("api/customers", {"q": q}, limit=limit, cursor=cursor)
|
|
21
|
+
|
|
22
|
+
def get(self, id: str) -> ZazuResponse:
|
|
23
|
+
return self.http_get(self.encode_path("api/customers", id))
|
|
24
|
+
|
|
25
|
+
def create(self, **attributes: Any) -> ZazuResponse:
|
|
26
|
+
return self.http_post("api/customers", body=attributes)
|
|
27
|
+
|
|
28
|
+
def update(self, id: str, **attributes: Any) -> ZazuResponse:
|
|
29
|
+
return self.http_patch(self.encode_path("api/customers", id), body=attributes)
|
|
30
|
+
|
|
31
|
+
def delete(self, id: str) -> ZazuResponse:
|
|
32
|
+
return self.http_delete(self.encode_path("api/customers", id))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Mirrors lib/zazu/resources/entity.rb."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..response import ZazuResponse
|
|
6
|
+
from .base import ResourceBase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Entity(ResourceBase):
|
|
10
|
+
def get(self) -> ZazuResponse:
|
|
11
|
+
return self.http_get("api/entity")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Mirrors lib/zazu/resources/invoices.rb."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..page import MAX_PER_PAGE, Page
|
|
8
|
+
from ..response import ZazuResponse
|
|
9
|
+
from .base import ResourceBase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Invoices(ResourceBase):
|
|
13
|
+
def list(
|
|
14
|
+
self,
|
|
15
|
+
*,
|
|
16
|
+
status: str | None = None,
|
|
17
|
+
customer_id: str | None = None,
|
|
18
|
+
limit: int = MAX_PER_PAGE,
|
|
19
|
+
cursor: str | None = None,
|
|
20
|
+
) -> Page[Any]:
|
|
21
|
+
return self.list_page(
|
|
22
|
+
"api/invoices",
|
|
23
|
+
{"status": status, "customer_id": customer_id},
|
|
24
|
+
limit=limit,
|
|
25
|
+
cursor=cursor,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def get(self, id: str) -> ZazuResponse:
|
|
29
|
+
return self.http_get(self.encode_path("api/invoices", id))
|
|
30
|
+
|
|
31
|
+
def create(self, **attributes: Any) -> ZazuResponse:
|
|
32
|
+
return self.http_post("api/invoices", body=attributes)
|
|
33
|
+
|
|
34
|
+
def update(self, id: str, **attributes: Any) -> ZazuResponse:
|
|
35
|
+
return self.http_patch(self.encode_path("api/invoices", id), body=attributes)
|
|
36
|
+
|
|
37
|
+
def send_invoice(self, id: str) -> ZazuResponse:
|
|
38
|
+
return self.http_post(self.encode_path("api/invoices", id, "send"))
|
|
39
|
+
|
|
40
|
+
def mark_as_paid(self, id: str) -> ZazuResponse:
|
|
41
|
+
return self.http_post(self.encode_path("api/invoices", id, "mark_as_paid"))
|
|
42
|
+
|
|
43
|
+
def cancel(self, id: str) -> ZazuResponse:
|
|
44
|
+
return self.http_post(self.encode_path("api/invoices", id, "cancel"))
|
|
45
|
+
|
|
46
|
+
def credit_note(self, id: str) -> ZazuResponse:
|
|
47
|
+
return self.http_post(self.encode_path("api/invoices", id, "credit_note"))
|
|
48
|
+
|
|
49
|
+
def delete(self, id: str) -> ZazuResponse:
|
|
50
|
+
return self.http_delete(self.encode_path("api/invoices", id))
|
|
51
|
+
|
|
52
|
+
def create_payment_link(self, invoice_id: str, *, account_id: str) -> ZazuResponse:
|
|
53
|
+
return self.http_post(
|
|
54
|
+
self.encode_path("api/invoices", invoice_id, "payment_link"),
|
|
55
|
+
body={"account_id": account_id},
|
|
56
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Mirrors lib/zazu/resources/payment_links.rb."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..page import MAX_PER_PAGE, Page
|
|
8
|
+
from ..response import ZazuResponse
|
|
9
|
+
from .base import ResourceBase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PaymentLinks(ResourceBase):
|
|
13
|
+
def list(
|
|
14
|
+
self,
|
|
15
|
+
*,
|
|
16
|
+
status: str | None = None,
|
|
17
|
+
link_type: str | None = None,
|
|
18
|
+
limit: int = MAX_PER_PAGE,
|
|
19
|
+
cursor: str | None = None,
|
|
20
|
+
) -> Page[Any]:
|
|
21
|
+
return self.list_page(
|
|
22
|
+
"api/payment_links",
|
|
23
|
+
{"status": status, "link_type": link_type},
|
|
24
|
+
limit=limit,
|
|
25
|
+
cursor=cursor,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def get(self, id: str) -> ZazuResponse:
|
|
29
|
+
return self.http_get(self.encode_path("api/payment_links", id))
|
|
30
|
+
|
|
31
|
+
def create(self, **attributes: Any) -> ZazuResponse:
|
|
32
|
+
return self.http_post("api/payment_links", body=attributes)
|
|
33
|
+
|
|
34
|
+
def cancel(self, id: str) -> ZazuResponse:
|
|
35
|
+
return self.http_post(self.encode_path("api/payment_links", id, "cancel"))
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Mirrors lib/zazu/resources/transfer_drafts.rb.
|
|
2
|
+
|
|
3
|
+
API-initiated transfers. Creating a draft routes it into the workspace's
|
|
4
|
+
in-app approval flow — the API never executes a transfer itself. Poll
|
|
5
|
+
``get`` (status: requested → processing → completed / failed) or
|
|
6
|
+
subscribe to the ``transfer.executed`` webhook.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ..response import ZazuResponse
|
|
14
|
+
from .base import ResourceBase
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TransferDrafts(ResourceBase):
|
|
18
|
+
def create(self, **attributes: Any) -> ZazuResponse:
|
|
19
|
+
return self.http_post("api/transfer_drafts", body=attributes)
|
|
20
|
+
|
|
21
|
+
def get(self, id: str) -> ZazuResponse:
|
|
22
|
+
return self.http_get(self.encode_path("api/transfer_drafts", id))
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Mirrors lib/zazu/resources/webhook_endpoints.rb."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..page import MAX_PER_PAGE, Page
|
|
9
|
+
from ..response import ZazuResponse
|
|
10
|
+
from .base import ResourceBase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WebhookEndpoints(ResourceBase):
|
|
14
|
+
def list(
|
|
15
|
+
self,
|
|
16
|
+
*,
|
|
17
|
+
limit: int = MAX_PER_PAGE,
|
|
18
|
+
cursor: str | None = None,
|
|
19
|
+
) -> Page[Any]:
|
|
20
|
+
return self.list_page("api/webhook_endpoints", {}, limit=limit, cursor=cursor)
|
|
21
|
+
|
|
22
|
+
def get(self, id: str) -> ZazuResponse:
|
|
23
|
+
return self.http_get(self.encode_path("api/webhook_endpoints", id))
|
|
24
|
+
|
|
25
|
+
def create(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
url: str,
|
|
29
|
+
events: Sequence[str],
|
|
30
|
+
description: str | None = None,
|
|
31
|
+
) -> ZazuResponse:
|
|
32
|
+
body: dict[str, Any] = {"url": url, "events": list(events)}
|
|
33
|
+
if description is not None:
|
|
34
|
+
body["description"] = description
|
|
35
|
+
return self.http_post("api/webhook_endpoints", body=body)
|
|
36
|
+
|
|
37
|
+
def update(self, id: str, **attributes: Any) -> ZazuResponse:
|
|
38
|
+
return self.http_patch(self.encode_path("api/webhook_endpoints", id), body=attributes)
|
|
39
|
+
|
|
40
|
+
def delete(self, id: str) -> ZazuResponse:
|
|
41
|
+
return self.http_delete(self.encode_path("api/webhook_endpoints", id))
|
|
42
|
+
|
|
43
|
+
def test_endpoint(self, id: str) -> ZazuResponse:
|
|
44
|
+
return self.http_post(self.encode_path("api/webhook_endpoints", id, "test"))
|
|
45
|
+
|
|
46
|
+
def regenerate_secret(self, id: str) -> ZazuResponse:
|
|
47
|
+
return self.http_post(self.encode_path("api/webhook_endpoints", id, "regenerate_secret"))
|
|
48
|
+
|
|
49
|
+
def enable(self, id: str) -> ZazuResponse:
|
|
50
|
+
return self.http_post(self.encode_path("api/webhook_endpoints", id, "enable"))
|
|
51
|
+
|
|
52
|
+
def disable(self, id: str) -> ZazuResponse:
|
|
53
|
+
return self.http_post(self.encode_path("api/webhook_endpoints", id, "disable"))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Mirrors lib/zazu/response.rb."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ZazuResponse:
|
|
11
|
+
"""Thin wrapper over httpx.Response — exposes status, headers, and parsed body."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, raw: httpx.Response, body: Any) -> None:
|
|
14
|
+
self._raw = raw
|
|
15
|
+
self.status: int = raw.status_code
|
|
16
|
+
self.headers: httpx.Headers = raw.headers
|
|
17
|
+
self.body: Any = body
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def request_id(self) -> str | None:
|
|
21
|
+
value = self.headers.get("X-Request-Id")
|
|
22
|
+
return value if isinstance(value, str) else None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def success(self) -> bool:
|
|
26
|
+
return 200 <= self.status < 300
|