manza 1.0.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.
- manza-1.0.0/.gitignore +18 -0
- manza-1.0.0/CHANGELOG.md +84 -0
- manza-1.0.0/LICENSE +21 -0
- manza-1.0.0/PKG-INFO +190 -0
- manza-1.0.0/README.md +157 -0
- manza-1.0.0/pyproject.toml +91 -0
- manza-1.0.0/src/manza/__init__.py +63 -0
- manza-1.0.0/src/manza/_env.py +39 -0
- manza-1.0.0/src/manza/_version.py +1 -0
- manza-1.0.0/src/manza/client.py +213 -0
- manza-1.0.0/src/manza/errors.py +84 -0
- manza-1.0.0/src/manza/page.py +54 -0
- manza-1.0.0/src/manza/py.typed +0 -0
- manza-1.0.0/src/manza/resources/__init__.py +0 -0
- manza-1.0.0/src/manza/resources/accounts.py +64 -0
- manza-1.0.0/src/manza/resources/base.py +89 -0
- manza-1.0.0/src/manza/resources/beneficiaries.py +61 -0
- manza-1.0.0/src/manza/resources/checkout_sessions.py +23 -0
- manza-1.0.0/src/manza/resources/customers.py +38 -0
- manza-1.0.0/src/manza/resources/entity.py +11 -0
- manza-1.0.0/src/manza/resources/invoices.py +56 -0
- manza-1.0.0/src/manza/resources/payee_trust_requests.py +25 -0
- manza-1.0.0/src/manza/resources/payment_links.py +40 -0
- manza-1.0.0/src/manza/resources/transfer_drafts.py +66 -0
- manza-1.0.0/src/manza/resources/webhook_endpoints.py +53 -0
- manza-1.0.0/src/manza/response.py +26 -0
- manza-1.0.0/src/manza/transfer_authorization.py +84 -0
manza-1.0.0/.gitignore
ADDED
|
@@ -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
|
manza-1.0.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `manza` (formerly `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
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `ManzaConflictError` (409), the 10th error class. A duplicate
|
|
13
|
+
`client_reference` on a transfer draft raises it with `payment_id`
|
|
14
|
+
naming the existing draft. 400 now maps to `ManzaValidationError`
|
|
15
|
+
(lists return 400 for a malformed `limit`/`cursor`).
|
|
16
|
+
- `TransferDrafts.authorize(id, authorization_id, signature)` and
|
|
17
|
+
`decline(id, authorization_id, reason=None)` for machine-authorized
|
|
18
|
+
transfers. A blank signature raises `ManzaArgumentError` locally, because
|
|
19
|
+
the API would count it as a failed attempt.
|
|
20
|
+
- `TransferDrafts.create` documents the new optional `client_reference`;
|
|
21
|
+
responses carry `client_reference` and `authorization`.
|
|
22
|
+
- `manza.transfer_authorization` with `signature_input`, `sign` and
|
|
23
|
+
`payee_for`, the HMAC-SHA256 signer for authorization challenges, tested
|
|
24
|
+
with the fixed vector shared across SDKs.
|
|
25
|
+
- `Beneficiaries.create`, `list_external_accounts`, `get_external_account`
|
|
26
|
+
and `create_external_account`.
|
|
27
|
+
- `PayeeTrustRequests` (`client.payee_trust_requests`) with
|
|
28
|
+
`create(external_account_ids)` and `get(id)`.
|
|
29
|
+
- Docs for new pass-through fields: checkout session `customer_name`,
|
|
30
|
+
`collect_billing_address`, `billing_address`, `settled_at`, `transaction`
|
|
31
|
+
and the `clearing` status; payment link billing fields; customer
|
|
32
|
+
`registration_number` / `vat_number` (and MA-only `tax_id` / `ice_number`).
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **Renamed from `zazu-sdk` to `manza`** (1.0.0 under the new name; see the
|
|
37
|
+
migration guide below). The package, import, classes, User-Agent and
|
|
38
|
+
environment variables all move from Zazu to Manza.
|
|
39
|
+
- The version header is now `Manza-Version` (the API accepts both).
|
|
40
|
+
- User-Agent is `manza-python/<version>` (was `zazu-sdk/<version>`).
|
|
41
|
+
- Cassettes are fetched from `getmanza/manza-ruby`, pinned to `v1.0.0`.
|
|
42
|
+
- The replay placeholders are `<MANZA_API_KEY>` / `<MANZA_VERSION>` and the
|
|
43
|
+
fixture variables are `MANZA_FIXTURE_*` (no fallback, dev-only).
|
|
44
|
+
- Default base URL is now `https://ma.manza.finance` (Morocco production;
|
|
45
|
+
South Africa is `https://za.manza.finance`). Cassettes are recorded against
|
|
46
|
+
`https://ma.manza.dev`.
|
|
47
|
+
- Request bodies are serialized compactly (no spaces after `,` and `:`), the
|
|
48
|
+
same bytes the Ruby SDK sends.
|
|
49
|
+
- Replay harness: one cassette per test, with optional exact and
|
|
50
|
+
signature-less body matching for the new cassettes.
|
|
51
|
+
|
|
52
|
+
### Deprecated
|
|
53
|
+
|
|
54
|
+
- `ZAZU_API_KEY`, `ZAZU_BASE_URL`, `ZAZU_API_VERSION` and `ZAZU_TIMEOUT` still
|
|
55
|
+
work for all of 1.x. `MANZA_*` wins when both are set; the legacy name emits
|
|
56
|
+
one `FutureWarning` per variable per process.
|
|
57
|
+
|
|
58
|
+
### Migrating from `zazu-sdk` 0.x
|
|
59
|
+
|
|
60
|
+
| Before | After |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `pip install zazu-sdk` | `pip install manza` |
|
|
63
|
+
| `from zazu_sdk import Zazu` | `from manza import Manza` |
|
|
64
|
+
| `ZazuError`, `ZazuValidationError`, ... (every `Zazu*` class) | `ManzaError`, `ManzaValidationError`, ... |
|
|
65
|
+
| `zazu_sdk.transfer_authorization` | `manza.transfer_authorization` |
|
|
66
|
+
| `ZAZU_API_KEY` / `ZAZU_BASE_URL` / `ZAZU_API_VERSION` / `ZAZU_TIMEOUT` | `MANZA_API_KEY` / `MANZA_BASE_URL` / `MANZA_API_VERSION` / `MANZA_TIMEOUT` |
|
|
67
|
+
| `Zazu-Version` header | `Manza-Version` |
|
|
68
|
+
| repo `getzazu/zazu-python` | `getmanza/manza-python` |
|
|
69
|
+
|
|
70
|
+
## [0.2.1]
|
|
71
|
+
|
|
72
|
+
Version alignment: the whole SDK family now releases in lockstep with zazu-ruby. No functional changes since [0.1.0].
|
|
73
|
+
|
|
74
|
+
## [0.1.0]
|
|
75
|
+
|
|
76
|
+
Initial release.
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- Sync `Zazu` client built on `httpx`
|
|
81
|
+
- Resource modules: `accounts`, `beneficiaries`, `checkout_sessions`, `customers`, `entity`, `invoices`, `payment_links`, `transfer_drafts`, `webhook_endpoints`
|
|
82
|
+
- Cursor-based `Page` with `auto_paging_iter()`
|
|
83
|
+
- Nine-class error hierarchy mirroring `zazu-ruby`
|
|
84
|
+
- Cassette-replay test harness driven by the Ruby SDK's release tarball
|
manza-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Manza
|
|
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.
|
manza-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: manza
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for the Manza API.
|
|
5
|
+
Project-URL: Homepage, https://github.com/getmanza/manza-python
|
|
6
|
+
Project-URL: Documentation, https://github.com/getmanza/manza-python#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/getmanza/manza-python
|
|
8
|
+
Project-URL: Issues, https://github.com/getmanza/manza-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/getmanza/manza-python/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Manza <hello@get-manza.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api,fintech,manza,morocco,sdk
|
|
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
|
+
# manza
|
|
35
|
+
|
|
36
|
+
Python SDK for the [Manza API](https://ma.manza.finance).
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install manza
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Requires Python 3.11+.
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from manza import Manza
|
|
50
|
+
|
|
51
|
+
client = Manza(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 `MANZA_API_KEY`, `MANZA_BASE_URL`, `MANZA_API_VERSION`, and
|
|
76
|
+
`MANZA_TIMEOUT` from the environment if you don't pass them. The old `ZAZU_*`
|
|
77
|
+
names still work for all of 1.x and emit a one-time `FutureWarning`.
|
|
78
|
+
|
|
79
|
+
### Hosts
|
|
80
|
+
|
|
81
|
+
The default base URL is `https://ma.manza.finance` (Morocco). For South Africa:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
client = Manza(api_key="sk_live_...", base_url="https://za.manza.finance")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The replay tests run against the staging host `https://ma.manza.dev`.
|
|
88
|
+
|
|
89
|
+
## Beneficiaries and trusted payees
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
beneficiary = client.beneficiaries.create(
|
|
93
|
+
beneficiary_type="business", # "individual" | "business"
|
|
94
|
+
company_name="Acme Corp",
|
|
95
|
+
email="ap@acme.com",
|
|
96
|
+
).body
|
|
97
|
+
|
|
98
|
+
account = client.beneficiaries.create_external_account(
|
|
99
|
+
beneficiary["id"], account_number="..." # bank_identifier is required in ZA only
|
|
100
|
+
).body
|
|
101
|
+
client.beneficiaries.list_external_accounts(beneficiary["id"]) # Page
|
|
102
|
+
client.beneficiaries.get_external_account(beneficiary["id"], account["id"])
|
|
103
|
+
|
|
104
|
+
# Ask for the account to be trusted; a member approves it in the app.
|
|
105
|
+
request = client.payee_trust_requests.create([account["id"]]).body
|
|
106
|
+
client.payee_trust_requests.get(request["id"])
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Transfers and machine authorization
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
draft = client.transfer_drafts.create(
|
|
113
|
+
account_id=account_id,
|
|
114
|
+
beneficiary_id=beneficiary_id,
|
|
115
|
+
amount="150.00",
|
|
116
|
+
client_reference="po_1", # unique per entity, at most 128 characters
|
|
117
|
+
).body
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A duplicate `client_reference` raises `ManzaConflictError` with `payment_id`
|
|
121
|
+
naming the existing draft. A draft inside the entity's machine-authorization
|
|
122
|
+
envelope is sent to the enrolled authorizer as a
|
|
123
|
+
`payment.authorization_requested` webhook. Answer it with a different API key
|
|
124
|
+
holding `transfers:authorize`, signing with `manza.transfer_authorization`:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from manza import transfer_authorization as ta
|
|
128
|
+
|
|
129
|
+
signature_input = ta.signature_input(
|
|
130
|
+
payment_id=draft["id"],
|
|
131
|
+
nonce=nonce, # from the webhook
|
|
132
|
+
amount=draft["amount"], # the API's decimal string, verbatim
|
|
133
|
+
currency_code=draft["currency_code"],
|
|
134
|
+
account_id=draft["account_id"],
|
|
135
|
+
payee=ta.payee_for(external_account_id=draft["external_account_id"]),
|
|
136
|
+
client_reference=draft["client_reference"],
|
|
137
|
+
)
|
|
138
|
+
signature = ta.sign(signing_secret, signature_input) # lowercase hex HMAC-SHA256
|
|
139
|
+
|
|
140
|
+
authorizer.transfer_drafts.authorize(draft["id"], authorization_id, signature)
|
|
141
|
+
# or: authorizer.transfer_drafts.decline(draft["id"], authorization_id, "reason")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`payee_for` takes exactly one of `external_account_id` / `destination_account_id`.
|
|
145
|
+
A blank signature raises `ManzaArgumentError` before any request is made.
|
|
146
|
+
|
|
147
|
+
## Pagination
|
|
148
|
+
|
|
149
|
+
List endpoints return a `Page`. Iterate one page or chase cursors:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
page = client.invoices.list(limit=25)
|
|
153
|
+
for invoice in page:
|
|
154
|
+
...
|
|
155
|
+
|
|
156
|
+
# All items, automatic cursor chasing:
|
|
157
|
+
for invoice in client.invoices.list().auto_paging_iter():
|
|
158
|
+
...
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Errors
|
|
162
|
+
|
|
163
|
+
Ten concrete subclasses; discriminate with `isinstance`. `ManzaValidationError`
|
|
164
|
+
covers 400 and 422, `ManzaConflictError` covers 409 and exposes `payment_id`:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from manza import (
|
|
168
|
+
ManzaValidationError,
|
|
169
|
+
ManzaRateLimitError,
|
|
170
|
+
ManzaNotFoundError,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
client.invoices.get("nope")
|
|
175
|
+
except ManzaNotFoundError as err:
|
|
176
|
+
print(err.status, err.request_id, err.body)
|
|
177
|
+
except ManzaRateLimitError as err:
|
|
178
|
+
print("retry after", err.retry_after, "seconds")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Cross-SDK contract
|
|
182
|
+
|
|
183
|
+
`manza` is one of several Manza SDKs that all replay cassettes recorded by
|
|
184
|
+
the canonical [`manza-ruby`](https://github.com/getmanza/manza-ruby) SDK against
|
|
185
|
+
staging (`ma.manza.dev`). The wire format is snake_case JSON; request and response shapes match
|
|
186
|
+
across Ruby, TypeScript, Python, Go, and Rust.
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT.
|
manza-1.0.0/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# manza
|
|
2
|
+
|
|
3
|
+
Python SDK for the [Manza API](https://ma.manza.finance).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install manza
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.11+.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from manza import Manza
|
|
17
|
+
|
|
18
|
+
client = Manza(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 `MANZA_API_KEY`, `MANZA_BASE_URL`, `MANZA_API_VERSION`, and
|
|
43
|
+
`MANZA_TIMEOUT` from the environment if you don't pass them. The old `ZAZU_*`
|
|
44
|
+
names still work for all of 1.x and emit a one-time `FutureWarning`.
|
|
45
|
+
|
|
46
|
+
### Hosts
|
|
47
|
+
|
|
48
|
+
The default base URL is `https://ma.manza.finance` (Morocco). For South Africa:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
client = Manza(api_key="sk_live_...", base_url="https://za.manza.finance")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The replay tests run against the staging host `https://ma.manza.dev`.
|
|
55
|
+
|
|
56
|
+
## Beneficiaries and trusted payees
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
beneficiary = client.beneficiaries.create(
|
|
60
|
+
beneficiary_type="business", # "individual" | "business"
|
|
61
|
+
company_name="Acme Corp",
|
|
62
|
+
email="ap@acme.com",
|
|
63
|
+
).body
|
|
64
|
+
|
|
65
|
+
account = client.beneficiaries.create_external_account(
|
|
66
|
+
beneficiary["id"], account_number="..." # bank_identifier is required in ZA only
|
|
67
|
+
).body
|
|
68
|
+
client.beneficiaries.list_external_accounts(beneficiary["id"]) # Page
|
|
69
|
+
client.beneficiaries.get_external_account(beneficiary["id"], account["id"])
|
|
70
|
+
|
|
71
|
+
# Ask for the account to be trusted; a member approves it in the app.
|
|
72
|
+
request = client.payee_trust_requests.create([account["id"]]).body
|
|
73
|
+
client.payee_trust_requests.get(request["id"])
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Transfers and machine authorization
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
draft = client.transfer_drafts.create(
|
|
80
|
+
account_id=account_id,
|
|
81
|
+
beneficiary_id=beneficiary_id,
|
|
82
|
+
amount="150.00",
|
|
83
|
+
client_reference="po_1", # unique per entity, at most 128 characters
|
|
84
|
+
).body
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A duplicate `client_reference` raises `ManzaConflictError` with `payment_id`
|
|
88
|
+
naming the existing draft. A draft inside the entity's machine-authorization
|
|
89
|
+
envelope is sent to the enrolled authorizer as a
|
|
90
|
+
`payment.authorization_requested` webhook. Answer it with a different API key
|
|
91
|
+
holding `transfers:authorize`, signing with `manza.transfer_authorization`:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from manza import transfer_authorization as ta
|
|
95
|
+
|
|
96
|
+
signature_input = ta.signature_input(
|
|
97
|
+
payment_id=draft["id"],
|
|
98
|
+
nonce=nonce, # from the webhook
|
|
99
|
+
amount=draft["amount"], # the API's decimal string, verbatim
|
|
100
|
+
currency_code=draft["currency_code"],
|
|
101
|
+
account_id=draft["account_id"],
|
|
102
|
+
payee=ta.payee_for(external_account_id=draft["external_account_id"]),
|
|
103
|
+
client_reference=draft["client_reference"],
|
|
104
|
+
)
|
|
105
|
+
signature = ta.sign(signing_secret, signature_input) # lowercase hex HMAC-SHA256
|
|
106
|
+
|
|
107
|
+
authorizer.transfer_drafts.authorize(draft["id"], authorization_id, signature)
|
|
108
|
+
# or: authorizer.transfer_drafts.decline(draft["id"], authorization_id, "reason")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`payee_for` takes exactly one of `external_account_id` / `destination_account_id`.
|
|
112
|
+
A blank signature raises `ManzaArgumentError` before any request is made.
|
|
113
|
+
|
|
114
|
+
## Pagination
|
|
115
|
+
|
|
116
|
+
List endpoints return a `Page`. Iterate one page or chase cursors:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
page = client.invoices.list(limit=25)
|
|
120
|
+
for invoice in page:
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
# All items, automatic cursor chasing:
|
|
124
|
+
for invoice in client.invoices.list().auto_paging_iter():
|
|
125
|
+
...
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Errors
|
|
129
|
+
|
|
130
|
+
Ten concrete subclasses; discriminate with `isinstance`. `ManzaValidationError`
|
|
131
|
+
covers 400 and 422, `ManzaConflictError` covers 409 and exposes `payment_id`:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from manza import (
|
|
135
|
+
ManzaValidationError,
|
|
136
|
+
ManzaRateLimitError,
|
|
137
|
+
ManzaNotFoundError,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
client.invoices.get("nope")
|
|
142
|
+
except ManzaNotFoundError as err:
|
|
143
|
+
print(err.status, err.request_id, err.body)
|
|
144
|
+
except ManzaRateLimitError as err:
|
|
145
|
+
print("retry after", err.retry_after, "seconds")
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Cross-SDK contract
|
|
149
|
+
|
|
150
|
+
`manza` is one of several Manza SDKs that all replay cassettes recorded by
|
|
151
|
+
the canonical [`manza-ruby`](https://github.com/getmanza/manza-ruby) SDK against
|
|
152
|
+
staging (`ma.manza.dev`). The wire format is snake_case JSON; request and response shapes match
|
|
153
|
+
across Ruby, TypeScript, Python, Go, and Rust.
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "manza"
|
|
7
|
+
description = "Python SDK for the Manza API."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
license-files = ["LICENSE"]
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "Manza", email = "hello@get-manza.com" }]
|
|
13
|
+
keywords = ["manza", "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/getmanza/manza-python"
|
|
40
|
+
Documentation = "https://github.com/getmanza/manza-python#readme"
|
|
41
|
+
Repository = "https://github.com/getmanza/manza-python"
|
|
42
|
+
Issues = "https://github.com/getmanza/manza-python/issues"
|
|
43
|
+
Changelog = "https://github.com/getmanza/manza-python/blob/main/CHANGELOG.md"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.version]
|
|
46
|
+
path = "src/manza/_version.py"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/manza"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
include = ["src/manza", "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/manza"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Manza Python SDK. Mirrors lib/manza.rb."""
|
|
2
|
+
|
|
3
|
+
from . import transfer_authorization
|
|
4
|
+
from ._version import __version__
|
|
5
|
+
from .client import Manza
|
|
6
|
+
from .errors import (
|
|
7
|
+
ManzaArgumentError,
|
|
8
|
+
ManzaAuthenticationError,
|
|
9
|
+
ManzaConfigurationError,
|
|
10
|
+
ManzaConflictError,
|
|
11
|
+
ManzaConnectionError,
|
|
12
|
+
ManzaError,
|
|
13
|
+
ManzaForbiddenError,
|
|
14
|
+
ManzaNotFoundError,
|
|
15
|
+
ManzaRateLimitError,
|
|
16
|
+
ManzaServerError,
|
|
17
|
+
ManzaValidationError,
|
|
18
|
+
)
|
|
19
|
+
from .page import MAX_PER_PAGE, Page
|
|
20
|
+
from .resources.accounts import Accounts
|
|
21
|
+
from .resources.beneficiaries import Beneficiaries
|
|
22
|
+
from .resources.checkout_sessions import CheckoutSessions
|
|
23
|
+
from .resources.customers import Customers
|
|
24
|
+
from .resources.entity import Entity
|
|
25
|
+
from .resources.invoices import Invoices
|
|
26
|
+
from .resources.payee_trust_requests import PayeeTrustRequests
|
|
27
|
+
from .resources.payment_links import PaymentLinks
|
|
28
|
+
from .resources.transfer_drafts import TransferDrafts
|
|
29
|
+
from .resources.webhook_endpoints import WebhookEndpoints
|
|
30
|
+
from .response import ManzaResponse
|
|
31
|
+
|
|
32
|
+
VERSION = __version__
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"MAX_PER_PAGE",
|
|
36
|
+
"VERSION",
|
|
37
|
+
"Accounts",
|
|
38
|
+
"Beneficiaries",
|
|
39
|
+
"CheckoutSessions",
|
|
40
|
+
"Customers",
|
|
41
|
+
"Entity",
|
|
42
|
+
"Invoices",
|
|
43
|
+
"Manza",
|
|
44
|
+
"ManzaArgumentError",
|
|
45
|
+
"ManzaAuthenticationError",
|
|
46
|
+
"ManzaConfigurationError",
|
|
47
|
+
"ManzaConflictError",
|
|
48
|
+
"ManzaConnectionError",
|
|
49
|
+
"ManzaError",
|
|
50
|
+
"ManzaForbiddenError",
|
|
51
|
+
"ManzaNotFoundError",
|
|
52
|
+
"ManzaRateLimitError",
|
|
53
|
+
"ManzaResponse",
|
|
54
|
+
"ManzaServerError",
|
|
55
|
+
"ManzaValidationError",
|
|
56
|
+
"Page",
|
|
57
|
+
"PayeeTrustRequests",
|
|
58
|
+
"PaymentLinks",
|
|
59
|
+
"TransferDrafts",
|
|
60
|
+
"WebhookEndpoints",
|
|
61
|
+
"__version__",
|
|
62
|
+
"transfer_authorization",
|
|
63
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Environment lookup: `MANZA_<NAME>` first, then the legacy `ZAZU_<NAME>`.
|
|
2
|
+
|
|
3
|
+
The legacy name still works for all of 1.x and warns once per variable."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import threading
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
_warned: set[str] = set()
|
|
12
|
+
_lock = threading.Lock()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get(name: str) -> str | None:
|
|
16
|
+
"""Return `MANZA_<name>`, else `ZAZU_<name>` (warning once), else None."""
|
|
17
|
+
value = os.getenv(f"MANZA_{name}")
|
|
18
|
+
if value is not None:
|
|
19
|
+
return value
|
|
20
|
+
legacy = f"ZAZU_{name}"
|
|
21
|
+
value = os.getenv(legacy)
|
|
22
|
+
if value:
|
|
23
|
+
with _lock:
|
|
24
|
+
first = legacy not in _warned
|
|
25
|
+
_warned.add(legacy)
|
|
26
|
+
if first:
|
|
27
|
+
warnings.warn(
|
|
28
|
+
f"{legacy} is deprecated; set MANZA_{name} instead. "
|
|
29
|
+
"The ZAZU_* fallback will be removed in 2.0.",
|
|
30
|
+
FutureWarning,
|
|
31
|
+
stacklevel=3,
|
|
32
|
+
)
|
|
33
|
+
return value
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def reset_warnings() -> None:
|
|
38
|
+
"""Forget which legacy variables already warned (for tests)."""
|
|
39
|
+
_warned.clear()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|