nocfo-cli 1.7.2__tar.gz → 1.9.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 (79) hide show
  1. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/PKG-INFO +1 -1
  2. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/pyproject.toml +1 -1
  3. nocfo_cli-1.9.0/src/nocfo_toolkit/mcp/curated/batch.py +95 -0
  4. nocfo_cli-1.9.0/src/nocfo_toolkit/mcp/curated/bookkeeping/account.py +228 -0
  5. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/bookkeeping/document.py +271 -99
  6. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/bookkeeping/header.py +16 -11
  7. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/bookkeeping/relation.py +39 -33
  8. nocfo_cli-1.9.0/src/nocfo_toolkit/mcp/curated/bookkeeping/tag_file.py +357 -0
  9. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/instructions.py +8 -0
  10. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/invoicing/contact.py +83 -58
  11. nocfo_cli-1.9.0/src/nocfo_toolkit/mcp/curated/invoicing/product.py +195 -0
  12. nocfo_cli-1.9.0/src/nocfo_toolkit/mcp/curated/invoicing/purchase_invoice.py +186 -0
  13. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/invoicing/sales_invoice.py +114 -67
  14. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/reporting/report.py +83 -64
  15. nocfo_cli-1.9.0/src/nocfo_toolkit/mcp/curated/schema/batch.py +131 -0
  16. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schema/bookkeeping/account.py +3 -14
  17. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schema/bookkeeping/document.py +71 -24
  18. nocfo_cli-1.9.0/src/nocfo_toolkit/mcp/curated/schema/bookkeeping/document_suggestion.py +238 -0
  19. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schema/bookkeeping/header.py +0 -6
  20. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schema/bookkeeping/tag_file.py +20 -2
  21. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schema/common.py +0 -18
  22. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schema/invoicing/contact.py +22 -2
  23. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schema/invoicing/purchase_invoice.py +31 -2
  24. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schema/invoicing/sales_invoice.py +0 -23
  25. nocfo_cli-1.9.0/src/nocfo_toolkit/mcp/curated/schema/invoicing/sales_invoice_batch.py +47 -0
  26. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schemas.py +3 -0
  27. nocfo_cli-1.7.2/src/nocfo_toolkit/mcp/curated/bookkeeping/account.py +0 -204
  28. nocfo_cli-1.7.2/src/nocfo_toolkit/mcp/curated/bookkeeping/tag_file.py +0 -322
  29. nocfo_cli-1.7.2/src/nocfo_toolkit/mcp/curated/invoicing/product.py +0 -178
  30. nocfo_cli-1.7.2/src/nocfo_toolkit/mcp/curated/invoicing/purchase_invoice.py +0 -146
  31. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/LICENSE +0 -0
  32. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/README.md +0 -0
  33. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/__init__.py +0 -0
  34. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/api_client.py +0 -0
  35. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/__init__.py +0 -0
  36. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/app.py +0 -0
  37. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/commands/__init__.py +0 -0
  38. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/commands/_helpers.py +0 -0
  39. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/commands/accounts.py +0 -0
  40. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/commands/auth.py +0 -0
  41. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/commands/businesses.py +0 -0
  42. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/commands/contacts.py +0 -0
  43. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/commands/documents.py +0 -0
  44. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/commands/files.py +0 -0
  45. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/commands/invoices.py +0 -0
  46. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/commands/products.py +0 -0
  47. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/commands/purchase_invoices.py +0 -0
  48. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/commands/reports.py +0 -0
  49. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/commands/tags.py +0 -0
  50. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/commands/user.py +0 -0
  51. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/context.py +0 -0
  52. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/cli/output.py +0 -0
  53. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/config.py +0 -0
  54. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/__init__.py +0 -0
  55. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/auth.py +0 -0
  56. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/__init__.py +0 -0
  57. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/bookkeeping/__init__.py +0 -0
  58. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/client.py +0 -0
  59. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/common.py +0 -0
  60. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/constants/__init__.py +0 -0
  61. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/constants/docs.py +0 -0
  62. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/errors.py +0 -0
  63. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/invoicing/__init__.py +0 -0
  64. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/reporting/__init__.py +0 -0
  65. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/runtime.py +0 -0
  66. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schema/__init__.py +0 -0
  67. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schema/bookkeeping/__init__.py +0 -0
  68. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schema/constants/__init__.py +0 -0
  69. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schema/constants/docs.py +0 -0
  70. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schema/invoicing/__init__.py +0 -0
  71. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schema/invoicing/product.py +0 -0
  72. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schema/reporting/__init__.py +0 -0
  73. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/schema/reporting/report.py +0 -0
  74. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/curated/utils.py +0 -0
  75. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/error_handling.py +0 -0
  76. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/http_error_capture.py +0 -0
  77. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/middleware.py +0 -0
  78. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/search.py +0 -0
  79. {nocfo_cli-1.7.2 → nocfo_cli-1.9.0}/src/nocfo_toolkit/mcp/server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nocfo-cli
3
- Version: 1.7.2
3
+ Version: 1.9.0
4
4
  Summary: NoCFO CLI, MCP server, and Cursor skill toolkit.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "nocfo-cli"
7
- version = "1.7.2"
7
+ version = "1.9.0"
8
8
  description = "NoCFO CLI, MCP server, and Cursor skill toolkit."
9
9
  authors = ["NoCFO"]
10
10
  readme = "README.md"
@@ -0,0 +1,95 @@
1
+ """Sequential, best-effort batch runner for mutating curated MCP tools.
2
+
3
+ One tool call targets many resources, so one confirmation covers the whole batch.
4
+ Each target runs the tool's existing single-target logic; a failing target is
5
+ captured as a failed result instead of aborting the batch.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from collections.abc import Awaitable, Callable, Sequence
12
+ from typing import Any
13
+
14
+ from fastmcp.exceptions import ToolError
15
+
16
+ from nocfo_toolkit.mcp.curated.errors import raise_tool_error
17
+ from nocfo_toolkit.mcp.curated.schemas import (
18
+ BatchItemResult,
19
+ BatchResponse,
20
+ ToolErrorPayload,
21
+ dump_model,
22
+ )
23
+
24
+
25
+ def _error_payload(exc: ToolError) -> ToolErrorPayload:
26
+ raw = exc.args[0] if exc.args else str(exc)
27
+ try:
28
+ data = json.loads(raw)
29
+ except (TypeError, ValueError):
30
+ data = None
31
+ if isinstance(data, dict):
32
+ try:
33
+ return ToolErrorPayload.model_validate(data)
34
+ except Exception: # noqa: BLE001 - fall back to a plain message payload
35
+ pass
36
+ return ToolErrorPayload(error_type="tool_error", message=str(raw))
37
+
38
+
39
+ async def run_batch(
40
+ targets: Sequence[Any],
41
+ handler: Callable[[Any], Awaitable[dict[str, Any]]],
42
+ *,
43
+ label: Callable[[Any], Any] = lambda target: target,
44
+ ) -> dict[str, Any]:
45
+ """Run ``handler`` for each target, aggregating per-target results.
46
+
47
+ ``handler`` returns the same dict a single-target tool would return.
48
+ ``label`` maps a target to the value echoed back in ``results[].target``.
49
+ """
50
+ if not targets:
51
+ raise_tool_error(
52
+ "invalid_request",
53
+ "No targets provided for this batch operation.",
54
+ "Provide at least one target (id, number, tool_handle, or payload).",
55
+ status_code=400,
56
+ )
57
+ results: list[BatchItemResult] = []
58
+ for target in targets:
59
+ # Resolve the echo key once, fault-isolated, so a misbehaving label callable
60
+ # can never abort the batch or mislabel a target across branches.
61
+ try:
62
+ key = label(target)
63
+ except Exception: # noqa: BLE001 - labelling must never abort the batch
64
+ key = None
65
+ try:
66
+ payload = await handler(target)
67
+ results.append(BatchItemResult(ok=True, target=key, result=payload))
68
+ except ToolError as exc:
69
+ results.append(
70
+ BatchItemResult(ok=False, target=key, error=_error_payload(exc))
71
+ )
72
+ except Exception as exc: # noqa: BLE001 - isolate unexpected per-target failures
73
+ # Keep the batch best-effort: a non-ToolError (e.g. response validation
74
+ # or transport error) on one target must not discard the results of the
75
+ # targets already processed. CancelledError/KeyboardInterrupt are
76
+ # BaseException and intentionally still propagate.
77
+ results.append(
78
+ BatchItemResult(
79
+ ok=False,
80
+ target=key,
81
+ error=ToolErrorPayload(
82
+ error_type="internal_error",
83
+ message=str(exc) or exc.__class__.__name__,
84
+ ),
85
+ )
86
+ )
87
+ succeeded = sum(1 for item in results if item.ok)
88
+ return dump_model(
89
+ BatchResponse(
90
+ total=len(results),
91
+ succeeded=succeeded,
92
+ failed=len(results) - succeeded,
93
+ results=results,
94
+ )
95
+ )
@@ -0,0 +1,228 @@
1
+ """Bookkeeping account MCP tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from fastmcp.tools import tool
8
+ from fastmcp.tools.tool import ToolAnnotations
9
+ from nocfo_toolkit.mcp.curated.batch import run_batch
10
+ from nocfo_toolkit.mcp.curated.runtime import business_slug, get_client
11
+ from nocfo_toolkit.mcp.curated.schemas import (
12
+ AccountListItem,
13
+ AccountListInput,
14
+ AccountNumbersActionInput,
15
+ AccountNumbersInput,
16
+ AccountNumbersPayloadInput,
17
+ AccountRetrieveInput,
18
+ AccountSummary,
19
+ ActionResponse,
20
+ BatchResponse,
21
+ DeletedResponse,
22
+ ListEnvelope,
23
+ PayloadsInput,
24
+ dump_model,
25
+ dump_model_from_backend,
26
+ )
27
+ from nocfo_toolkit.mcp.curated.utils import decode_tool_handle
28
+
29
+
30
+ account_fields = (
31
+ "number",
32
+ "name",
33
+ "type",
34
+ "description",
35
+ "is_shown",
36
+ "is_used",
37
+ "balance",
38
+ "header_id",
39
+ "header_path",
40
+ "default_vat_code",
41
+ "default_vat_rate",
42
+ )
43
+
44
+
45
+ @tool(
46
+ name="bookkeeping_accounts_list",
47
+ annotations=ToolAnnotations(
48
+ readOnlyHint=True,
49
+ destructiveHint=False,
50
+ idempotentHint=True,
51
+ openWorldHint=False,
52
+ ),
53
+ description="List bookkeeping accounts by account number, account range, name query, type, usage, or visibility. Use this first to ground exact account numbers/tool_handles before updating, deleting, showing, or hiding accounts. Use account numbers when talking with users.",
54
+ output_schema=ListEnvelope[AccountListItem].model_json_schema(),
55
+ )
56
+ async def bookkeeping_accounts_list(params: AccountListInput) -> dict[str, Any]:
57
+ args = params
58
+ slug = await business_slug(args.business)
59
+ return await get_client().list_page(
60
+ f"/v1/business/{slug}/account/",
61
+ params=args.query_params(),
62
+ cursor=args.cursor,
63
+ limit=args.limit,
64
+ business_slug=slug,
65
+ fields=account_fields,
66
+ item_model=AccountListItem,
67
+ handle_resource="bookkeeping_account",
68
+ usage_hint=(
69
+ "For account number lookup (e.g. 1910), set number filter and then use tool_handle with "
70
+ "bookkeeping_account_retrieve. Before update/delete/show/hide actions, ground the exact targets here "
71
+ "first and then batch the confirmed account numbers into one mutation call."
72
+ ),
73
+ )
74
+
75
+
76
+ @tool(
77
+ name="bookkeeping_account_retrieve",
78
+ annotations=ToolAnnotations(
79
+ readOnlyHint=True,
80
+ destructiveHint=False,
81
+ idempotentHint=True,
82
+ openWorldHint=False,
83
+ ),
84
+ description="Retrieve one account from bookkeeping_accounts_list.items[].tool_handle. Use this to verify an account before updating, deleting, showing, or hiding it.",
85
+ )
86
+ async def bookkeeping_account_retrieve(
87
+ params: AccountRetrieveInput,
88
+ ) -> dict[str, Any]:
89
+ args = params
90
+ slug = await business_slug(args.business)
91
+ account_id = decode_tool_handle(
92
+ args.tool_handle,
93
+ expected_resource="bookkeeping_account",
94
+ )
95
+ result = await get_client().request(
96
+ "GET",
97
+ f"/v1/business/{slug}/account/{account_id}/",
98
+ business_slug=slug,
99
+ )
100
+ return dump_model_from_backend(AccountSummary, result)
101
+
102
+
103
+ @tool(
104
+ name="bookkeeping_account_create",
105
+ annotations=ToolAnnotations(
106
+ readOnlyHint=False,
107
+ destructiveHint=False,
108
+ idempotentHint=False,
109
+ openWorldHint=False,
110
+ ),
111
+ description="Create one or more bookkeeping accounts in a single call — pass each new account as an entry in payloads. Use account numbers and names that match the user request.",
112
+ output_schema=BatchResponse.model_json_schema(),
113
+ )
114
+ async def bookkeeping_account_create(params: PayloadsInput) -> dict[str, Any]:
115
+ slug = await business_slug(params.business)
116
+ path = f"/v1/business/{slug}/account/"
117
+
118
+ async def _create(payload: dict[str, Any]) -> dict[str, Any]:
119
+ result = await get_client().request(
120
+ "POST", path, json_body=payload, business_slug=slug
121
+ )
122
+ return dump_model_from_backend(AccountSummary, result)
123
+
124
+ return await run_batch(
125
+ params.payloads,
126
+ _create,
127
+ label=lambda payload: payload.get("number") or payload.get("name"),
128
+ )
129
+
130
+
131
+ @tool(
132
+ name="bookkeeping_account_update",
133
+ annotations=ToolAnnotations(
134
+ readOnlyHint=False,
135
+ destructiveHint=False,
136
+ idempotentHint=False,
137
+ openWorldHint=False,
138
+ ),
139
+ description="Update one or more bookkeeping accounts selected by account_numbers; the same payload is applied to every account. Ground the exact account numbers first, then batch all confirmed targets into one call.",
140
+ output_schema=BatchResponse.model_json_schema(),
141
+ )
142
+ async def bookkeeping_account_update(
143
+ params: AccountNumbersPayloadInput,
144
+ ) -> dict[str, Any]:
145
+ slug = await business_slug(params.business)
146
+
147
+ async def _update(account_number: int) -> dict[str, Any]:
148
+ account_id = await get_client().resolve_id(
149
+ f"/v1/business/{slug}/account/",
150
+ lookup_field="number",
151
+ lookup_value=account_number,
152
+ business_slug=slug,
153
+ )
154
+ result = await get_client().request(
155
+ "PATCH",
156
+ f"/v1/business/{slug}/account/{account_id}/",
157
+ json_body=params.payload,
158
+ business_slug=slug,
159
+ )
160
+ return dump_model_from_backend(AccountSummary, result)
161
+
162
+ return await run_batch(params.account_numbers, _update)
163
+
164
+
165
+ @tool(
166
+ name="bookkeeping_account_delete",
167
+ annotations=ToolAnnotations(
168
+ readOnlyHint=False,
169
+ destructiveHint=True,
170
+ idempotentHint=False,
171
+ openWorldHint=False,
172
+ ),
173
+ description="Delete one or more bookkeeping accounts in a single call — pass every target in account_numbers. Ground the exact account numbers first with bookkeeping_accounts_list and/or bookkeeping_account_retrieve, then batch all confirmed targets into one call. Never call this with guessed placeholders or an empty target set. Prefer one batched call over repeated single-target calls (each call needs its own confirmation).",
174
+ output_schema=BatchResponse.model_json_schema(),
175
+ )
176
+ async def bookkeeping_account_delete(params: AccountNumbersInput) -> dict[str, Any]:
177
+ slug = await business_slug(params.business)
178
+
179
+ async def _delete(account_number: int) -> dict[str, Any]:
180
+ account_id = await get_client().resolve_id(
181
+ f"/v1/business/{slug}/account/",
182
+ lookup_field="number",
183
+ lookup_value=account_number,
184
+ business_slug=slug,
185
+ )
186
+ await get_client().request(
187
+ "DELETE",
188
+ f"/v1/business/{slug}/account/{account_id}/",
189
+ business_slug=slug,
190
+ )
191
+ return dump_model(DeletedResponse(account_number=account_number))
192
+
193
+ return await run_batch(params.account_numbers, _delete)
194
+
195
+
196
+ @tool(
197
+ name="bookkeeping_account_action",
198
+ annotations=ToolAnnotations(
199
+ readOnlyHint=False,
200
+ destructiveHint=False,
201
+ idempotentHint=False,
202
+ openWorldHint=False,
203
+ ),
204
+ description="Show or hide one or more bookkeeping accounts in a single call — pass every target in account_numbers and the same action applies to all. First obtain the exact account numbers from bookkeeping_accounts_list or bookkeeping_account_retrieve, then pass those values unchanged here. Prefer one batched call over repeated single-account calls.",
205
+ output_schema=BatchResponse.model_json_schema(),
206
+ )
207
+ async def bookkeeping_account_action(
208
+ params: AccountNumbersActionInput,
209
+ ) -> dict[str, Any]:
210
+ slug = await business_slug(params.business)
211
+
212
+ async def _act(account_number: int) -> dict[str, Any]:
213
+ account_id = await get_client().resolve_id(
214
+ f"/v1/business/{slug}/account/",
215
+ lookup_field="number",
216
+ lookup_value=account_number,
217
+ business_slug=slug,
218
+ )
219
+ await get_client().request(
220
+ "POST",
221
+ f"/v1/business/{slug}/account/{account_id}/{params.action.value}/",
222
+ business_slug=slug,
223
+ )
224
+ return dump_model(
225
+ ActionResponse(account_number=account_number, action=params.action.value)
226
+ )
227
+
228
+ return await run_batch(params.account_numbers, _act)