nomba-python 0.2.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.
Files changed (33) hide show
  1. nomba_python-0.2.0/LICENSE +21 -0
  2. nomba_python-0.2.0/PKG-INFO +318 -0
  3. nomba_python-0.2.0/README.md +296 -0
  4. nomba_python-0.2.0/pyproject.toml +36 -0
  5. nomba_python-0.2.0/src/nomba/__init__.py +40 -0
  6. nomba_python-0.2.0/src/nomba/client.py +178 -0
  7. nomba_python-0.2.0/src/nomba/concurrency.py +54 -0
  8. nomba_python-0.2.0/src/nomba/data/__init__.py +0 -0
  9. nomba_python-0.2.0/src/nomba/data/nomba_openapi.json +18848 -0
  10. nomba_python-0.2.0/src/nomba/exceptions.py +49 -0
  11. nomba_python-0.2.0/src/nomba/flows/__init__.py +3 -0
  12. nomba_python-0.2.0/src/nomba/flows/card_payment.py +204 -0
  13. nomba_python-0.2.0/src/nomba/http.py +418 -0
  14. nomba_python-0.2.0/src/nomba/models.py +1034 -0
  15. nomba_python-0.2.0/src/nomba/pagination.py +111 -0
  16. nomba_python-0.2.0/src/nomba/py.typed +0 -0
  17. nomba_python-0.2.0/src/nomba/resources/__init__.py +45 -0
  18. nomba_python-0.2.0/src/nomba/resources/accounts.py +553 -0
  19. nomba_python-0.2.0/src/nomba/resources/airtime_data.py +261 -0
  20. nomba_python-0.2.0/src/nomba/resources/betting.py +205 -0
  21. nomba_python-0.2.0/src/nomba/resources/cabletv.py +173 -0
  22. nomba_python-0.2.0/src/nomba/resources/charge.py +411 -0
  23. nomba_python-0.2.0/src/nomba/resources/checkout.py +327 -0
  24. nomba_python-0.2.0/src/nomba/resources/direct_debits.py +343 -0
  25. nomba_python-0.2.0/src/nomba/resources/electricity.py +205 -0
  26. nomba_python-0.2.0/src/nomba/resources/global_collections.py +133 -0
  27. nomba_python-0.2.0/src/nomba/resources/global_payout.py +381 -0
  28. nomba_python-0.2.0/src/nomba/resources/terminals.py +227 -0
  29. nomba_python-0.2.0/src/nomba/resources/transactions.py +461 -0
  30. nomba_python-0.2.0/src/nomba/resources/transfers.py +321 -0
  31. nomba_python-0.2.0/src/nomba/resources/virtual_accounts.py +317 -0
  32. nomba_python-0.2.0/src/nomba/validation.py +97 -0
  33. nomba_python-0.2.0/src/nomba/webhooks.py +190 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Righteousness
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,318 @@
1
+ Metadata-Version: 2.4
2
+ Name: nomba-python
3
+ Version: 0.2.0
4
+ Summary: Unofficial Python SDK for the Nomba payments API
5
+ Keywords: nomba,payments,nigeria,fintech,sdk
6
+ Author: Righteousness
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
+ Classifier: Typing :: Typed
14
+ Requires-Dist: httpx>=0.28.1
15
+ Requires-Dist: pytest>=8.4.2
16
+ Requires-Dist: twine>=6.2.0
17
+ Requires-Python: >=3.9
18
+ Project-URL: Homepage, https://github.com/RightFix/nomba
19
+ Project-URL: Documentation, https://rightfix.github.io/nomba-docs/
20
+ Project-URL: Repository, https://github.com/RightFix/nomba
21
+ Description-Content-Type: text/markdown
22
+
23
+ # nomba
24
+
25
+ Unofficial Python SDK for the [Nomba](https://developer.nomba.com) payments API, built with [`uv`](https://docs.astral.sh/uv/) and [`httpx`](https://www.python-httpx.org/).
26
+
27
+ Covers **every endpoint** in Nomba's official [OpenAPI spec](https://developer.nomba.com/nomba-api-reference/openapi.json) — 86 methods across 14 resource groups, generated directly from the spec so field names and required/optional parameters match Nomba's docs exactly.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ uv add nomba-python
33
+ # or
34
+ pip install nomba-python
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```python
40
+ from nomba import Nomba
41
+
42
+ nomba = Nomba(
43
+ client_id="...",
44
+ client_secret="...",
45
+ account_id="...",
46
+ sandbox=True, # set False for live
47
+ )
48
+
49
+ account = nomba.virtual_accounts.create_virtual_account(
50
+ account_ref="ref-123",
51
+ account_name="Jane Doe",
52
+ )
53
+
54
+ nomba.close()
55
+ ```
56
+
57
+ Or as a context manager:
58
+
59
+ ```python
60
+ with Nomba(client_id=..., client_secret=..., account_id=...) as nomba:
61
+ nomba.virtual_accounts.fetch_a_virtual_account("ref-123")
62
+ ```
63
+
64
+ ## Async
65
+
66
+ Every resource is also available via `AsyncNomba`, built on `httpx.AsyncClient`:
67
+
68
+ ```python
69
+ import asyncio
70
+ from nomba import AsyncNomba
71
+
72
+ async def main():
73
+ async with AsyncNomba(
74
+ client_id="...",
75
+ client_secret="...",
76
+ account_id="...",
77
+ sandbox=True,
78
+ ) as nomba:
79
+ account = await nomba.virtual_accounts.create_virtual_account(
80
+ account_ref="ref-123",
81
+ account_name="Jane Doe",
82
+ )
83
+ transfer = await nomba.transfers.perform_bank_account_transfer_the_parent_account(
84
+ amount="5000.00",
85
+ account_number="0123456789",
86
+ account_name="John Doe",
87
+ bank_code="058",
88
+ merchant_tx_ref="txn-001",
89
+ sender_name="Jane Sender",
90
+ )
91
+
92
+ asyncio.run(main())
93
+ ```
94
+
95
+ ## Resource groups
96
+
97
+ Each group is available on both `Nomba` and `AsyncNomba` (async methods are awaited):
98
+
99
+ | Group | Examples |
100
+ |---------------------|----------|
101
+ | `accounts` | `list_all_accounts`, `create_a_sub_account`, `fetch_account_balance`, `suspend_an_account` |
102
+ | `virtual_accounts` | `create_virtual_account`, `fetch_a_virtual_account`, `update_a_virtual_account`, `expire_a_virtual_account` |
103
+ | `checkout` | `create_an_online_checkout_order`, `charge_customer_with_tokenized_card_data`, `list_all_tokenized_cards_for_merchant` |
104
+ | `charge` | `submit_customer_card_details`, `submit_customer_payment_otp`, `fetch_checkout_order_details` |
105
+ | `transfers` | `perform_bank_account_lookup`, `perform_bank_account_transfer_the_parent_account`, `perform_wallet_transfer_from_a_sub_account` |
106
+ | `terminals` | `assign_a_terminal_to_the_parent_account`, `un_assign_terminal_from_an_account` |
107
+ | `transactions` | `fetch_transactions_on_the_parent_account`, `filter_account_transactions`, `confirm_a_transaction_s_status_by_session_id` |
108
+ | `airtime_data` | `make_airtime_purchases_via_parent_account`, `vend_data_bundles_via_parent_account`, `fetch_data_plans_available_on_a_telco_network_provider` |
109
+ | `cabletv` | `cabletv_lookup`, `cable_tv_subscription_via_parent_account` |
110
+ | `electricity` | `fetch_electricity_providers`, `electricity_customer_lookup`, `vend_electricity_via_parent_account` |
111
+ | `betting` | `fetch_betting_providers`, `name_lookup_for_betting`, `pay_for_betting_via_parent_account` |
112
+ | `direct_debits` | `create_direct_debit_mandate`, `debit_a_mandate`, `check_direct_debit_status`, `list_direct_debit_mandates` |
113
+ | `global_collections` | `fetch_drc_inflow_providers`, `initiate_mobile_money_inflow`, `fetch_mobile_money_transaction` |
114
+ | `global_payout` | `fetch_exchange_rates`, `convert_money`, `authorize_transfer`, `authorize_exchange`, `fetch_transaction` |
115
+
116
+ Every method's docstring lists its required and optional body fields straight from Nomba's schema — check `help(nomba.transfers.perform_bank_account_transfer_the_parent_account)` or your editor's signature hints.
117
+
118
+ ## Reliability (token locking + retry/backoff)
119
+
120
+ `NombaClient`/`AsyncNombaClient` guard token fetching with a lock, so
121
+ concurrent requests never race to re-fetch a token — only one fetch happens,
122
+ the rest wait and reuse it. Requests that hit a `429` or transient `5xx` are
123
+ automatically retried with exponential backoff (respecting `Retry-After` if
124
+ Nomba sends one):
125
+
126
+ ```python
127
+ nomba = Nomba(
128
+ client_id="...", client_secret="...", account_id="...",
129
+ max_retries=3, # default
130
+ backoff_factor=0.5, # default; delay ~= backoff_factor * 2^attempt
131
+ )
132
+ ```
133
+
134
+ ## Typed responses
135
+
136
+ Every method's return type is a `TypedDict` generated from Nomba's response
137
+ schema (in `nomba.models`), giving editor autocomplete and type-checking on
138
+ the actual JSON keys (e.g. `resp["data"]["accountNumber"]`). This costs
139
+ nothing at runtime — methods still return plain dicts; the TypedDict is
140
+ purely a type hint, which matters on constrained hardware.
141
+
142
+ ## Nested body validation
143
+
144
+ Flat method signatures (e.g. `order_reference=`, `amount=`) are validated by
145
+ Python itself. But some fields are nested objects — like `order={...}` in
146
+ checkout order creation — whose *inner* required fields a flat signature
147
+ can't enforce. Every write call is checked against Nomba's own OpenAPI spec
148
+ (bundled with the package) before any network call, so a typo or missing
149
+ nested field fails fast locally instead of round-tripping to Nomba's API:
150
+
151
+ ```python
152
+ from nomba import NombaValidationError
153
+
154
+ try:
155
+ nomba.checkout.create_an_online_checkout_order(order={"orderReference": "x"})
156
+ except NombaValidationError as e:
157
+ print(e) # Missing required field(s) in request body: order.amount, order.currency, ...
158
+ ```
159
+
160
+ ## Webhook signature verification
161
+
162
+ Implements Nomba's documented HMAC-SHA256 scheme
163
+ (`nomba-signature` / `nomba-timestamp` headers, base64-encoded signature),
164
+ including replay protection via the timestamp:
165
+
166
+ ```python
167
+ from nomba import verify_webhook_request, NombaValidationError
168
+
169
+ @app.post("/webhooks/nomba")
170
+ def handle_webhook():
171
+ try:
172
+ payload = verify_webhook_request(
173
+ signature_key="...", # the key you set up on the Nomba dashboard
174
+ body=request.get_data(),
175
+ headers=request.headers,
176
+ max_age_seconds=300, # default; reject webhooks older than 5 min
177
+ )
178
+ except NombaValidationError:
179
+ return "invalid signature", 401
180
+
181
+ if payload["event_type"] == "payment_success":
182
+ ...
183
+ return "", 200
184
+ ```
185
+
186
+ A valid signature alone doesn't prove a webhook wasn't captured and resent
187
+ later — `max_age_seconds` rejects stale ones. Pass `max_age_seconds=None` to
188
+ disable that check if you have your own replay protection.
189
+
190
+ ## Bounded concurrency for fan-out calls
191
+
192
+ `asyncio.gather` has no built-in limit — firing off many calls at once can
193
+ trigger a retry storm if several start failing together, or just trip
194
+ Nomba's rate limit by bursting too many requests in one window.
195
+ `gather_limited` runs the same calls with a concurrency cap:
196
+
197
+ ```python
198
+ from nomba import AsyncNomba, gather_limited
199
+
200
+ async with AsyncNomba(...) as nomba:
201
+ calls = [
202
+ (lambda ref=ref: nomba.virtual_accounts.fetch_a_virtual_account(ref))
203
+ for ref in account_refs
204
+ ]
205
+ results = await gather_limited(calls, limit=5)
206
+ ```
207
+
208
+ Extra/undocumented fields can always be passed as kwargs; they get merged into the JSON body verbatim.
209
+
210
+ ## Pagination
211
+
212
+ Nomba's list endpoints are cursor-paginated server-side (they return a
213
+ `cursor` you feed back in for the next page). `paginate`/`apaginate` just
214
+ drive that loop for you — no extra pagination scheme is introduced.
215
+
216
+ ```python
217
+ from nomba import Nomba, paginate
218
+
219
+ nomba = Nomba(...)
220
+ for account in paginate(nomba.accounts.list_all_accounts, limit=50):
221
+ print(account["accountRef"])
222
+ ```
223
+
224
+ Async:
225
+
226
+ ```python
227
+ from nomba import AsyncNomba, apaginate
228
+
229
+ async with AsyncNomba(...) as nomba:
230
+ async for txn in apaginate(nomba.transactions.fetch_transactions_on_the_parent_account, limit=50):
231
+ print(txn)
232
+ ```
233
+
234
+ Confirmed paginated endpoints (per Nomba's spec): `accounts.list_all_accounts`,
235
+ `accounts.fetch_terminals_assigned_to_an_account`,
236
+ `accounts.fetch_terminals_assigned_to_the_parent_account`,
237
+ `virtual_accounts.filter_virtual_accounts`, and all six
238
+ `transactions.*` list/filter methods.
239
+
240
+ ## Card payment flow
241
+
242
+ Card checkout is a multi-step sequence (submit card → maybe OTP → maybe 3D
243
+ Secure → confirm). `CardPaymentFlow` wraps it so you don't have to track
244
+ `transactionId` by hand or look up what Nomba's `responseCode` values mean:
245
+
246
+ ```python
247
+ order = nomba.checkout.create_an_online_checkout_order(
248
+ order={"orderReference": "order-001", "amount": "1000", ...}
249
+ )
250
+
251
+ flow = nomba.card_payment(order_reference="order-001")
252
+ step = flow.submit_card(card_details="...", key="")
253
+
254
+ if step.requires_otp:
255
+ step = flow.submit_otp("123456")
256
+ elif step.requires_3ds:
257
+ # redirect the user using step.secure_authentication_data
258
+ ...
259
+
260
+ if step.completed:
261
+ result = flow.confirm()
262
+ ```
263
+
264
+ `nomba.card_payment(...)` returns a `CardPaymentFlow` (or `AsyncCardPaymentFlow`
265
+ off `AsyncNomba`). `step` is a `CardPaymentStep` with `.completed`,
266
+ `.requires_otp`, `.requires_3ds`, `.transaction_id`, `.message`, etc.
267
+
268
+ ## Error handling
269
+
270
+ ```python
271
+ from nomba import NombaAPIError, NombaAuthError
272
+
273
+ try:
274
+ nomba.virtual_accounts.fetch_a_virtual_account("missing-ref")
275
+ except NombaAuthError as e:
276
+ print("auth failed:", e)
277
+ except NombaAPIError as e:
278
+ print("api error:", e.status_code, e.code, e.response_body)
279
+ ```
280
+
281
+ ## Authentication
282
+
283
+ The SDK handles the OAuth2 client-credentials flow automatically: it fetches an
284
+ access token on first request, caches it, and refreshes it transparently when
285
+ it expires or is rejected (a 401 triggers exactly one retry with a fresh token).
286
+ Get your `client_id` / `client_secret` / `accountId` from your Nomba dashboard
287
+ under **Webhook & API Keys**.
288
+
289
+ ## Regenerating from the spec
290
+
291
+ The resource modules in `src/nomba/resources/` and response models in
292
+ `src/nomba/models.py` are generated from `src/nomba/data/nomba_openapi.json`
293
+ (a snapshot of Nomba's published OpenAPI spec, also bundled into the package
294
+ for runtime nested-body validation). To pick up upstream API changes:
295
+
296
+ ```bash
297
+ curl -sL -o src/nomba/data/nomba_openapi.json \
298
+ "https://developer.nomba.com/nomba-api-reference/openapi.json"
299
+ uv run python scripts/generate_resources.py
300
+ ```
301
+
302
+ ## Development
303
+
304
+ ```bash
305
+ uv sync
306
+ uv run python -m pytest # once tests are added
307
+ ```
308
+
309
+ ## Status
310
+
311
+ Generated from Nomba's official OpenAPI spec (v1.0.0) — all 86 documented
312
+ endpoints across Accounts, Virtual Accounts, Online Checkout, Charge,
313
+ Transfers, Terminals, Transactions, Airtime/Data, CableTV, Electricity,
314
+ Betting, Direct Debits, Global Collections, and Global Payout.
315
+
316
+ Also includes: typed responses, cursor pagination helpers, a guided
317
+ card-payment flow, locked/retrying HTTP clients, local nested-body
318
+ validation against Nomba's own spec, and webhook signature verification.
@@ -0,0 +1,296 @@
1
+ # nomba
2
+
3
+ Unofficial Python SDK for the [Nomba](https://developer.nomba.com) payments API, built with [`uv`](https://docs.astral.sh/uv/) and [`httpx`](https://www.python-httpx.org/).
4
+
5
+ Covers **every endpoint** in Nomba's official [OpenAPI spec](https://developer.nomba.com/nomba-api-reference/openapi.json) — 86 methods across 14 resource groups, generated directly from the spec so field names and required/optional parameters match Nomba's docs exactly.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ uv add nomba-python
11
+ # or
12
+ pip install nomba-python
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ from nomba import Nomba
19
+
20
+ nomba = Nomba(
21
+ client_id="...",
22
+ client_secret="...",
23
+ account_id="...",
24
+ sandbox=True, # set False for live
25
+ )
26
+
27
+ account = nomba.virtual_accounts.create_virtual_account(
28
+ account_ref="ref-123",
29
+ account_name="Jane Doe",
30
+ )
31
+
32
+ nomba.close()
33
+ ```
34
+
35
+ Or as a context manager:
36
+
37
+ ```python
38
+ with Nomba(client_id=..., client_secret=..., account_id=...) as nomba:
39
+ nomba.virtual_accounts.fetch_a_virtual_account("ref-123")
40
+ ```
41
+
42
+ ## Async
43
+
44
+ Every resource is also available via `AsyncNomba`, built on `httpx.AsyncClient`:
45
+
46
+ ```python
47
+ import asyncio
48
+ from nomba import AsyncNomba
49
+
50
+ async def main():
51
+ async with AsyncNomba(
52
+ client_id="...",
53
+ client_secret="...",
54
+ account_id="...",
55
+ sandbox=True,
56
+ ) as nomba:
57
+ account = await nomba.virtual_accounts.create_virtual_account(
58
+ account_ref="ref-123",
59
+ account_name="Jane Doe",
60
+ )
61
+ transfer = await nomba.transfers.perform_bank_account_transfer_the_parent_account(
62
+ amount="5000.00",
63
+ account_number="0123456789",
64
+ account_name="John Doe",
65
+ bank_code="058",
66
+ merchant_tx_ref="txn-001",
67
+ sender_name="Jane Sender",
68
+ )
69
+
70
+ asyncio.run(main())
71
+ ```
72
+
73
+ ## Resource groups
74
+
75
+ Each group is available on both `Nomba` and `AsyncNomba` (async methods are awaited):
76
+
77
+ | Group | Examples |
78
+ |---------------------|----------|
79
+ | `accounts` | `list_all_accounts`, `create_a_sub_account`, `fetch_account_balance`, `suspend_an_account` |
80
+ | `virtual_accounts` | `create_virtual_account`, `fetch_a_virtual_account`, `update_a_virtual_account`, `expire_a_virtual_account` |
81
+ | `checkout` | `create_an_online_checkout_order`, `charge_customer_with_tokenized_card_data`, `list_all_tokenized_cards_for_merchant` |
82
+ | `charge` | `submit_customer_card_details`, `submit_customer_payment_otp`, `fetch_checkout_order_details` |
83
+ | `transfers` | `perform_bank_account_lookup`, `perform_bank_account_transfer_the_parent_account`, `perform_wallet_transfer_from_a_sub_account` |
84
+ | `terminals` | `assign_a_terminal_to_the_parent_account`, `un_assign_terminal_from_an_account` |
85
+ | `transactions` | `fetch_transactions_on_the_parent_account`, `filter_account_transactions`, `confirm_a_transaction_s_status_by_session_id` |
86
+ | `airtime_data` | `make_airtime_purchases_via_parent_account`, `vend_data_bundles_via_parent_account`, `fetch_data_plans_available_on_a_telco_network_provider` |
87
+ | `cabletv` | `cabletv_lookup`, `cable_tv_subscription_via_parent_account` |
88
+ | `electricity` | `fetch_electricity_providers`, `electricity_customer_lookup`, `vend_electricity_via_parent_account` |
89
+ | `betting` | `fetch_betting_providers`, `name_lookup_for_betting`, `pay_for_betting_via_parent_account` |
90
+ | `direct_debits` | `create_direct_debit_mandate`, `debit_a_mandate`, `check_direct_debit_status`, `list_direct_debit_mandates` |
91
+ | `global_collections` | `fetch_drc_inflow_providers`, `initiate_mobile_money_inflow`, `fetch_mobile_money_transaction` |
92
+ | `global_payout` | `fetch_exchange_rates`, `convert_money`, `authorize_transfer`, `authorize_exchange`, `fetch_transaction` |
93
+
94
+ Every method's docstring lists its required and optional body fields straight from Nomba's schema — check `help(nomba.transfers.perform_bank_account_transfer_the_parent_account)` or your editor's signature hints.
95
+
96
+ ## Reliability (token locking + retry/backoff)
97
+
98
+ `NombaClient`/`AsyncNombaClient` guard token fetching with a lock, so
99
+ concurrent requests never race to re-fetch a token — only one fetch happens,
100
+ the rest wait and reuse it. Requests that hit a `429` or transient `5xx` are
101
+ automatically retried with exponential backoff (respecting `Retry-After` if
102
+ Nomba sends one):
103
+
104
+ ```python
105
+ nomba = Nomba(
106
+ client_id="...", client_secret="...", account_id="...",
107
+ max_retries=3, # default
108
+ backoff_factor=0.5, # default; delay ~= backoff_factor * 2^attempt
109
+ )
110
+ ```
111
+
112
+ ## Typed responses
113
+
114
+ Every method's return type is a `TypedDict` generated from Nomba's response
115
+ schema (in `nomba.models`), giving editor autocomplete and type-checking on
116
+ the actual JSON keys (e.g. `resp["data"]["accountNumber"]`). This costs
117
+ nothing at runtime — methods still return plain dicts; the TypedDict is
118
+ purely a type hint, which matters on constrained hardware.
119
+
120
+ ## Nested body validation
121
+
122
+ Flat method signatures (e.g. `order_reference=`, `amount=`) are validated by
123
+ Python itself. But some fields are nested objects — like `order={...}` in
124
+ checkout order creation — whose *inner* required fields a flat signature
125
+ can't enforce. Every write call is checked against Nomba's own OpenAPI spec
126
+ (bundled with the package) before any network call, so a typo or missing
127
+ nested field fails fast locally instead of round-tripping to Nomba's API:
128
+
129
+ ```python
130
+ from nomba import NombaValidationError
131
+
132
+ try:
133
+ nomba.checkout.create_an_online_checkout_order(order={"orderReference": "x"})
134
+ except NombaValidationError as e:
135
+ print(e) # Missing required field(s) in request body: order.amount, order.currency, ...
136
+ ```
137
+
138
+ ## Webhook signature verification
139
+
140
+ Implements Nomba's documented HMAC-SHA256 scheme
141
+ (`nomba-signature` / `nomba-timestamp` headers, base64-encoded signature),
142
+ including replay protection via the timestamp:
143
+
144
+ ```python
145
+ from nomba import verify_webhook_request, NombaValidationError
146
+
147
+ @app.post("/webhooks/nomba")
148
+ def handle_webhook():
149
+ try:
150
+ payload = verify_webhook_request(
151
+ signature_key="...", # the key you set up on the Nomba dashboard
152
+ body=request.get_data(),
153
+ headers=request.headers,
154
+ max_age_seconds=300, # default; reject webhooks older than 5 min
155
+ )
156
+ except NombaValidationError:
157
+ return "invalid signature", 401
158
+
159
+ if payload["event_type"] == "payment_success":
160
+ ...
161
+ return "", 200
162
+ ```
163
+
164
+ A valid signature alone doesn't prove a webhook wasn't captured and resent
165
+ later — `max_age_seconds` rejects stale ones. Pass `max_age_seconds=None` to
166
+ disable that check if you have your own replay protection.
167
+
168
+ ## Bounded concurrency for fan-out calls
169
+
170
+ `asyncio.gather` has no built-in limit — firing off many calls at once can
171
+ trigger a retry storm if several start failing together, or just trip
172
+ Nomba's rate limit by bursting too many requests in one window.
173
+ `gather_limited` runs the same calls with a concurrency cap:
174
+
175
+ ```python
176
+ from nomba import AsyncNomba, gather_limited
177
+
178
+ async with AsyncNomba(...) as nomba:
179
+ calls = [
180
+ (lambda ref=ref: nomba.virtual_accounts.fetch_a_virtual_account(ref))
181
+ for ref in account_refs
182
+ ]
183
+ results = await gather_limited(calls, limit=5)
184
+ ```
185
+
186
+ Extra/undocumented fields can always be passed as kwargs; they get merged into the JSON body verbatim.
187
+
188
+ ## Pagination
189
+
190
+ Nomba's list endpoints are cursor-paginated server-side (they return a
191
+ `cursor` you feed back in for the next page). `paginate`/`apaginate` just
192
+ drive that loop for you — no extra pagination scheme is introduced.
193
+
194
+ ```python
195
+ from nomba import Nomba, paginate
196
+
197
+ nomba = Nomba(...)
198
+ for account in paginate(nomba.accounts.list_all_accounts, limit=50):
199
+ print(account["accountRef"])
200
+ ```
201
+
202
+ Async:
203
+
204
+ ```python
205
+ from nomba import AsyncNomba, apaginate
206
+
207
+ async with AsyncNomba(...) as nomba:
208
+ async for txn in apaginate(nomba.transactions.fetch_transactions_on_the_parent_account, limit=50):
209
+ print(txn)
210
+ ```
211
+
212
+ Confirmed paginated endpoints (per Nomba's spec): `accounts.list_all_accounts`,
213
+ `accounts.fetch_terminals_assigned_to_an_account`,
214
+ `accounts.fetch_terminals_assigned_to_the_parent_account`,
215
+ `virtual_accounts.filter_virtual_accounts`, and all six
216
+ `transactions.*` list/filter methods.
217
+
218
+ ## Card payment flow
219
+
220
+ Card checkout is a multi-step sequence (submit card → maybe OTP → maybe 3D
221
+ Secure → confirm). `CardPaymentFlow` wraps it so you don't have to track
222
+ `transactionId` by hand or look up what Nomba's `responseCode` values mean:
223
+
224
+ ```python
225
+ order = nomba.checkout.create_an_online_checkout_order(
226
+ order={"orderReference": "order-001", "amount": "1000", ...}
227
+ )
228
+
229
+ flow = nomba.card_payment(order_reference="order-001")
230
+ step = flow.submit_card(card_details="...", key="")
231
+
232
+ if step.requires_otp:
233
+ step = flow.submit_otp("123456")
234
+ elif step.requires_3ds:
235
+ # redirect the user using step.secure_authentication_data
236
+ ...
237
+
238
+ if step.completed:
239
+ result = flow.confirm()
240
+ ```
241
+
242
+ `nomba.card_payment(...)` returns a `CardPaymentFlow` (or `AsyncCardPaymentFlow`
243
+ off `AsyncNomba`). `step` is a `CardPaymentStep` with `.completed`,
244
+ `.requires_otp`, `.requires_3ds`, `.transaction_id`, `.message`, etc.
245
+
246
+ ## Error handling
247
+
248
+ ```python
249
+ from nomba import NombaAPIError, NombaAuthError
250
+
251
+ try:
252
+ nomba.virtual_accounts.fetch_a_virtual_account("missing-ref")
253
+ except NombaAuthError as e:
254
+ print("auth failed:", e)
255
+ except NombaAPIError as e:
256
+ print("api error:", e.status_code, e.code, e.response_body)
257
+ ```
258
+
259
+ ## Authentication
260
+
261
+ The SDK handles the OAuth2 client-credentials flow automatically: it fetches an
262
+ access token on first request, caches it, and refreshes it transparently when
263
+ it expires or is rejected (a 401 triggers exactly one retry with a fresh token).
264
+ Get your `client_id` / `client_secret` / `accountId` from your Nomba dashboard
265
+ under **Webhook & API Keys**.
266
+
267
+ ## Regenerating from the spec
268
+
269
+ The resource modules in `src/nomba/resources/` and response models in
270
+ `src/nomba/models.py` are generated from `src/nomba/data/nomba_openapi.json`
271
+ (a snapshot of Nomba's published OpenAPI spec, also bundled into the package
272
+ for runtime nested-body validation). To pick up upstream API changes:
273
+
274
+ ```bash
275
+ curl -sL -o src/nomba/data/nomba_openapi.json \
276
+ "https://developer.nomba.com/nomba-api-reference/openapi.json"
277
+ uv run python scripts/generate_resources.py
278
+ ```
279
+
280
+ ## Development
281
+
282
+ ```bash
283
+ uv sync
284
+ uv run python -m pytest # once tests are added
285
+ ```
286
+
287
+ ## Status
288
+
289
+ Generated from Nomba's official OpenAPI spec (v1.0.0) — all 86 documented
290
+ endpoints across Accounts, Virtual Accounts, Online Checkout, Charge,
291
+ Transfers, Terminals, Transactions, Airtime/Data, CableTV, Electricity,
292
+ Betting, Direct Debits, Global Collections, and Global Payout.
293
+
294
+ Also includes: typed responses, cursor pagination helpers, a guided
295
+ card-payment flow, locked/retrying HTTP clients, local nested-body
296
+ validation against Nomba's own spec, and webhook signature verification.
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "nomba-python"
3
+ version = "0.2.0"
4
+ description = "Unofficial Python SDK for the Nomba payments API"
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [
10
+ { name = "Righteousness" },
11
+ ]
12
+ keywords = ["nomba", "payments", "nigeria", "fintech", "sdk"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Operating System :: OS Independent",
16
+ "Intended Audience :: Developers",
17
+ "Topic :: Software Development :: Libraries :: Python Modules",
18
+ "Typing :: Typed",
19
+ ]
20
+ dependencies = [
21
+ "httpx>=0.28.1",
22
+ "pytest>=8.4.2",
23
+ "twine>=6.2.0",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/RightFix/nomba"
28
+ Documentation = "https://rightfix.github.io/nomba-docs/"
29
+ Repository = "https://github.com/RightFix/nomba"
30
+
31
+ [tool.uv.build-backend]
32
+ module-name = "nomba"
33
+
34
+ [build-system]
35
+ requires = ["uv_build>=0.11.7,<0.12.0"]
36
+ build-backend = "uv_build"