fulfil-cli 0.1.3__tar.gz → 0.2.0__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.
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/PKG-INFO +21 -15
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/README.md +20 -14
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/pyproject.toml +1 -1
- fulfil_cli-0.2.0/src/fulfil_cli/__init__.py +6 -0
- fulfil_cli-0.2.0/src/fulfil_cli/auth/__init__.py +3 -0
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/auth/api_key.py +8 -42
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/cli/app.py +92 -61
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/cli/commands/api.py +18 -16
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/cli/commands/auth.py +14 -24
- fulfil_cli-0.2.0/src/fulfil_cli/cli/commands/common.py +54 -0
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/cli/commands/completion.py +4 -2
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/cli/commands/config.py +18 -11
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/cli/commands/model.py +112 -144
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/cli/commands/report.py +30 -49
- fulfil_cli-0.2.0/src/fulfil_cli/cli/state.py +106 -0
- fulfil_cli-0.2.0/src/fulfil_cli/client/__init__.py +4 -0
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/client/errors.py +12 -0
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/client/http.py +8 -8
- fulfil_cli-0.2.0/src/fulfil_cli/config/__init__.py +3 -0
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/config/paths.py +14 -4
- fulfil_cli-0.2.0/src/fulfil_cli/output/__init__.py +4 -0
- fulfil_cli-0.2.0/src/fulfil_cli/output/formatter.py +125 -0
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/output/json_output.py +6 -7
- fulfil_cli-0.1.3/src/fulfil_cli/__init__.py +0 -5
- fulfil_cli-0.1.3/src/fulfil_cli/cli/state.py +0 -89
- fulfil_cli-0.1.3/src/fulfil_cli/client/__init__.py +0 -0
- fulfil_cli-0.1.3/src/fulfil_cli/config/__init__.py +0 -0
- fulfil_cli-0.1.3/src/fulfil_cli/output/__init__.py +0 -0
- fulfil_cli-0.1.3/src/fulfil_cli/output/formatter.py +0 -129
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/__main__.py +0 -0
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/auth/keyring_store.py +0 -0
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/auth/oauth.py +0 -0
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/cli/__init__.py +0 -0
- {fulfil_cli-0.1.3/src/fulfil_cli/auth → fulfil_cli-0.2.0/src/fulfil_cli/cli/commands}/__init__.py +0 -0
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/config/manager.py +0 -0
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/output/describe.py +0 -0
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/output/report.py +0 -0
- {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/output/table.py +0 -0
- /fulfil_cli-0.1.3/src/fulfil_cli/cli/commands/__init__.py → /fulfil_cli-0.2.0/src/fulfil_cli/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fulfil-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: The Fulfil CLI — primary interface for humans and AI agents
|
|
5
5
|
Keywords: fulfil,cli,erp,ecommerce
|
|
6
6
|
Author: Fulfil.IO
|
|
@@ -108,12 +108,12 @@ fulfil sales_order list --fields reference,state,total_amount
|
|
|
108
108
|
|
|
109
109
|
# Filter with MongoDB-style queries
|
|
110
110
|
fulfil sales_order list --where '{"state": "confirmed"}'
|
|
111
|
-
fulfil sales_order list --where '{"
|
|
111
|
+
fulfil sales_order list --where '{"sale_date": {"gte": "2025-01-01"}}'
|
|
112
112
|
fulfil sales_order list --where '{"or": [{"state": "draft"}, {"state": "confirmed"}]}'
|
|
113
113
|
|
|
114
114
|
# Sort results
|
|
115
115
|
fulfil sales_order list --order sale_date:desc
|
|
116
|
-
fulfil sales_order list --order
|
|
116
|
+
fulfil sales_order list --order sale_date:desc,reference:asc
|
|
117
117
|
|
|
118
118
|
# Control page size
|
|
119
119
|
fulfil sales_order list --limit 50
|
|
@@ -134,20 +134,23 @@ fulfil sales_order get 1,2,3
|
|
|
134
134
|
### Creating records
|
|
135
135
|
|
|
136
136
|
```bash
|
|
137
|
-
# Single record
|
|
138
|
-
|
|
137
|
+
# Single record (from stdin)
|
|
138
|
+
echo '{"name": "Acme Corp"}' | fulfil contact create
|
|
139
139
|
|
|
140
140
|
# Record with nested lines (e.g. contact with addresses)
|
|
141
|
-
|
|
141
|
+
echo '{"name": "Acme Corp", "addresses": [{"street": "100 Broadway", "city": "New York"}, {"street": "45 Industrial Pkwy", "city": "Newark"}]}' | fulfil contact create
|
|
142
142
|
|
|
143
|
-
# Multiple records at once
|
|
144
|
-
|
|
143
|
+
# Multiple records at once (preferred — never loop single creates)
|
|
144
|
+
echo '[{"name": "Alice"}, {"name": "Bob"}]' | fulfil contact create
|
|
145
|
+
|
|
146
|
+
# From a file
|
|
147
|
+
fulfil contact create data.json
|
|
145
148
|
```
|
|
146
149
|
|
|
147
150
|
### Updating records
|
|
148
151
|
|
|
149
152
|
```bash
|
|
150
|
-
|
|
153
|
+
echo '{"comment": "Approved by finance"}' | fulfil sales_order update 42
|
|
151
154
|
```
|
|
152
155
|
|
|
153
156
|
### Deleting records
|
|
@@ -218,14 +221,17 @@ echo '{"method": "model.product.count", "params": {}}' | fulfil api -
|
|
|
218
221
|
|---|---|
|
|
219
222
|
| Terminal | Rich tables with colors |
|
|
220
223
|
| Piped / redirected / CI | JSON (automatic) |
|
|
221
|
-
| `--
|
|
224
|
+
| `--format` flag | table, json, csv, or ndjson |
|
|
222
225
|
|
|
223
226
|
```bash
|
|
224
227
|
# Force JSON and pipe to jq
|
|
225
|
-
fulfil sales_order list --fields reference,state --json | jq '.data[].reference'
|
|
228
|
+
fulfil sales_order list --fields reference,state --format json | jq '.data[].reference'
|
|
229
|
+
|
|
230
|
+
# Force format via env var
|
|
231
|
+
FULFIL_FORMAT=json fulfil sales_order list
|
|
226
232
|
|
|
227
|
-
#
|
|
228
|
-
|
|
233
|
+
# CSV output
|
|
234
|
+
fulfil sales_order list --fields reference,state --format csv
|
|
229
235
|
```
|
|
230
236
|
|
|
231
237
|
## Configuration
|
|
@@ -245,7 +251,7 @@ Config file location: `~/.config/fulfil/config.toml`
|
|
|
245
251
|
--workspace TEXT Workspace domain (e.g. acme.fulfil.io)
|
|
246
252
|
--debug Show HTTP request/response details
|
|
247
253
|
--quiet, -q Suppress hints and decorative output
|
|
248
|
-
--json
|
|
254
|
+
--format TEXT Output format: table, json, csv, ndjson
|
|
249
255
|
-h, --help Show help
|
|
250
256
|
```
|
|
251
257
|
|
|
@@ -274,7 +280,7 @@ fulfil completion # auto-detects zsh/bash/fish
|
|
|
274
280
|
|
|
275
281
|
The CLI is designed for programmatic use:
|
|
276
282
|
|
|
277
|
-
- **JSON output by default** when stdout is piped or redirected
|
|
283
|
+
- **JSON output by default** when stdout is piped or redirected (override with `--format` or `FULFIL_FORMAT`)
|
|
278
284
|
- **Structured exit codes** for error handling
|
|
279
285
|
- **Errors on stderr**, data on stdout — safe to parse stdout directly
|
|
280
286
|
- **Environment variable auth** — no interactive prompts: `FULFIL_API_KEY` + `FULFIL_WORKSPACE`
|
|
@@ -78,12 +78,12 @@ fulfil sales_order list --fields reference,state,total_amount
|
|
|
78
78
|
|
|
79
79
|
# Filter with MongoDB-style queries
|
|
80
80
|
fulfil sales_order list --where '{"state": "confirmed"}'
|
|
81
|
-
fulfil sales_order list --where '{"
|
|
81
|
+
fulfil sales_order list --where '{"sale_date": {"gte": "2025-01-01"}}'
|
|
82
82
|
fulfil sales_order list --where '{"or": [{"state": "draft"}, {"state": "confirmed"}]}'
|
|
83
83
|
|
|
84
84
|
# Sort results
|
|
85
85
|
fulfil sales_order list --order sale_date:desc
|
|
86
|
-
fulfil sales_order list --order
|
|
86
|
+
fulfil sales_order list --order sale_date:desc,reference:asc
|
|
87
87
|
|
|
88
88
|
# Control page size
|
|
89
89
|
fulfil sales_order list --limit 50
|
|
@@ -104,20 +104,23 @@ fulfil sales_order get 1,2,3
|
|
|
104
104
|
### Creating records
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
|
-
# Single record
|
|
108
|
-
|
|
107
|
+
# Single record (from stdin)
|
|
108
|
+
echo '{"name": "Acme Corp"}' | fulfil contact create
|
|
109
109
|
|
|
110
110
|
# Record with nested lines (e.g. contact with addresses)
|
|
111
|
-
|
|
111
|
+
echo '{"name": "Acme Corp", "addresses": [{"street": "100 Broadway", "city": "New York"}, {"street": "45 Industrial Pkwy", "city": "Newark"}]}' | fulfil contact create
|
|
112
112
|
|
|
113
|
-
# Multiple records at once
|
|
114
|
-
|
|
113
|
+
# Multiple records at once (preferred — never loop single creates)
|
|
114
|
+
echo '[{"name": "Alice"}, {"name": "Bob"}]' | fulfil contact create
|
|
115
|
+
|
|
116
|
+
# From a file
|
|
117
|
+
fulfil contact create data.json
|
|
115
118
|
```
|
|
116
119
|
|
|
117
120
|
### Updating records
|
|
118
121
|
|
|
119
122
|
```bash
|
|
120
|
-
|
|
123
|
+
echo '{"comment": "Approved by finance"}' | fulfil sales_order update 42
|
|
121
124
|
```
|
|
122
125
|
|
|
123
126
|
### Deleting records
|
|
@@ -188,14 +191,17 @@ echo '{"method": "model.product.count", "params": {}}' | fulfil api -
|
|
|
188
191
|
|---|---|
|
|
189
192
|
| Terminal | Rich tables with colors |
|
|
190
193
|
| Piped / redirected / CI | JSON (automatic) |
|
|
191
|
-
| `--
|
|
194
|
+
| `--format` flag | table, json, csv, or ndjson |
|
|
192
195
|
|
|
193
196
|
```bash
|
|
194
197
|
# Force JSON and pipe to jq
|
|
195
|
-
fulfil sales_order list --fields reference,state --json | jq '.data[].reference'
|
|
198
|
+
fulfil sales_order list --fields reference,state --format json | jq '.data[].reference'
|
|
199
|
+
|
|
200
|
+
# Force format via env var
|
|
201
|
+
FULFIL_FORMAT=json fulfil sales_order list
|
|
196
202
|
|
|
197
|
-
#
|
|
198
|
-
|
|
203
|
+
# CSV output
|
|
204
|
+
fulfil sales_order list --fields reference,state --format csv
|
|
199
205
|
```
|
|
200
206
|
|
|
201
207
|
## Configuration
|
|
@@ -215,7 +221,7 @@ Config file location: `~/.config/fulfil/config.toml`
|
|
|
215
221
|
--workspace TEXT Workspace domain (e.g. acme.fulfil.io)
|
|
216
222
|
--debug Show HTTP request/response details
|
|
217
223
|
--quiet, -q Suppress hints and decorative output
|
|
218
|
-
--json
|
|
224
|
+
--format TEXT Output format: table, json, csv, ndjson
|
|
219
225
|
-h, --help Show help
|
|
220
226
|
```
|
|
221
227
|
|
|
@@ -244,7 +250,7 @@ fulfil completion # auto-detects zsh/bash/fish
|
|
|
244
250
|
|
|
245
251
|
The CLI is designed for programmatic use:
|
|
246
252
|
|
|
247
|
-
- **JSON output by default** when stdout is piped or redirected
|
|
253
|
+
- **JSON output by default** when stdout is piped or redirected (override with `--format` or `FULFIL_FORMAT`)
|
|
248
254
|
- **Structured exit codes** for error handling
|
|
249
255
|
- **Errors on stderr**, data on stdout — safe to parse stdout directly
|
|
250
256
|
- **Environment variable auth** — no interactive prompts: `FULFIL_API_KEY` + `FULFIL_WORKSPACE`
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Credential resolution — flag > env > keyring, with workspace normalization."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
+
from typing import Literal
|
|
7
8
|
|
|
8
9
|
from fulfil_cli.auth.keyring_store import get_api_key, get_oauth_tokens, store_oauth_tokens
|
|
10
|
+
from fulfil_cli.auth.oauth import OAuthTokens, discover_oidc, refresh_access_token
|
|
9
11
|
from fulfil_cli.client.errors import AuthError
|
|
10
12
|
|
|
11
13
|
|
|
@@ -13,42 +15,12 @@ from fulfil_cli.client.errors import AuthError
|
|
|
13
15
|
class Credentials:
|
|
14
16
|
"""Resolved authentication credentials."""
|
|
15
17
|
|
|
16
|
-
method:
|
|
18
|
+
method: Literal["api_key", "oauth"]
|
|
17
19
|
api_key: str | None = None
|
|
18
20
|
access_token: str | None = None
|
|
19
21
|
workspace: str | None = None
|
|
20
22
|
|
|
21
23
|
|
|
22
|
-
def resolve_api_key(
|
|
23
|
-
*,
|
|
24
|
-
token_flag: str | None = None,
|
|
25
|
-
workspace: str | None = None,
|
|
26
|
-
) -> str:
|
|
27
|
-
"""Resolve API key from: --token flag > FULFIL_API_KEY env > keyring.
|
|
28
|
-
|
|
29
|
-
Raises AuthError if no key can be found.
|
|
30
|
-
"""
|
|
31
|
-
# 1. Explicit --token flag
|
|
32
|
-
if token_flag:
|
|
33
|
-
return token_flag
|
|
34
|
-
|
|
35
|
-
# 2. Environment variable
|
|
36
|
-
env_key = os.environ.get("FULFIL_API_KEY")
|
|
37
|
-
if env_key:
|
|
38
|
-
return env_key
|
|
39
|
-
|
|
40
|
-
# 3. Keyring (requires workspace)
|
|
41
|
-
if workspace:
|
|
42
|
-
stored = get_api_key(workspace)
|
|
43
|
-
if stored:
|
|
44
|
-
return stored
|
|
45
|
-
|
|
46
|
-
raise AuthError(
|
|
47
|
-
message="No API key found",
|
|
48
|
-
hint="Run 'fulfil auth login' or set FULFIL_API_KEY environment variable.",
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
|
|
52
24
|
def resolve_credentials(
|
|
53
25
|
*,
|
|
54
26
|
token_flag: str | None = None,
|
|
@@ -57,23 +29,18 @@ def resolve_credentials(
|
|
|
57
29
|
) -> Credentials:
|
|
58
30
|
"""Resolve credentials, supporting both API key and OAuth.
|
|
59
31
|
|
|
60
|
-
Priority: --token flag
|
|
32
|
+
Priority: --token flag > FULFIL_API_KEY env > check auth method > keyring.
|
|
61
33
|
"""
|
|
62
|
-
# 1. Explicit --token flag (always treated as API key)
|
|
63
34
|
if token_flag:
|
|
64
35
|
return Credentials(method="api_key", api_key=token_flag)
|
|
65
36
|
|
|
66
|
-
# 2. Environment variable
|
|
67
37
|
env_key = os.environ.get("FULFIL_API_KEY")
|
|
68
38
|
if env_key:
|
|
69
39
|
return Credentials(method="api_key", api_key=env_key)
|
|
70
40
|
|
|
71
|
-
# 3. Based on configured auth method
|
|
72
41
|
if auth_method == "oauth" and workspace:
|
|
73
42
|
tokens_json = get_oauth_tokens(workspace)
|
|
74
43
|
if tokens_json:
|
|
75
|
-
from fulfil_cli.auth.oauth import OAuthTokens, discover_oidc, refresh_access_token
|
|
76
|
-
|
|
77
44
|
tokens = OAuthTokens.from_json(tokens_json)
|
|
78
45
|
if tokens.is_expired and tokens.refresh_token:
|
|
79
46
|
oidc = discover_oidc(workspace)
|
|
@@ -85,7 +52,6 @@ def resolve_credentials(
|
|
|
85
52
|
workspace=workspace,
|
|
86
53
|
)
|
|
87
54
|
|
|
88
|
-
# 4. Fall back to API key from keyring
|
|
89
55
|
if workspace:
|
|
90
56
|
stored = get_api_key(workspace)
|
|
91
57
|
if stored:
|
|
@@ -108,11 +74,11 @@ def resolve_workspace(
|
|
|
108
74
|
Raises AuthError if no workspace can be found.
|
|
109
75
|
"""
|
|
110
76
|
if workspace_flag:
|
|
111
|
-
return
|
|
77
|
+
return normalize_workspace(workspace_flag)
|
|
112
78
|
|
|
113
79
|
env_workspace = os.environ.get("FULFIL_WORKSPACE")
|
|
114
80
|
if env_workspace:
|
|
115
|
-
return
|
|
81
|
+
return normalize_workspace(env_workspace)
|
|
116
82
|
|
|
117
83
|
if config_workspace:
|
|
118
84
|
return config_workspace
|
|
@@ -123,7 +89,7 @@ def resolve_workspace(
|
|
|
123
89
|
)
|
|
124
90
|
|
|
125
91
|
|
|
126
|
-
def
|
|
92
|
+
def normalize_workspace(value: str) -> str:
|
|
127
93
|
"""Normalize workspace input — accept slug or full domain.
|
|
128
94
|
|
|
129
95
|
'acme' → 'acme.fulfil.io'
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
"""Root CLI application with dynamic model subcommands.
|
|
1
|
+
"""Root CLI application with dynamic model and report subcommands.
|
|
2
|
+
|
|
3
|
+
Typer handles the root app and static commands (auth, config, version)
|
|
4
|
+
where type-driven argument parsing shines. Click groups are used for
|
|
5
|
+
dynamic model and report routing because Typer does not support runtime
|
|
6
|
+
subcommand creation via ``get_command`` overrides — the ``FulfilGroup``
|
|
7
|
+
and ``ReportGroup`` classes intercept unknown subcommand names and
|
|
8
|
+
resolve them as Fulfil model or report names respectively.
|
|
9
|
+
"""
|
|
2
10
|
|
|
3
11
|
from __future__ import annotations
|
|
4
12
|
|
|
@@ -10,24 +18,17 @@ from rich.console import Console
|
|
|
10
18
|
from fulfil_cli import __version__
|
|
11
19
|
from fulfil_cli.cli.commands import auth, config
|
|
12
20
|
from fulfil_cli.cli.commands.api import api_cmd
|
|
21
|
+
from fulfil_cli.cli.commands.common import handle_error
|
|
13
22
|
from fulfil_cli.cli.commands.completion import completion_install
|
|
14
23
|
from fulfil_cli.cli.commands.model import create_model_group
|
|
15
24
|
from fulfil_cli.cli.commands.report import create_report_group
|
|
16
|
-
from fulfil_cli.cli.state import
|
|
17
|
-
from fulfil_cli.client.errors import FulfilError
|
|
25
|
+
from fulfil_cli.cli.state import VALID_FORMATS, AppContext, format_option
|
|
26
|
+
from fulfil_cli.client.errors import EXIT_USAGE, FulfilError
|
|
18
27
|
from fulfil_cli.output.formatter import output
|
|
19
28
|
|
|
20
29
|
console = Console(stderr=True)
|
|
21
30
|
|
|
22
31
|
|
|
23
|
-
def _handle_error(exc: FulfilError) -> None:
|
|
24
|
-
"""Print error and exit with appropriate code."""
|
|
25
|
-
console.print(f"[red]Error: {exc}[/red]")
|
|
26
|
-
if exc.hint and not is_quiet():
|
|
27
|
-
console.print(f"[dim]Hint: {exc.hint}[/dim]")
|
|
28
|
-
raise typer.Exit(code=exc.exit_code)
|
|
29
|
-
|
|
30
|
-
|
|
31
32
|
class ReportGroup(click.Group):
|
|
32
33
|
"""Click group that resolves unknown subcommands as report names."""
|
|
33
34
|
|
|
@@ -41,7 +42,6 @@ class ReportGroup(click.Group):
|
|
|
41
42
|
class FulfilGroup(typer.core.TyperGroup):
|
|
42
43
|
"""Custom Click group that resolves unknown subcommands as model or report names."""
|
|
43
44
|
|
|
44
|
-
# Dynamic commands that should appear in help alongside static ones
|
|
45
45
|
_dynamic_commands = ("models", "reports")
|
|
46
46
|
|
|
47
47
|
def list_commands(self, ctx: click.Context) -> list[str]:
|
|
@@ -52,17 +52,13 @@ class FulfilGroup(typer.core.TyperGroup):
|
|
|
52
52
|
return commands
|
|
53
53
|
|
|
54
54
|
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
|
|
55
|
-
# Check static commands first
|
|
56
55
|
rv = super().get_command(ctx, cmd_name)
|
|
57
56
|
if rv is not None:
|
|
58
57
|
return rv
|
|
59
|
-
# `fulfil models` / `fulfil models list`
|
|
60
58
|
if cmd_name == "models":
|
|
61
59
|
return models_group
|
|
62
|
-
# `fulfil reports` / `fulfil reports list` / `fulfil reports <name> execute`
|
|
63
60
|
if cmd_name == "reports":
|
|
64
61
|
return reports_group
|
|
65
|
-
# Treat as model name → return model sub-group
|
|
66
62
|
return create_model_group(cmd_name)
|
|
67
63
|
|
|
68
64
|
|
|
@@ -113,34 +109,64 @@ def main_callback(
|
|
|
113
109
|
base_url: str | None = typer.Option(None, "--base-url", hidden=True, help="Override base URL"),
|
|
114
110
|
debug: bool = typer.Option(False, "--debug", help="Show debug output"),
|
|
115
111
|
quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress hints and decorative output"),
|
|
112
|
+
output_format: str | None = typer.Option(
|
|
113
|
+
None,
|
|
114
|
+
"--format",
|
|
115
|
+
help="Output format: table, json, csv, ndjson (default: table for TTY, json when piped)",
|
|
116
|
+
),
|
|
116
117
|
) -> None:
|
|
117
118
|
"""Root callback — sets global auth state."""
|
|
118
|
-
|
|
119
|
+
if output_format and output_format not in VALID_FORMATS:
|
|
120
|
+
console.print(
|
|
121
|
+
f"[red]Error: Invalid format '{output_format}'. "
|
|
122
|
+
f"Choose from: {', '.join(VALID_FORMATS)}[/red]"
|
|
123
|
+
)
|
|
124
|
+
raise typer.Exit(code=EXIT_USAGE)
|
|
125
|
+
ctx.obj = AppContext(
|
|
126
|
+
token=token,
|
|
127
|
+
workspace=workspace,
|
|
128
|
+
base_url=base_url,
|
|
129
|
+
debug=debug,
|
|
130
|
+
quiet=quiet,
|
|
131
|
+
output_format=output_format,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
FORMAT_OPTION = typer.Option(
|
|
136
|
+
None,
|
|
137
|
+
"--format",
|
|
138
|
+
help="Output format: table, json, csv, ndjson",
|
|
139
|
+
)
|
|
119
140
|
|
|
120
141
|
|
|
121
142
|
@app.command()
|
|
122
143
|
def version(
|
|
123
|
-
|
|
144
|
+
ctx: typer.Context,
|
|
145
|
+
output_format: str | None = FORMAT_OPTION,
|
|
124
146
|
) -> None:
|
|
125
147
|
"""Show CLI version."""
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
148
|
+
app_ctx: AppContext = ctx.obj
|
|
149
|
+
fmt = app_ctx.get_effective_format(output_format)
|
|
150
|
+
if fmt == "table":
|
|
129
151
|
typer.echo(f"fulfil-cli {__version__}")
|
|
152
|
+
else:
|
|
153
|
+
output({"version": __version__}, fmt=fmt)
|
|
130
154
|
|
|
131
155
|
|
|
132
156
|
@app.command()
|
|
133
157
|
def whoami(
|
|
134
|
-
|
|
158
|
+
ctx: typer.Context,
|
|
159
|
+
output_format: str | None = FORMAT_OPTION,
|
|
135
160
|
) -> None:
|
|
136
161
|
"""Show current user and workspace info."""
|
|
162
|
+
app_ctx: AppContext = ctx.obj
|
|
137
163
|
try:
|
|
138
|
-
client = get_client()
|
|
164
|
+
client = app_ctx.get_client()
|
|
139
165
|
result = client.call("system.whoami")
|
|
140
166
|
except FulfilError as exc:
|
|
141
|
-
|
|
167
|
+
handle_error(exc)
|
|
142
168
|
|
|
143
|
-
output(result,
|
|
169
|
+
output(result, fmt=app_ctx.get_effective_format(output_format))
|
|
144
170
|
|
|
145
171
|
|
|
146
172
|
def _flatten_model_row(row: dict) -> dict:
|
|
@@ -157,13 +183,14 @@ def _flatten_model_row(row: dict) -> dict:
|
|
|
157
183
|
}
|
|
158
184
|
|
|
159
185
|
|
|
160
|
-
def _list_models(
|
|
186
|
+
def _list_models(fmt: str, search: str | None = None) -> None:
|
|
161
187
|
"""Fetch and display all available models."""
|
|
188
|
+
app_ctx: AppContext = click.get_current_context().obj
|
|
162
189
|
try:
|
|
163
|
-
client = get_client()
|
|
190
|
+
client = app_ctx.get_client()
|
|
164
191
|
result = client.call("system.list_models")
|
|
165
192
|
except FulfilError as exc:
|
|
166
|
-
|
|
193
|
+
handle_error(exc)
|
|
167
194
|
|
|
168
195
|
if search and isinstance(result, list):
|
|
169
196
|
term = search.lower()
|
|
@@ -178,43 +205,43 @@ def _list_models(json_flag: bool, search: str | None = None) -> None:
|
|
|
178
205
|
)
|
|
179
206
|
]
|
|
180
207
|
|
|
181
|
-
if
|
|
208
|
+
if fmt == "table" and isinstance(result, list):
|
|
182
209
|
result = [_flatten_model_row(r) for r in result if isinstance(r, dict)]
|
|
183
210
|
|
|
184
|
-
output(result,
|
|
211
|
+
output(result, fmt=fmt, title="Available Models")
|
|
185
212
|
|
|
186
213
|
|
|
187
214
|
@click.group(name="models", help="List available models.", invoke_without_command=True)
|
|
188
|
-
@click.option("--json", "json_flag", is_flag=True, help="Output as JSON")
|
|
189
215
|
@click.option("--search", "-s", default=None, help="Filter by name, description, or category.")
|
|
216
|
+
@format_option
|
|
190
217
|
@click.pass_context
|
|
191
|
-
def models_group(ctx: click.Context,
|
|
218
|
+
def models_group(ctx: click.Context, search: str | None, output_format: str | None) -> None:
|
|
192
219
|
"""List all available models."""
|
|
193
|
-
ctx.ensure_object(dict)
|
|
194
|
-
ctx.obj["json_flag"] = json_flag
|
|
195
|
-
ctx.obj["search"] = search
|
|
196
220
|
if ctx.invoked_subcommand is None:
|
|
197
|
-
|
|
221
|
+
app_ctx: AppContext = ctx.obj
|
|
222
|
+
_list_models(app_ctx.get_effective_format(output_format), search=search)
|
|
198
223
|
|
|
199
224
|
|
|
200
225
|
@models_group.command("list")
|
|
201
|
-
@click.option("--json", "json_flag", is_flag=True, help="Output as JSON")
|
|
202
226
|
@click.option("--search", "-s", default=None, help="Filter by name, description, or category.")
|
|
227
|
+
@format_option
|
|
203
228
|
@click.pass_context
|
|
204
|
-
def models_list_cmd(ctx: click.Context,
|
|
229
|
+
def models_list_cmd(ctx: click.Context, search: str | None, output_format: str | None) -> None:
|
|
205
230
|
"""List all available models."""
|
|
206
|
-
|
|
231
|
+
app_ctx: AppContext = ctx.obj
|
|
232
|
+
_list_models(app_ctx.get_effective_format(output_format), search=search)
|
|
207
233
|
|
|
208
234
|
|
|
209
|
-
def _list_reports(
|
|
235
|
+
def _list_reports(fmt: str) -> None:
|
|
210
236
|
"""Fetch and display all available reports."""
|
|
237
|
+
app_ctx: AppContext = click.get_current_context().obj
|
|
211
238
|
try:
|
|
212
|
-
client = get_client()
|
|
239
|
+
client = app_ctx.get_client()
|
|
213
240
|
result = client.call("system.list_reports")
|
|
214
241
|
except FulfilError as exc:
|
|
215
|
-
|
|
242
|
+
handle_error(exc)
|
|
216
243
|
|
|
217
|
-
output(result,
|
|
244
|
+
output(result, fmt=fmt, title="Available Reports")
|
|
218
245
|
|
|
219
246
|
|
|
220
247
|
@click.group(
|
|
@@ -223,42 +250,46 @@ def _list_reports(json_flag: bool) -> None:
|
|
|
223
250
|
help="Interact with reports.",
|
|
224
251
|
invoke_without_command=True,
|
|
225
252
|
)
|
|
226
|
-
@
|
|
253
|
+
@format_option
|
|
227
254
|
@click.pass_context
|
|
228
|
-
def reports_group(ctx: click.Context,
|
|
255
|
+
def reports_group(ctx: click.Context, output_format: str | None) -> None:
|
|
229
256
|
"""List all available reports."""
|
|
230
|
-
ctx.ensure_object(dict)
|
|
231
|
-
ctx.obj["json_flag"] = json_flag
|
|
232
257
|
if ctx.invoked_subcommand is None:
|
|
233
|
-
|
|
258
|
+
app_ctx: AppContext = ctx.obj
|
|
259
|
+
_list_reports(app_ctx.get_effective_format(output_format))
|
|
234
260
|
|
|
235
261
|
|
|
236
262
|
@reports_group.command("list")
|
|
237
|
-
@
|
|
263
|
+
@format_option
|
|
238
264
|
@click.pass_context
|
|
239
|
-
def reports_list_cmd(ctx: click.Context,
|
|
265
|
+
def reports_list_cmd(ctx: click.Context, output_format: str | None) -> None:
|
|
240
266
|
"""List all available reports."""
|
|
241
|
-
|
|
267
|
+
app_ctx: AppContext = ctx.obj
|
|
268
|
+
_list_reports(app_ctx.get_effective_format(output_format))
|
|
242
269
|
|
|
243
270
|
|
|
244
271
|
@app.command()
|
|
245
272
|
def docs(
|
|
273
|
+
ctx: typer.Context,
|
|
246
274
|
query: str = typer.Argument(..., help="Search query for Fulfil documentation"),
|
|
247
|
-
|
|
275
|
+
output_format: str | None = FORMAT_OPTION,
|
|
248
276
|
) -> None:
|
|
249
277
|
"""Search Fulfil documentation."""
|
|
278
|
+
app_ctx: AppContext = ctx.obj
|
|
250
279
|
try:
|
|
251
|
-
client = get_client()
|
|
280
|
+
client = app_ctx.get_client()
|
|
252
281
|
results = client.call("system.search_docs", query=query)
|
|
253
282
|
except FulfilError as exc:
|
|
254
|
-
|
|
283
|
+
handle_error(exc)
|
|
255
284
|
|
|
256
285
|
if not results:
|
|
257
|
-
|
|
286
|
+
if not app_ctx.quiet:
|
|
287
|
+
console.print("[dim]No results found.[/dim]")
|
|
258
288
|
raise typer.Exit()
|
|
259
289
|
|
|
260
|
-
|
|
261
|
-
|
|
290
|
+
fmt = app_ctx.get_effective_format(output_format)
|
|
291
|
+
if fmt != "table":
|
|
292
|
+
output(results, fmt=fmt)
|
|
262
293
|
else:
|
|
263
294
|
from rich.markdown import Markdown
|
|
264
295
|
from rich.panel import Panel
|
|
@@ -315,8 +346,8 @@ fulfil sales_order get 42
|
|
|
315
346
|
## 4. Create and update records
|
|
316
347
|
|
|
317
348
|
```
|
|
318
|
-
|
|
319
|
-
fulfil sales_order update 42
|
|
349
|
+
echo '{"name": "Acme Corp"}' | fulfil contact create
|
|
350
|
+
fulfil sales_order update 42 updates.json
|
|
320
351
|
```
|
|
321
352
|
|
|
322
353
|
## 5. Call workflow methods
|
|
@@ -338,7 +369,8 @@ fulfil reports price_list_report execute --params '{"date_from": "2024-01-01"}'
|
|
|
338
369
|
|
|
339
370
|
## Tips
|
|
340
371
|
|
|
341
|
-
- Use `--json` to force JSON output (automatic when piped)
|
|
372
|
+
- Use `--format json` to force JSON output (automatic when piped)
|
|
373
|
+
- Use `--format csv` or `--format ndjson` for other machine-readable formats
|
|
342
374
|
- Use `--debug` to see HTTP request/response details
|
|
343
375
|
- Use `-h` on any command for help: `fulfil sales_order list -h`
|
|
344
376
|
- Run `fulfil completion` to install shell completions
|
|
@@ -350,6 +382,5 @@ fulfil reports price_list_report execute --params '{"date_from": "2024-01-01"}'
|
|
|
350
382
|
app.command(name="api")(api_cmd)
|
|
351
383
|
app.command(name="completion")(completion_install)
|
|
352
384
|
|
|
353
|
-
|
|
354
385
|
# Top-level aliases for frequently-used auth subcommands
|
|
355
386
|
app.command(name="workspaces")(auth.workspaces)
|