flowbase-cli 0.1.4__tar.gz → 0.1.6__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.
- {flowbase_cli-0.1.4 → flowbase_cli-0.1.6}/PKG-INFO +1 -1
- {flowbase_cli-0.1.4 → flowbase_cli-0.1.6}/pyproject.toml +1 -1
- {flowbase_cli-0.1.4 → flowbase_cli-0.1.6}/src/flowbase_cli/__init__.py +1 -1
- {flowbase_cli-0.1.4 → flowbase_cli-0.1.6}/src/flowbase_cli/endpoints.py +154 -0
- {flowbase_cli-0.1.4 → flowbase_cli-0.1.6}/src/flowbase_cli/main.py +352 -4
- {flowbase_cli-0.1.4 → flowbase_cli-0.1.6}/src/flowbase_cli.egg-info/PKG-INFO +1 -1
- {flowbase_cli-0.1.4 → flowbase_cli-0.1.6}/tests/test_cli.py +141 -0
- {flowbase_cli-0.1.4 → flowbase_cli-0.1.6}/README.md +0 -0
- {flowbase_cli-0.1.4 → flowbase_cli-0.1.6}/setup.cfg +0 -0
- {flowbase_cli-0.1.4 → flowbase_cli-0.1.6}/src/flowbase_cli.egg-info/SOURCES.txt +0 -0
- {flowbase_cli-0.1.4 → flowbase_cli-0.1.6}/src/flowbase_cli.egg-info/dependency_links.txt +0 -0
- {flowbase_cli-0.1.4 → flowbase_cli-0.1.6}/src/flowbase_cli.egg-info/entry_points.txt +0 -0
- {flowbase_cli-0.1.4 → flowbase_cli-0.1.6}/src/flowbase_cli.egg-info/top_level.txt +0 -0
|
@@ -20,6 +20,86 @@ class Endpoint:
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
ENDPOINTS = (
|
|
23
|
+
Endpoint("apps.list", "flowbase_apps_list", "GET", "/apps", "metadata:read", "List apps.", "flowbase apps list"),
|
|
24
|
+
Endpoint("apps.get", "flowbase_apps_get", "GET", "/apps/{app_id}", "metadata:read", "Get app metadata with groups and objects.", "flowbase apps get app_xxx"),
|
|
25
|
+
Endpoint("apps.create", "flowbase_apps_create", "POST", "/apps", "metadata:write", "Create an app draft.", "flowbase apps create --json-file app.json"),
|
|
26
|
+
Endpoint("apps.update", "flowbase_apps_update", "PUT", "/apps/{app_id}", "metadata:write", "Update an app draft.", "flowbase apps update app_xxx --label CRM --color '#2563EB'"),
|
|
27
|
+
Endpoint("apps.set-enabled", "flowbase_apps_set_enabled", "POST", "/apps/{app_id}/enabled", "metadata:write", "Enable or disable an app.", "flowbase apps set-enabled app_xxx --enabled --yes", True),
|
|
28
|
+
Endpoint("apps.delete", "flowbase_apps_delete", "DELETE", "/apps/{app_id}", "metadata:write", "Delete an app (unlinks objects).", "flowbase apps delete app_xxx --yes", True),
|
|
29
|
+
Endpoint(
|
|
30
|
+
"apps.groups.create",
|
|
31
|
+
"flowbase_apps_groups_create",
|
|
32
|
+
"POST",
|
|
33
|
+
"/apps/{app_id}/groups",
|
|
34
|
+
"metadata:write",
|
|
35
|
+
"Create an app group draft.",
|
|
36
|
+
"flowbase groups create app_xxx --name Sales --icon IconFolder",
|
|
37
|
+
),
|
|
38
|
+
Endpoint(
|
|
39
|
+
"apps.groups.reorder",
|
|
40
|
+
"flowbase_apps_groups_reorder",
|
|
41
|
+
"PUT",
|
|
42
|
+
"/apps/{app_id}/groups/order",
|
|
43
|
+
"metadata:write",
|
|
44
|
+
"Reorder app groups.",
|
|
45
|
+
"flowbase groups reorder app_xxx --json '{\"groupIds\":[\"grp_a\",\"grp_b\"]}'",
|
|
46
|
+
),
|
|
47
|
+
Endpoint(
|
|
48
|
+
"apps.groups.update",
|
|
49
|
+
"flowbase_apps_groups_update",
|
|
50
|
+
"PUT",
|
|
51
|
+
"/apps/{app_id}/groups/{group_id}",
|
|
52
|
+
"metadata:write",
|
|
53
|
+
"Update an app group draft.",
|
|
54
|
+
"flowbase groups update app_xxx grp_xxx --name Sales",
|
|
55
|
+
),
|
|
56
|
+
Endpoint(
|
|
57
|
+
"apps.groups.delete",
|
|
58
|
+
"flowbase_apps_groups_delete",
|
|
59
|
+
"DELETE",
|
|
60
|
+
"/apps/{app_id}/groups/{group_id}",
|
|
61
|
+
"metadata:write",
|
|
62
|
+
"Delete an app group (objects become ungrouped).",
|
|
63
|
+
"flowbase groups delete app_xxx grp_xxx --yes",
|
|
64
|
+
True,
|
|
65
|
+
),
|
|
66
|
+
Endpoint(
|
|
67
|
+
"apps.objects.add",
|
|
68
|
+
"flowbase_apps_objects_add",
|
|
69
|
+
"POST",
|
|
70
|
+
"/apps/{app_id}/objects",
|
|
71
|
+
"metadata:write",
|
|
72
|
+
"Add objects to an app (association only).",
|
|
73
|
+
"flowbase apps add-objects app_xxx --object-ids obj_a,obj_b",
|
|
74
|
+
),
|
|
75
|
+
Endpoint(
|
|
76
|
+
"apps.objects.reorder",
|
|
77
|
+
"flowbase_apps_objects_reorder",
|
|
78
|
+
"PUT",
|
|
79
|
+
"/apps/{app_id}/objects/order",
|
|
80
|
+
"metadata:write",
|
|
81
|
+
"Reorder app-linked objects.",
|
|
82
|
+
"flowbase apps reorder-objects app_xxx --json '{\"objectIds\":[\"obj_a\",\"obj_b\"]}'",
|
|
83
|
+
),
|
|
84
|
+
Endpoint(
|
|
85
|
+
"apps.objects.remove",
|
|
86
|
+
"flowbase_apps_objects_remove",
|
|
87
|
+
"DELETE",
|
|
88
|
+
"/apps/{app_id}/objects/{object_id}",
|
|
89
|
+
"metadata:write",
|
|
90
|
+
"Remove an object association from an app.",
|
|
91
|
+
"flowbase apps remove-object app_xxx obj_xxx --yes",
|
|
92
|
+
True,
|
|
93
|
+
),
|
|
94
|
+
Endpoint(
|
|
95
|
+
"apps.objects.set-group",
|
|
96
|
+
"flowbase_apps_objects_set_group",
|
|
97
|
+
"POST",
|
|
98
|
+
"/apps/{app_id}/objects/{object_id}/group",
|
|
99
|
+
"metadata:write",
|
|
100
|
+
"Move an app-linked object into a group or ungroup it.",
|
|
101
|
+
"flowbase apps set-group app_xxx obj_xxx --group-id grp_xxx",
|
|
102
|
+
),
|
|
23
103
|
Endpoint("objects.list", "flowbase_objects_list", "GET", "/objects", "metadata:read", "List object metadata.", "flowbase objects list"),
|
|
24
104
|
Endpoint("objects.get", "flowbase_objects_get", "GET", "/objects/{object_id}", "metadata:read", "Get object and field metadata.", "flowbase objects get obj_xxx"),
|
|
25
105
|
Endpoint("objects.create", "flowbase_objects_create", "POST", "/objects", "metadata:write", "Create an object draft.", "flowbase objects create --json-file object.json"),
|
|
@@ -36,6 +116,14 @@ ENDPOINTS = (
|
|
|
36
116
|
Endpoint("views.reorder", "flowbase_views_reorder", "PUT", "/objects/{object_id}/views/order", "metadata:write", "Reorder object views.", "flowbase views reorder obj_xxx --json '{\"baseRevision\":4,\"viewIds\":[\"viw_xxx\"]}'"),
|
|
37
117
|
Endpoint("views.set-enabled", "flowbase_views_set_enabled", "POST", "/objects/{object_id}/views/{view_id}/enabled", "metadata:write", "Enable or disable an object view.", "flowbase views set-enabled obj_xxx viw_xxx --base-revision 4 --enabled --yes", True),
|
|
38
118
|
Endpoint("views.delete", "flowbase_views_delete", "DELETE", "/objects/{object_id}/views/{view_id}", "metadata:write", "Logically delete an object view.", "flowbase views delete obj_xxx viw_xxx --base-revision 4 --yes", True),
|
|
119
|
+
Endpoint("workflows.list", "flowbase_workflows_list", "GET", "/objects/{object_id}/workflows", "metadata:read", "List object workflows.", "flowbase workflows list obj_xxx"),
|
|
120
|
+
Endpoint("workflows.get", "flowbase_workflows_get", "GET", "/objects/{object_id}/workflows/{workflow_id}", "metadata:read", "Get a workflow graph.", "flowbase workflows get obj_xxx wfl_xxx"),
|
|
121
|
+
Endpoint("workflows.create", "flowbase_workflows_create", "POST", "/objects/{object_id}/workflows", "metadata:write", "Create a workflow draft.", "flowbase workflows create obj_xxx --json-file workflow.json"),
|
|
122
|
+
Endpoint("workflows.update", "flowbase_workflows_update", "PUT", "/objects/{object_id}/workflows/{workflow_id}", "metadata:write", "Update a workflow graph.", "flowbase workflows update obj_xxx wfl_xxx --json-file workflow.json"),
|
|
123
|
+
Endpoint("workflows.reorder", "flowbase_workflows_reorder", "PUT", "/objects/{object_id}/workflows/order", "metadata:write", "Reorder object workflows.", "flowbase workflows reorder obj_xxx --json '{\"baseRevision\":4,\"workflowIds\":[\"wfl_a\",\"wfl_b\"]}'"),
|
|
124
|
+
Endpoint("workflows.set-enabled", "flowbase_workflows_set_enabled", "POST", "/objects/{object_id}/workflows/{workflow_id}/enabled", "metadata:write", "Enable or disable a workflow.", "flowbase workflows set-enabled obj_xxx wfl_xxx --base-revision 4 --enabled --yes", True),
|
|
125
|
+
Endpoint("workflows.delete", "flowbase_workflows_delete", "POST", "/objects/{object_id}/workflows/{workflow_id}/delete", "metadata:write", "Logically delete a workflow.", "flowbase workflows delete obj_xxx wfl_xxx --base-revision 4 --yes", True),
|
|
126
|
+
Endpoint("workflows.executions", "flowbase_workflows_executions", "GET", "/objects/{object_id}/workflows/{workflow_id}/executions", "metadata:read", "List workflow execution records.", "flowbase workflows executions obj_xxx wfl_xxx --result failure"),
|
|
39
127
|
Endpoint("pages.list", "flowbase_pages_list", "GET", "/pages", "metadata:read", "List custom Page metadata.", "flowbase pages list --object-id obj_xxx"),
|
|
40
128
|
Endpoint("pages.options", "flowbase_pages_options", "GET", "/pages/options", "metadata:read", "Get custom Page design options.", "flowbase pages options"),
|
|
41
129
|
Endpoint("pages.get", "flowbase_pages_get", "GET", "/pages/{page_id}", "metadata:read", "Get a custom Page.", "flowbase pages get pag_xxx"),
|
|
@@ -97,9 +185,75 @@ ENDPOINTS = (
|
|
|
97
185
|
),
|
|
98
186
|
Endpoint("records.list", "flowbase_records_list", "GET", "/objects/{object_id}/records", "records:read", "List records.", "flowbase records list obj_xxx --page 1"),
|
|
99
187
|
Endpoint("records.get", "flowbase_records_get", "GET", "/objects/{object_id}/records/{record_id}", "records:read", "Get a record.", "flowbase records get obj_xxx rec_xxx"),
|
|
188
|
+
Endpoint("records.upload-attachment", "flowbase_records_upload_attachment", "POST", "/objects/{object_id}/fields/{field_id}/attachments", "records:write", "Upload an attachment before writing its ID to a record.", "flowbase records upload-attachment obj_xxx fld_xxx ./document.pdf"),
|
|
189
|
+
Endpoint("records.download-attachment", "flowbase_records_download_attachment", "GET", "/objects/{object_id}/records/{record_id}/attachments/{attachment_id}", "records:read", "Download an attachment bound to a record.", "flowbase records download-attachment obj_xxx rec_xxx att_xxx --output ./document.pdf"),
|
|
100
190
|
Endpoint("records.create", "flowbase_records_create", "POST", "/objects/{object_id}/records", "records:write", "Create a record.", "flowbase records create obj_xxx --json-file record.json"),
|
|
101
191
|
Endpoint("records.update", "flowbase_records_update", "PUT", "/objects/{object_id}/records/{record_id}", "records:write", "Replace active field values on a record.", "flowbase records update obj_xxx rec_xxx --json-file record.json"),
|
|
102
192
|
Endpoint("records.delete", "flowbase_records_delete", "DELETE", "/objects/{object_id}/records/{record_id}", "records:write", "Delete a record.", "flowbase records delete obj_xxx rec_xxx --yes", True),
|
|
193
|
+
Endpoint(
|
|
194
|
+
"roles.list",
|
|
195
|
+
"flowbase_roles_list",
|
|
196
|
+
"GET",
|
|
197
|
+
"/roles",
|
|
198
|
+
"metadata:read",
|
|
199
|
+
"List organization roles.",
|
|
200
|
+
"flowbase roles list --page 1",
|
|
201
|
+
environments=("production",),
|
|
202
|
+
),
|
|
203
|
+
Endpoint(
|
|
204
|
+
"roles.options",
|
|
205
|
+
"flowbase_roles_options",
|
|
206
|
+
"GET",
|
|
207
|
+
"/roles/options",
|
|
208
|
+
"metadata:read",
|
|
209
|
+
"List role editor option sources (apps and objects).",
|
|
210
|
+
"flowbase roles options",
|
|
211
|
+
environments=("production",),
|
|
212
|
+
),
|
|
213
|
+
Endpoint(
|
|
214
|
+
"roles.get",
|
|
215
|
+
"flowbase_roles_get",
|
|
216
|
+
"GET",
|
|
217
|
+
"/roles/{role_id}",
|
|
218
|
+
"metadata:read",
|
|
219
|
+
"Get a role with permissions and members.",
|
|
220
|
+
"flowbase roles get rol_xxx",
|
|
221
|
+
environments=("production",),
|
|
222
|
+
),
|
|
223
|
+
Endpoint(
|
|
224
|
+
"roles.create",
|
|
225
|
+
"flowbase_roles_create",
|
|
226
|
+
"POST",
|
|
227
|
+
"/roles",
|
|
228
|
+
"metadata:write",
|
|
229
|
+
"Create a custom role.",
|
|
230
|
+
"flowbase roles create --json-file role.json",
|
|
231
|
+
environments=("production",),
|
|
232
|
+
audit_event="role.created",
|
|
233
|
+
),
|
|
234
|
+
Endpoint(
|
|
235
|
+
"roles.update",
|
|
236
|
+
"flowbase_roles_update",
|
|
237
|
+
"PUT",
|
|
238
|
+
"/roles/{role_id}",
|
|
239
|
+
"metadata:write",
|
|
240
|
+
"Update a custom role.",
|
|
241
|
+
"flowbase roles update rol_xxx --json-file role.json",
|
|
242
|
+
environments=("production",),
|
|
243
|
+
audit_event="role.updated",
|
|
244
|
+
),
|
|
245
|
+
Endpoint(
|
|
246
|
+
"roles.delete",
|
|
247
|
+
"flowbase_roles_delete",
|
|
248
|
+
"DELETE",
|
|
249
|
+
"/roles/{role_id}",
|
|
250
|
+
"metadata:write",
|
|
251
|
+
"Delete a custom role.",
|
|
252
|
+
"flowbase roles delete rol_xxx --yes",
|
|
253
|
+
True,
|
|
254
|
+
environments=("production",),
|
|
255
|
+
audit_event="role.deleted",
|
|
256
|
+
),
|
|
103
257
|
)
|
|
104
258
|
|
|
105
259
|
ENDPOINT_BY_KEY = {endpoint.key: endpoint for endpoint in ENDPOINTS}
|
|
@@ -4,7 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
import argparse
|
|
5
5
|
from decimal import Decimal
|
|
6
6
|
import json
|
|
7
|
+
import mimetypes
|
|
7
8
|
import os
|
|
9
|
+
import secrets
|
|
8
10
|
import sys
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
from typing import Any
|
|
@@ -97,20 +99,27 @@ class OpenApiClient:
|
|
|
97
99
|
path_params: dict[str, Any] | None = None,
|
|
98
100
|
query: dict[str, Any] | None = None,
|
|
99
101
|
body: dict[str, Any] | None = None,
|
|
102
|
+
raw_body: bytes | None = None,
|
|
103
|
+
content_type: str | None = None,
|
|
104
|
+
accept: str = "application/json",
|
|
100
105
|
) -> bytes:
|
|
101
106
|
endpoint = get_endpoint(endpoint_key)
|
|
102
107
|
if not self.api_key:
|
|
103
108
|
raise CliError(f"Missing API key. Set {ENV_API_KEY} or pass --api-key.")
|
|
104
109
|
url = self.build_url(endpoint, path_params, query)
|
|
105
110
|
headers = {
|
|
106
|
-
"Accept":
|
|
111
|
+
"Accept": accept,
|
|
107
112
|
"Authorization": f"Bearer {self.api_key}",
|
|
108
113
|
"X-FlowBase-Channel": "cli",
|
|
109
114
|
}
|
|
110
|
-
|
|
115
|
+
if body is not None and raw_body is not None:
|
|
116
|
+
raise CliError("JSON and raw request bodies cannot be used together.")
|
|
117
|
+
data = raw_body
|
|
111
118
|
if body is not None:
|
|
112
119
|
data = exact_json_dumps(body).encode("utf-8")
|
|
113
120
|
headers["Content-Type"] = "application/json"
|
|
121
|
+
elif raw_body is not None and content_type:
|
|
122
|
+
headers["Content-Type"] = content_type
|
|
114
123
|
request = Request(url, data=data, headers=headers, method=endpoint.method)
|
|
115
124
|
try:
|
|
116
125
|
with urlopen(request) as response: # noqa: S310 - the user selects the FlowBase host.
|
|
@@ -164,6 +173,30 @@ def exact_json_dumps(value: Any) -> str:
|
|
|
164
173
|
raise CliError(f"Unsupported JSON value: {type(value).__name__}")
|
|
165
174
|
|
|
166
175
|
|
|
176
|
+
def multipart_upload(path_value: str) -> tuple[bytes, str]:
|
|
177
|
+
path = Path(path_value)
|
|
178
|
+
try:
|
|
179
|
+
content = path.read_bytes()
|
|
180
|
+
except OSError as exc:
|
|
181
|
+
raise CliError(f"Cannot read attachment: {exc}") from exc
|
|
182
|
+
filename = path.name.replace('"', "_").replace("\r", "_").replace("\n", "_") or "file"
|
|
183
|
+
boundary = f"flowbase-{secrets.token_hex(16)}"
|
|
184
|
+
content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
|
185
|
+
ascii_name = filename.encode("ascii", errors="replace").decode("ascii")
|
|
186
|
+
disposition = (
|
|
187
|
+
f'Content-Disposition: form-data; name="upload"; filename="{ascii_name}"; '
|
|
188
|
+
f"filename*=UTF-8''{quote(filename, safe='')}\r\n"
|
|
189
|
+
).encode("ascii")
|
|
190
|
+
payload = b"".join([
|
|
191
|
+
f"--{boundary}\r\n".encode("ascii"),
|
|
192
|
+
disposition,
|
|
193
|
+
f"Content-Type: {content_type}\r\n\r\n".encode("ascii"),
|
|
194
|
+
content,
|
|
195
|
+
f"\r\n--{boundary}--\r\n".encode("ascii"),
|
|
196
|
+
])
|
|
197
|
+
return payload, f"multipart/form-data; boundary={boundary}"
|
|
198
|
+
|
|
199
|
+
|
|
167
200
|
def print_output(data: bytes, *, pretty: bool, raw: bool) -> None:
|
|
168
201
|
text = data.decode("utf-8", errors="replace")
|
|
169
202
|
if raw:
|
|
@@ -276,6 +309,90 @@ def _payload_from_args(args: argparse.Namespace, direct: dict[str, Any], require
|
|
|
276
309
|
return payload
|
|
277
310
|
|
|
278
311
|
|
|
312
|
+
def handle_apps(args: argparse.Namespace) -> int:
|
|
313
|
+
action = args.apps_action
|
|
314
|
+
if action == "list":
|
|
315
|
+
return run_request(args, "apps.list")
|
|
316
|
+
if action == "get":
|
|
317
|
+
return run_request(args, "apps.get", path_params={"app_id": args.app_id})
|
|
318
|
+
if action == "create":
|
|
319
|
+
body = _payload_from_args(
|
|
320
|
+
args,
|
|
321
|
+
{"label": args.label, "key": args.key, "icon": args.icon, "color": args.color},
|
|
322
|
+
("label", "key"),
|
|
323
|
+
)
|
|
324
|
+
return run_request(args, "apps.create", body=body)
|
|
325
|
+
if action == "update":
|
|
326
|
+
body = _payload_from_args(
|
|
327
|
+
args,
|
|
328
|
+
{"label": args.label, "icon": args.icon, "color": args.color},
|
|
329
|
+
("label", "icon", "color"),
|
|
330
|
+
)
|
|
331
|
+
return run_request(args, "apps.update", path_params={"app_id": args.app_id}, body=body)
|
|
332
|
+
if action == "set-enabled":
|
|
333
|
+
return run_request(
|
|
334
|
+
args,
|
|
335
|
+
"apps.set-enabled",
|
|
336
|
+
path_params={"app_id": args.app_id},
|
|
337
|
+
body={"enabled": args.enabled},
|
|
338
|
+
)
|
|
339
|
+
if action == "add-objects":
|
|
340
|
+
ids = [item.strip() for item in (args.object_ids or "").split(",") if item.strip()]
|
|
341
|
+
if not ids:
|
|
342
|
+
raise CliError("Provide --object-ids as a comma-separated list.")
|
|
343
|
+
return run_request(
|
|
344
|
+
args,
|
|
345
|
+
"apps.objects.add",
|
|
346
|
+
path_params={"app_id": args.app_id},
|
|
347
|
+
body={"objectIds": ids},
|
|
348
|
+
)
|
|
349
|
+
if action == "remove-object":
|
|
350
|
+
return run_request(
|
|
351
|
+
args,
|
|
352
|
+
"apps.objects.remove",
|
|
353
|
+
path_params={"app_id": args.app_id, "object_id": args.object_id},
|
|
354
|
+
)
|
|
355
|
+
if action == "set-group":
|
|
356
|
+
if args.ungrouped:
|
|
357
|
+
body = {"groupId": None}
|
|
358
|
+
elif args.group_id is not None:
|
|
359
|
+
body = {"groupId": args.group_id}
|
|
360
|
+
else:
|
|
361
|
+
raise CliError("Provide --group-id or --ungrouped.")
|
|
362
|
+
return run_request(
|
|
363
|
+
args,
|
|
364
|
+
"apps.objects.set-group",
|
|
365
|
+
path_params={"app_id": args.app_id, "object_id": args.object_id},
|
|
366
|
+
body=body,
|
|
367
|
+
)
|
|
368
|
+
if action == "reorder-objects":
|
|
369
|
+
body = _payload_from_args(args, {}, ("objectIds",))
|
|
370
|
+
return run_request(
|
|
371
|
+
args,
|
|
372
|
+
"apps.objects.reorder",
|
|
373
|
+
path_params={"app_id": args.app_id},
|
|
374
|
+
body=body,
|
|
375
|
+
)
|
|
376
|
+
return run_request(args, "apps.delete", path_params={"app_id": args.app_id})
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def handle_groups(args: argparse.Namespace) -> int:
|
|
380
|
+
action = args.groups_action
|
|
381
|
+
path = {"app_id": args.app_id}
|
|
382
|
+
if action == "create":
|
|
383
|
+
body = _payload_from_args(args, {"name": args.name, "icon": args.icon}, ("name",))
|
|
384
|
+
return run_request(args, "apps.groups.create", path_params=path, body=body)
|
|
385
|
+
if action == "reorder":
|
|
386
|
+
body = _payload_from_args(args, {}, ("groupIds",))
|
|
387
|
+
return run_request(args, "apps.groups.reorder", path_params=path, body=body)
|
|
388
|
+
if action == "update":
|
|
389
|
+
path["group_id"] = args.group_id
|
|
390
|
+
body = _payload_from_args(args, {"name": args.name, "icon": args.icon}, ("name", "icon"))
|
|
391
|
+
return run_request(args, "apps.groups.update", path_params=path, body=body)
|
|
392
|
+
path["group_id"] = args.group_id
|
|
393
|
+
return run_request(args, "apps.groups.delete", path_params=path)
|
|
394
|
+
|
|
395
|
+
|
|
279
396
|
def handle_objects(args: argparse.Namespace) -> int:
|
|
280
397
|
action = args.objects_action
|
|
281
398
|
if action == "list":
|
|
@@ -285,12 +402,23 @@ def handle_objects(args: argparse.Namespace) -> int:
|
|
|
285
402
|
if action == "create":
|
|
286
403
|
body = _payload_from_args(
|
|
287
404
|
args,
|
|
288
|
-
{
|
|
405
|
+
{
|
|
406
|
+
"label": args.label,
|
|
407
|
+
"key": args.key,
|
|
408
|
+
"description": args.description,
|
|
409
|
+
},
|
|
289
410
|
("label", "key", "description"),
|
|
290
411
|
)
|
|
291
412
|
return run_request(args, "objects.create", body=body)
|
|
292
413
|
if action == "update":
|
|
293
|
-
body = _payload_from_args(
|
|
414
|
+
body = _payload_from_args(
|
|
415
|
+
args,
|
|
416
|
+
{
|
|
417
|
+
"label": args.label,
|
|
418
|
+
"description": args.description,
|
|
419
|
+
},
|
|
420
|
+
("label", "description"),
|
|
421
|
+
)
|
|
294
422
|
return run_request(args, "objects.update", path_params={"object_id": args.object_id}, body=body)
|
|
295
423
|
return run_request(args, f"objects.{action}", path_params={"object_id": args.object_id})
|
|
296
424
|
|
|
@@ -346,6 +474,46 @@ def handle_views(args: argparse.Namespace) -> int:
|
|
|
346
474
|
)
|
|
347
475
|
|
|
348
476
|
|
|
477
|
+
def handle_workflows(args: argparse.Namespace) -> int:
|
|
478
|
+
action = args.workflows_action
|
|
479
|
+
path = {"object_id": args.object_id}
|
|
480
|
+
if hasattr(args, "workflow_id"):
|
|
481
|
+
path["workflow_id"] = args.workflow_id
|
|
482
|
+
if action == "list":
|
|
483
|
+
return run_request(args, "workflows.list", path_params=path)
|
|
484
|
+
if action == "get":
|
|
485
|
+
return run_request(args, "workflows.get", path_params=path)
|
|
486
|
+
if action == "executions":
|
|
487
|
+
return run_request(
|
|
488
|
+
args,
|
|
489
|
+
"workflows.executions",
|
|
490
|
+
path_params=path,
|
|
491
|
+
query={"result": args.result},
|
|
492
|
+
)
|
|
493
|
+
if action in ("create", "update"):
|
|
494
|
+
body = load_json_payload(args.json, args.json_file)
|
|
495
|
+
require_fields(body, "label")
|
|
496
|
+
return run_request(args, f"workflows.{action}", path_params=path, body=body)
|
|
497
|
+
if action == "reorder":
|
|
498
|
+
body = load_json_payload(args.json, args.json_file)
|
|
499
|
+
require_fields(body, "baseRevision", "workflowIds")
|
|
500
|
+
if not isinstance(body["workflowIds"], list):
|
|
501
|
+
raise CliError("Request field 'workflowIds' must be an array.")
|
|
502
|
+
return run_request(args, "workflows.reorder", path_params=path, body=body)
|
|
503
|
+
if action == "set-enabled":
|
|
504
|
+
body = {
|
|
505
|
+
"baseRevision": args.base_revision,
|
|
506
|
+
"enabled": args.enabled,
|
|
507
|
+
}
|
|
508
|
+
return run_request(args, "workflows.set-enabled", path_params=path, body=body)
|
|
509
|
+
return run_request(
|
|
510
|
+
args,
|
|
511
|
+
"workflows.delete",
|
|
512
|
+
path_params=path,
|
|
513
|
+
body={"baseRevision": args.base_revision},
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
|
|
349
517
|
def handle_pages(args: argparse.Namespace) -> int:
|
|
350
518
|
action = args.pages_action
|
|
351
519
|
if action == "list":
|
|
@@ -513,6 +681,31 @@ def handle_environments(args: argparse.Namespace) -> int:
|
|
|
513
681
|
def handle_records(args: argparse.Namespace) -> int:
|
|
514
682
|
action = args.records_action
|
|
515
683
|
path = {"object_id": args.object_id}
|
|
684
|
+
if action == "upload-attachment":
|
|
685
|
+
body, content_type = multipart_upload(args.file)
|
|
686
|
+
data = client_from_args(args).request(
|
|
687
|
+
"records.upload-attachment",
|
|
688
|
+
path_params={**path, "field_id": args.field_id},
|
|
689
|
+
raw_body=body,
|
|
690
|
+
content_type=content_type,
|
|
691
|
+
)
|
|
692
|
+
print_output(data, pretty=args.pretty, raw=args.raw)
|
|
693
|
+
return 0
|
|
694
|
+
if action == "download-attachment":
|
|
695
|
+
data = client_from_args(args).request(
|
|
696
|
+
"records.download-attachment",
|
|
697
|
+
path_params={
|
|
698
|
+
**path,
|
|
699
|
+
"record_id": args.record_id,
|
|
700
|
+
"attachment_id": args.attachment_id,
|
|
701
|
+
},
|
|
702
|
+
accept="application/octet-stream",
|
|
703
|
+
)
|
|
704
|
+
try:
|
|
705
|
+
Path(args.output).write_bytes(data)
|
|
706
|
+
except OSError as exc:
|
|
707
|
+
raise CliError(f"Cannot write attachment: {exc}") from exc
|
|
708
|
+
return 0
|
|
516
709
|
if hasattr(args, "record_id"):
|
|
517
710
|
path["record_id"] = args.record_id
|
|
518
711
|
if action == "list":
|
|
@@ -526,6 +719,27 @@ def handle_records(args: argparse.Namespace) -> int:
|
|
|
526
719
|
return run_request(args, f"records.{action}", path_params=path, body=body)
|
|
527
720
|
|
|
528
721
|
|
|
722
|
+
def handle_roles(args: argparse.Namespace) -> int:
|
|
723
|
+
action = args.roles_action
|
|
724
|
+
if action == "list":
|
|
725
|
+
return run_request(args, "roles.list", query={"q": args.q or "", "page": args.page})
|
|
726
|
+
if action == "options":
|
|
727
|
+
return run_request(args, "roles.options")
|
|
728
|
+
if action == "get":
|
|
729
|
+
return run_request(args, "roles.get", path_params={"role_id": args.role_id})
|
|
730
|
+
if action == "delete":
|
|
731
|
+
return run_request(args, "roles.delete", path_params={"role_id": args.role_id})
|
|
732
|
+
body = load_json_payload(args.json, args.json_file)
|
|
733
|
+
require_fields(body, "name", "permissions")
|
|
734
|
+
if not isinstance(body["permissions"], dict):
|
|
735
|
+
raise CliError("Request field 'permissions' must be an object.")
|
|
736
|
+
if action == "create":
|
|
737
|
+
return run_request(args, "roles.create", body=body)
|
|
738
|
+
if "updatedAt" not in body and "updated_at" not in body:
|
|
739
|
+
raise CliError("Missing request field(s): updatedAt")
|
|
740
|
+
return run_request(args, "roles.update", path_params={"role_id": args.role_id}, body=body)
|
|
741
|
+
|
|
742
|
+
|
|
529
743
|
def add_json_args(parser: argparse.ArgumentParser) -> None:
|
|
530
744
|
group = parser.add_mutually_exclusive_group()
|
|
531
745
|
group.add_argument("--json", help="Request body as a JSON object.")
|
|
@@ -560,6 +774,77 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
560
774
|
docs_show.add_argument("endpoint", choices=sorted(ENDPOINT_BY_KEY))
|
|
561
775
|
docs.set_defaults(handler=handle_docs)
|
|
562
776
|
|
|
777
|
+
apps = groups.add_parser("apps", help="Manage app metadata.")
|
|
778
|
+
app_actions = apps.add_subparsers(dest="apps_action", required=True)
|
|
779
|
+
app_actions.add_parser("list")
|
|
780
|
+
app_get = app_actions.add_parser("get")
|
|
781
|
+
app_get.add_argument("app_id")
|
|
782
|
+
app_create = app_actions.add_parser("create")
|
|
783
|
+
app_create.add_argument("--label")
|
|
784
|
+
app_create.add_argument("--key")
|
|
785
|
+
app_create.add_argument("--icon")
|
|
786
|
+
app_create.add_argument("--color")
|
|
787
|
+
add_json_args(app_create)
|
|
788
|
+
app_update = app_actions.add_parser("update")
|
|
789
|
+
app_update.add_argument("app_id")
|
|
790
|
+
app_update.add_argument("--label")
|
|
791
|
+
app_update.add_argument("--icon")
|
|
792
|
+
app_update.add_argument("--color")
|
|
793
|
+
add_json_args(app_update)
|
|
794
|
+
app_enabled = app_actions.add_parser("set-enabled")
|
|
795
|
+
app_enabled.add_argument("app_id")
|
|
796
|
+
app_enabled.add_argument(
|
|
797
|
+
"--enabled",
|
|
798
|
+
action=argparse.BooleanOptionalAction,
|
|
799
|
+
required=True,
|
|
800
|
+
)
|
|
801
|
+
add_yes(app_enabled)
|
|
802
|
+
app_delete = app_actions.add_parser("delete")
|
|
803
|
+
app_delete.add_argument("app_id")
|
|
804
|
+
add_yes(app_delete)
|
|
805
|
+
app_add_objects = app_actions.add_parser("add-objects")
|
|
806
|
+
app_add_objects.add_argument("app_id")
|
|
807
|
+
app_add_objects.add_argument("--object-ids", required=True, help="Comma-separated object IDs.")
|
|
808
|
+
app_remove_object = app_actions.add_parser("remove-object")
|
|
809
|
+
app_remove_object.add_argument("app_id")
|
|
810
|
+
app_remove_object.add_argument("object_id")
|
|
811
|
+
add_yes(app_remove_object)
|
|
812
|
+
app_set_group = app_actions.add_parser("set-group")
|
|
813
|
+
app_set_group.add_argument("app_id")
|
|
814
|
+
app_set_group.add_argument("object_id")
|
|
815
|
+
app_set_group.add_argument("--group-id", help="Target group ID. Omit with --ungrouped.")
|
|
816
|
+
app_set_group.add_argument(
|
|
817
|
+
"--ungrouped",
|
|
818
|
+
action="store_true",
|
|
819
|
+
help="Remove the object from its current group in this app.",
|
|
820
|
+
)
|
|
821
|
+
app_reorder_objects = app_actions.add_parser("reorder-objects")
|
|
822
|
+
app_reorder_objects.add_argument("app_id")
|
|
823
|
+
add_json_args(app_reorder_objects)
|
|
824
|
+
apps.set_defaults(handler=handle_apps)
|
|
825
|
+
|
|
826
|
+
app_groups = groups.add_parser("groups", help="Manage app object groups.")
|
|
827
|
+
group_actions = app_groups.add_subparsers(dest="groups_action", required=True)
|
|
828
|
+
group_create = group_actions.add_parser("create")
|
|
829
|
+
group_create.add_argument("app_id")
|
|
830
|
+
group_create.add_argument("--name")
|
|
831
|
+
group_create.add_argument("--icon")
|
|
832
|
+
add_json_args(group_create)
|
|
833
|
+
group_reorder = group_actions.add_parser("reorder")
|
|
834
|
+
group_reorder.add_argument("app_id")
|
|
835
|
+
add_json_args(group_reorder)
|
|
836
|
+
group_update = group_actions.add_parser("update")
|
|
837
|
+
group_update.add_argument("app_id")
|
|
838
|
+
group_update.add_argument("group_id")
|
|
839
|
+
group_update.add_argument("--name")
|
|
840
|
+
group_update.add_argument("--icon")
|
|
841
|
+
add_json_args(group_update)
|
|
842
|
+
group_delete = group_actions.add_parser("delete")
|
|
843
|
+
group_delete.add_argument("app_id")
|
|
844
|
+
group_delete.add_argument("group_id")
|
|
845
|
+
add_yes(group_delete)
|
|
846
|
+
app_groups.set_defaults(handler=handle_groups)
|
|
847
|
+
|
|
563
848
|
objects = groups.add_parser("objects", help="Manage object metadata.")
|
|
564
849
|
object_actions = objects.add_subparsers(dest="objects_action", required=True)
|
|
565
850
|
object_list = object_actions.add_parser("list")
|
|
@@ -622,6 +907,42 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
622
907
|
add_yes(view_delete)
|
|
623
908
|
views.set_defaults(handler=handle_views)
|
|
624
909
|
|
|
910
|
+
workflows = groups.add_parser("workflows", help="Manage object workflow metadata.")
|
|
911
|
+
workflow_actions = workflows.add_subparsers(dest="workflows_action", required=True)
|
|
912
|
+
workflow_list = workflow_actions.add_parser("list")
|
|
913
|
+
workflow_list.add_argument("object_id")
|
|
914
|
+
workflow_get = workflow_actions.add_parser("get")
|
|
915
|
+
workflow_get.add_argument("object_id")
|
|
916
|
+
workflow_get.add_argument("workflow_id")
|
|
917
|
+
for action in ("create", "reorder"):
|
|
918
|
+
item = workflow_actions.add_parser(action)
|
|
919
|
+
item.add_argument("object_id")
|
|
920
|
+
add_json_args(item)
|
|
921
|
+
workflow_update = workflow_actions.add_parser("update")
|
|
922
|
+
workflow_update.add_argument("object_id")
|
|
923
|
+
workflow_update.add_argument("workflow_id")
|
|
924
|
+
add_json_args(workflow_update)
|
|
925
|
+
workflow_enabled = workflow_actions.add_parser("set-enabled")
|
|
926
|
+
workflow_enabled.add_argument("object_id")
|
|
927
|
+
workflow_enabled.add_argument("workflow_id")
|
|
928
|
+
workflow_enabled.add_argument("--base-revision", required=True, type=int)
|
|
929
|
+
workflow_enabled.add_argument(
|
|
930
|
+
"--enabled",
|
|
931
|
+
action=argparse.BooleanOptionalAction,
|
|
932
|
+
required=True,
|
|
933
|
+
)
|
|
934
|
+
add_yes(workflow_enabled)
|
|
935
|
+
workflow_delete = workflow_actions.add_parser("delete")
|
|
936
|
+
workflow_delete.add_argument("object_id")
|
|
937
|
+
workflow_delete.add_argument("workflow_id")
|
|
938
|
+
workflow_delete.add_argument("--base-revision", required=True, type=int)
|
|
939
|
+
add_yes(workflow_delete)
|
|
940
|
+
workflow_executions = workflow_actions.add_parser("executions")
|
|
941
|
+
workflow_executions.add_argument("object_id")
|
|
942
|
+
workflow_executions.add_argument("workflow_id")
|
|
943
|
+
workflow_executions.add_argument("--result", choices=("success", "failure"))
|
|
944
|
+
workflows.set_defaults(handler=handle_workflows)
|
|
945
|
+
|
|
625
946
|
pages = groups.add_parser("pages", help="Manage custom Page metadata.")
|
|
626
947
|
page_actions = pages.add_subparsers(dest="pages_action", required=True)
|
|
627
948
|
page_list = page_actions.add_parser("list")
|
|
@@ -693,6 +1014,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
693
1014
|
record_get = record_actions.add_parser("get")
|
|
694
1015
|
record_get.add_argument("object_id")
|
|
695
1016
|
record_get.add_argument("record_id")
|
|
1017
|
+
record_upload = record_actions.add_parser("upload-attachment")
|
|
1018
|
+
record_upload.add_argument("object_id")
|
|
1019
|
+
record_upload.add_argument("field_id")
|
|
1020
|
+
record_upload.add_argument("file")
|
|
1021
|
+
record_download = record_actions.add_parser("download-attachment")
|
|
1022
|
+
record_download.add_argument("object_id")
|
|
1023
|
+
record_download.add_argument("record_id")
|
|
1024
|
+
record_download.add_argument("attachment_id")
|
|
1025
|
+
record_download.add_argument("--output", required=True)
|
|
696
1026
|
record_create = record_actions.add_parser("create")
|
|
697
1027
|
record_create.add_argument("object_id")
|
|
698
1028
|
add_json_args(record_create)
|
|
@@ -705,6 +1035,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
705
1035
|
record_delete.add_argument("record_id")
|
|
706
1036
|
add_yes(record_delete)
|
|
707
1037
|
records.set_defaults(handler=handle_records)
|
|
1038
|
+
|
|
1039
|
+
roles = groups.add_parser("roles", help="Manage organization roles (production Host only).")
|
|
1040
|
+
role_actions = roles.add_subparsers(dest="roles_action", required=True)
|
|
1041
|
+
role_list = role_actions.add_parser("list")
|
|
1042
|
+
role_list.add_argument("--q", default="", help="Filter by role name.")
|
|
1043
|
+
role_list.add_argument("--page", type=int, default=1)
|
|
1044
|
+
role_actions.add_parser("options")
|
|
1045
|
+
role_get = role_actions.add_parser("get")
|
|
1046
|
+
role_get.add_argument("role_id")
|
|
1047
|
+
role_create = role_actions.add_parser("create")
|
|
1048
|
+
add_json_args(role_create)
|
|
1049
|
+
role_update = role_actions.add_parser("update")
|
|
1050
|
+
role_update.add_argument("role_id")
|
|
1051
|
+
add_json_args(role_update)
|
|
1052
|
+
role_delete = role_actions.add_parser("delete")
|
|
1053
|
+
role_delete.add_argument("role_id")
|
|
1054
|
+
add_yes(role_delete)
|
|
1055
|
+
roles.set_defaults(handler=handle_roles)
|
|
708
1056
|
return parser
|
|
709
1057
|
|
|
710
1058
|
|
|
@@ -89,6 +89,9 @@ class FlowBaseCliTest(unittest.TestCase):
|
|
|
89
89
|
self.assertTrue(any(item["key"] == "versions.get" for item in payload))
|
|
90
90
|
self.assertTrue(any(item["key"] == "pages.create" for item in payload))
|
|
91
91
|
self.assertTrue(any(item["key"] == "views.create" for item in payload))
|
|
92
|
+
self.assertTrue(any(item["key"] == "roles.create" for item in payload))
|
|
93
|
+
role_create = next(item for item in payload if item["key"] == "roles.create")
|
|
94
|
+
self.assertEqual(role_create["environments"], ["production"])
|
|
92
95
|
|
|
93
96
|
def test_view_create_sends_complete_snapshot(self):
|
|
94
97
|
body = {
|
|
@@ -227,6 +230,106 @@ class FlowBaseCliTest(unittest.TestCase):
|
|
|
227
230
|
self.assertEqual(code, 1)
|
|
228
231
|
self.assertIn("description", stderr.getvalue())
|
|
229
232
|
|
|
233
|
+
def test_object_create_without_app_id(self):
|
|
234
|
+
with patch(
|
|
235
|
+
"flowbase_cli.main.OpenApiClient.request",
|
|
236
|
+
return_value=b'{"objectId":"obj_1","draftRevision":2}',
|
|
237
|
+
) as request:
|
|
238
|
+
code = main([
|
|
239
|
+
"--api-key",
|
|
240
|
+
"fbk_test",
|
|
241
|
+
"objects",
|
|
242
|
+
"create",
|
|
243
|
+
"--label",
|
|
244
|
+
"Customer",
|
|
245
|
+
"--key",
|
|
246
|
+
"customer",
|
|
247
|
+
"--description",
|
|
248
|
+
"Customer records",
|
|
249
|
+
])
|
|
250
|
+
self.assertEqual(code, 0)
|
|
251
|
+
self.assertEqual(request.call_args.args[0], "objects.create")
|
|
252
|
+
self.assertEqual(
|
|
253
|
+
request.call_args.kwargs["body"],
|
|
254
|
+
{
|
|
255
|
+
"label": "Customer",
|
|
256
|
+
"key": "customer",
|
|
257
|
+
"description": "Customer records",
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def test_group_create_and_app_set_group(self):
|
|
262
|
+
with patch(
|
|
263
|
+
"flowbase_cli.main.OpenApiClient.request",
|
|
264
|
+
return_value=b'{"groupId":"grp_1","draftRevision":3}',
|
|
265
|
+
) as request:
|
|
266
|
+
code = main([
|
|
267
|
+
"--api-key",
|
|
268
|
+
"fbk_test",
|
|
269
|
+
"groups",
|
|
270
|
+
"create",
|
|
271
|
+
"app_1",
|
|
272
|
+
"--name",
|
|
273
|
+
"Sales",
|
|
274
|
+
"--icon",
|
|
275
|
+
"IconFolder",
|
|
276
|
+
])
|
|
277
|
+
self.assertEqual(code, 0)
|
|
278
|
+
self.assertEqual(request.call_args.args[0], "apps.groups.create")
|
|
279
|
+
self.assertEqual(request.call_args.kwargs["path_params"], {"app_id": "app_1"})
|
|
280
|
+
self.assertEqual(
|
|
281
|
+
request.call_args.kwargs["body"],
|
|
282
|
+
{"name": "Sales", "icon": "IconFolder"},
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
with patch(
|
|
286
|
+
"flowbase_cli.main.OpenApiClient.request",
|
|
287
|
+
return_value=b'{"ok":true,"draftRevision":4}',
|
|
288
|
+
) as request:
|
|
289
|
+
code = main([
|
|
290
|
+
"--api-key",
|
|
291
|
+
"fbk_test",
|
|
292
|
+
"apps",
|
|
293
|
+
"set-group",
|
|
294
|
+
"app_1",
|
|
295
|
+
"obj_1",
|
|
296
|
+
"--group-id",
|
|
297
|
+
"grp_1",
|
|
298
|
+
])
|
|
299
|
+
self.assertEqual(code, 0)
|
|
300
|
+
self.assertEqual(request.call_args.args[0], "apps.objects.set-group")
|
|
301
|
+
self.assertEqual(
|
|
302
|
+
request.call_args.kwargs["path_params"],
|
|
303
|
+
{"app_id": "app_1", "object_id": "obj_1"},
|
|
304
|
+
)
|
|
305
|
+
self.assertEqual(
|
|
306
|
+
request.call_args.kwargs["body"],
|
|
307
|
+
{"groupId": "grp_1"},
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
with patch(
|
|
311
|
+
"flowbase_cli.main.OpenApiClient.request",
|
|
312
|
+
return_value=b'{"ok":true,"draftRevision":5}',
|
|
313
|
+
) as request:
|
|
314
|
+
code = main([
|
|
315
|
+
"--api-key",
|
|
316
|
+
"fbk_test",
|
|
317
|
+
"apps",
|
|
318
|
+
"set-group",
|
|
319
|
+
"app_1",
|
|
320
|
+
"obj_1",
|
|
321
|
+
"--ungrouped",
|
|
322
|
+
])
|
|
323
|
+
self.assertEqual(code, 0)
|
|
324
|
+
self.assertEqual(request.call_args.kwargs["body"], {"groupId": None})
|
|
325
|
+
|
|
326
|
+
def test_group_delete_requires_yes(self):
|
|
327
|
+
stderr = io.StringIO()
|
|
328
|
+
with patch("sys.stderr", stderr), patch("sys.stdin.isatty", return_value=False):
|
|
329
|
+
code = main(["--api-key", "fbk_test", "groups", "delete", "app_1", "grp_1"])
|
|
330
|
+
self.assertEqual(code, 1)
|
|
331
|
+
self.assertIn("without --yes", stderr.getvalue())
|
|
332
|
+
|
|
230
333
|
def test_noninteractive_delete_requires_yes(self):
|
|
231
334
|
stderr = io.StringIO()
|
|
232
335
|
with patch("sys.stderr", stderr), patch("sys.stdin.isatty", return_value=False):
|
|
@@ -234,6 +337,44 @@ class FlowBaseCliTest(unittest.TestCase):
|
|
|
234
337
|
self.assertEqual(code, 1)
|
|
235
338
|
self.assertIn("without --yes", stderr.getvalue())
|
|
236
339
|
|
|
340
|
+
def test_record_attachment_upload_sends_multipart_body(self):
|
|
341
|
+
with TemporaryDirectory() as directory:
|
|
342
|
+
path = Path(directory) / "note.txt"
|
|
343
|
+
path.write_text("hello", encoding="utf-8")
|
|
344
|
+
with patch(
|
|
345
|
+
"flowbase_cli.main.OpenApiClient.request",
|
|
346
|
+
return_value=b'{"id":"att_1"}',
|
|
347
|
+
) as request:
|
|
348
|
+
code = main([
|
|
349
|
+
"--api-key", "fbk_test", "records", "upload-attachment",
|
|
350
|
+
"obj_1", "fld_1", str(path),
|
|
351
|
+
])
|
|
352
|
+
self.assertEqual(code, 0)
|
|
353
|
+
self.assertEqual(request.call_args.args[0], "records.upload-attachment")
|
|
354
|
+
self.assertEqual(
|
|
355
|
+
request.call_args.kwargs["path_params"],
|
|
356
|
+
{"object_id": "obj_1", "field_id": "fld_1"},
|
|
357
|
+
)
|
|
358
|
+
self.assertTrue(request.call_args.kwargs["content_type"].startswith("multipart/form-data; boundary="))
|
|
359
|
+
self.assertIn(b'name="upload"', request.call_args.kwargs["raw_body"])
|
|
360
|
+
self.assertIn(b"hello", request.call_args.kwargs["raw_body"])
|
|
361
|
+
|
|
362
|
+
def test_record_attachment_download_writes_exact_bytes(self):
|
|
363
|
+
with TemporaryDirectory() as directory:
|
|
364
|
+
output = Path(directory) / "download.pdf"
|
|
365
|
+
with patch(
|
|
366
|
+
"flowbase_cli.main.OpenApiClient.request",
|
|
367
|
+
return_value=b"%PDF-1.7",
|
|
368
|
+
) as request:
|
|
369
|
+
code = main([
|
|
370
|
+
"--api-key", "fbk_test", "records", "download-attachment",
|
|
371
|
+
"obj_1", "rec_1", "att_1", "--output", str(output),
|
|
372
|
+
])
|
|
373
|
+
self.assertEqual(output.read_bytes(), b"%PDF-1.7")
|
|
374
|
+
self.assertEqual(code, 0)
|
|
375
|
+
self.assertEqual(request.call_args.args[0], "records.download-attachment")
|
|
376
|
+
self.assertEqual(request.call_args.kwargs["accept"], "application/octet-stream")
|
|
377
|
+
|
|
237
378
|
def test_noninteractive_commit_requires_yes(self):
|
|
238
379
|
stderr = io.StringIO()
|
|
239
380
|
with patch("sys.stderr", stderr), patch("sys.stdin.isatty", return_value=False):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|