nomba-python 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,190 @@
1
+ """
2
+ Webhook signature verification, per Nomba's documented scheme
3
+ (https://developer.nomba.com/products/webhooks/signature-verification-new
4
+ and .../webhooks/introduction):
5
+
6
+ Headers sent with every webhook POST:
7
+ nomba-signature base64-encoded HMAC
8
+ nomba-signature-algorithm always "HmacSHA256"
9
+ nomba-signature-version currently "1.0.0"
10
+ nomba-timestamp RFC-3339 timestamp the payload was sent at
11
+
12
+ Signing scheme (confirmed from Nomba's own sample code):
13
+ hashing_payload = ":".join([
14
+ event_type,
15
+ requestId,
16
+ data.merchant.userId,
17
+ data.merchant.walletId,
18
+ data.transaction.transactionId,
19
+ data.transaction.type,
20
+ data.transaction.time,
21
+ data.transaction.responseCode,
22
+ ])
23
+ message = f"{hashing_payload}:{timestamp}"
24
+ signature = base64(HMAC-SHA256(key=signature_key, msg=message))
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import base64
29
+ import hmac
30
+ import json
31
+ from datetime import datetime, timezone
32
+ from hashlib import sha256
33
+ from typing import Any
34
+
35
+ from .exceptions import NombaValidationError
36
+
37
+ SIGNATURE_HEADER = "nomba-signature"
38
+ ALGORITHM_HEADER = "nomba-signature-algorithm"
39
+ VERSION_HEADER = "nomba-signature-version"
40
+ TIMESTAMP_HEADER = "nomba-timestamp"
41
+
42
+
43
+ def _get_path(payload: dict[str, Any], *path: str, default: str = "") -> str:
44
+ node: Any = payload
45
+ for key in path:
46
+ if not isinstance(node, dict):
47
+ return default
48
+ node = node.get(key)
49
+ return "" if node is None else str(node)
50
+
51
+
52
+ def _parse_rfc3339(timestamp: str) -> datetime:
53
+ # Python's fromisoformat doesn't accept a trailing "Z" before 3.11;
54
+ # normalize it to an explicit UTC offset for broad compatibility.
55
+ ts = timestamp.strip()
56
+ if ts.endswith("Z"):
57
+ ts = ts[:-1] + "+00:00"
58
+ return datetime.fromisoformat(ts)
59
+
60
+
61
+ def check_timestamp_freshness(timestamp: str, *, max_age_seconds: float) -> None:
62
+ """
63
+ Raise NombaValidationError if `timestamp` (the nomba-timestamp header,
64
+ RFC-3339) is older than `max_age_seconds` compared to now, or is in the
65
+ future by more than the same window (clock skew allowance). This guards
66
+ against replay attacks: a captured, validly-signed webhook being resent
67
+ later. Nomba's signature alone does not protect against replay -- it
68
+ only proves the payload+timestamp pair was signed with your key.
69
+ """
70
+ try:
71
+ sent_at = _parse_rfc3339(timestamp)
72
+ except ValueError as exc:
73
+ raise NombaValidationError(f"Invalid nomba-timestamp format: {timestamp!r}") from exc
74
+
75
+ if sent_at.tzinfo is None:
76
+ sent_at = sent_at.replace(tzinfo=timezone.utc)
77
+
78
+ age = (datetime.now(timezone.utc) - sent_at).total_seconds()
79
+ if age > max_age_seconds:
80
+ raise NombaValidationError(
81
+ f"Webhook timestamp is {age:.0f}s old, exceeding max_age_seconds={max_age_seconds} "
82
+ "(possible replay attack, or a webhook retried long after the original send)"
83
+ )
84
+ if age < -max_age_seconds:
85
+ raise NombaValidationError(
86
+ f"Webhook timestamp is {-age:.0f}s in the future, exceeding max_age_seconds="
87
+ f"{max_age_seconds} (check for clock skew, or treat with suspicion)"
88
+ )
89
+
90
+
91
+ def compute_signature(signature_key: str, payload: dict[str, Any], timestamp: str) -> str:
92
+ """
93
+ Compute the base64 HMAC-SHA256 signature Nomba expects for a webhook
94
+ payload + timestamp, per their documented scheme.
95
+ """
96
+ hashing_payload = ":".join(
97
+ [
98
+ _get_path(payload, "event_type"),
99
+ _get_path(payload, "requestId"),
100
+ _get_path(payload, "data", "merchant", "userId"),
101
+ _get_path(payload, "data", "merchant", "walletId"),
102
+ _get_path(payload, "data", "transaction", "transactionId"),
103
+ _get_path(payload, "data", "transaction", "type"),
104
+ _get_path(payload, "data", "transaction", "time"),
105
+ _get_path(payload, "data", "transaction", "responseCode"),
106
+ ]
107
+ )
108
+ message = f"{hashing_payload}:{timestamp}"
109
+ mac = hmac.new(signature_key.encode("utf-8"), message.encode("utf-8"), sha256).digest()
110
+ return base64.b64encode(mac).decode("ascii")
111
+
112
+
113
+ def verify_webhook_signature(
114
+ signature_key: str,
115
+ payload: dict[str, Any],
116
+ *,
117
+ signature: str,
118
+ timestamp: str,
119
+ ) -> bool:
120
+ """
121
+ Verify a webhook's signature using a constant-time comparison.
122
+
123
+ Example (Flask):
124
+ from nomba.webhooks import verify_webhook_signature
125
+
126
+ @app.post("/webhooks/nomba")
127
+ def handle_webhook():
128
+ payload = request.get_json()
129
+ ok = verify_webhook_signature(
130
+ signature_key="...",
131
+ payload=payload,
132
+ signature=request.headers["nomba-signature"],
133
+ timestamp=request.headers["nomba-timestamp"],
134
+ )
135
+ if not ok:
136
+ return "invalid signature", 401
137
+ ...
138
+ """
139
+ expected = compute_signature(signature_key, payload, timestamp)
140
+ return hmac.compare_digest(expected, signature)
141
+
142
+
143
+ def verify_webhook_request(
144
+ signature_key: str,
145
+ *,
146
+ body: bytes | str,
147
+ headers: dict[str, str],
148
+ max_age_seconds: float | None = 300.0,
149
+ ) -> dict[str, Any]:
150
+ """
151
+ Convenience wrapper: parses the raw body as JSON, pulls the
152
+ nomba-signature/nomba-timestamp headers (case-insensitively), verifies
153
+ the signature, checks the timestamp isn't stale (replay protection),
154
+ and returns the parsed payload on success.
155
+
156
+ `max_age_seconds` (default 300s / 5 min) rejects webhooks whose
157
+ nomba-timestamp is older than this -- protects against a captured,
158
+ validly-signed webhook being replayed later. Pass None to disable this
159
+ check (signature is still verified).
160
+
161
+ Raises NombaValidationError if the signature is missing, invalid, the
162
+ timestamp is stale, or the body isn't valid JSON. Does not raise on a
163
+ valid-but-unsigned request -- if you didn't configure a signature key on
164
+ the Nomba dashboard, no signature header will be sent; check for that
165
+ yourself if relevant.
166
+ """
167
+ lower_headers = {k.lower(): v for k, v in headers.items()}
168
+ signature = lower_headers.get(SIGNATURE_HEADER)
169
+ timestamp = lower_headers.get(TIMESTAMP_HEADER)
170
+ if not signature or not timestamp:
171
+ raise NombaValidationError(
172
+ f"Missing '{SIGNATURE_HEADER}' or '{TIMESTAMP_HEADER}' header on webhook request"
173
+ )
174
+
175
+ if max_age_seconds is not None:
176
+ check_timestamp_freshness(timestamp, max_age_seconds=max_age_seconds)
177
+
178
+ if isinstance(body, bytes):
179
+ body = body.decode("utf-8")
180
+ try:
181
+ payload = json.loads(body)
182
+ except ValueError as exc:
183
+ raise NombaValidationError(f"Webhook body is not valid JSON: {exc}") from exc
184
+
185
+ if not verify_webhook_signature(
186
+ signature_key, payload, signature=signature, timestamp=timestamp
187
+ ):
188
+ raise NombaValidationError("Webhook signature verification failed")
189
+
190
+ return payload
@@ -0,0 +1,312 @@
1
+ Metadata-Version: 2.4
2
+ Name: nomba-python
3
+ Version: 0.1.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-Python: >=3.9
17
+ Project-URL: Homepage, https://github.com/RightFix/nomba
18
+ Project-URL: Documentation, https://rightfix.github.io/nomba-docs/
19
+ Project-URL: Repository, https://github.com/RightFix/nomba
20
+ Description-Content-Type: text/markdown
21
+
22
+ # nomba
23
+
24
+ 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/).
25
+
26
+ Covers **every endpoint** in Nomba's official [OpenAPI spec](https://github.com/kudi-inc/vendor-openapi-spec) — 64 methods across 10 resource groups, generated directly from the spec so field names and required/optional parameters match Nomba's docs exactly.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ uv add nomba-python
32
+ # or
33
+ pip install nomba-python
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```python
39
+ from nomba import Nomba
40
+
41
+ nomba = Nomba(
42
+ client_id="...",
43
+ client_secret="...",
44
+ account_id="...",
45
+ sandbox=True, # set False for live
46
+ )
47
+
48
+ account = nomba.virtual_accounts.create_virtual_account(
49
+ account_ref="ref-123",
50
+ account_name="Jane Doe",
51
+ )
52
+
53
+ nomba.close()
54
+ ```
55
+
56
+ Or as a context manager:
57
+
58
+ ```python
59
+ with Nomba(client_id=..., client_secret=..., account_id=...) as nomba:
60
+ nomba.virtual_accounts.fetch_a_virtual_account("ref-123")
61
+ ```
62
+
63
+ ## Async
64
+
65
+ Every resource is also available via `AsyncNomba`, built on `httpx.AsyncClient`:
66
+
67
+ ```python
68
+ import asyncio
69
+ from nomba import AsyncNomba
70
+
71
+ async def main():
72
+ async with AsyncNomba(
73
+ client_id="...",
74
+ client_secret="...",
75
+ account_id="...",
76
+ sandbox=True,
77
+ ) as nomba:
78
+ account = await nomba.virtual_accounts.create_virtual_account(
79
+ account_ref="ref-123",
80
+ account_name="Jane Doe",
81
+ )
82
+ transfer = await nomba.transfers.perform_bank_account_transfer_the_parent_account(
83
+ amount="5000.00",
84
+ account_number="0123456789",
85
+ account_name="John Doe",
86
+ bank_code="058",
87
+ merchant_tx_ref="txn-001",
88
+ sender_name="Jane Sender",
89
+ )
90
+
91
+ asyncio.run(main())
92
+ ```
93
+
94
+ ## Resource groups
95
+
96
+ Each group is available on both `Nomba` and `AsyncNomba` (async methods are awaited):
97
+
98
+ | Group | Examples |
99
+ |---------------------|----------|
100
+ | `accounts` | `list_all_accounts`, `create_a_sub_account`, `fetch_account_balance`, `suspend_an_account` |
101
+ | `virtual_accounts` | `create_virtual_account`, `fetch_a_virtual_account`, `update_a_virtual_account`, `expire_a_virtual_account` |
102
+ | `checkout` | `create_an_online_checkout_order`, `charge_customer_with_tokenized_card_data`, `list_all_tokenized_cards_for_merchant` |
103
+ | `charge` | `submit_customer_card_details`, `submit_customer_payment_otp`, `fetch_checkout_order_details` |
104
+ | `transfers` | `perform_bank_account_lookup`, `perform_bank_account_transfer_the_parent_account`, `perform_wallet_transfer_from_a_sub_account` |
105
+ | `terminals` | `assign_a_terminal_to_the_parent_account`, `un_assign_terminal_from_an_account` |
106
+ | `transactions` | `fetch_transactions_on_the_parent_account`, `filter_account_transactions`, `confirm_a_transaction_s_status_by_session_id` |
107
+ | `airtime_data` | `make_airtime_purchases_via_parent_account`, `vend_data_bundles_via_parent_account`, `fetch_data_plans_available_on_a_telco_network_provider` |
108
+ | `cabletv` | `cabletv_lookup`, `cable_tv_subscription_via_parent_account` |
109
+ | `electricity` | `fetch_electricity_providers`, `electricity_customer_lookup`, `vend_electricity_via_parent_account` |
110
+
111
+ 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.
112
+
113
+ ## Reliability (token locking + retry/backoff)
114
+
115
+ `NombaClient`/`AsyncNombaClient` guard token fetching with a lock, so
116
+ concurrent requests never race to re-fetch a token — only one fetch happens,
117
+ the rest wait and reuse it. Requests that hit a `429` or transient `5xx` are
118
+ automatically retried with exponential backoff (respecting `Retry-After` if
119
+ Nomba sends one):
120
+
121
+ ```python
122
+ nomba = Nomba(
123
+ client_id="...", client_secret="...", account_id="...",
124
+ max_retries=3, # default
125
+ backoff_factor=0.5, # default; delay ~= backoff_factor * 2^attempt
126
+ )
127
+ ```
128
+
129
+ ## Typed responses
130
+
131
+ Every method's return type is a `TypedDict` generated from Nomba's response
132
+ schema (in `nomba.models`), giving editor autocomplete and type-checking on
133
+ the actual JSON keys (e.g. `resp["data"]["accountNumber"]`). This costs
134
+ nothing at runtime — methods still return plain dicts; the TypedDict is
135
+ purely a type hint, which matters on constrained hardware.
136
+
137
+ ## Nested body validation
138
+
139
+ Flat method signatures (e.g. `order_reference=`, `amount=`) are validated by
140
+ Python itself. But some fields are nested objects — like `order={...}` in
141
+ checkout order creation — whose *inner* required fields a flat signature
142
+ can't enforce. Every write call is checked against Nomba's own OpenAPI spec
143
+ (bundled with the package) before any network call, so a typo or missing
144
+ nested field fails fast locally instead of round-tripping to Nomba's API:
145
+
146
+ ```python
147
+ from nomba import NombaValidationError
148
+
149
+ try:
150
+ nomba.checkout.create_an_online_checkout_order(order={"orderReference": "x"})
151
+ except NombaValidationError as e:
152
+ print(e) # Missing required field(s) in request body: order.amount, order.currency, ...
153
+ ```
154
+
155
+ ## Webhook signature verification
156
+
157
+ Implements Nomba's documented HMAC-SHA256 scheme
158
+ (`nomba-signature` / `nomba-timestamp` headers, base64-encoded signature),
159
+ including replay protection via the timestamp:
160
+
161
+ ```python
162
+ from nomba import verify_webhook_request, NombaValidationError
163
+
164
+ @app.post("/webhooks/nomba")
165
+ def handle_webhook():
166
+ try:
167
+ payload = verify_webhook_request(
168
+ signature_key="...", # the key you set up on the Nomba dashboard
169
+ body=request.get_data(),
170
+ headers=request.headers,
171
+ max_age_seconds=300, # default; reject webhooks older than 5 min
172
+ )
173
+ except NombaValidationError:
174
+ return "invalid signature", 401
175
+
176
+ if payload["event_type"] == "payment_success":
177
+ ...
178
+ return "", 200
179
+ ```
180
+
181
+ A valid signature alone doesn't prove a webhook wasn't captured and resent
182
+ later — `max_age_seconds` rejects stale ones. Pass `max_age_seconds=None` to
183
+ disable that check if you have your own replay protection.
184
+
185
+ ## Bounded concurrency for fan-out calls
186
+
187
+ `asyncio.gather` has no built-in limit — firing off many calls at once can
188
+ trigger a retry storm if several start failing together, or just trip
189
+ Nomba's rate limit by bursting too many requests in one window.
190
+ `gather_limited` runs the same calls with a concurrency cap:
191
+
192
+ ```python
193
+ from nomba import AsyncNomba, gather_limited
194
+
195
+ async with AsyncNomba(...) as nomba:
196
+ calls = [
197
+ (lambda ref=ref: nomba.virtual_accounts.fetch_a_virtual_account(ref))
198
+ for ref in account_refs
199
+ ]
200
+ results = await gather_limited(calls, limit=5)
201
+ ```
202
+
203
+ Extra/undocumented fields can always be passed as kwargs; they get merged into the JSON body verbatim.
204
+
205
+ ## Pagination
206
+
207
+ Nomba's list endpoints are cursor-paginated server-side (they return a
208
+ `cursor` you feed back in for the next page). `paginate`/`apaginate` just
209
+ drive that loop for you — no extra pagination scheme is introduced.
210
+
211
+ ```python
212
+ from nomba import Nomba, paginate
213
+
214
+ nomba = Nomba(...)
215
+ for account in paginate(nomba.accounts.list_all_accounts, limit=50):
216
+ print(account["accountRef"])
217
+ ```
218
+
219
+ Async:
220
+
221
+ ```python
222
+ from nomba import AsyncNomba, apaginate
223
+
224
+ async with AsyncNomba(...) as nomba:
225
+ async for txn in apaginate(nomba.transactions.fetch_transactions_on_the_parent_account, limit=50):
226
+ print(txn)
227
+ ```
228
+
229
+ Confirmed paginated endpoints (per Nomba's spec): `accounts.list_all_accounts`,
230
+ `accounts.fetch_terminals_assigned_to_an_account`,
231
+ `accounts.fetch_terminals_assigned_to_the_parent_account`,
232
+ `virtual_accounts.filter_virtual_accounts`, and all six
233
+ `transactions.*` list/filter methods.
234
+
235
+ ## Card payment flow
236
+
237
+ Card checkout is a multi-step sequence (submit card → maybe OTP → maybe 3D
238
+ Secure → confirm). `CardPaymentFlow` wraps it so you don't have to track
239
+ `transactionId` by hand or look up what Nomba's `responseCode` values mean:
240
+
241
+ ```python
242
+ order = nomba.checkout.create_an_online_checkout_order(
243
+ order={"orderReference": "order-001", "amount": "1000", ...}
244
+ )
245
+
246
+ flow = nomba.card_payment(order_reference="order-001")
247
+ step = flow.submit_card(card_details="...", key="")
248
+
249
+ if step.requires_otp:
250
+ step = flow.submit_otp("123456")
251
+ elif step.requires_3ds:
252
+ # redirect the user using step.secure_authentication_data
253
+ ...
254
+
255
+ if step.completed:
256
+ result = flow.confirm()
257
+ ```
258
+
259
+ `nomba.card_payment(...)` returns a `CardPaymentFlow` (or `AsyncCardPaymentFlow`
260
+ off `AsyncNomba`). `step` is a `CardPaymentStep` with `.completed`,
261
+ `.requires_otp`, `.requires_3ds`, `.transaction_id`, `.message`, etc.
262
+
263
+ ## Error handling
264
+
265
+ ```python
266
+ from nomba import NombaAPIError, NombaAuthError
267
+
268
+ try:
269
+ nomba.virtual_accounts.fetch_a_virtual_account("missing-ref")
270
+ except NombaAuthError as e:
271
+ print("auth failed:", e)
272
+ except NombaAPIError as e:
273
+ print("api error:", e.status_code, e.code, e.response_body)
274
+ ```
275
+
276
+ ## Authentication
277
+
278
+ The SDK handles the OAuth2 client-credentials flow automatically: it fetches an
279
+ access token on first request, caches it, and refreshes it transparently when
280
+ it expires or is rejected (a 401 triggers exactly one retry with a fresh token).
281
+ Get your `client_id` / `client_secret` / `accountId` from your Nomba dashboard
282
+ under **Webhook & API Keys**.
283
+
284
+ ## Regenerating from the spec
285
+
286
+ The resource modules in `src/nomba/resources/` and response models in
287
+ `src/nomba/models.py` are generated from `src/nomba/data/nomba_openapi.json`
288
+ (a snapshot of Nomba's published OpenAPI spec, also bundled into the package
289
+ for runtime nested-body validation). To pick up upstream API changes:
290
+
291
+ ```bash
292
+ curl -sL -o src/nomba/data/nomba_openapi.json \
293
+ "https://github.com/kudi-inc/vendor-openapi-spec/raw/refs/heads/main/openapi3_0_v_1_0_0.json"
294
+ uv run python scripts/generate_resources.py
295
+ ```
296
+
297
+ ## Development
298
+
299
+ ```bash
300
+ uv sync
301
+ uv run python -m pytest # once tests are added
302
+ ```
303
+
304
+ ## Status
305
+
306
+ Generated from Nomba's official OpenAPI spec (v1.0.0) — all 64 documented
307
+ endpoints across Accounts, Virtual Accounts, Online Checkout, Charge,
308
+ Transfers, Terminals, Transactions, Airtime/Data, CableTV, and Electricity.
309
+
310
+ Also includes: typed responses, cursor pagination helpers, a guided
311
+ card-payment flow, locked/retrying HTTP clients, local nested-body
312
+ validation against Nomba's own spec, and webhook signature verification.
@@ -0,0 +1,29 @@
1
+ nomba_python/__init__.py,sha256=EqZbsXjq9agMJaBHqjxHjGvjchFDhqWehDQYAMb21uI,941
2
+ nomba_python/client.py,sha256=TwieNaDU-eHRIsgiVRITPc7aIuIiF1LyzF6Mn-UdMHA,4781
3
+ nomba_python/concurrency.py,sha256=0wTRJ57uLR-rXaIEMfivSRr4qEKvZMu3j5EXgAyLKoo,1870
4
+ nomba_python/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ nomba_python/data/nomba_openapi.json,sha256=92D2QdcKaC6oe45fEQv_sJTbKV1oXqVDjSJUIFJxU-4,437490
6
+ nomba_python/exceptions.py,sha256=Dx6OASM-7bvzsejmVbw_Iy6PXhm09K6Cu52mxlOjVY8,1484
7
+ nomba_python/flows/__init__.py,sha256=v2yT_QfBLIzWRvhCiDXXELFMWFTf3ye_VkVlEGA2kb0,155
8
+ nomba_python/flows/card_payment.py,sha256=Z0eJeFYrvJ_Za_JWl4qxalhU9Q-s-3LjO318ShtbN_k,7512
9
+ nomba_python/http.py,sha256=DVuw6Btcnwlqq95FFE2x62Zd-lp9QtB8-qVvB9sFc6Q,14078
10
+ nomba_python/models.py,sha256=5m4lG7B4yMUaLvH7DpFAHBAnI9HFS_pGRTP-ue33_DU,19898
11
+ nomba_python/pagination.py,sha256=g5EagLomsdkN9YojCCnp55mm9IrFCfXBaU9500G_Ri8,3678
12
+ nomba_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ nomba_python/resources/__init__.py,sha256=Is2J4R6YQhUKzl4KF9nHE1qXCqWfinrgBY5cY9xe1ew,927
14
+ nomba_python/resources/accounts.py,sha256=60Z3pKulFjw1wbxej3Qp7plAOFS2r3w9jTPPoJTLWcg,16774
15
+ nomba_python/resources/airtime_data.py,sha256=rQEOpteN8rjMTyY0qSh2ilivwhW1Yy_nUvKGnU1Z0tM,12281
16
+ nomba_python/resources/cabletv.py,sha256=Xa2tNdjNP7D5jsu9Q-7J6mhK8oGQvxw3jLaO9IhcnRI,7180
17
+ nomba_python/resources/charge.py,sha256=9Nz0AE0YLo3vCvBqcyjnTTRkNVaqN2cKEZQ_RaCweTM,19549
18
+ nomba_python/resources/checkout.py,sha256=g-K9eBRZ0mR_H8o7GpKHKxrrD0tWpfWzdaUkRfBY5rs,10090
19
+ nomba_python/resources/electricity.py,sha256=t-Cq34fJwsMlUn4Gar5VCMZh8Ami34z758CNMZsMEts,8403
20
+ nomba_python/resources/terminals.py,sha256=Ftdl4bOeeoeh8lOy3ARhYWYP_SORs9zOnXX2n1OV9dY,7769
21
+ nomba_python/resources/transactions.py,sha256=HSDsuhYaZ3SIM4KyNOFxA6_4f1nfno1URyAU7xbZyk8,22269
22
+ nomba_python/resources/transfers.py,sha256=Rkqp18KgzHcOoJSx7VRgF8M3tvT3i3H-dmQGgU6XfCc,14089
23
+ nomba_python/resources/virtual_accounts.py,sha256=FpHhnBxJS5-C-dQ-jt-Fb8OoMP2Qq96wyFi7hDTD8hw,9873
24
+ nomba_python/validation.py,sha256=sAlddQ0TDtQO4AiTgNQRRRg9GN-fHArJEfCsulPpNyw,3489
25
+ nomba_python/webhooks.py,sha256=wNJwMqNgkrWmk8zR8Ein_ynlW0XkW6wwrDfhh4gB4Ic,7016
26
+ nomba_python-0.1.0.dist-info/licenses/LICENSE,sha256=5ddBfMvz-MOkk_vJtDwfqiDR8gchx1KRaetdXNL238s,1070
27
+ nomba_python-0.1.0.dist-info/WHEEL,sha256=8ZlpUMJ7mlDirmlHRhDirEx_nPnARrwDjeE92mlk68E,81
28
+ nomba_python-0.1.0.dist-info/METADATA,sha256=2RKImLUdgWQ8Qxw477-3ySTC4jo6w7xwdB7uaIKz9p8,11232
29
+ nomba_python-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.21
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.