frappectl 0.13.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.
Files changed (46) hide show
  1. frappectl-0.13.1/.github/workflows/ci.yml +216 -0
  2. frappectl-0.13.1/.github/workflows/publish.yml +52 -0
  3. frappectl-0.13.1/.github/workflows/release.yml +51 -0
  4. frappectl-0.13.1/.gitignore +11 -0
  5. frappectl-0.13.1/.pre-commit-config.yaml +56 -0
  6. frappectl-0.13.1/.releaserc +19 -0
  7. frappectl-0.13.1/LICENSE +21 -0
  8. frappectl-0.13.1/PHILOSOPHY.md +8 -0
  9. frappectl-0.13.1/PKG-INFO +177 -0
  10. frappectl-0.13.1/README.md +156 -0
  11. frappectl-0.13.1/pyproject.toml +56 -0
  12. frappectl-0.13.1/src/frappectl/__init__.py +8 -0
  13. frappectl-0.13.1/src/frappectl/__main__.py +4 -0
  14. frappectl-0.13.1/src/frappectl/cli.py +148 -0
  15. frappectl-0.13.1/src/frappectl/client.py +591 -0
  16. frappectl-0.13.1/src/frappectl/commands/__init__.py +1 -0
  17. frappectl-0.13.1/src/frappectl/commands/api.py +101 -0
  18. frappectl-0.13.1/src/frappectl/commands/assistant.py +270 -0
  19. frappectl-0.13.1/src/frappectl/commands/auth.py +391 -0
  20. frappectl-0.13.1/src/frappectl/commands/doc.py +346 -0
  21. frappectl-0.13.1/src/frappectl/commands/doctype.py +133 -0
  22. frappectl-0.13.1/src/frappectl/commands/file.py +137 -0
  23. frappectl-0.13.1/src/frappectl/commands/guide.py +73 -0
  24. frappectl-0.13.1/src/frappectl/commands/method.py +295 -0
  25. frappectl-0.13.1/src/frappectl/commands/report.py +93 -0
  26. frappectl-0.13.1/src/frappectl/commands/update.py +345 -0
  27. frappectl-0.13.1/src/frappectl/config.py +492 -0
  28. frappectl-0.13.1/src/frappectl/errors.py +186 -0
  29. frappectl-0.13.1/src/frappectl/helpers.py +149 -0
  30. frappectl-0.13.1/src/frappectl/oauth.py +456 -0
  31. frappectl-0.13.1/src/frappectl/output.py +155 -0
  32. frappectl-0.13.1/src/frappectl/py.typed +0 -0
  33. frappectl-0.13.1/src/frappectl/session.py +67 -0
  34. frappectl-0.13.1/tests/conftest.py +35 -0
  35. frappectl-0.13.1/tests/integration/test_smoke.py +206 -0
  36. frappectl-0.13.1/tests/test_assistant.py +131 -0
  37. frappectl-0.13.1/tests/test_cli.py +298 -0
  38. frappectl-0.13.1/tests/test_client.py +330 -0
  39. frappectl-0.13.1/tests/test_config.py +330 -0
  40. frappectl-0.13.1/tests/test_errors.py +64 -0
  41. frappectl-0.13.1/tests/test_file.py +66 -0
  42. frappectl-0.13.1/tests/test_helpers.py +80 -0
  43. frappectl-0.13.1/tests/test_method.py +475 -0
  44. frappectl-0.13.1/tests/test_oauth.py +283 -0
  45. frappectl-0.13.1/tests/test_update.py +336 -0
  46. frappectl-0.13.1/uv.lock +828 -0
@@ -0,0 +1,216 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [develop]
6
+ pull_request:
7
+ branches: [develop]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v7
20
+
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+
26
+ - name: Install dependencies
27
+ run: uv sync --extra dev
28
+
29
+ - name: Run tests
30
+ run: uv run pytest
31
+
32
+ pre-commit:
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@v7
36
+
37
+ - name: Install uv
38
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39
39
+ with:
40
+ python-version: "3.14"
41
+
42
+ - name: Run pre-commit
43
+ env:
44
+ SKIP: no-commit-to-branch
45
+ run: uvx pre-commit run --all-files --show-diff-on-failure
46
+
47
+ # The integration suite runs on pull requests (and manual dispatches) but not
48
+ # on push, so branch pushes stay fast. Its result is rolled up by the `ci`
49
+ # gate job below.
50
+ integration:
51
+ runs-on: ubuntu-latest
52
+ if: github.event_name != 'push'
53
+
54
+ services:
55
+ mariadb:
56
+ image: mariadb:11.4
57
+ env:
58
+ MARIADB_ROOT_PASSWORD: root
59
+ ports:
60
+ - 3306:3306
61
+ # tmpfs for the datadir keeps the ephemeral CI DB fast (mirrors Frappe's own CI).
62
+ options: >-
63
+ --health-cmd="healthcheck.sh --connect --innodb_initialized"
64
+ --health-interval=5s
65
+ --health-timeout=5s
66
+ --health-retries=10
67
+ --tmpfs=/var/lib/mysql:rw,noexec,nosuid,size=2g
68
+
69
+ redis:
70
+ image: redis:7
71
+ ports:
72
+ - 6379:6379
73
+ options: >-
74
+ --health-cmd="redis-cli ping"
75
+ --health-interval=5s
76
+ --health-timeout=5s
77
+ --health-retries=10
78
+
79
+ env:
80
+ # The site the CLI talks to. Served as the bench's default site, so the
81
+ # CLI can reach it over plain 127.0.0.1 without any /etc/hosts juggling.
82
+ SITE: test.localhost
83
+
84
+ steps:
85
+ - name: Checkout frappectl
86
+ uses: actions/checkout@v7
87
+
88
+ # --- The CLI under test (managed by uv, isolated from the bench env) ---
89
+ - name: Install uv
90
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39
91
+ with:
92
+ python-version: "3.14"
93
+
94
+ - name: Install frappectl
95
+ run: uv sync --extra dev
96
+
97
+ # --- Toolchain for provisioning a Frappe bench ---
98
+ - name: Set up Node
99
+ uses: actions/setup-node@v6
100
+ with:
101
+ node-version: 24
102
+
103
+ # Warm yarn's download cache so `bench init` installs frappe's node deps
104
+ # from disk instead of re-fetching them from the registry every run. The
105
+ # run_id in the key always writes a fresh cache; restore-keys pulls the
106
+ # newest prior one so it stays current as frappe's deps drift.
107
+ - name: Cache yarn packages
108
+ uses: actions/cache@v4
109
+ with:
110
+ path: ~/.cache/yarn
111
+ key: yarn-${{ runner.os }}-${{ github.run_id }}
112
+ restore-keys: |
113
+ yarn-${{ runner.os }}-
114
+
115
+ - name: Install system dependencies
116
+ run: |
117
+ sudo apt-get update
118
+ sudo apt-get install -y --no-install-recommends \
119
+ build-essential pkg-config \
120
+
121
+ - name: Install Bench
122
+ run: |
123
+ uv tool install frappe-bench
124
+ echo "$HOME/.local/bin" >> "$GITHUB_PATH"
125
+
126
+ - name: Provision a Frappe site
127
+ run: |
128
+ set -euo pipefail
129
+
130
+ bench init --skip-assets --no-backups \
131
+ --skip-redis-config-generation \
132
+ --frappe-branch develop ~/frappe-bench
133
+ cd ~/frappe-bench
134
+
135
+ # Point every redis role at the service container instead of bench's
136
+ # own bundled redis, so we don't need honcho/`bench start` running.
137
+ bench set-config -g redis_cache "redis://localhost:6379"
138
+ bench set-config -g redis_queue "redis://localhost:6379"
139
+ bench set-config -g redis_socketio "redis://localhost:6379"
140
+ bench set-config -g use_mysqlclient 0
141
+
142
+ # DB root creds + admin password via global config — the path Frappe's
143
+ # own CI uses, rather than the (renamed) new-site CLI flags.
144
+ bench set-config -g root_login root
145
+ bench set-config -g root_password root
146
+ bench set-config -g admin_password admin
147
+
148
+ bench new-site "$SITE" \
149
+ --db-type mariadb \
150
+ --db-host 127.0.0.1 \
151
+ --db-name test_frappe \
152
+ --db-password test_frappe \
153
+ --verbose
154
+ bench use "$SITE"
155
+
156
+ - name: Generate Administrator API keys
157
+ run: |
158
+ set -euo pipefail
159
+ cd ~/frappe-bench
160
+ bench --site "$SITE" execute \
161
+ frappe.core.doctype.user.user.generate_keys \
162
+ --kwargs '{"user": "Administrator"}' > keys.out
163
+ python - <<'PY'
164
+ import json, os, re
165
+ raw = open(os.path.expanduser("~/frappe-bench/keys.out")).read()
166
+ match = re.search(r'\{[^{}]*"api_secret"[^{}]*\}', raw, re.S)
167
+ keys = json.loads(match.group(0))
168
+ with open(os.environ["GITHUB_ENV"], "a") as env:
169
+ env.write(f"FRAPPE_API_KEY={keys['api_key']}\n")
170
+ env.write(f"FRAPPE_API_SECRET={keys['api_secret']}\n")
171
+ PY
172
+
173
+ - name: Serve the site
174
+ run: |
175
+ cd ~/frappe-bench
176
+ bench start &
177
+ for i in $(seq 1 30); do
178
+ if curl -sf http://test.localhost:8000/api/method/ping >/dev/null; then
179
+ echo "site is up"; exit 0
180
+ fi
181
+ sleep 2
182
+ done
183
+ echo "site did not come up in time" >&2
184
+ exit 1
185
+
186
+ - name: Run integration tests
187
+ run: uv run pytest tests/integration -m integration -v
188
+ env:
189
+ FRAPPE_SITE: http://test.localhost:8000
190
+
191
+ # Single roll-up status check — make THIS the only required check in the
192
+ # ruleset. It aggregates the individual jobs so the required-checks list never
193
+ # has to be maintained. `!cancelled()` lets it run even when integration is
194
+ # skipped (on push); it then fails unless every job that should have run for
195
+ # this event succeeded.
196
+ ci:
197
+ name: ci
198
+ runs-on: ubuntu-latest
199
+ needs: [test, pre-commit, integration]
200
+ if: ${{ !cancelled() }}
201
+ steps:
202
+ - name: Verify all checks passed
203
+ run: |
204
+ echo "test=${{ needs.test.result }} pre-commit=${{ needs['pre-commit'].result }} integration=${{ needs.integration.result }} (event=${{ github.event_name }})"
205
+
206
+ # Unit tests and linters must pass on every event.
207
+ [ "${{ needs.test.result }}" = "success" ] || { echo "::error::unit tests failed"; exit 1; }
208
+ [ "${{ needs['pre-commit'].result }}" = "success" ] || { echo "::error::pre-commit failed"; exit 1; }
209
+
210
+ # Integration runs on everything except push; require it there, ignore
211
+ # its (skipped) result on push.
212
+ if [ "${{ github.event_name }}" != "push" ]; then
213
+ [ "${{ needs.integration.result }}" = "success" ] || { echo "::error::integration tests failed"; exit 1; }
214
+ fi
215
+
216
+ echo "All required checks passed."
@@ -0,0 +1,52 @@
1
+ name: Publish
2
+
3
+ # Publish to PyPI whenever semantic-release cuts a GitHub release. This fires
4
+ # because release.yml authenticates with a PAT (RELEASE_TOKEN) — releases
5
+ # created with the default GITHUB_TOKEN would not trigger other workflows.
6
+ #
7
+ # Authentication is PyPI "trusted publishing" (OIDC): no API token secret.
8
+ # PyPI must be configured with a trusted publisher for the `frappectl`
9
+ # project pointing at this repo, workflow `publish.yml`, environment `pypi`.
10
+ on:
11
+ release:
12
+ types: [published]
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ publish:
19
+ name: Publish to PyPI
20
+ runs-on: ubuntu-latest
21
+ environment:
22
+ name: pypi
23
+ url: https://pypi.org/project/frappectl/
24
+ permissions:
25
+ id-token: write # mint the OIDC token PyPI trusted publishing verifies
26
+
27
+ steps:
28
+ # Build from the released tag, not the branch tip: a commit landing on
29
+ # develop between the release and this run must not leak into the wheel.
30
+ - name: Checkout release tag
31
+ uses: actions/checkout@v7
32
+ with:
33
+ ref: ${{ github.event.release.tag_name }}
34
+ persist-credentials: false
35
+
36
+ - name: Install uv
37
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39
38
+ with:
39
+ python-version: "3.14"
40
+
41
+ - name: Build sdist and wheel
42
+ run: uv build
43
+
44
+ # Catch a broken artifact before it is immortalised on PyPI: install the
45
+ # wheel into a clean environment and run both entry points.
46
+ - name: Smoke-test the wheel
47
+ run: |
48
+ uvx --from ./dist/frappectl-*.whl frappectl --version
49
+ uvx --from ./dist/frappectl-*.whl fr guide > /dev/null
50
+
51
+ - name: Publish to PyPI
52
+ run: uv publish
@@ -0,0 +1,51 @@
1
+ name: Release
2
+
3
+ # Every push to develop cuts a release. No CI gate is needed here: nothing
4
+ # reaches develop except through the merge queue, which already runs the full
5
+ # suite (unit tests, linters, integration) before fast-forwarding the branch.
6
+ # semantic-release is a no-op when a push carries no releasable commits (e.g.
7
+ # the bot's own `chore(release):` commit), so re-runs are harmless.
8
+ on:
9
+ push:
10
+ branches: [develop]
11
+
12
+ permissions:
13
+ contents: write
14
+
15
+ jobs:
16
+ release:
17
+ name: Release
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - name: Checkout Entire Repository
21
+ uses: actions/checkout@v7
22
+ with:
23
+ fetch-depth: 0
24
+ persist-credentials: false
25
+
26
+ - name: Setup Node.js
27
+ uses: actions/setup-node@v6
28
+ with:
29
+ node-version: 22
30
+
31
+ # `uv` is needed by the semantic-release prepare step, which runs
32
+ # `uv lock` to keep the lockfile's project version in sync with the
33
+ # bumped pyproject.toml (both are committed in the release commit).
34
+ - name: Install uv
35
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39
36
+ with:
37
+ python-version: "3.14"
38
+
39
+ - name: Setup dependencies
40
+ run: |
41
+ npm install @semantic-release/git @semantic-release/exec --no-save
42
+
43
+ - name: Create Release
44
+ env:
45
+ GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
46
+ GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
47
+ GIT_AUTHOR_NAME: "Frappe CLI Bot"
48
+ GIT_AUTHOR_EMAIL: "developers@frappe.io"
49
+ GIT_COMMITTER_NAME: "Frappe CLI Bot"
50
+ GIT_COMMITTER_EMAIL: "developers@frappe.io"
51
+ run: npx semantic-release
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
@@ -0,0 +1,56 @@
1
+ exclude: '.git'
2
+ default_stages: [pre-commit]
3
+ default_install_hook_types: [pre-commit, commit-msg]
4
+ fail_fast: false
5
+
6
+
7
+ repos:
8
+ - repo: https://github.com/pre-commit/pre-commit-hooks
9
+ rev: v6.0.0
10
+ hooks:
11
+ - id: trailing-whitespace
12
+ exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
13
+ - id: no-commit-to-branch
14
+ args: ['--branch', 'develop']
15
+ - id: check-merge-conflict
16
+ - id: check-ast
17
+ - id: check-json
18
+ - id: check-toml
19
+ - id: check-yaml
20
+ - id: debug-statements
21
+
22
+ - repo: https://github.com/astral-sh/ruff-pre-commit
23
+ rev: v0.14.10
24
+ hooks:
25
+ - id: ruff
26
+ name: "Run ruff import sorter"
27
+ args: ["--select=I", "--fix"]
28
+
29
+ - id: ruff
30
+ name: "Run ruff linter"
31
+
32
+ - id: ruff-format
33
+ name: "Run ruff formatter"
34
+
35
+ # mypy runs through uv so it resolves against the locked project + typed deps
36
+ # (typer, httpx, rich…). An isolated pre-commit env wouldn't see them and would
37
+ # drown in false import errors. It reads its config (strict, files = ["src"])
38
+ # from pyproject.toml, so it types the whole package regardless of staged files.
39
+ - repo: local
40
+ hooks:
41
+ - id: mypy
42
+ name: "Run mypy (strict)"
43
+ entry: uv run --extra dev mypy
44
+ language: system
45
+ types: [python]
46
+ pass_filenames: false
47
+ always_run: true
48
+
49
+
50
+ ci:
51
+ autoupdate_schedule: weekly
52
+ # `uv` isn't available on the hosted pre-commit.ci runners, so the local
53
+ # mypy hook can't run there. It still runs locally and in the GitHub Actions
54
+ # `pre-commit` job (which installs uv) — the check that actually gates merges.
55
+ skip: [mypy]
56
+ submodules: false
@@ -0,0 +1,19 @@
1
+ {
2
+ "branches": ["develop"],
3
+ "plugins": [
4
+ "@semantic-release/commit-analyzer",
5
+ "@semantic-release/release-notes-generator",
6
+ [
7
+ "@semantic-release/exec", {
8
+ "prepareCmd": "sed -i -E 's/^version = \"[0-9]+\\.[0-9]+\\.[0-9]+\"/version = \"${nextRelease.version}\"/' pyproject.toml && uv lock"
9
+ }
10
+ ],
11
+ [
12
+ "@semantic-release/git", {
13
+ "assets": ["pyproject.toml", "uv.lock"],
14
+ "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}"
15
+ }
16
+ ],
17
+ "@semantic-release/github"
18
+ ]
19
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Frappe Technologies Pvt Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,8 @@
1
+ # Philosophy
2
+
3
+ - **For humans and agents, equally.** A TTY gets rich tables and prompts; a pipe (or `--json`) gets clean JSON on stdout and nothing else. Same command, both audiences.
4
+ - **A thin, honest wrapper.** Verbs map to the Frappe REST API v2 with little magic, and `api` is always the escape hatch — the CLI never hides the platform underneath.
5
+ - **Predictable I/O contract.** stdout is data, stderr is chatter. Exit `0`/`1`/`2`. Errors are plain text with a tip pointing at the fix.
6
+ - **Safe by default.** Mutations refuse to run non-interactively without `--yes`; updates are optimistic unless `--force`.
7
+ - **Credentials are the human's job.** Key/secret live in env or the OS keyring — never plaintext, never mutated by the CLI, never set up for you.
8
+ - **Self-orienting.** `guide` and `doctype show` let anyone learn the surface before touching a live site. Orient, don't guess.
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.4
2
+ Name: frappectl
3
+ Version: 0.13.1
4
+ Summary: frappectl — a command-line client for Frappe sites, built for humans and AI agents.
5
+ Project-URL: Homepage, https://github.com/frappe/frappectl
6
+ Author-email: Ankush Menat <ankush@frappe.io>
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: api,cli,erpnext,frappe
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: authlib>=1.3
12
+ Requires-Dist: httpx>=0.27
13
+ Requires-Dist: keyring>=24.0
14
+ Requires-Dist: rich>=13.0
15
+ Requires-Dist: typer>=0.12
16
+ Provides-Extra: dev
17
+ Requires-Dist: mypy>=1.13; extra == 'dev'
18
+ Requires-Dist: pytest>=8.0; extra == 'dev'
19
+ Requires-Dist: respx>=0.21; extra == 'dev'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # frappectl
23
+
24
+ [![CI](https://github.com/frappe/frappectl/actions/workflows/ci.yml/badge.svg)](https://github.com/frappe/frappectl/actions/workflows/ci.yml)
25
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](#license)
26
+
27
+ A command-line REST API v2 client for Frappe v16+ — built for humans and AI agents.
28
+
29
+ Using an agent? Start with `frappectl guide`. It fits site selection, core verbs,
30
+ filtering and the raw-API escape hatch on one screen.
31
+
32
+ ```sh
33
+ frappectl auth login https://erp.example.com # key/secret → OS keyring
34
+ frappectl -s raven doc list "Raven Channel" --json # -s/--site selects a profile
35
+
36
+ frappectl doc list "Sales Invoice" -f status=Overdue -f 'grand_total>1000' \
37
+ --fields name,customer,grand_total --all --json
38
+ frappectl doc get "Sales Invoice" SINV-0001
39
+ frappectl doc create ToDo --set description="Follow up" --set priority=High
40
+ cat invoice.json | frappectl doc create "Sales Invoice"
41
+ frappectl doc submit "Sales Invoice" SINV-0001
42
+ frappectl doc delete ToDo abc123 --yes
43
+
44
+ frappectl doctype show "Sales Invoice" --json # discover the schema first
45
+ frappectl report run "Accounts Receivable" -f company="Frappe" --json
46
+ frappectl file upload ./contract.pdf --doctype "Sales Invoice" --name SINV-0001 --private
47
+ frappectl api method/frappe.client.get_count -F doctype=User
48
+ frappectl api method/gameplan.api.get_unread_count # raw API, when verbs aren't enough
49
+ ```
50
+
51
+ ## Install
52
+
53
+ ```sh
54
+ uv tool install frappectl
55
+ # or: pip install frappectl
56
+ # development version: uv tool install git+https://github.com/frappe/frappectl
57
+ ```
58
+
59
+ This installs two commands: `frappectl` and its short alias `fr`.
60
+
61
+ ## Authentication
62
+
63
+ frappectl supports 3 authentication paths.
64
+
65
+ ### Environment variables
66
+
67
+ Environment variables always win and never touch the keyring.
68
+
69
+ ```sh
70
+ export FRAPPE_SITE=https://erp.example.com
71
+ export FRAPPE_API_KEY=xxxxxxxx
72
+ export FRAPPE_API_SECRET=yyyyyyyy
73
+ ```
74
+
75
+ ### Stored profiles
76
+
77
+ Generate an API key + secret from **User → Settings → API Access**, then log in:
78
+
79
+ ```sh
80
+ frappectl auth login https://erp.example.com
81
+ frappectl auth login https://raven.example.com --name raven
82
+ frappectl auth login https://raven.example.com --name raven --default
83
+ frappectl auth list
84
+ frappectl auth default raven # change the default
85
+ frappectl -s raven doc list "Raven Channel" # select per command
86
+ frappectl auth whoami
87
+ ```
88
+
89
+ The site URL lives in `~/.config/frappe/config.json`; the secret lives in the **OS
90
+ keyring**. There is no plaintext fallback. If the machine has no keyring, use environment
91
+ variables.
92
+
93
+ ### OAuth
94
+
95
+ Interactive login selects OAuth by default. It stores no secret; short-lived access
96
+ tokens refresh automatically.
97
+
98
+ ```sh
99
+ frappectl auth login https://erp.example.com
100
+ frappectl auth login https://erp.example.com --client-id <public-client-id>
101
+ ```
102
+
103
+ Sites with dynamic client registration create the client automatically. Frappe v15+
104
+ supports this by default; other sites need a pre-registered public `--client-id`.
105
+
106
+ OAuth needs a terminal and local browser, so it isn't the headless path. Use `--oauth`
107
+ to skip the authentication-method prompt.
108
+
109
+ ### Read-only profiles
110
+
111
+ A read-only profile refuses every unsafe HTTP method **before the request leaves your
112
+ machine**. This removes the obvious production footgun: an exploratory command can't
113
+ accidentally mutate the site.
114
+
115
+ ```sh
116
+ frappectl auth login https://prod.example.com --name prod --read-only
117
+ frappectl auth configure prod --read-only # lock an existing profile
118
+ frappectl auth configure prod --writable # allow writes again
119
+ ```
120
+
121
+ This blocks `doc create/update/delete/submit`, `file upload`, and method calls through
122
+ `api`/`method`. Reads such as `doc list/get`, `doctype show`, and `report run` keep
123
+ working.
124
+
125
+ For environment-variable auth, set `FRAPPE_READ_ONLY=1`. To invoke a whitelisted read
126
+ method, make the safe verb explicit: `frappectl api method/… -X GET`.
127
+
128
+ ## Output and scripting
129
+
130
+ - TTY → rich tables, colours and confirmation prompts.
131
+ - Pipe or `--json` → clean JSON on stdout. Logs and errors stay on stderr, so piping to
132
+ `jq` is safe.
133
+ - Mutations → refuse to run non-interactively without `--yes`.
134
+ - Exit codes → `0` success, `1` failure, `2` usage error.
135
+ - `--debug` → traces method, URL and headers to stderr. Credentials are redacted. For
136
+ endpoints that expose it, such as `doc list`, it also prints server-side SQL.
137
+
138
+ Server error detail is plain text; HTML is stripped. `--debug` never touches stdout, so
139
+ it won't corrupt `--json` output.
140
+
141
+ ## Commands
142
+
143
+ | Command | What it does |
144
+ |---|---|
145
+ | `frappectl doc list <DocType>` | List documents. Supports `-f`, `--fields`, `--limit` and `--all`. |
146
+ | `frappectl doc get <DocType> <name>` | Fetch one document. |
147
+ | `frappectl doc create <DocType>` | Create from `--set` scalars and/or piped/`--input` JSON. |
148
+ | `frappectl doc update <DocType> <name>` | Update optimistically; use `--force` to override. |
149
+ | `frappectl doc delete <DocType> <name>` | Delete after confirmation, or pass `--yes`. |
150
+ | `frappectl doc submit\|cancel\|amend` | Run document lifecycle actions. |
151
+ | `frappectl doctype list` / `frappectl doctype show <name>` | Discover doctypes and schema. |
152
+ | `frappectl report run <name>` | Run a report with the same filter syntax as `doc list`. |
153
+ | `frappectl file upload\|download` | Transfer files; upload supports `--doctype/--name` and `--private`. |
154
+ | `frappectl api <path>` | Call raw v2 APIs: `frappectl api method/<path> -F key=value`. |
155
+ | `frappectl guide` | Print the agent primer; no site or authentication needed. |
156
+ | `frappectl assistant [pi\|claude\|codex]` | Launch a coding agent configured as a Frappe assistant. |
157
+ | `frappectl update` | Self-upgrade through the `uv`/`pip` backend used for installation. |
158
+
159
+ ### Filtering
160
+
161
+ Simple filters use `-f`. Repeat the flag to combine them:
162
+
163
+ ```sh
164
+ frappectl doc list ToDo -f status=Open -f priority=High
165
+ frappectl doc list "Sales Invoice" -f 'grand_total>1000' -f 'customer like %Inc%'
166
+ ```
167
+
168
+ For `in`, `between`, child-table filters or anything else that doesn't fit cleanly in a
169
+ shell argument, pass full Frappe filter JSON:
170
+
171
+ ```sh
172
+ frappectl doc list "Sales Invoice" --filters-json '[["status","in",["Paid","Overdue"]]]'
173
+ ```
174
+
175
+ ## License
176
+
177
+ MIT