flowbase-cli 0.1.3__tar.gz → 0.1.5__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.3 → flowbase_cli-0.1.5}/PKG-INFO +3 -1
- {flowbase_cli-0.1.3 → flowbase_cli-0.1.5}/README.md +2 -0
- {flowbase_cli-0.1.3 → flowbase_cli-0.1.5}/pyproject.toml +1 -1
- {flowbase_cli-0.1.3 → flowbase_cli-0.1.5}/src/flowbase_cli/__init__.py +1 -1
- {flowbase_cli-0.1.3 → flowbase_cli-0.1.5}/src/flowbase_cli/endpoints.py +51 -0
- {flowbase_cli-0.1.3 → flowbase_cli-0.1.5}/src/flowbase_cli/main.py +261 -4
- {flowbase_cli-0.1.3 → flowbase_cli-0.1.5}/src/flowbase_cli.egg-info/PKG-INFO +3 -1
- {flowbase_cli-0.1.3 → flowbase_cli-0.1.5}/tests/test_cli.py +207 -0
- {flowbase_cli-0.1.3 → flowbase_cli-0.1.5}/setup.cfg +0 -0
- {flowbase_cli-0.1.3 → flowbase_cli-0.1.5}/src/flowbase_cli.egg-info/SOURCES.txt +0 -0
- {flowbase_cli-0.1.3 → flowbase_cli-0.1.5}/src/flowbase_cli.egg-info/dependency_links.txt +0 -0
- {flowbase_cli-0.1.3 → flowbase_cli-0.1.5}/src/flowbase_cli.egg-info/entry_points.txt +0 -0
- {flowbase_cli-0.1.3 → flowbase_cli-0.1.5}/src/flowbase_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowbase-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Command line client for the FlowBase OpenAPI
|
|
5
5
|
Author: DeepFlowAI
|
|
6
6
|
License: Apache-2.0
|
|
@@ -22,6 +22,8 @@ export FLOWBASE_HOST=https://flowbase-test-a.deepflowagent.com
|
|
|
22
22
|
export FLOWBASE_API_KEY=fbk_xxx
|
|
23
23
|
|
|
24
24
|
flowbase objects list
|
|
25
|
+
flowbase views list obj_xxx
|
|
26
|
+
flowbase views create obj_xxx --json-file view.json
|
|
25
27
|
flowbase pages list --object-id obj_xxx
|
|
26
28
|
flowbase pages create --json-file page.json
|
|
27
29
|
flowbase records create obj_xxx --json '{"values":{"name":"Example"}}'
|
|
@@ -13,6 +13,8 @@ export FLOWBASE_HOST=https://flowbase-test-a.deepflowagent.com
|
|
|
13
13
|
export FLOWBASE_API_KEY=fbk_xxx
|
|
14
14
|
|
|
15
15
|
flowbase objects list
|
|
16
|
+
flowbase views list obj_xxx
|
|
17
|
+
flowbase views create obj_xxx --json-file view.json
|
|
16
18
|
flowbase pages list --object-id obj_xxx
|
|
17
19
|
flowbase pages create --json-file page.json
|
|
18
20
|
flowbase records create obj_xxx --json '{"values":{"name":"Example"}}'
|
|
@@ -20,16 +20,65 @@ 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 empty app.", "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.update",
|
|
40
|
+
"flowbase_apps_groups_update",
|
|
41
|
+
"PUT",
|
|
42
|
+
"/apps/{app_id}/groups/{group_id}",
|
|
43
|
+
"metadata:write",
|
|
44
|
+
"Update an app group draft.",
|
|
45
|
+
"flowbase groups update app_xxx grp_xxx --name Sales",
|
|
46
|
+
),
|
|
47
|
+
Endpoint(
|
|
48
|
+
"apps.groups.delete",
|
|
49
|
+
"flowbase_apps_groups_delete",
|
|
50
|
+
"DELETE",
|
|
51
|
+
"/apps/{app_id}/groups/{group_id}",
|
|
52
|
+
"metadata:write",
|
|
53
|
+
"Delete an app group (objects become ungrouped).",
|
|
54
|
+
"flowbase groups delete app_xxx grp_xxx --yes",
|
|
55
|
+
True,
|
|
56
|
+
),
|
|
23
57
|
Endpoint("objects.list", "flowbase_objects_list", "GET", "/objects", "metadata:read", "List object metadata.", "flowbase objects list"),
|
|
24
58
|
Endpoint("objects.get", "flowbase_objects_get", "GET", "/objects/{object_id}", "metadata:read", "Get object and field metadata.", "flowbase objects get obj_xxx"),
|
|
25
59
|
Endpoint("objects.create", "flowbase_objects_create", "POST", "/objects", "metadata:write", "Create an object draft.", "flowbase objects create --json-file object.json"),
|
|
26
60
|
Endpoint("objects.update", "flowbase_objects_update", "PUT", "/objects/{object_id}", "metadata:write", "Update an object draft.", "flowbase objects update obj_xxx --label Customer --description 'Customer records'"),
|
|
61
|
+
Endpoint(
|
|
62
|
+
"objects.set-group",
|
|
63
|
+
"flowbase_objects_set_group",
|
|
64
|
+
"POST",
|
|
65
|
+
"/objects/{object_id}/group",
|
|
66
|
+
"metadata:write",
|
|
67
|
+
"Move an object into an app group or ungroup it.",
|
|
68
|
+
"flowbase objects set-group obj_xxx --group-id grp_xxx",
|
|
69
|
+
),
|
|
27
70
|
Endpoint("objects.delete", "flowbase_objects_delete", "DELETE", "/objects/{object_id}", "metadata:write", "Logically delete an object.", "flowbase objects delete obj_xxx --yes", True),
|
|
28
71
|
Endpoint("objects.restore", "flowbase_objects_restore", "POST", "/objects/{object_id}/restore", "metadata:write", "Restore an object.", "flowbase objects restore obj_xxx --yes", True),
|
|
29
72
|
Endpoint("fields.create", "flowbase_fields_create", "POST", "/objects/{object_id}/fields", "metadata:write", "Create a field draft.", "flowbase fields create obj_xxx --json-file field.json"),
|
|
30
73
|
Endpoint("fields.update", "flowbase_fields_update", "PUT", "/objects/{object_id}/fields/{field_id}", "metadata:write", "Update a field draft.", "flowbase fields update obj_xxx fld_xxx --label Name --description 'Customer name'"),
|
|
31
74
|
Endpoint("fields.delete", "flowbase_fields_delete", "DELETE", "/objects/{object_id}/fields/{field_id}", "metadata:write", "Logically delete a field.", "flowbase fields delete obj_xxx fld_xxx --yes", True),
|
|
32
75
|
Endpoint("fields.restore", "flowbase_fields_restore", "POST", "/objects/{object_id}/fields/{field_id}/restore", "metadata:write", "Restore a field.", "flowbase fields restore obj_xxx fld_xxx --yes", True),
|
|
76
|
+
Endpoint("views.list", "flowbase_views_list", "GET", "/objects/{object_id}/views", "metadata:read", "List object view metadata.", "flowbase views list obj_xxx"),
|
|
77
|
+
Endpoint("views.create", "flowbase_views_create", "POST", "/objects/{object_id}/views", "metadata:write", "Create an object view draft.", "flowbase views create obj_xxx --json-file view.json"),
|
|
78
|
+
Endpoint("views.update", "flowbase_views_update", "PUT", "/objects/{object_id}/views/{view_id}", "metadata:write", "Update an object view draft.", "flowbase views update obj_xxx viw_xxx --json-file view.json"),
|
|
79
|
+
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\"]}'"),
|
|
80
|
+
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),
|
|
81
|
+
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),
|
|
33
82
|
Endpoint("pages.list", "flowbase_pages_list", "GET", "/pages", "metadata:read", "List custom Page metadata.", "flowbase pages list --object-id obj_xxx"),
|
|
34
83
|
Endpoint("pages.options", "flowbase_pages_options", "GET", "/pages/options", "metadata:read", "Get custom Page design options.", "flowbase pages options"),
|
|
35
84
|
Endpoint("pages.get", "flowbase_pages_get", "GET", "/pages/{page_id}", "metadata:read", "Get a custom Page.", "flowbase pages get pag_xxx"),
|
|
@@ -91,6 +140,8 @@ ENDPOINTS = (
|
|
|
91
140
|
),
|
|
92
141
|
Endpoint("records.list", "flowbase_records_list", "GET", "/objects/{object_id}/records", "records:read", "List records.", "flowbase records list obj_xxx --page 1"),
|
|
93
142
|
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"),
|
|
143
|
+
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"),
|
|
144
|
+
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"),
|
|
94
145
|
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"),
|
|
95
146
|
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"),
|
|
96
147
|
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),
|
|
@@ -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,50 @@ 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
|
+
return run_request(args, "apps.delete", path_params={"app_id": args.app_id})
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def handle_groups(args: argparse.Namespace) -> int:
|
|
343
|
+
action = args.groups_action
|
|
344
|
+
path = {"app_id": args.app_id}
|
|
345
|
+
if action == "create":
|
|
346
|
+
body = _payload_from_args(args, {"name": args.name, "icon": args.icon}, ("name",))
|
|
347
|
+
return run_request(args, "apps.groups.create", path_params=path, body=body)
|
|
348
|
+
if action == "update":
|
|
349
|
+
path["group_id"] = args.group_id
|
|
350
|
+
body = _payload_from_args(args, {"name": args.name, "icon": args.icon}, ("name", "icon"))
|
|
351
|
+
return run_request(args, "apps.groups.update", path_params=path, body=body)
|
|
352
|
+
path["group_id"] = args.group_id
|
|
353
|
+
return run_request(args, "apps.groups.delete", path_params=path)
|
|
354
|
+
|
|
355
|
+
|
|
279
356
|
def handle_objects(args: argparse.Namespace) -> int:
|
|
280
357
|
action = args.objects_action
|
|
281
358
|
if action == "list":
|
|
@@ -285,13 +362,39 @@ def handle_objects(args: argparse.Namespace) -> int:
|
|
|
285
362
|
if action == "create":
|
|
286
363
|
body = _payload_from_args(
|
|
287
364
|
args,
|
|
288
|
-
{
|
|
365
|
+
{
|
|
366
|
+
"label": args.label,
|
|
367
|
+
"key": args.key,
|
|
368
|
+
"description": args.description,
|
|
369
|
+
"appId": args.app_id,
|
|
370
|
+
},
|
|
289
371
|
("label", "key", "description"),
|
|
290
372
|
)
|
|
291
373
|
return run_request(args, "objects.create", body=body)
|
|
292
374
|
if action == "update":
|
|
293
|
-
body = _payload_from_args(
|
|
375
|
+
body = _payload_from_args(
|
|
376
|
+
args,
|
|
377
|
+
{
|
|
378
|
+
"label": args.label,
|
|
379
|
+
"description": args.description,
|
|
380
|
+
"appId": args.app_id,
|
|
381
|
+
},
|
|
382
|
+
("label", "description"),
|
|
383
|
+
)
|
|
294
384
|
return run_request(args, "objects.update", path_params={"object_id": args.object_id}, body=body)
|
|
385
|
+
if action == "set-group":
|
|
386
|
+
if args.ungrouped:
|
|
387
|
+
body = {"groupId": None}
|
|
388
|
+
elif args.group_id is not None:
|
|
389
|
+
body = {"groupId": args.group_id}
|
|
390
|
+
else:
|
|
391
|
+
raise CliError("Provide --group-id or --ungrouped.")
|
|
392
|
+
return run_request(
|
|
393
|
+
args,
|
|
394
|
+
"objects.set-group",
|
|
395
|
+
path_params={"object_id": args.object_id},
|
|
396
|
+
body=body,
|
|
397
|
+
)
|
|
295
398
|
return run_request(args, f"objects.{action}", path_params={"object_id": args.object_id})
|
|
296
399
|
|
|
297
400
|
|
|
@@ -313,6 +416,39 @@ def handle_fields(args: argparse.Namespace) -> int:
|
|
|
313
416
|
return run_request(args, f"fields.{action}", path_params=path)
|
|
314
417
|
|
|
315
418
|
|
|
419
|
+
def handle_views(args: argparse.Namespace) -> int:
|
|
420
|
+
action = args.views_action
|
|
421
|
+
path = {"object_id": args.object_id}
|
|
422
|
+
if hasattr(args, "view_id"):
|
|
423
|
+
path["view_id"] = args.view_id
|
|
424
|
+
if action == "list":
|
|
425
|
+
return run_request(args, "views.list", path_params=path)
|
|
426
|
+
if action in ("create", "update"):
|
|
427
|
+
body = load_json_payload(args.json, args.json_file)
|
|
428
|
+
require_fields(body, "baseRevision", "name", "defaultFieldIds")
|
|
429
|
+
if not isinstance(body["defaultFieldIds"], list):
|
|
430
|
+
raise CliError("Request field 'defaultFieldIds' must be an array.")
|
|
431
|
+
return run_request(args, f"views.{action}", path_params=path, body=body)
|
|
432
|
+
if action == "reorder":
|
|
433
|
+
body = load_json_payload(args.json, args.json_file)
|
|
434
|
+
require_fields(body, "baseRevision", "viewIds")
|
|
435
|
+
if not isinstance(body["viewIds"], list):
|
|
436
|
+
raise CliError("Request field 'viewIds' must be an array.")
|
|
437
|
+
return run_request(args, "views.reorder", path_params=path, body=body)
|
|
438
|
+
if action == "set-enabled":
|
|
439
|
+
body = {
|
|
440
|
+
"baseRevision": args.base_revision,
|
|
441
|
+
"enabled": args.enabled,
|
|
442
|
+
}
|
|
443
|
+
return run_request(args, "views.set-enabled", path_params=path, body=body)
|
|
444
|
+
return run_request(
|
|
445
|
+
args,
|
|
446
|
+
"views.delete",
|
|
447
|
+
path_params=path,
|
|
448
|
+
body={"baseRevision": args.base_revision},
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
|
|
316
452
|
def handle_pages(args: argparse.Namespace) -> int:
|
|
317
453
|
action = args.pages_action
|
|
318
454
|
if action == "list":
|
|
@@ -480,6 +616,31 @@ def handle_environments(args: argparse.Namespace) -> int:
|
|
|
480
616
|
def handle_records(args: argparse.Namespace) -> int:
|
|
481
617
|
action = args.records_action
|
|
482
618
|
path = {"object_id": args.object_id}
|
|
619
|
+
if action == "upload-attachment":
|
|
620
|
+
body, content_type = multipart_upload(args.file)
|
|
621
|
+
data = client_from_args(args).request(
|
|
622
|
+
"records.upload-attachment",
|
|
623
|
+
path_params={**path, "field_id": args.field_id},
|
|
624
|
+
raw_body=body,
|
|
625
|
+
content_type=content_type,
|
|
626
|
+
)
|
|
627
|
+
print_output(data, pretty=args.pretty, raw=args.raw)
|
|
628
|
+
return 0
|
|
629
|
+
if action == "download-attachment":
|
|
630
|
+
data = client_from_args(args).request(
|
|
631
|
+
"records.download-attachment",
|
|
632
|
+
path_params={
|
|
633
|
+
**path,
|
|
634
|
+
"record_id": args.record_id,
|
|
635
|
+
"attachment_id": args.attachment_id,
|
|
636
|
+
},
|
|
637
|
+
accept="application/octet-stream",
|
|
638
|
+
)
|
|
639
|
+
try:
|
|
640
|
+
Path(args.output).write_bytes(data)
|
|
641
|
+
except OSError as exc:
|
|
642
|
+
raise CliError(f"Cannot write attachment: {exc}") from exc
|
|
643
|
+
return 0
|
|
483
644
|
if hasattr(args, "record_id"):
|
|
484
645
|
path["record_id"] = args.record_id
|
|
485
646
|
if action == "list":
|
|
@@ -504,6 +665,7 @@ def add_object_fields(parser: argparse.ArgumentParser, *, include_key: bool) ->
|
|
|
504
665
|
if include_key:
|
|
505
666
|
parser.add_argument("--key")
|
|
506
667
|
parser.add_argument("--description")
|
|
668
|
+
parser.add_argument("--app-id", help="Owning app ID (appId).")
|
|
507
669
|
add_json_args(parser)
|
|
508
670
|
|
|
509
671
|
|
|
@@ -527,6 +689,55 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
527
689
|
docs_show.add_argument("endpoint", choices=sorted(ENDPOINT_BY_KEY))
|
|
528
690
|
docs.set_defaults(handler=handle_docs)
|
|
529
691
|
|
|
692
|
+
apps = groups.add_parser("apps", help="Manage app metadata.")
|
|
693
|
+
app_actions = apps.add_subparsers(dest="apps_action", required=True)
|
|
694
|
+
app_actions.add_parser("list")
|
|
695
|
+
app_get = app_actions.add_parser("get")
|
|
696
|
+
app_get.add_argument("app_id")
|
|
697
|
+
app_create = app_actions.add_parser("create")
|
|
698
|
+
app_create.add_argument("--label")
|
|
699
|
+
app_create.add_argument("--key")
|
|
700
|
+
app_create.add_argument("--icon")
|
|
701
|
+
app_create.add_argument("--color")
|
|
702
|
+
add_json_args(app_create)
|
|
703
|
+
app_update = app_actions.add_parser("update")
|
|
704
|
+
app_update.add_argument("app_id")
|
|
705
|
+
app_update.add_argument("--label")
|
|
706
|
+
app_update.add_argument("--icon")
|
|
707
|
+
app_update.add_argument("--color")
|
|
708
|
+
add_json_args(app_update)
|
|
709
|
+
app_enabled = app_actions.add_parser("set-enabled")
|
|
710
|
+
app_enabled.add_argument("app_id")
|
|
711
|
+
app_enabled.add_argument(
|
|
712
|
+
"--enabled",
|
|
713
|
+
action=argparse.BooleanOptionalAction,
|
|
714
|
+
required=True,
|
|
715
|
+
)
|
|
716
|
+
add_yes(app_enabled)
|
|
717
|
+
app_delete = app_actions.add_parser("delete")
|
|
718
|
+
app_delete.add_argument("app_id")
|
|
719
|
+
add_yes(app_delete)
|
|
720
|
+
apps.set_defaults(handler=handle_apps)
|
|
721
|
+
|
|
722
|
+
app_groups = groups.add_parser("groups", help="Manage app object groups.")
|
|
723
|
+
group_actions = app_groups.add_subparsers(dest="groups_action", required=True)
|
|
724
|
+
group_create = group_actions.add_parser("create")
|
|
725
|
+
group_create.add_argument("app_id")
|
|
726
|
+
group_create.add_argument("--name")
|
|
727
|
+
group_create.add_argument("--icon")
|
|
728
|
+
add_json_args(group_create)
|
|
729
|
+
group_update = group_actions.add_parser("update")
|
|
730
|
+
group_update.add_argument("app_id")
|
|
731
|
+
group_update.add_argument("group_id")
|
|
732
|
+
group_update.add_argument("--name")
|
|
733
|
+
group_update.add_argument("--icon")
|
|
734
|
+
add_json_args(group_update)
|
|
735
|
+
group_delete = group_actions.add_parser("delete")
|
|
736
|
+
group_delete.add_argument("app_id")
|
|
737
|
+
group_delete.add_argument("group_id")
|
|
738
|
+
add_yes(group_delete)
|
|
739
|
+
app_groups.set_defaults(handler=handle_groups)
|
|
740
|
+
|
|
530
741
|
objects = groups.add_parser("objects", help="Manage object metadata.")
|
|
531
742
|
object_actions = objects.add_subparsers(dest="objects_action", required=True)
|
|
532
743
|
object_list = object_actions.add_parser("list")
|
|
@@ -538,6 +749,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
538
749
|
object_update = object_actions.add_parser("update")
|
|
539
750
|
object_update.add_argument("object_id")
|
|
540
751
|
add_object_fields(object_update, include_key=False)
|
|
752
|
+
object_set_group = object_actions.add_parser("set-group")
|
|
753
|
+
object_set_group.add_argument("object_id")
|
|
754
|
+
object_set_group.add_argument("--group-id", help="Target group ID. Omit with --ungrouped.")
|
|
755
|
+
object_set_group.add_argument(
|
|
756
|
+
"--ungrouped",
|
|
757
|
+
action="store_true",
|
|
758
|
+
help="Remove the object from its current group.",
|
|
759
|
+
)
|
|
541
760
|
for action in ("delete", "restore"):
|
|
542
761
|
item = object_actions.add_parser(action)
|
|
543
762
|
item.add_argument("object_id")
|
|
@@ -560,6 +779,35 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
560
779
|
add_yes(item)
|
|
561
780
|
fields.set_defaults(handler=handle_fields)
|
|
562
781
|
|
|
782
|
+
views = groups.add_parser("views", help="Manage object view metadata.")
|
|
783
|
+
view_actions = views.add_subparsers(dest="views_action", required=True)
|
|
784
|
+
view_list = view_actions.add_parser("list")
|
|
785
|
+
view_list.add_argument("object_id")
|
|
786
|
+
for action in ("create", "reorder"):
|
|
787
|
+
item = view_actions.add_parser(action)
|
|
788
|
+
item.add_argument("object_id")
|
|
789
|
+
add_json_args(item)
|
|
790
|
+
view_update = view_actions.add_parser("update")
|
|
791
|
+
view_update.add_argument("object_id")
|
|
792
|
+
view_update.add_argument("view_id")
|
|
793
|
+
add_json_args(view_update)
|
|
794
|
+
view_enabled = view_actions.add_parser("set-enabled")
|
|
795
|
+
view_enabled.add_argument("object_id")
|
|
796
|
+
view_enabled.add_argument("view_id")
|
|
797
|
+
view_enabled.add_argument("--base-revision", required=True, type=int)
|
|
798
|
+
view_enabled.add_argument(
|
|
799
|
+
"--enabled",
|
|
800
|
+
action=argparse.BooleanOptionalAction,
|
|
801
|
+
required=True,
|
|
802
|
+
)
|
|
803
|
+
add_yes(view_enabled)
|
|
804
|
+
view_delete = view_actions.add_parser("delete")
|
|
805
|
+
view_delete.add_argument("object_id")
|
|
806
|
+
view_delete.add_argument("view_id")
|
|
807
|
+
view_delete.add_argument("--base-revision", required=True, type=int)
|
|
808
|
+
add_yes(view_delete)
|
|
809
|
+
views.set_defaults(handler=handle_views)
|
|
810
|
+
|
|
563
811
|
pages = groups.add_parser("pages", help="Manage custom Page metadata.")
|
|
564
812
|
page_actions = pages.add_subparsers(dest="pages_action", required=True)
|
|
565
813
|
page_list = page_actions.add_parser("list")
|
|
@@ -631,6 +879,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
631
879
|
record_get = record_actions.add_parser("get")
|
|
632
880
|
record_get.add_argument("object_id")
|
|
633
881
|
record_get.add_argument("record_id")
|
|
882
|
+
record_upload = record_actions.add_parser("upload-attachment")
|
|
883
|
+
record_upload.add_argument("object_id")
|
|
884
|
+
record_upload.add_argument("field_id")
|
|
885
|
+
record_upload.add_argument("file")
|
|
886
|
+
record_download = record_actions.add_parser("download-attachment")
|
|
887
|
+
record_download.add_argument("object_id")
|
|
888
|
+
record_download.add_argument("record_id")
|
|
889
|
+
record_download.add_argument("attachment_id")
|
|
890
|
+
record_download.add_argument("--output", required=True)
|
|
634
891
|
record_create = record_actions.add_parser("create")
|
|
635
892
|
record_create.add_argument("object_id")
|
|
636
893
|
add_json_args(record_create)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowbase-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Command line client for the FlowBase OpenAPI
|
|
5
5
|
Author: DeepFlowAI
|
|
6
6
|
License: Apache-2.0
|
|
@@ -22,6 +22,8 @@ export FLOWBASE_HOST=https://flowbase-test-a.deepflowagent.com
|
|
|
22
22
|
export FLOWBASE_API_KEY=fbk_xxx
|
|
23
23
|
|
|
24
24
|
flowbase objects list
|
|
25
|
+
flowbase views list obj_xxx
|
|
26
|
+
flowbase views create obj_xxx --json-file view.json
|
|
25
27
|
flowbase pages list --object-id obj_xxx
|
|
26
28
|
flowbase pages create --json-file page.json
|
|
27
29
|
flowbase records create obj_xxx --json '{"values":{"name":"Example"}}'
|
|
@@ -88,6 +88,78 @@ class FlowBaseCliTest(unittest.TestCase):
|
|
|
88
88
|
self.assertTrue(any(item["key"] == "changes.release" for item in payload))
|
|
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
|
+
self.assertTrue(any(item["key"] == "views.create" for item in payload))
|
|
92
|
+
|
|
93
|
+
def test_view_create_sends_complete_snapshot(self):
|
|
94
|
+
body = {
|
|
95
|
+
"baseRevision": 4,
|
|
96
|
+
"name": "Active customers",
|
|
97
|
+
"defaultFieldIds": ["fld_1"],
|
|
98
|
+
}
|
|
99
|
+
stdout = io.StringIO()
|
|
100
|
+
with (
|
|
101
|
+
patch(
|
|
102
|
+
"flowbase_cli.main.OpenApiClient.request",
|
|
103
|
+
return_value=b'{"viewId":"viw_1","draftRevision":5}',
|
|
104
|
+
) as request,
|
|
105
|
+
patch("sys.stdout", stdout),
|
|
106
|
+
):
|
|
107
|
+
code = main([
|
|
108
|
+
"--api-key",
|
|
109
|
+
"fbk_test",
|
|
110
|
+
"views",
|
|
111
|
+
"create",
|
|
112
|
+
"obj_1",
|
|
113
|
+
"--json",
|
|
114
|
+
json.dumps(body),
|
|
115
|
+
])
|
|
116
|
+
self.assertEqual(code, 0)
|
|
117
|
+
self.assertEqual(request.call_args.args[0], "views.create")
|
|
118
|
+
self.assertEqual(
|
|
119
|
+
request.call_args.kwargs["path_params"],
|
|
120
|
+
{"object_id": "obj_1"},
|
|
121
|
+
)
|
|
122
|
+
self.assertEqual(request.call_args.kwargs["body"], body)
|
|
123
|
+
|
|
124
|
+
def test_view_set_enabled_sends_revision_and_requires_yes(self):
|
|
125
|
+
stderr = io.StringIO()
|
|
126
|
+
with patch("sys.stderr", stderr), patch("sys.stdin.isatty", return_value=False):
|
|
127
|
+
code = main([
|
|
128
|
+
"--api-key",
|
|
129
|
+
"fbk_test",
|
|
130
|
+
"views",
|
|
131
|
+
"set-enabled",
|
|
132
|
+
"obj_1",
|
|
133
|
+
"viw_1",
|
|
134
|
+
"--base-revision",
|
|
135
|
+
"4",
|
|
136
|
+
"--no-enabled",
|
|
137
|
+
])
|
|
138
|
+
self.assertEqual(code, 1)
|
|
139
|
+
self.assertIn("without --yes", stderr.getvalue())
|
|
140
|
+
|
|
141
|
+
with patch(
|
|
142
|
+
"flowbase_cli.main.OpenApiClient.request",
|
|
143
|
+
return_value=b'{"ok":true,"draftRevision":5}',
|
|
144
|
+
) as request:
|
|
145
|
+
code = main([
|
|
146
|
+
"--api-key",
|
|
147
|
+
"fbk_test",
|
|
148
|
+
"views",
|
|
149
|
+
"set-enabled",
|
|
150
|
+
"obj_1",
|
|
151
|
+
"viw_1",
|
|
152
|
+
"--base-revision",
|
|
153
|
+
"4",
|
|
154
|
+
"--no-enabled",
|
|
155
|
+
"--yes",
|
|
156
|
+
])
|
|
157
|
+
self.assertEqual(code, 0)
|
|
158
|
+
self.assertEqual(request.call_args.args[0], "views.set-enabled")
|
|
159
|
+
self.assertEqual(
|
|
160
|
+
request.call_args.kwargs["body"],
|
|
161
|
+
{"baseRevision": 4, "enabled": False},
|
|
162
|
+
)
|
|
91
163
|
|
|
92
164
|
def test_page_set_default_sends_revision_and_requires_yes(self):
|
|
93
165
|
stderr = io.StringIO()
|
|
@@ -155,6 +227,103 @@ class FlowBaseCliTest(unittest.TestCase):
|
|
|
155
227
|
self.assertEqual(code, 1)
|
|
156
228
|
self.assertIn("description", stderr.getvalue())
|
|
157
229
|
|
|
230
|
+
def test_object_create_sends_app_id(self):
|
|
231
|
+
with patch(
|
|
232
|
+
"flowbase_cli.main.OpenApiClient.request",
|
|
233
|
+
return_value=b'{"objectId":"obj_1","draftRevision":2}',
|
|
234
|
+
) as request:
|
|
235
|
+
code = main([
|
|
236
|
+
"--api-key",
|
|
237
|
+
"fbk_test",
|
|
238
|
+
"objects",
|
|
239
|
+
"create",
|
|
240
|
+
"--label",
|
|
241
|
+
"Customer",
|
|
242
|
+
"--key",
|
|
243
|
+
"customer",
|
|
244
|
+
"--description",
|
|
245
|
+
"Customer records",
|
|
246
|
+
"--app-id",
|
|
247
|
+
"app_1",
|
|
248
|
+
])
|
|
249
|
+
self.assertEqual(code, 0)
|
|
250
|
+
self.assertEqual(request.call_args.args[0], "objects.create")
|
|
251
|
+
self.assertEqual(
|
|
252
|
+
request.call_args.kwargs["body"],
|
|
253
|
+
{
|
|
254
|
+
"label": "Customer",
|
|
255
|
+
"key": "customer",
|
|
256
|
+
"description": "Customer records",
|
|
257
|
+
"appId": "app_1",
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def test_group_create_and_object_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
|
+
"objects",
|
|
293
|
+
"set-group",
|
|
294
|
+
"obj_1",
|
|
295
|
+
"--group-id",
|
|
296
|
+
"grp_1",
|
|
297
|
+
])
|
|
298
|
+
self.assertEqual(code, 0)
|
|
299
|
+
self.assertEqual(request.call_args.args[0], "objects.set-group")
|
|
300
|
+
self.assertEqual(
|
|
301
|
+
request.call_args.kwargs["body"],
|
|
302
|
+
{"groupId": "grp_1"},
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
with patch(
|
|
306
|
+
"flowbase_cli.main.OpenApiClient.request",
|
|
307
|
+
return_value=b'{"ok":true,"draftRevision":5}',
|
|
308
|
+
) as request:
|
|
309
|
+
code = main([
|
|
310
|
+
"--api-key",
|
|
311
|
+
"fbk_test",
|
|
312
|
+
"objects",
|
|
313
|
+
"set-group",
|
|
314
|
+
"obj_1",
|
|
315
|
+
"--ungrouped",
|
|
316
|
+
])
|
|
317
|
+
self.assertEqual(code, 0)
|
|
318
|
+
self.assertEqual(request.call_args.kwargs["body"], {"groupId": None})
|
|
319
|
+
|
|
320
|
+
def test_group_delete_requires_yes(self):
|
|
321
|
+
stderr = io.StringIO()
|
|
322
|
+
with patch("sys.stderr", stderr), patch("sys.stdin.isatty", return_value=False):
|
|
323
|
+
code = main(["--api-key", "fbk_test", "groups", "delete", "app_1", "grp_1"])
|
|
324
|
+
self.assertEqual(code, 1)
|
|
325
|
+
self.assertIn("without --yes", stderr.getvalue())
|
|
326
|
+
|
|
158
327
|
def test_noninteractive_delete_requires_yes(self):
|
|
159
328
|
stderr = io.StringIO()
|
|
160
329
|
with patch("sys.stderr", stderr), patch("sys.stdin.isatty", return_value=False):
|
|
@@ -162,6 +331,44 @@ class FlowBaseCliTest(unittest.TestCase):
|
|
|
162
331
|
self.assertEqual(code, 1)
|
|
163
332
|
self.assertIn("without --yes", stderr.getvalue())
|
|
164
333
|
|
|
334
|
+
def test_record_attachment_upload_sends_multipart_body(self):
|
|
335
|
+
with TemporaryDirectory() as directory:
|
|
336
|
+
path = Path(directory) / "note.txt"
|
|
337
|
+
path.write_text("hello", encoding="utf-8")
|
|
338
|
+
with patch(
|
|
339
|
+
"flowbase_cli.main.OpenApiClient.request",
|
|
340
|
+
return_value=b'{"id":"att_1"}',
|
|
341
|
+
) as request:
|
|
342
|
+
code = main([
|
|
343
|
+
"--api-key", "fbk_test", "records", "upload-attachment",
|
|
344
|
+
"obj_1", "fld_1", str(path),
|
|
345
|
+
])
|
|
346
|
+
self.assertEqual(code, 0)
|
|
347
|
+
self.assertEqual(request.call_args.args[0], "records.upload-attachment")
|
|
348
|
+
self.assertEqual(
|
|
349
|
+
request.call_args.kwargs["path_params"],
|
|
350
|
+
{"object_id": "obj_1", "field_id": "fld_1"},
|
|
351
|
+
)
|
|
352
|
+
self.assertTrue(request.call_args.kwargs["content_type"].startswith("multipart/form-data; boundary="))
|
|
353
|
+
self.assertIn(b'name="upload"', request.call_args.kwargs["raw_body"])
|
|
354
|
+
self.assertIn(b"hello", request.call_args.kwargs["raw_body"])
|
|
355
|
+
|
|
356
|
+
def test_record_attachment_download_writes_exact_bytes(self):
|
|
357
|
+
with TemporaryDirectory() as directory:
|
|
358
|
+
output = Path(directory) / "download.pdf"
|
|
359
|
+
with patch(
|
|
360
|
+
"flowbase_cli.main.OpenApiClient.request",
|
|
361
|
+
return_value=b"%PDF-1.7",
|
|
362
|
+
) as request:
|
|
363
|
+
code = main([
|
|
364
|
+
"--api-key", "fbk_test", "records", "download-attachment",
|
|
365
|
+
"obj_1", "rec_1", "att_1", "--output", str(output),
|
|
366
|
+
])
|
|
367
|
+
self.assertEqual(output.read_bytes(), b"%PDF-1.7")
|
|
368
|
+
self.assertEqual(code, 0)
|
|
369
|
+
self.assertEqual(request.call_args.args[0], "records.download-attachment")
|
|
370
|
+
self.assertEqual(request.call_args.kwargs["accept"], "application/octet-stream")
|
|
371
|
+
|
|
165
372
|
def test_noninteractive_commit_requires_yes(self):
|
|
166
373
|
stderr = io.StringIO()
|
|
167
374
|
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
|