flowbase-cli 0.1.0__tar.gz → 0.1.1__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.1}/PKG-INFO +4 -2
- flowbase_cli-0.1.1/README.md +22 -0
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.1}/pyproject.toml +1 -1
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.1}/src/flowbase_cli/__init__.py +1 -1
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.1}/src/flowbase_cli/endpoints.py +2 -0
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.1}/src/flowbase_cli/main.py +42 -2
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.1}/src/flowbase_cli.egg-info/PKG-INFO +4 -2
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.1}/tests/test_cli.py +29 -1
- flowbase_cli-0.1.0/README.md +0 -20
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.1}/setup.cfg +0 -0
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.1}/src/flowbase_cli.egg-info/SOURCES.txt +0 -0
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.1}/src/flowbase_cli.egg-info/dependency_links.txt +0 -0
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.1}/src/flowbase_cli.egg-info/entry_points.txt +0 -0
- {flowbase_cli-0.1.0 → flowbase_cli-0.1.1}/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.1
|
|
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.
|
|
@@ -27,6 +27,8 @@ ENDPOINTS = (
|
|
|
27
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
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
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("changes.commit", "flowbase_changes_commit", "POST", "/changes/commit", "metadata:write", "Commit current test-environment drafts.", "flowbase changes commit --message 'Add customer field' --yes", True),
|
|
31
|
+
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),
|
|
30
32
|
Endpoint("records.list", "flowbase_records_list", "GET", "/objects/{object_id}/records", "records:read", "List records.", "flowbase records list obj_xxx --page 1"),
|
|
31
33
|
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
34
|
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"),
|
|
@@ -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
|
|
@@ -49,7 +50,7 @@ def load_json_payload(json_text: str | None, json_file: str | None) -> dict[str,
|
|
|
49
50
|
except OSError as exc:
|
|
50
51
|
raise CliError(f"Cannot read JSON file: {exc}") from exc
|
|
51
52
|
try:
|
|
52
|
-
payload = json.loads(source)
|
|
53
|
+
payload = json.loads(source, parse_float=Decimal)
|
|
53
54
|
except json.JSONDecodeError as exc:
|
|
54
55
|
raise CliError(f"Invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}") from exc
|
|
55
56
|
if not isinstance(payload, dict):
|
|
@@ -99,7 +100,7 @@ class OpenApiClient:
|
|
|
99
100
|
headers = {"Accept": "application/json", "Authorization": f"Bearer {self.api_key}"}
|
|
100
101
|
data = None
|
|
101
102
|
if body is not None:
|
|
102
|
-
data =
|
|
103
|
+
data = exact_json_dumps(body).encode("utf-8")
|
|
103
104
|
headers["Content-Type"] = "application/json"
|
|
104
105
|
request = Request(url, data=data, headers=headers, method=endpoint.method)
|
|
105
106
|
try:
|
|
@@ -127,6 +128,33 @@ def _error_detail(payload: str) -> str:
|
|
|
127
128
|
return json.dumps(parsed, ensure_ascii=False)
|
|
128
129
|
|
|
129
130
|
|
|
131
|
+
def exact_json_dumps(value: Any) -> str:
|
|
132
|
+
if value is None:
|
|
133
|
+
return "null"
|
|
134
|
+
if value is True:
|
|
135
|
+
return "true"
|
|
136
|
+
if value is False:
|
|
137
|
+
return "false"
|
|
138
|
+
if isinstance(value, str):
|
|
139
|
+
return json.dumps(value, ensure_ascii=False)
|
|
140
|
+
if isinstance(value, Decimal):
|
|
141
|
+
if not value.is_finite():
|
|
142
|
+
raise CliError("JSON numbers must be finite.")
|
|
143
|
+
return format(value, "f")
|
|
144
|
+
if isinstance(value, int):
|
|
145
|
+
return str(value)
|
|
146
|
+
if isinstance(value, float):
|
|
147
|
+
raise CliError("Binary floating-point values are not accepted.")
|
|
148
|
+
if isinstance(value, list):
|
|
149
|
+
return "[" + ",".join(exact_json_dumps(item) for item in value) + "]"
|
|
150
|
+
if isinstance(value, dict):
|
|
151
|
+
return "{" + ",".join(
|
|
152
|
+
f"{json.dumps(str(key), ensure_ascii=False)}:{exact_json_dumps(item)}"
|
|
153
|
+
for key, item in value.items()
|
|
154
|
+
) + "}"
|
|
155
|
+
raise CliError(f"Unsupported JSON value: {type(value).__name__}")
|
|
156
|
+
|
|
157
|
+
|
|
130
158
|
def print_output(data: bytes, *, pretty: bool, raw: bool) -> None:
|
|
131
159
|
text = data.decode("utf-8", errors="replace")
|
|
132
160
|
if raw:
|
|
@@ -240,6 +268,10 @@ def handle_fields(args: argparse.Namespace) -> int:
|
|
|
240
268
|
return run_request(args, f"fields.{action}", path_params=path)
|
|
241
269
|
|
|
242
270
|
|
|
271
|
+
def handle_changes(args: argparse.Namespace) -> int:
|
|
272
|
+
return run_request(args, f"changes.{args.changes_action}", body={"message": args.message})
|
|
273
|
+
|
|
274
|
+
|
|
243
275
|
def handle_records(args: argparse.Namespace) -> int:
|
|
244
276
|
action = args.records_action
|
|
245
277
|
path = {"object_id": args.object_id}
|
|
@@ -322,6 +354,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
322
354
|
add_yes(item)
|
|
323
355
|
fields.set_defaults(handler=handle_fields)
|
|
324
356
|
|
|
357
|
+
changes = groups.add_parser("changes", help="Commit or release eligible test-environment metadata versions.")
|
|
358
|
+
change_actions = changes.add_subparsers(dest="changes_action", required=True)
|
|
359
|
+
for action in ("commit", "release"):
|
|
360
|
+
item = change_actions.add_parser(action)
|
|
361
|
+
item.add_argument("--message", required=True)
|
|
362
|
+
add_yes(item)
|
|
363
|
+
changes.set_defaults(handler=handle_changes)
|
|
364
|
+
|
|
325
365
|
records = groups.add_parser("records", help="Manage business records.")
|
|
326
366
|
record_actions = records.add_subparsers(dest="records_action", required=True)
|
|
327
367
|
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.1
|
|
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,7 @@ 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))
|
|
56
77
|
|
|
57
78
|
def test_object_create_requires_business_description(self):
|
|
58
79
|
stderr = io.StringIO()
|
|
@@ -68,6 +89,13 @@ class FlowBaseCliTest(unittest.TestCase):
|
|
|
68
89
|
self.assertEqual(code, 1)
|
|
69
90
|
self.assertIn("without --yes", stderr.getvalue())
|
|
70
91
|
|
|
92
|
+
def test_noninteractive_commit_requires_yes(self):
|
|
93
|
+
stderr = io.StringIO()
|
|
94
|
+
with patch("sys.stderr", stderr), patch("sys.stdin.isatty", return_value=False):
|
|
95
|
+
code = main(["--api-key", "fbk_test", "changes", "commit", "--message", "Add customer field"])
|
|
96
|
+
self.assertEqual(code, 1)
|
|
97
|
+
self.assertIn("without --yes", stderr.getvalue())
|
|
98
|
+
|
|
71
99
|
|
|
72
100
|
if __name__ == "__main__":
|
|
73
101
|
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
|