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,298 @@
1
+ # This file is auto-generated from Nomba's OpenAPI spec. Do not edit by hand;
2
+ # regenerate via scripts/generate_resources.py instead.
3
+ from __future__ import annotations
4
+
5
+
6
+ from ..http import AsyncNombaClient, NombaClient
7
+ from ..validation import validate_body
8
+ from .. import models as _models
9
+
10
+
11
+ class Transfers:
12
+ """Sync resource methods for the Transfers group."""
13
+
14
+ def __init__(self, client: NombaClient) -> None:
15
+ self._client = client
16
+
17
+ def fetch_bank_codes_and_names(self, **extra: object) -> _models.FetchBankCodesAndNamesResponse:
18
+ """
19
+ Fetch bank codes and names
20
+
21
+ You can use this endpoint to fetch all banks, their names and codes.
22
+ """
23
+ path = "/v1/transfers/banks"
24
+ params = None
25
+ return self._client.get(path, params=params) # type: ignore[return-value]
26
+
27
+ def perform_bank_account_lookup(self, account_number, bank_code, **extra: object) -> _models.PerformBankAccountLookupResponse:
28
+ """
29
+ Perform bank account lookup
30
+
31
+ You can use this endpoint to perform bank account lookup.
32
+
33
+ Body fields:
34
+ accountNumber (required): The account number to be looked up.
35
+ bankCode (required): The bankCode of the bank the account number belongs to. This can be obtained from a call to `/v1/transfers/bank`
36
+ """
37
+ path = "/v1/transfers/bank/lookup"
38
+ params = None
39
+ body: dict[str, object] = {}
40
+ body["accountNumber"] = account_number
41
+ body["bankCode"] = bank_code
42
+ body.update(extra)
43
+ validate_body("post", "/v1/transfers/bank/lookup", body)
44
+ return self._client.post(path, json=body, params=params) # type: ignore[return-value]
45
+
46
+ def perform_bank_account_transfer_the_parent_account(self, amount, account_number, account_name, bank_code, merchant_tx_ref, sender_name, *, narration: object | None = None, **extra: object) -> _models.PerformBankAccountTransferTheParentAccountResponse:
47
+ """
48
+ Perform bank account transfer from the parent account
49
+
50
+ You can use this endpoint to perform bank account transfer.
51
+
52
+ Body fields:
53
+ amount (required): The amount to be transferred.
54
+ accountNumber (required): The destination bank account number.
55
+ accountName (required): The name on the account.
56
+ bankCode (required): The code of the recipient bank.
57
+ merchantTxRef (required): Unique reference used to track a transaction from an external process.
58
+ senderName (required): Sender name
59
+ narration: The payment narration
60
+ """
61
+ path = "/v1/transfers/bank"
62
+ params = None
63
+ body: dict[str, object] = {}
64
+ body["amount"] = amount
65
+ body["accountNumber"] = account_number
66
+ body["accountName"] = account_name
67
+ body["bankCode"] = bank_code
68
+ body["merchantTxRef"] = merchant_tx_ref
69
+ body["senderName"] = sender_name
70
+ if narration is not None:
71
+ body["narration"] = narration
72
+ body.update(extra)
73
+ validate_body("post", "/v1/transfers/bank", body)
74
+ return self._client.post(path, json=body, params=params) # type: ignore[return-value]
75
+
76
+ def perform_bank_account_transfer_from_account(self, sub_account_id: str, amount, account_number, account_name, bank_code, merchant_tx_ref, sender_name, *, narration: object | None = None, **extra: object) -> _models.PerformBankAccountTransferFromAccountResponse:
77
+ """
78
+ Perform bank account transfer from a sub account
79
+
80
+ You can use this endpoint to perform bank account transfer using a sub account
81
+
82
+ Body fields:
83
+ amount (required): The amount to be transferred.
84
+ accountNumber (required): The destination bank account number.
85
+ accountName (required): The name on the account.
86
+ bankCode (required): The code of the recipient bank.
87
+ merchantTxRef (required): Unique reference used to track a transaction from an external process.
88
+ senderName (required): Sender name
89
+ narration: The payment narration
90
+ """
91
+ path = f"/v1/transfers/bank/{sub_account_id}"
92
+ params = None
93
+ body: dict[str, object] = {}
94
+ body["amount"] = amount
95
+ body["accountNumber"] = account_number
96
+ body["accountName"] = account_name
97
+ body["bankCode"] = bank_code
98
+ body["merchantTxRef"] = merchant_tx_ref
99
+ body["senderName"] = sender_name
100
+ if narration is not None:
101
+ body["narration"] = narration
102
+ body.update(extra)
103
+ validate_body("post", "/v1/transfers/bank/{subAccountId}", body)
104
+ return self._client.post(path, json=body, params=params) # type: ignore[return-value]
105
+
106
+ def perform_wallet_transfer_from_the_parent_account(self, amount, receiver_account_id, merchant_tx_ref, *, narration: object | None = None, **extra: object) -> _models.PerformWalletTransferFromTheParentAccountResponse:
107
+ """
108
+ Perform wallet transfer from the parent account
109
+
110
+ You can use this endpoint to perform a wallet transfer.
111
+
112
+ Body fields:
113
+ amount (required): The amount to be transferred.
114
+ receiverAccountId (required): The receiver's accountId.
115
+ merchantTxRef (required): Unique reference used to track a transaction from an external process.
116
+ narration: The payment narration
117
+ """
118
+ path = "/v1/transfers/wallet"
119
+ params = None
120
+ body: dict[str, object] = {}
121
+ body["amount"] = amount
122
+ body["receiverAccountId"] = receiver_account_id
123
+ body["merchantTxRef"] = merchant_tx_ref
124
+ if narration is not None:
125
+ body["narration"] = narration
126
+ body.update(extra)
127
+ validate_body("post", "/v1/transfers/wallet", body)
128
+ return self._client.post(path, json=body, params=params) # type: ignore[return-value]
129
+
130
+ def perform_wallet_transfer_from_a_sub_account(self, sub_account_id: str, amount, receiver_account_id, merchant_tx_ref, *, narration: object | None = None, **extra: object) -> _models.PerformWalletTransferFromASubAccountResponse:
131
+ """
132
+ Perform wallet transfer from a sub account
133
+
134
+ You can use this endpoint to perform a wallet transfer from a sub account
135
+
136
+ Body fields:
137
+ amount (required): The amount to be transferred.
138
+ receiverAccountId (required): The receiver's accountId.
139
+ merchantTxRef (required): Unique reference used to track a transaction from an external process.
140
+ narration: The payment narration
141
+ """
142
+ path = f"/v1/transfers/wallet/{sub_account_id}"
143
+ params = None
144
+ body: dict[str, object] = {}
145
+ body["amount"] = amount
146
+ body["receiverAccountId"] = receiver_account_id
147
+ body["merchantTxRef"] = merchant_tx_ref
148
+ if narration is not None:
149
+ body["narration"] = narration
150
+ body.update(extra)
151
+ validate_body("post", "/v1/transfers/wallet/{subAccountId}", body)
152
+ return self._client.post(path, json=body, params=params) # type: ignore[return-value]
153
+
154
+
155
+
156
+ class AsyncTransfers:
157
+ """Async resource methods for the Transfers group."""
158
+
159
+ def __init__(self, client: AsyncNombaClient) -> None:
160
+ self._client = client
161
+
162
+ async def fetch_bank_codes_and_names(self, **extra: object) -> _models.FetchBankCodesAndNamesResponse:
163
+ """
164
+ Fetch bank codes and names
165
+
166
+ You can use this endpoint to fetch all banks, their names and codes.
167
+ """
168
+ path = "/v1/transfers/banks"
169
+ params = None
170
+ return await self._client.get(path, params=params) # type: ignore[return-value]
171
+
172
+ async def perform_bank_account_lookup(self, account_number, bank_code, **extra: object) -> _models.PerformBankAccountLookupResponse:
173
+ """
174
+ Perform bank account lookup
175
+
176
+ You can use this endpoint to perform bank account lookup.
177
+
178
+ Body fields:
179
+ accountNumber (required): The account number to be looked up.
180
+ bankCode (required): The bankCode of the bank the account number belongs to. This can be obtained from a call to `/v1/transfers/bank`
181
+ """
182
+ path = "/v1/transfers/bank/lookup"
183
+ params = None
184
+ body: dict[str, object] = {}
185
+ body["accountNumber"] = account_number
186
+ body["bankCode"] = bank_code
187
+ body.update(extra)
188
+ validate_body("post", "/v1/transfers/bank/lookup", body)
189
+ return await self._client.post(path, json=body, params=params) # type: ignore[return-value]
190
+
191
+ async def perform_bank_account_transfer_the_parent_account(self, amount, account_number, account_name, bank_code, merchant_tx_ref, sender_name, *, narration: object | None = None, **extra: object) -> _models.PerformBankAccountTransferTheParentAccountResponse:
192
+ """
193
+ Perform bank account transfer from the parent account
194
+
195
+ You can use this endpoint to perform bank account transfer.
196
+
197
+ Body fields:
198
+ amount (required): The amount to be transferred.
199
+ accountNumber (required): The destination bank account number.
200
+ accountName (required): The name on the account.
201
+ bankCode (required): The code of the recipient bank.
202
+ merchantTxRef (required): Unique reference used to track a transaction from an external process.
203
+ senderName (required): Sender name
204
+ narration: The payment narration
205
+ """
206
+ path = "/v1/transfers/bank"
207
+ params = None
208
+ body: dict[str, object] = {}
209
+ body["amount"] = amount
210
+ body["accountNumber"] = account_number
211
+ body["accountName"] = account_name
212
+ body["bankCode"] = bank_code
213
+ body["merchantTxRef"] = merchant_tx_ref
214
+ body["senderName"] = sender_name
215
+ if narration is not None:
216
+ body["narration"] = narration
217
+ body.update(extra)
218
+ validate_body("post", "/v1/transfers/bank", body)
219
+ return await self._client.post(path, json=body, params=params) # type: ignore[return-value]
220
+
221
+ async def perform_bank_account_transfer_from_account(self, sub_account_id: str, amount, account_number, account_name, bank_code, merchant_tx_ref, sender_name, *, narration: object | None = None, **extra: object) -> _models.PerformBankAccountTransferFromAccountResponse:
222
+ """
223
+ Perform bank account transfer from a sub account
224
+
225
+ You can use this endpoint to perform bank account transfer using a sub account
226
+
227
+ Body fields:
228
+ amount (required): The amount to be transferred.
229
+ accountNumber (required): The destination bank account number.
230
+ accountName (required): The name on the account.
231
+ bankCode (required): The code of the recipient bank.
232
+ merchantTxRef (required): Unique reference used to track a transaction from an external process.
233
+ senderName (required): Sender name
234
+ narration: The payment narration
235
+ """
236
+ path = f"/v1/transfers/bank/{sub_account_id}"
237
+ params = None
238
+ body: dict[str, object] = {}
239
+ body["amount"] = amount
240
+ body["accountNumber"] = account_number
241
+ body["accountName"] = account_name
242
+ body["bankCode"] = bank_code
243
+ body["merchantTxRef"] = merchant_tx_ref
244
+ body["senderName"] = sender_name
245
+ if narration is not None:
246
+ body["narration"] = narration
247
+ body.update(extra)
248
+ validate_body("post", "/v1/transfers/bank/{subAccountId}", body)
249
+ return await self._client.post(path, json=body, params=params) # type: ignore[return-value]
250
+
251
+ async def perform_wallet_transfer_from_the_parent_account(self, amount, receiver_account_id, merchant_tx_ref, *, narration: object | None = None, **extra: object) -> _models.PerformWalletTransferFromTheParentAccountResponse:
252
+ """
253
+ Perform wallet transfer from the parent account
254
+
255
+ You can use this endpoint to perform a wallet transfer.
256
+
257
+ Body fields:
258
+ amount (required): The amount to be transferred.
259
+ receiverAccountId (required): The receiver's accountId.
260
+ merchantTxRef (required): Unique reference used to track a transaction from an external process.
261
+ narration: The payment narration
262
+ """
263
+ path = "/v1/transfers/wallet"
264
+ params = None
265
+ body: dict[str, object] = {}
266
+ body["amount"] = amount
267
+ body["receiverAccountId"] = receiver_account_id
268
+ body["merchantTxRef"] = merchant_tx_ref
269
+ if narration is not None:
270
+ body["narration"] = narration
271
+ body.update(extra)
272
+ validate_body("post", "/v1/transfers/wallet", body)
273
+ return await self._client.post(path, json=body, params=params) # type: ignore[return-value]
274
+
275
+ async def perform_wallet_transfer_from_a_sub_account(self, sub_account_id: str, amount, receiver_account_id, merchant_tx_ref, *, narration: object | None = None, **extra: object) -> _models.PerformWalletTransferFromASubAccountResponse:
276
+ """
277
+ Perform wallet transfer from a sub account
278
+
279
+ You can use this endpoint to perform a wallet transfer from a sub account
280
+
281
+ Body fields:
282
+ amount (required): The amount to be transferred.
283
+ receiverAccountId (required): The receiver's accountId.
284
+ merchantTxRef (required): Unique reference used to track a transaction from an external process.
285
+ narration: The payment narration
286
+ """
287
+ path = f"/v1/transfers/wallet/{sub_account_id}"
288
+ params = None
289
+ body: dict[str, object] = {}
290
+ body["amount"] = amount
291
+ body["receiverAccountId"] = receiver_account_id
292
+ body["merchantTxRef"] = merchant_tx_ref
293
+ if narration is not None:
294
+ body["narration"] = narration
295
+ body.update(extra)
296
+ validate_body("post", "/v1/transfers/wallet/{subAccountId}", body)
297
+ return await self._client.post(path, json=body, params=params) # type: ignore[return-value]
298
+
@@ -0,0 +1,230 @@
1
+ # This file is auto-generated from Nomba's OpenAPI spec. Do not edit by hand;
2
+ # regenerate via scripts/generate_resources.py instead.
3
+ from __future__ import annotations
4
+
5
+
6
+ from ..http import AsyncNombaClient, NombaClient
7
+ from ..validation import validate_body
8
+ from .. import models as _models
9
+
10
+
11
+ class VirtualAccounts:
12
+ """Sync resource methods for the VirtualAccounts group."""
13
+
14
+ def __init__(self, client: NombaClient) -> None:
15
+ self._client = client
16
+
17
+ def create_virtual_account(self, account_ref, account_name, **extra: object) -> _models.CreateVirtualAccountResponse:
18
+ """
19
+ Create virtual account
20
+
21
+ You can use this endpoint to create a virtual account to receive payments.
22
+
23
+ Body fields:
24
+ accountRef (required): Account reference
25
+ accountName (required): Account holder's name
26
+ """
27
+ path = "/v1/accounts/virtual"
28
+ params = None
29
+ body: dict[str, object] = {}
30
+ body["accountRef"] = account_ref
31
+ body["accountName"] = account_name
32
+ body.update(extra)
33
+ validate_body("post", "/v1/accounts/virtual", body)
34
+ return self._client.post(path, json=body, params=params) # type: ignore[return-value]
35
+
36
+ def filter_virtual_accounts(self, *, limit: str | None = None, cursor: str | None = None, account_name: object | None = None, account_ref: object | None = None, bvn: object | None = None, bank_account_number: object | None = None, date_created_from: object | None = None, date_created_to: object | None = None, expired: object | None = None, resource_acquired: object | None = None, **extra: object) -> _models.FilterVirtualAccountsResponse:
37
+ """
38
+ Filter virtual accounts
39
+
40
+ You can use this endpoint to filter your virtual accounts.
41
+
42
+ Body fields:
43
+ accountName: Account holder's name
44
+ accountRef: Account reference
45
+ bvn: Bank Verification Number (BVN)
46
+ bankAccountNumber: Bank account number
47
+ dateCreatedFrom: Date created from
48
+ dateCreatedTo: Date created to
49
+ expired: Whether the virtual account is expired or not
50
+ resourceAcquired: Whether the virtual account is in use or not
51
+ """
52
+ path = "/v1/accounts/virtual/list"
53
+ params: dict[str, object] = {}
54
+ if limit is not None:
55
+ params["limit"] = limit
56
+ if cursor is not None:
57
+ params["cursor"] = cursor
58
+ body: dict[str, object] = {}
59
+ if account_name is not None:
60
+ body["accountName"] = account_name
61
+ if account_ref is not None:
62
+ body["accountRef"] = account_ref
63
+ if bvn is not None:
64
+ body["bvn"] = bvn
65
+ if bank_account_number is not None:
66
+ body["bankAccountNumber"] = bank_account_number
67
+ if date_created_from is not None:
68
+ body["dateCreatedFrom"] = date_created_from
69
+ if date_created_to is not None:
70
+ body["dateCreatedTo"] = date_created_to
71
+ if expired is not None:
72
+ body["expired"] = expired
73
+ if resource_acquired is not None:
74
+ body["resourceAcquired"] = resource_acquired
75
+ body.update(extra)
76
+ validate_body("post", "/v1/accounts/virtual/list", body)
77
+ return self._client.post(path, json=body, params=params) # type: ignore[return-value]
78
+
79
+ def update_a_virtual_account(self, account_ref: str, *, account_name: object | None = None, callback_url: object | None = None, **extra: object) -> _models.UpdateAVirtualAccountResponse:
80
+ """
81
+ Update a virtual account
82
+
83
+ You can use this endpoint to update a virtual account.
84
+
85
+ Body fields:
86
+ accountName: Account holder's name
87
+ callbackUrl: Callback url
88
+ """
89
+ path = f"/v1/accounts/virtual/{account_ref}"
90
+ params = None
91
+ body: dict[str, object] = {}
92
+ if account_name is not None:
93
+ body["accountName"] = account_name
94
+ if callback_url is not None:
95
+ body["callbackUrl"] = callback_url
96
+ body.update(extra)
97
+ validate_body("put", "/v1/accounts/virtual/{accountRef}", body)
98
+ return self._client.put(path, json=body, params=params) # type: ignore[return-value]
99
+
100
+ def fetch_a_virtual_account(self, account_ref: str, **extra: object) -> _models.FetchAVirtualAccountResponse:
101
+ """
102
+ Fetch a virtual account
103
+
104
+ You can use this endpoint to fetch a virtual account.
105
+ """
106
+ path = f"/v1/accounts/virtual/{account_ref}"
107
+ params = None
108
+ return self._client.get(path, params=params) # type: ignore[return-value]
109
+
110
+ def expire_a_virtual_account(self, account_ref: str, **extra: object) -> _models.ExpireAVirtualAccountResponse:
111
+ """
112
+ Expire a virtual account
113
+
114
+ You can use this endpoint to expire a virtual account.
115
+ """
116
+ path = f"/v1/accounts/virtual/{account_ref}"
117
+ params = None
118
+ return self._client.delete(path, params=params) # type: ignore[return-value]
119
+
120
+
121
+
122
+ class AsyncVirtualAccounts:
123
+ """Async resource methods for the VirtualAccounts group."""
124
+
125
+ def __init__(self, client: AsyncNombaClient) -> None:
126
+ self._client = client
127
+
128
+ async def create_virtual_account(self, account_ref, account_name, **extra: object) -> _models.CreateVirtualAccountResponse:
129
+ """
130
+ Create virtual account
131
+
132
+ You can use this endpoint to create a virtual account to receive payments.
133
+
134
+ Body fields:
135
+ accountRef (required): Account reference
136
+ accountName (required): Account holder's name
137
+ """
138
+ path = "/v1/accounts/virtual"
139
+ params = None
140
+ body: dict[str, object] = {}
141
+ body["accountRef"] = account_ref
142
+ body["accountName"] = account_name
143
+ body.update(extra)
144
+ validate_body("post", "/v1/accounts/virtual", body)
145
+ return await self._client.post(path, json=body, params=params) # type: ignore[return-value]
146
+
147
+ async def filter_virtual_accounts(self, *, limit: str | None = None, cursor: str | None = None, account_name: object | None = None, account_ref: object | None = None, bvn: object | None = None, bank_account_number: object | None = None, date_created_from: object | None = None, date_created_to: object | None = None, expired: object | None = None, resource_acquired: object | None = None, **extra: object) -> _models.FilterVirtualAccountsResponse:
148
+ """
149
+ Filter virtual accounts
150
+
151
+ You can use this endpoint to filter your virtual accounts.
152
+
153
+ Body fields:
154
+ accountName: Account holder's name
155
+ accountRef: Account reference
156
+ bvn: Bank Verification Number (BVN)
157
+ bankAccountNumber: Bank account number
158
+ dateCreatedFrom: Date created from
159
+ dateCreatedTo: Date created to
160
+ expired: Whether the virtual account is expired or not
161
+ resourceAcquired: Whether the virtual account is in use or not
162
+ """
163
+ path = "/v1/accounts/virtual/list"
164
+ params: dict[str, object] = {}
165
+ if limit is not None:
166
+ params["limit"] = limit
167
+ if cursor is not None:
168
+ params["cursor"] = cursor
169
+ body: dict[str, object] = {}
170
+ if account_name is not None:
171
+ body["accountName"] = account_name
172
+ if account_ref is not None:
173
+ body["accountRef"] = account_ref
174
+ if bvn is not None:
175
+ body["bvn"] = bvn
176
+ if bank_account_number is not None:
177
+ body["bankAccountNumber"] = bank_account_number
178
+ if date_created_from is not None:
179
+ body["dateCreatedFrom"] = date_created_from
180
+ if date_created_to is not None:
181
+ body["dateCreatedTo"] = date_created_to
182
+ if expired is not None:
183
+ body["expired"] = expired
184
+ if resource_acquired is not None:
185
+ body["resourceAcquired"] = resource_acquired
186
+ body.update(extra)
187
+ validate_body("post", "/v1/accounts/virtual/list", body)
188
+ return await self._client.post(path, json=body, params=params) # type: ignore[return-value]
189
+
190
+ async def update_a_virtual_account(self, account_ref: str, *, account_name: object | None = None, callback_url: object | None = None, **extra: object) -> _models.UpdateAVirtualAccountResponse:
191
+ """
192
+ Update a virtual account
193
+
194
+ You can use this endpoint to update a virtual account.
195
+
196
+ Body fields:
197
+ accountName: Account holder's name
198
+ callbackUrl: Callback url
199
+ """
200
+ path = f"/v1/accounts/virtual/{account_ref}"
201
+ params = None
202
+ body: dict[str, object] = {}
203
+ if account_name is not None:
204
+ body["accountName"] = account_name
205
+ if callback_url is not None:
206
+ body["callbackUrl"] = callback_url
207
+ body.update(extra)
208
+ validate_body("put", "/v1/accounts/virtual/{accountRef}", body)
209
+ return await self._client.put(path, json=body, params=params) # type: ignore[return-value]
210
+
211
+ async def fetch_a_virtual_account(self, account_ref: str, **extra: object) -> _models.FetchAVirtualAccountResponse:
212
+ """
213
+ Fetch a virtual account
214
+
215
+ You can use this endpoint to fetch a virtual account.
216
+ """
217
+ path = f"/v1/accounts/virtual/{account_ref}"
218
+ params = None
219
+ return await self._client.get(path, params=params) # type: ignore[return-value]
220
+
221
+ async def expire_a_virtual_account(self, account_ref: str, **extra: object) -> _models.ExpireAVirtualAccountResponse:
222
+ """
223
+ Expire a virtual account
224
+
225
+ You can use this endpoint to expire a virtual account.
226
+ """
227
+ path = f"/v1/accounts/virtual/{account_ref}"
228
+ params = None
229
+ return await self._client.delete(path, params=params) # type: ignore[return-value]
230
+
@@ -0,0 +1,97 @@
1
+ """
2
+ Lightweight, dependency-free validation of request bodies against Nomba's
3
+ own OpenAPI spec — specifically nested required fields that a flat method
4
+ signature can't enforce (e.g. `order={...}` in checkout order creation).
5
+
6
+ This only checks presence of required keys recursively (and a coarse
7
+ type check for "object"/"array"), not full JSON-Schema validation. It's a
8
+ local fast-fail before any network call, not a replacement for Nomba's own
9
+ server-side validation.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from functools import lru_cache
15
+ from importlib import resources
16
+ from typing import Any
17
+
18
+ from .exceptions import NombaValidationError
19
+
20
+
21
+ @lru_cache(maxsize=1)
22
+ def _load_spec() -> dict[str, Any]:
23
+ with resources.files("nomba.data").joinpath("nomba_openapi.json").open(
24
+ "r", encoding="utf-8"
25
+ ) as f:
26
+ return json.load(f)
27
+
28
+
29
+ def _resolve(spec: dict[str, Any], schema: Any) -> Any:
30
+ if not isinstance(schema, dict):
31
+ return schema
32
+ if "$ref" in schema:
33
+ name = schema["$ref"].split("/")[-1]
34
+ return spec["components"]["schemas"].get(name, {})
35
+ return schema
36
+
37
+
38
+ @lru_cache(maxsize=None)
39
+ def _request_schema_key(verb: str, path_template: str) -> Any:
40
+ spec = _load_spec()
41
+ op = spec.get("paths", {}).get(path_template, {}).get(verb.lower())
42
+ if not op:
43
+ return None
44
+ rb = op.get("requestBody")
45
+ if not rb:
46
+ return None
47
+ schema = rb.get("content", {}).get("application/json", {}).get("schema")
48
+ return _resolve(spec, schema)
49
+
50
+
51
+ def _check(spec: dict[str, Any], schema: Any, value: Any, path: str, missing: list[str]) -> None:
52
+ schema = _resolve(spec, schema)
53
+ if not isinstance(schema, dict):
54
+ return
55
+ schema_type = schema.get("type")
56
+
57
+ if schema_type == "object" or "properties" in schema:
58
+ if not isinstance(value, dict):
59
+ if value is not None:
60
+ missing.append(f"{path} (expected an object)")
61
+ return
62
+ for required_name in schema.get("required", []):
63
+ if required_name not in value or value[required_name] is None:
64
+ missing.append(f"{path}.{required_name}" if path else required_name)
65
+ props = schema.get("properties", {})
66
+ for key, sub_value in value.items():
67
+ if key in props:
68
+ _check(spec, props[key], sub_value, f"{path}.{key}" if path else key, missing)
69
+
70
+ elif schema_type == "array":
71
+ if not isinstance(value, list):
72
+ return
73
+ items_schema = schema.get("items")
74
+ if items_schema:
75
+ for i, item in enumerate(value):
76
+ _check(spec, items_schema, item, f"{path}[{i}]", missing)
77
+
78
+
79
+ def validate_body(verb: str, path_template: str, body: dict[str, Any]) -> None:
80
+ """
81
+ Validate `body` against the requestBody schema for `verb`/`path_template`
82
+ in Nomba's OpenAPI spec, recursively checking required fields on nested
83
+ objects/arrays. Raises NombaValidationError listing every missing field
84
+ if any are absent. No-ops if the spec has no requestBody for this
85
+ operation, or no nested object/array fields to check.
86
+ """
87
+ schema = _request_schema_key(verb, path_template)
88
+ if schema is None:
89
+ return
90
+ spec = _load_spec()
91
+ missing: list[str] = []
92
+ _check(spec, schema, body, "", missing)
93
+ if missing:
94
+ raise NombaValidationError(
95
+ f"Missing required field(s) in request body: {', '.join(missing)}",
96
+ missing=missing,
97
+ )