flowbase-cli 0.1.0__tar.gz → 0.1.2__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.0 → flowbase_cli-0.1.2}/PKG-INFO +4 -2
- flowbase_cli-0.1.2/README.md +22 -0
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.2}/pyproject.toml +1 -1
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.2}/src/flowbase_cli/__init__.py +1 -1
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.2}/src/flowbase_cli/endpoints.py +37 -0
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.2}/src/flowbase_cli/main.py +125 -4
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.2}/src/flowbase_cli.egg-info/PKG-INFO +4 -2
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.2}/tests/test_cli.py +84 -1
- flowbase_cli-0.1.0/README.md +0 -20
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.2}/setup.cfg +0 -0
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.2}/src/flowbase_cli.egg-info/SOURCES.txt +0 -0
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.2}/src/flowbase_cli.egg-info/dependency_links.txt +0 -0
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.2}/src/flowbase_cli.egg-info/entry_points.txt +0 -0
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.2}/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.2
|
|
4
4
|
Summary: Command line client for the FlowBase OpenAPI
|
|
5
5
|
Author: DeepFlowAI
|
|
6
6
|
License: Apache-2.0
|
|
@@ -23,7 +23,9 @@ export FLOWBASE_API_KEY=fbk_xxx
|
|
|
23
23
|
|
|
24
24
|
flowbase objects list
|
|
25
25
|
flowbase records create obj_xxx --json '{"values":{"name":"Example"}}'
|
|
26
|
+
flowbase changes commit --message "Add customer field" --yes
|
|
27
|
+
flowbase changes release --message "Release customer field" --yes
|
|
26
28
|
flowbase docs list
|
|
27
29
|
```
|
|
28
30
|
|
|
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.
|
|
31
|
+
`--host` selects the environment through its Host. Version commands are limited to the current enabled test environment; release can succeed only when its current version meets FlowBase's fast-forward checks. 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,22 @@
|
|
|
1
|
+
# FlowBase CLI
|
|
2
|
+
|
|
3
|
+
Install from this checkout:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
python -m pip install ./packages/flowbase-cli
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Configure an environment Base URL and an API key:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
export FLOWBASE_HOST=https://flowbase-test-a.deepflowagent.com
|
|
13
|
+
export FLOWBASE_API_KEY=fbk_xxx
|
|
14
|
+
|
|
15
|
+
flowbase objects list
|
|
16
|
+
flowbase records create obj_xxx --json '{"values":{"name":"Example"}}'
|
|
17
|
+
flowbase changes commit --message "Add customer field" --yes
|
|
18
|
+
flowbase changes release --message "Release customer field" --yes
|
|
19
|
+
flowbase docs list
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`--host` selects the environment through its Host. Version commands are limited to the current enabled test environment; release can succeed only when its current version meets FlowBase's fast-forward checks. 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.
|
|
@@ -14,6 +14,9 @@ class Endpoint:
|
|
|
14
14
|
summary: str
|
|
15
15
|
example: str
|
|
16
16
|
requires_confirmation: bool = False
|
|
17
|
+
environments: tuple[str, ...] = ("production", "test-a", "test-b")
|
|
18
|
+
preflight_operation_id: str | None = None
|
|
19
|
+
audit_event: str | None = None
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
ENDPOINTS = (
|
|
@@ -27,6 +30,31 @@ ENDPOINTS = (
|
|
|
27
30
|
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
31
|
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
32
|
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),
|
|
33
|
+
Endpoint("changes.commit", "flowbase_changes_commit", "POST", "/changes/commit", "metadata:write", "Commit current test-environment drafts.", "flowbase changes commit --message 'Add customer field' --yes", True, environments=("test-a", "test-b"), audit_event="version.commit"),
|
|
34
|
+
Endpoint("changes.release", "flowbase_changes_release", "POST", "/changes/release", "metadata:write", "Release the current test-environment version.", "flowbase changes release --message 'Release customer field' --yes", True, environments=("test-a", "test-b"), audit_event="version.release"),
|
|
35
|
+
Endpoint(
|
|
36
|
+
"changes.direct-release-preflight",
|
|
37
|
+
"flowbase_changes_direct_release_preflight",
|
|
38
|
+
"POST",
|
|
39
|
+
"/changes/direct-release/preflight",
|
|
40
|
+
"metadata:write",
|
|
41
|
+
"Preflight a production direct release.",
|
|
42
|
+
"flowbase changes direct-release --message 'Fix production field' --yes",
|
|
43
|
+
environments=("production",),
|
|
44
|
+
),
|
|
45
|
+
Endpoint(
|
|
46
|
+
"changes.direct-release",
|
|
47
|
+
"flowbase_changes_direct_release",
|
|
48
|
+
"POST",
|
|
49
|
+
"/changes/direct-release",
|
|
50
|
+
"metadata:write",
|
|
51
|
+
"Directly release production drafts.",
|
|
52
|
+
"flowbase changes direct-release --message 'Fix production field' --yes",
|
|
53
|
+
requires_confirmation=True,
|
|
54
|
+
environments=("production",),
|
|
55
|
+
preflight_operation_id="flowbase_changes_direct_release_preflight",
|
|
56
|
+
audit_event="version.forward-fix",
|
|
57
|
+
),
|
|
30
58
|
Endpoint("records.list", "flowbase_records_list", "GET", "/objects/{object_id}/records", "records:read", "List records.", "flowbase records list obj_xxx --page 1"),
|
|
31
59
|
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
60
|
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"),
|
|
@@ -35,3 +63,12 @@ ENDPOINTS = (
|
|
|
35
63
|
)
|
|
36
64
|
|
|
37
65
|
ENDPOINT_BY_KEY = {endpoint.key: endpoint for endpoint in ENDPOINTS}
|
|
66
|
+
|
|
67
|
+
PENDING_MANAGEMENT_OPERATIONS = (
|
|
68
|
+
{"key": "environments.enable", "status": "pending", "reason": "Requires an independent capability and confirmation contract."},
|
|
69
|
+
{"key": "environments.restart", "status": "pending", "reason": "Requires destructive data-reset confirmation."},
|
|
70
|
+
{"key": "changes.clear-drafts", "status": "pending", "reason": "Requires an independent capability and confirmation contract."},
|
|
71
|
+
{"key": "api-keys.manage", "status": "pending", "reason": "API keys cannot delegate or expand their own authority."},
|
|
72
|
+
{"key": "git-credentials.manage", "status": "pending", "reason": "Requires a dedicated credential-management capability."},
|
|
73
|
+
{"key": "metadata.purge", "status": "pending", "reason": "Physical purge is not delivered."},
|
|
74
|
+
)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
|
+
from decimal import Decimal
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
7
8
|
import sys
|
|
@@ -11,7 +12,12 @@ from urllib.error import HTTPError, URLError
|
|
|
11
12
|
from urllib.parse import quote, urlencode
|
|
12
13
|
from urllib.request import Request, urlopen
|
|
13
14
|
|
|
14
|
-
from flowbase_cli.endpoints import
|
|
15
|
+
from flowbase_cli.endpoints import (
|
|
16
|
+
ENDPOINTS,
|
|
17
|
+
ENDPOINT_BY_KEY,
|
|
18
|
+
PENDING_MANAGEMENT_OPERATIONS,
|
|
19
|
+
Endpoint,
|
|
20
|
+
)
|
|
15
21
|
|
|
16
22
|
DEFAULT_HOST = "http://flowbase.localhost:5001"
|
|
17
23
|
API_PREFIX = "/api/openapi/v1"
|
|
@@ -49,7 +55,7 @@ def load_json_payload(json_text: str | None, json_file: str | None) -> dict[str,
|
|
|
49
55
|
except OSError as exc:
|
|
50
56
|
raise CliError(f"Cannot read JSON file: {exc}") from exc
|
|
51
57
|
try:
|
|
52
|
-
payload = json.loads(source)
|
|
58
|
+
payload = json.loads(source, parse_float=Decimal)
|
|
53
59
|
except json.JSONDecodeError as exc:
|
|
54
60
|
raise CliError(f"Invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}") from exc
|
|
55
61
|
if not isinstance(payload, dict):
|
|
@@ -99,7 +105,7 @@ class OpenApiClient:
|
|
|
99
105
|
headers = {"Accept": "application/json", "Authorization": f"Bearer {self.api_key}"}
|
|
100
106
|
data = None
|
|
101
107
|
if body is not None:
|
|
102
|
-
data =
|
|
108
|
+
data = exact_json_dumps(body).encode("utf-8")
|
|
103
109
|
headers["Content-Type"] = "application/json"
|
|
104
110
|
request = Request(url, data=data, headers=headers, method=endpoint.method)
|
|
105
111
|
try:
|
|
@@ -127,6 +133,33 @@ def _error_detail(payload: str) -> str:
|
|
|
127
133
|
return json.dumps(parsed, ensure_ascii=False)
|
|
128
134
|
|
|
129
135
|
|
|
136
|
+
def exact_json_dumps(value: Any) -> str:
|
|
137
|
+
if value is None:
|
|
138
|
+
return "null"
|
|
139
|
+
if value is True:
|
|
140
|
+
return "true"
|
|
141
|
+
if value is False:
|
|
142
|
+
return "false"
|
|
143
|
+
if isinstance(value, str):
|
|
144
|
+
return json.dumps(value, ensure_ascii=False)
|
|
145
|
+
if isinstance(value, Decimal):
|
|
146
|
+
if not value.is_finite():
|
|
147
|
+
raise CliError("JSON numbers must be finite.")
|
|
148
|
+
return format(value, "f")
|
|
149
|
+
if isinstance(value, int):
|
|
150
|
+
return str(value)
|
|
151
|
+
if isinstance(value, float):
|
|
152
|
+
raise CliError("Binary floating-point values are not accepted.")
|
|
153
|
+
if isinstance(value, list):
|
|
154
|
+
return "[" + ",".join(exact_json_dumps(item) for item in value) + "]"
|
|
155
|
+
if isinstance(value, dict):
|
|
156
|
+
return "{" + ",".join(
|
|
157
|
+
f"{json.dumps(str(key), ensure_ascii=False)}:{exact_json_dumps(item)}"
|
|
158
|
+
for key, item in value.items()
|
|
159
|
+
) + "}"
|
|
160
|
+
raise CliError(f"Unsupported JSON value: {type(value).__name__}")
|
|
161
|
+
|
|
162
|
+
|
|
130
163
|
def print_output(data: bytes, *, pretty: bool, raw: bool) -> None:
|
|
131
164
|
text = data.decode("utf-8", errors="replace")
|
|
132
165
|
if raw:
|
|
@@ -174,9 +207,42 @@ def run_request(
|
|
|
174
207
|
|
|
175
208
|
|
|
176
209
|
def handle_docs(args: argparse.Namespace) -> int:
|
|
210
|
+
if args.docs_action == "operations":
|
|
211
|
+
delivered = [
|
|
212
|
+
{
|
|
213
|
+
"key": endpoint.key,
|
|
214
|
+
"status": "delivered",
|
|
215
|
+
"operationId": endpoint.operation_id,
|
|
216
|
+
"method": endpoint.method,
|
|
217
|
+
"path": endpoint.path,
|
|
218
|
+
"scope": endpoint.scope,
|
|
219
|
+
"environments": list(endpoint.environments),
|
|
220
|
+
"requiresPreflight": endpoint.preflight_operation_id is not None,
|
|
221
|
+
"requiresConfirmation": endpoint.requires_confirmation,
|
|
222
|
+
"auditEvent": endpoint.audit_event,
|
|
223
|
+
}
|
|
224
|
+
for endpoint in ENDPOINTS
|
|
225
|
+
if endpoint.scope.startswith("metadata:")
|
|
226
|
+
]
|
|
227
|
+
print(
|
|
228
|
+
json.dumps(
|
|
229
|
+
delivered + list(PENDING_MANAGEMENT_OPERATIONS),
|
|
230
|
+
ensure_ascii=False,
|
|
231
|
+
indent=2 if args.pretty else None,
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
return 0
|
|
177
235
|
if args.docs_action == "list":
|
|
178
236
|
payload = [
|
|
179
|
-
{
|
|
237
|
+
{
|
|
238
|
+
"key": e.key,
|
|
239
|
+
"operationId": e.operation_id,
|
|
240
|
+
"method": e.method,
|
|
241
|
+
"path": e.path,
|
|
242
|
+
"scope": e.scope,
|
|
243
|
+
"environments": list(e.environments),
|
|
244
|
+
"summary": e.summary,
|
|
245
|
+
}
|
|
180
246
|
for e in ENDPOINTS
|
|
181
247
|
]
|
|
182
248
|
print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None))
|
|
@@ -190,6 +256,9 @@ def handle_docs(args: argparse.Namespace) -> int:
|
|
|
190
256
|
"scope": endpoint.scope,
|
|
191
257
|
"summary": endpoint.summary,
|
|
192
258
|
"requiresConfirmation": endpoint.requires_confirmation,
|
|
259
|
+
"environments": list(endpoint.environments),
|
|
260
|
+
"preflightOperationId": endpoint.preflight_operation_id,
|
|
261
|
+
"auditEvent": endpoint.audit_event,
|
|
193
262
|
"example": endpoint.example,
|
|
194
263
|
}
|
|
195
264
|
print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None))
|
|
@@ -240,6 +309,49 @@ def handle_fields(args: argparse.Namespace) -> int:
|
|
|
240
309
|
return run_request(args, f"fields.{action}", path_params=path)
|
|
241
310
|
|
|
242
311
|
|
|
312
|
+
def handle_changes(args: argparse.Namespace) -> int:
|
|
313
|
+
if args.changes_action == "direct-release":
|
|
314
|
+
client = client_from_args(args)
|
|
315
|
+
body = {"message": args.message}
|
|
316
|
+
preflight_data = client.request(
|
|
317
|
+
"changes.direct-release-preflight",
|
|
318
|
+
body=body,
|
|
319
|
+
)
|
|
320
|
+
try:
|
|
321
|
+
preflight = json.loads(preflight_data)
|
|
322
|
+
except json.JSONDecodeError as exc:
|
|
323
|
+
raise CliError("Direct-release preflight returned invalid JSON.") from exc
|
|
324
|
+
token = preflight.get("confirmationToken")
|
|
325
|
+
if not isinstance(token, str):
|
|
326
|
+
raise CliError("Direct-release preflight did not return a confirmation token.")
|
|
327
|
+
_print_direct_release_preflight(preflight)
|
|
328
|
+
endpoint = get_endpoint("changes.direct-release")
|
|
329
|
+
confirm_change(endpoint, args.yes)
|
|
330
|
+
data = client.request(
|
|
331
|
+
"changes.direct-release",
|
|
332
|
+
body={"message": args.message, "confirmationToken": token},
|
|
333
|
+
)
|
|
334
|
+
print_output(data, pretty=args.pretty, raw=args.raw)
|
|
335
|
+
return 0
|
|
336
|
+
return run_request(args, f"changes.{args.changes_action}", body={"message": args.message})
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _print_direct_release_preflight(preflight: dict[str, Any]) -> None:
|
|
340
|
+
print("Production direct-release preflight:", file=sys.stderr)
|
|
341
|
+
print(f" Current main: {preflight.get('mainCommit', 'unknown')}", file=sys.stderr)
|
|
342
|
+
print(f" Draft revision: {preflight.get('draftRevision', 'unknown')}", file=sys.stderr)
|
|
343
|
+
print(
|
|
344
|
+
f" Changes: {preflight.get('semanticCount', 0)} semantic, "
|
|
345
|
+
f"{preflight.get('fileCount', 0)} files",
|
|
346
|
+
file=sys.stderr,
|
|
347
|
+
)
|
|
348
|
+
for check in preflight.get("checks", []):
|
|
349
|
+
if isinstance(check, dict):
|
|
350
|
+
mark = "OK" if check.get("ok") else "FAIL"
|
|
351
|
+
print(f" [{mark}] {check.get('label', '')}", file=sys.stderr)
|
|
352
|
+
print(f" RISK: {preflight.get('risk', 'This bypasses test environments.')}", file=sys.stderr)
|
|
353
|
+
|
|
354
|
+
|
|
243
355
|
def handle_records(args: argparse.Namespace) -> int:
|
|
244
356
|
action = args.records_action
|
|
245
357
|
path = {"object_id": args.object_id}
|
|
@@ -285,6 +397,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
285
397
|
docs = groups.add_parser("docs", help="Inspect the offline endpoint catalog.")
|
|
286
398
|
docs_actions = docs.add_subparsers(dest="docs_action", required=True)
|
|
287
399
|
docs_actions.add_parser("list")
|
|
400
|
+
docs_actions.add_parser("operations")
|
|
288
401
|
docs_show = docs_actions.add_parser("show")
|
|
289
402
|
docs_show.add_argument("endpoint", choices=sorted(ENDPOINT_BY_KEY))
|
|
290
403
|
docs.set_defaults(handler=handle_docs)
|
|
@@ -322,6 +435,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
322
435
|
add_yes(item)
|
|
323
436
|
fields.set_defaults(handler=handle_fields)
|
|
324
437
|
|
|
438
|
+
changes = groups.add_parser("changes", help="Commit or release metadata versions.")
|
|
439
|
+
change_actions = changes.add_subparsers(dest="changes_action", required=True)
|
|
440
|
+
for action in ("commit", "release", "direct-release"):
|
|
441
|
+
item = change_actions.add_parser(action)
|
|
442
|
+
item.add_argument("--message", required=True)
|
|
443
|
+
add_yes(item)
|
|
444
|
+
changes.set_defaults(handler=handle_changes)
|
|
445
|
+
|
|
325
446
|
records = groups.add_parser("records", help="Manage business records.")
|
|
326
447
|
record_actions = records.add_subparsers(dest="records_action", required=True)
|
|
327
448
|
record_list = record_actions.add_parser("list")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowbase-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Command line client for the FlowBase OpenAPI
|
|
5
5
|
Author: DeepFlowAI
|
|
6
6
|
License: Apache-2.0
|
|
@@ -23,7 +23,9 @@ export FLOWBASE_API_KEY=fbk_xxx
|
|
|
23
23
|
|
|
24
24
|
flowbase objects list
|
|
25
25
|
flowbase records create obj_xxx --json '{"values":{"name":"Example"}}'
|
|
26
|
+
flowbase changes commit --message "Add customer field" --yes
|
|
27
|
+
flowbase changes release --message "Release customer field" --yes
|
|
26
28
|
flowbase docs list
|
|
27
29
|
```
|
|
28
30
|
|
|
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.
|
|
31
|
+
`--host` selects the environment through its Host. Version commands are limited to the current enabled test environment; release can succeed only when its current version meets FlowBase's fast-forward checks. 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.
|
|
@@ -3,12 +3,20 @@ from __future__ import annotations
|
|
|
3
3
|
import io
|
|
4
4
|
import json
|
|
5
5
|
import unittest
|
|
6
|
+
from decimal import Decimal
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from tempfile import TemporaryDirectory
|
|
8
9
|
from unittest.mock import patch
|
|
9
10
|
|
|
10
11
|
from flowbase_cli.endpoints import ENDPOINTS
|
|
11
|
-
from flowbase_cli.main import
|
|
12
|
+
from flowbase_cli.main import (
|
|
13
|
+
CliError,
|
|
14
|
+
OpenApiClient,
|
|
15
|
+
api_root,
|
|
16
|
+
exact_json_dumps,
|
|
17
|
+
load_json_payload,
|
|
18
|
+
main,
|
|
19
|
+
)
|
|
12
20
|
|
|
13
21
|
|
|
14
22
|
class FlowBaseCliTest(unittest.TestCase):
|
|
@@ -33,6 +41,18 @@ class FlowBaseCliTest(unittest.TestCase):
|
|
|
33
41
|
path.write_text('{"label":"客户"}', encoding="utf-8")
|
|
34
42
|
self.assertEqual(load_json_payload(None, str(path)), {"label": "客户"})
|
|
35
43
|
|
|
44
|
+
def test_decimal_payload_preserves_json_number_text(self):
|
|
45
|
+
payload = load_json_payload(
|
|
46
|
+
'{"values":{"amount":12345678901234.5678}}', None
|
|
47
|
+
)
|
|
48
|
+
self.assertEqual(
|
|
49
|
+
payload["values"]["amount"], Decimal("12345678901234.5678")
|
|
50
|
+
)
|
|
51
|
+
self.assertEqual(
|
|
52
|
+
exact_json_dumps(payload),
|
|
53
|
+
'{"values":{"amount":12345678901234.5678}}',
|
|
54
|
+
)
|
|
55
|
+
|
|
36
56
|
def test_missing_key_is_rejected_before_network(self):
|
|
37
57
|
client = OpenApiClient("https://example.com", None)
|
|
38
58
|
with patch("flowbase_cli.main.urlopen") as mocked:
|
|
@@ -53,6 +73,16 @@ class FlowBaseCliTest(unittest.TestCase):
|
|
|
53
73
|
self.assertEqual(code, 0)
|
|
54
74
|
payload = json.loads(stdout.getvalue())
|
|
55
75
|
self.assertTrue(any(item["key"] == "records.create" for item in payload))
|
|
76
|
+
self.assertTrue(any(item["key"] == "changes.release" for item in payload))
|
|
77
|
+
|
|
78
|
+
def test_management_operation_catalog_identifies_pending_gaps(self):
|
|
79
|
+
stdout = io.StringIO()
|
|
80
|
+
with patch("sys.stdout", stdout):
|
|
81
|
+
code = main(["docs", "operations"])
|
|
82
|
+
self.assertEqual(code, 0)
|
|
83
|
+
payload = json.loads(stdout.getvalue())
|
|
84
|
+
self.assertTrue(any(item["key"] == "changes.direct-release" and item["status"] == "delivered" for item in payload))
|
|
85
|
+
self.assertTrue(any(item["key"] == "changes.clear-drafts" and item["status"] == "pending" for item in payload))
|
|
56
86
|
|
|
57
87
|
def test_object_create_requires_business_description(self):
|
|
58
88
|
stderr = io.StringIO()
|
|
@@ -68,6 +98,59 @@ class FlowBaseCliTest(unittest.TestCase):
|
|
|
68
98
|
self.assertEqual(code, 1)
|
|
69
99
|
self.assertIn("without --yes", stderr.getvalue())
|
|
70
100
|
|
|
101
|
+
def test_noninteractive_commit_requires_yes(self):
|
|
102
|
+
stderr = io.StringIO()
|
|
103
|
+
with patch("sys.stderr", stderr), patch("sys.stdin.isatty", return_value=False):
|
|
104
|
+
code = main(["--api-key", "fbk_test", "changes", "commit", "--message", "Add customer field"])
|
|
105
|
+
self.assertEqual(code, 1)
|
|
106
|
+
self.assertIn("without --yes", stderr.getvalue())
|
|
107
|
+
|
|
108
|
+
def test_direct_release_always_preflights_before_execution(self):
|
|
109
|
+
preflight = {
|
|
110
|
+
"confirmationToken": "fbc_secret",
|
|
111
|
+
"mainCommit": "abc1234",
|
|
112
|
+
"draftRevision": 4,
|
|
113
|
+
"semanticCount": 2,
|
|
114
|
+
"fileCount": 1,
|
|
115
|
+
"checks": [{"label": "Configuration valid", "ok": True}],
|
|
116
|
+
"risk": "Bypasses test environments.",
|
|
117
|
+
}
|
|
118
|
+
stdout = io.StringIO()
|
|
119
|
+
stderr = io.StringIO()
|
|
120
|
+
with (
|
|
121
|
+
patch(
|
|
122
|
+
"flowbase_cli.main.OpenApiClient.request",
|
|
123
|
+
side_effect=[
|
|
124
|
+
json.dumps(preflight).encode(),
|
|
125
|
+
b'{"mainCommit":"def5678"}',
|
|
126
|
+
],
|
|
127
|
+
) as request,
|
|
128
|
+
patch("sys.stdout", stdout),
|
|
129
|
+
patch("sys.stderr", stderr),
|
|
130
|
+
):
|
|
131
|
+
code = main([
|
|
132
|
+
"--api-key",
|
|
133
|
+
"fbk_test",
|
|
134
|
+
"changes",
|
|
135
|
+
"direct-release",
|
|
136
|
+
"--message",
|
|
137
|
+
"Production fix",
|
|
138
|
+
"--yes",
|
|
139
|
+
])
|
|
140
|
+
self.assertEqual(code, 0)
|
|
141
|
+
self.assertEqual(request.call_count, 2)
|
|
142
|
+
self.assertEqual(
|
|
143
|
+
request.call_args_list[0].args[0],
|
|
144
|
+
"changes.direct-release-preflight",
|
|
145
|
+
)
|
|
146
|
+
self.assertEqual(
|
|
147
|
+
request.call_args_list[1].kwargs["body"]["confirmationToken"],
|
|
148
|
+
"fbc_secret",
|
|
149
|
+
)
|
|
150
|
+
self.assertNotIn("fbc_secret", stdout.getvalue())
|
|
151
|
+
self.assertNotIn("fbc_secret", stderr.getvalue())
|
|
152
|
+
self.assertIn("Bypasses test environments", stderr.getvalue())
|
|
153
|
+
|
|
71
154
|
|
|
72
155
|
if __name__ == "__main__":
|
|
73
156
|
unittest.main()
|
flowbase_cli-0.1.0/README.md
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# FlowBase CLI
|
|
2
|
-
|
|
3
|
-
Install from this checkout:
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
python -m pip install ./packages/flowbase-cli
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
Configure an environment Base URL and an API key:
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
export FLOWBASE_HOST=https://flowbase-test-a.deepflowagent.com
|
|
13
|
-
export FLOWBASE_API_KEY=fbk_xxx
|
|
14
|
-
|
|
15
|
-
flowbase objects list
|
|
16
|
-
flowbase records create obj_xxx --json '{"values":{"name":"Example"}}'
|
|
17
|
-
flowbase docs list
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
`--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.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|