flowbase-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flowbase_cli/__init__.py +3 -0
- flowbase_cli/endpoints.py +37 -0
- flowbase_cli/main.py +362 -0
- flowbase_cli-0.1.0.dist-info/METADATA +29 -0
- flowbase_cli-0.1.0.dist-info/RECORD +8 -0
- flowbase_cli-0.1.0.dist-info/WHEEL +5 -0
- flowbase_cli-0.1.0.dist-info/entry_points.txt +2 -0
- flowbase_cli-0.1.0.dist-info/top_level.txt +1 -0
flowbase_cli/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""CLI endpoint catalog, contract-tested against the server OpenAPI schema."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Endpoint:
|
|
9
|
+
key: str
|
|
10
|
+
operation_id: str
|
|
11
|
+
method: str
|
|
12
|
+
path: str
|
|
13
|
+
scope: str
|
|
14
|
+
summary: str
|
|
15
|
+
example: str
|
|
16
|
+
requires_confirmation: bool = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
ENDPOINTS = (
|
|
20
|
+
Endpoint("objects.list", "flowbase_objects_list", "GET", "/objects", "metadata:read", "List object metadata.", "flowbase objects list"),
|
|
21
|
+
Endpoint("objects.get", "flowbase_objects_get", "GET", "/objects/{object_id}", "metadata:read", "Get object and field metadata.", "flowbase objects get obj_xxx"),
|
|
22
|
+
Endpoint("objects.create", "flowbase_objects_create", "POST", "/objects", "metadata:write", "Create an object draft.", "flowbase objects create --json-file object.json"),
|
|
23
|
+
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'"),
|
|
24
|
+
Endpoint("objects.delete", "flowbase_objects_delete", "DELETE", "/objects/{object_id}", "metadata:write", "Logically delete an object.", "flowbase objects delete obj_xxx --yes", True),
|
|
25
|
+
Endpoint("objects.restore", "flowbase_objects_restore", "POST", "/objects/{object_id}/restore", "metadata:write", "Restore an object.", "flowbase objects restore obj_xxx --yes", True),
|
|
26
|
+
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"),
|
|
27
|
+
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'"),
|
|
28
|
+
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),
|
|
29
|
+
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),
|
|
30
|
+
Endpoint("records.list", "flowbase_records_list", "GET", "/objects/{object_id}/records", "records:read", "List records.", "flowbase records list obj_xxx --page 1"),
|
|
31
|
+
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"),
|
|
32
|
+
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"),
|
|
33
|
+
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"),
|
|
34
|
+
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),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
ENDPOINT_BY_KEY = {endpoint.key: endpoint for endpoint in ENDPOINTS}
|
flowbase_cli/main.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Command line client for the FlowBase OpenAPI."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib.error import HTTPError, URLError
|
|
11
|
+
from urllib.parse import quote, urlencode
|
|
12
|
+
from urllib.request import Request, urlopen
|
|
13
|
+
|
|
14
|
+
from flowbase_cli.endpoints import ENDPOINTS, ENDPOINT_BY_KEY, Endpoint
|
|
15
|
+
|
|
16
|
+
DEFAULT_HOST = "http://flowbase.localhost:5001"
|
|
17
|
+
API_PREFIX = "/api/openapi/v1"
|
|
18
|
+
ENV_HOST = "FLOWBASE_HOST"
|
|
19
|
+
ENV_API_KEY = "FLOWBASE_API_KEY"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CliError(Exception):
|
|
23
|
+
"""Expected user-facing CLI failure."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def api_root(host: str) -> str:
|
|
27
|
+
normalized = host.strip().rstrip("/")
|
|
28
|
+
if not normalized:
|
|
29
|
+
raise CliError("Host cannot be empty.")
|
|
30
|
+
if normalized.endswith(API_PREFIX):
|
|
31
|
+
return normalized
|
|
32
|
+
return f"{normalized}{API_PREFIX}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_endpoint(key: str) -> Endpoint:
|
|
36
|
+
try:
|
|
37
|
+
return ENDPOINT_BY_KEY[key]
|
|
38
|
+
except KeyError as exc:
|
|
39
|
+
raise CliError(f"Unknown endpoint: {key}") from exc
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_json_payload(json_text: str | None, json_file: str | None) -> dict[str, Any]:
|
|
43
|
+
if json_text and json_file:
|
|
44
|
+
raise CliError("--json and --json-file cannot be used together.")
|
|
45
|
+
if not json_text and not json_file:
|
|
46
|
+
return {}
|
|
47
|
+
try:
|
|
48
|
+
source = Path(json_file).read_text(encoding="utf-8") if json_file else (json_text or "{}")
|
|
49
|
+
except OSError as exc:
|
|
50
|
+
raise CliError(f"Cannot read JSON file: {exc}") from exc
|
|
51
|
+
try:
|
|
52
|
+
payload = json.loads(source)
|
|
53
|
+
except json.JSONDecodeError as exc:
|
|
54
|
+
raise CliError(f"Invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}") from exc
|
|
55
|
+
if not isinstance(payload, dict):
|
|
56
|
+
raise CliError("JSON payload must be an object.")
|
|
57
|
+
return payload
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def require_fields(payload: dict[str, Any], *names: str) -> None:
|
|
61
|
+
missing = [name for name in names if name not in payload or payload[name] is None]
|
|
62
|
+
if missing:
|
|
63
|
+
raise CliError(f"Missing request field(s): {', '.join(missing)}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class OpenApiClient:
|
|
67
|
+
def __init__(self, host: str, api_key: str | None) -> None:
|
|
68
|
+
self.root = api_root(host)
|
|
69
|
+
self.api_key = api_key
|
|
70
|
+
|
|
71
|
+
def build_url(
|
|
72
|
+
self,
|
|
73
|
+
endpoint: Endpoint,
|
|
74
|
+
path_params: dict[str, Any] | None = None,
|
|
75
|
+
query: dict[str, Any] | None = None,
|
|
76
|
+
) -> str:
|
|
77
|
+
path = endpoint.path
|
|
78
|
+
values = path_params or {}
|
|
79
|
+
for name in _path_parameter_names(endpoint.path):
|
|
80
|
+
if name not in values:
|
|
81
|
+
raise CliError(f"Missing path parameter: {name}")
|
|
82
|
+
path = path.replace("{" + name + "}", quote(str(values[name]), safe=""))
|
|
83
|
+
clean_query = {key: value for key, value in (query or {}).items() if value is not None}
|
|
84
|
+
suffix = f"?{urlencode(clean_query, doseq=True)}" if clean_query else ""
|
|
85
|
+
return f"{self.root}{path}{suffix}"
|
|
86
|
+
|
|
87
|
+
def request(
|
|
88
|
+
self,
|
|
89
|
+
endpoint_key: str,
|
|
90
|
+
*,
|
|
91
|
+
path_params: dict[str, Any] | None = None,
|
|
92
|
+
query: dict[str, Any] | None = None,
|
|
93
|
+
body: dict[str, Any] | None = None,
|
|
94
|
+
) -> bytes:
|
|
95
|
+
endpoint = get_endpoint(endpoint_key)
|
|
96
|
+
if not self.api_key:
|
|
97
|
+
raise CliError(f"Missing API key. Set {ENV_API_KEY} or pass --api-key.")
|
|
98
|
+
url = self.build_url(endpoint, path_params, query)
|
|
99
|
+
headers = {"Accept": "application/json", "Authorization": f"Bearer {self.api_key}"}
|
|
100
|
+
data = None
|
|
101
|
+
if body is not None:
|
|
102
|
+
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
103
|
+
headers["Content-Type"] = "application/json"
|
|
104
|
+
request = Request(url, data=data, headers=headers, method=endpoint.method)
|
|
105
|
+
try:
|
|
106
|
+
with urlopen(request) as response: # noqa: S310 - the user selects the FlowBase host.
|
|
107
|
+
return response.read()
|
|
108
|
+
except HTTPError as exc:
|
|
109
|
+
payload = exc.read().decode("utf-8", errors="replace")
|
|
110
|
+
raise CliError(f"HTTP {exc.code} for {endpoint.key}: {_error_detail(payload)}") from exc
|
|
111
|
+
except URLError as exc:
|
|
112
|
+
raise CliError(f"Failed to connect to {url}: {exc.reason}") from exc
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _path_parameter_names(path: str) -> list[str]:
|
|
116
|
+
return [part[1:-1] for part in path.split("/") if part.startswith("{") and part.endswith("}")]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _error_detail(payload: str) -> str:
|
|
120
|
+
try:
|
|
121
|
+
parsed = json.loads(payload)
|
|
122
|
+
except json.JSONDecodeError:
|
|
123
|
+
return payload or "empty response"
|
|
124
|
+
if isinstance(parsed, dict) and "detail" in parsed:
|
|
125
|
+
detail = parsed["detail"]
|
|
126
|
+
return detail if isinstance(detail, str) else json.dumps(detail, ensure_ascii=False)
|
|
127
|
+
return json.dumps(parsed, ensure_ascii=False)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def print_output(data: bytes, *, pretty: bool, raw: bool) -> None:
|
|
131
|
+
text = data.decode("utf-8", errors="replace")
|
|
132
|
+
if raw:
|
|
133
|
+
print(text, end="" if text.endswith("\n") else "\n")
|
|
134
|
+
return
|
|
135
|
+
try:
|
|
136
|
+
parsed = json.loads(text)
|
|
137
|
+
except json.JSONDecodeError:
|
|
138
|
+
print(text, end="" if text.endswith("\n") else "\n")
|
|
139
|
+
return
|
|
140
|
+
indent = 2 if pretty else None
|
|
141
|
+
separators = None if pretty else (",", ":")
|
|
142
|
+
print(json.dumps(parsed, ensure_ascii=False, indent=indent, separators=separators))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def confirm_change(endpoint: Endpoint, yes: bool) -> None:
|
|
146
|
+
if not endpoint.requires_confirmation or yes:
|
|
147
|
+
return
|
|
148
|
+
if not sys.stdin.isatty():
|
|
149
|
+
raise CliError("Refusing a state-changing command in non-interactive mode without --yes.")
|
|
150
|
+
answer = input(f"{endpoint.summary} Continue? [y/N] ").strip().lower()
|
|
151
|
+
if answer not in {"y", "yes"}:
|
|
152
|
+
raise CliError("Cancelled.")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def client_from_args(args: argparse.Namespace) -> OpenApiClient:
|
|
156
|
+
host = args.host or os.getenv(ENV_HOST) or DEFAULT_HOST
|
|
157
|
+
api_key = args.api_key or os.getenv(ENV_API_KEY)
|
|
158
|
+
return OpenApiClient(host, api_key)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def run_request(
|
|
162
|
+
args: argparse.Namespace,
|
|
163
|
+
endpoint_key: str,
|
|
164
|
+
*,
|
|
165
|
+
path_params: dict[str, Any] | None = None,
|
|
166
|
+
query: dict[str, Any] | None = None,
|
|
167
|
+
body: dict[str, Any] | None = None,
|
|
168
|
+
) -> int:
|
|
169
|
+
endpoint = get_endpoint(endpoint_key)
|
|
170
|
+
confirm_change(endpoint, getattr(args, "yes", False))
|
|
171
|
+
data = client_from_args(args).request(endpoint_key, path_params=path_params, query=query, body=body)
|
|
172
|
+
print_output(data, pretty=args.pretty, raw=args.raw)
|
|
173
|
+
return 0
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def handle_docs(args: argparse.Namespace) -> int:
|
|
177
|
+
if args.docs_action == "list":
|
|
178
|
+
payload = [
|
|
179
|
+
{"key": e.key, "operationId": e.operation_id, "method": e.method, "path": e.path, "scope": e.scope, "summary": e.summary}
|
|
180
|
+
for e in ENDPOINTS
|
|
181
|
+
]
|
|
182
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None))
|
|
183
|
+
return 0
|
|
184
|
+
endpoint = get_endpoint(args.endpoint)
|
|
185
|
+
payload = {
|
|
186
|
+
"key": endpoint.key,
|
|
187
|
+
"operationId": endpoint.operation_id,
|
|
188
|
+
"method": endpoint.method,
|
|
189
|
+
"path": endpoint.path,
|
|
190
|
+
"scope": endpoint.scope,
|
|
191
|
+
"summary": endpoint.summary,
|
|
192
|
+
"requiresConfirmation": endpoint.requires_confirmation,
|
|
193
|
+
"example": endpoint.example,
|
|
194
|
+
}
|
|
195
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None))
|
|
196
|
+
return 0
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _payload_from_args(args: argparse.Namespace, direct: dict[str, Any], required: tuple[str, ...]) -> dict[str, Any]:
|
|
200
|
+
payload = load_json_payload(args.json, args.json_file)
|
|
201
|
+
payload.update({key: value for key, value in direct.items() if value is not None})
|
|
202
|
+
require_fields(payload, *required)
|
|
203
|
+
return payload
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def handle_objects(args: argparse.Namespace) -> int:
|
|
207
|
+
action = args.objects_action
|
|
208
|
+
if action == "list":
|
|
209
|
+
return run_request(args, "objects.list", query={"status": args.status})
|
|
210
|
+
if action == "get":
|
|
211
|
+
return run_request(args, "objects.get", path_params={"object_id": args.object_id}, query={"status": args.status})
|
|
212
|
+
if action == "create":
|
|
213
|
+
body = _payload_from_args(
|
|
214
|
+
args,
|
|
215
|
+
{"label": args.label, "key": args.key, "description": args.description},
|
|
216
|
+
("label", "key", "description"),
|
|
217
|
+
)
|
|
218
|
+
return run_request(args, "objects.create", body=body)
|
|
219
|
+
if action == "update":
|
|
220
|
+
body = _payload_from_args(args, {"label": args.label, "description": args.description}, ("label", "description"))
|
|
221
|
+
return run_request(args, "objects.update", path_params={"object_id": args.object_id}, body=body)
|
|
222
|
+
return run_request(args, f"objects.{action}", path_params={"object_id": args.object_id})
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def handle_fields(args: argparse.Namespace) -> int:
|
|
226
|
+
action = args.fields_action
|
|
227
|
+
path = {"object_id": args.object_id}
|
|
228
|
+
if hasattr(args, "field_id"):
|
|
229
|
+
path["field_id"] = args.field_id
|
|
230
|
+
if action == "create":
|
|
231
|
+
body = _payload_from_args(
|
|
232
|
+
args,
|
|
233
|
+
{"label": args.label, "key": args.key, "description": args.description},
|
|
234
|
+
("label", "key", "description"),
|
|
235
|
+
)
|
|
236
|
+
return run_request(args, "fields.create", path_params=path, body=body)
|
|
237
|
+
if action == "update":
|
|
238
|
+
body = _payload_from_args(args, {"label": args.label, "description": args.description}, ("label", "description"))
|
|
239
|
+
return run_request(args, "fields.update", path_params=path, body=body)
|
|
240
|
+
return run_request(args, f"fields.{action}", path_params=path)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def handle_records(args: argparse.Namespace) -> int:
|
|
244
|
+
action = args.records_action
|
|
245
|
+
path = {"object_id": args.object_id}
|
|
246
|
+
if hasattr(args, "record_id"):
|
|
247
|
+
path["record_id"] = args.record_id
|
|
248
|
+
if action == "list":
|
|
249
|
+
return run_request(args, "records.list", path_params=path, query={"page": args.page})
|
|
250
|
+
if action == "get" or action == "delete":
|
|
251
|
+
return run_request(args, f"records.{action}", path_params=path)
|
|
252
|
+
body = load_json_payload(args.json, args.json_file)
|
|
253
|
+
require_fields(body, "values")
|
|
254
|
+
if not isinstance(body["values"], dict):
|
|
255
|
+
raise CliError("Request field 'values' must be an object.")
|
|
256
|
+
return run_request(args, f"records.{action}", path_params=path, body=body)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def add_json_args(parser: argparse.ArgumentParser) -> None:
|
|
260
|
+
group = parser.add_mutually_exclusive_group()
|
|
261
|
+
group.add_argument("--json", help="Request body as a JSON object.")
|
|
262
|
+
group.add_argument("--json-file", help="Read the request body from a UTF-8 JSON file.")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def add_object_fields(parser: argparse.ArgumentParser, *, include_key: bool) -> None:
|
|
266
|
+
parser.add_argument("--label")
|
|
267
|
+
if include_key:
|
|
268
|
+
parser.add_argument("--key")
|
|
269
|
+
parser.add_argument("--description")
|
|
270
|
+
add_json_args(parser)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def add_yes(parser: argparse.ArgumentParser) -> None:
|
|
274
|
+
parser.add_argument("--yes", action="store_true", help="Skip the interactive confirmation.")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
278
|
+
parser = argparse.ArgumentParser(prog="flowbase", description="FlowBase OpenAPI CLI")
|
|
279
|
+
parser.add_argument("--host", help=f"Environment Base URL. Defaults to ${ENV_HOST} or {DEFAULT_HOST}.")
|
|
280
|
+
parser.add_argument("--api-key", help=f"API key. Defaults to ${ENV_API_KEY}.")
|
|
281
|
+
parser.add_argument("--pretty", action=argparse.BooleanOptionalAction, default=True, help="Pretty-print JSON output.")
|
|
282
|
+
parser.add_argument("--raw", action="store_true", help="Print the response body without JSON formatting.")
|
|
283
|
+
groups = parser.add_subparsers(dest="group", required=True)
|
|
284
|
+
|
|
285
|
+
docs = groups.add_parser("docs", help="Inspect the offline endpoint catalog.")
|
|
286
|
+
docs_actions = docs.add_subparsers(dest="docs_action", required=True)
|
|
287
|
+
docs_actions.add_parser("list")
|
|
288
|
+
docs_show = docs_actions.add_parser("show")
|
|
289
|
+
docs_show.add_argument("endpoint", choices=sorted(ENDPOINT_BY_KEY))
|
|
290
|
+
docs.set_defaults(handler=handle_docs)
|
|
291
|
+
|
|
292
|
+
objects = groups.add_parser("objects", help="Manage object metadata.")
|
|
293
|
+
object_actions = objects.add_subparsers(dest="objects_action", required=True)
|
|
294
|
+
object_list = object_actions.add_parser("list")
|
|
295
|
+
object_list.add_argument("--status", choices=("active", "deleted"), default="active")
|
|
296
|
+
object_get = object_actions.add_parser("get")
|
|
297
|
+
object_get.add_argument("object_id")
|
|
298
|
+
object_get.add_argument("--status", choices=("active", "deleted"), default="active")
|
|
299
|
+
add_object_fields(object_actions.add_parser("create"), include_key=True)
|
|
300
|
+
object_update = object_actions.add_parser("update")
|
|
301
|
+
object_update.add_argument("object_id")
|
|
302
|
+
add_object_fields(object_update, include_key=False)
|
|
303
|
+
for action in ("delete", "restore"):
|
|
304
|
+
item = object_actions.add_parser(action)
|
|
305
|
+
item.add_argument("object_id")
|
|
306
|
+
add_yes(item)
|
|
307
|
+
objects.set_defaults(handler=handle_objects)
|
|
308
|
+
|
|
309
|
+
fields = groups.add_parser("fields", help="Manage field metadata.")
|
|
310
|
+
field_actions = fields.add_subparsers(dest="fields_action", required=True)
|
|
311
|
+
field_create = field_actions.add_parser("create")
|
|
312
|
+
field_create.add_argument("object_id")
|
|
313
|
+
add_object_fields(field_create, include_key=True)
|
|
314
|
+
field_update = field_actions.add_parser("update")
|
|
315
|
+
field_update.add_argument("object_id")
|
|
316
|
+
field_update.add_argument("field_id")
|
|
317
|
+
add_object_fields(field_update, include_key=False)
|
|
318
|
+
for action in ("delete", "restore"):
|
|
319
|
+
item = field_actions.add_parser(action)
|
|
320
|
+
item.add_argument("object_id")
|
|
321
|
+
item.add_argument("field_id")
|
|
322
|
+
add_yes(item)
|
|
323
|
+
fields.set_defaults(handler=handle_fields)
|
|
324
|
+
|
|
325
|
+
records = groups.add_parser("records", help="Manage business records.")
|
|
326
|
+
record_actions = records.add_subparsers(dest="records_action", required=True)
|
|
327
|
+
record_list = record_actions.add_parser("list")
|
|
328
|
+
record_list.add_argument("object_id")
|
|
329
|
+
record_list.add_argument("--page", type=int, default=1)
|
|
330
|
+
record_get = record_actions.add_parser("get")
|
|
331
|
+
record_get.add_argument("object_id")
|
|
332
|
+
record_get.add_argument("record_id")
|
|
333
|
+
record_create = record_actions.add_parser("create")
|
|
334
|
+
record_create.add_argument("object_id")
|
|
335
|
+
add_json_args(record_create)
|
|
336
|
+
record_update = record_actions.add_parser("update")
|
|
337
|
+
record_update.add_argument("object_id")
|
|
338
|
+
record_update.add_argument("record_id")
|
|
339
|
+
add_json_args(record_update)
|
|
340
|
+
record_delete = record_actions.add_parser("delete")
|
|
341
|
+
record_delete.add_argument("object_id")
|
|
342
|
+
record_delete.add_argument("record_id")
|
|
343
|
+
add_yes(record_delete)
|
|
344
|
+
records.set_defaults(handler=handle_records)
|
|
345
|
+
return parser
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def main(argv: list[str] | None = None) -> int:
|
|
349
|
+
args = build_parser().parse_args(argv)
|
|
350
|
+
try:
|
|
351
|
+
return args.handler(args)
|
|
352
|
+
except CliError as exc:
|
|
353
|
+
print(f"flowbase: error: {exc}", file=sys.stderr)
|
|
354
|
+
return 1
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def entrypoint() -> None:
|
|
358
|
+
raise SystemExit(main())
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
if __name__ == "__main__":
|
|
362
|
+
entrypoint()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flowbase-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command line client for the FlowBase OpenAPI
|
|
5
|
+
Author: DeepFlowAI
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# FlowBase CLI
|
|
11
|
+
|
|
12
|
+
Install from this checkout:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
python -m pip install ./packages/flowbase-cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Configure an environment Base URL and an API key:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export FLOWBASE_HOST=https://flowbase-test-a.deepflowagent.com
|
|
22
|
+
export FLOWBASE_API_KEY=fbk_xxx
|
|
23
|
+
|
|
24
|
+
flowbase objects list
|
|
25
|
+
flowbase records create obj_xxx --json '{"values":{"name":"Example"}}'
|
|
26
|
+
flowbase docs list
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`--host` selects the environment through its Host. The CLI never accepts a separate environment override and never stores API keys on disk. See `flowbase --help` and `flowbase docs list` for the complete command set.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
flowbase_cli/__init__.py,sha256=IXcJbeEA1I_Xo2f3BH-e0nw3DSIcwavbeipyUiYFUnI,51
|
|
2
|
+
flowbase_cli/endpoints.py,sha256=kDP3VJlyz-JPJs29mfwAvsg1WngCfDyNWUUAfaZ2z74,3348
|
|
3
|
+
flowbase_cli/main.py,sha256=lVYeXB4ZxbN-GwvLjhaVFLqwa3l6lD6-wwa_kfvz-B8,14662
|
|
4
|
+
flowbase_cli-0.1.0.dist-info/METADATA,sha256=JnaWIYMmW8wp1Gha2tTRr1rTkRqV-Mjx8p95TypmlwY,807
|
|
5
|
+
flowbase_cli-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
6
|
+
flowbase_cli-0.1.0.dist-info/entry_points.txt,sha256=kb3ZwOcvIIJ-2W3yyWgn7luZVbPsj57YusOyoIOv2YA,58
|
|
7
|
+
flowbase_cli-0.1.0.dist-info/top_level.txt,sha256=As7iuQHHfVT5d2WJ0cCN22Mmp0gTiOcoPpM0OC3XJxQ,13
|
|
8
|
+
flowbase_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flowbase_cli
|