This file provides guidance to AI coding agents when working with this repository.
frappecli is a Python CLI tool for managing Frappe instances via REST API. It provides command-line access to CRUD operations, file management (upload/download), reports, and custom RPC methods across multiple Frappe sites.
Example Use Cases:
- Business Apps: Work with any Frappe app (ERPNext, Healthcare, Education, etc.)
- Custom Applications: Manage custom Frappe apps and doctypes
- Automation: Batch operations, scheduled data imports, report generation, webhooks
- Integration: Connect Frappe with external systems, sync data, automated workflows
- DevOps: Site management, data migrations, testing automation
Tech Stack: Python 3.12+, Click, requests/httpx, Rich, PyYAML
This project uses uv for dependency management following workspace conventions.
# Clone and enter the project
cd /Users/pascal/projects/frappecli
# Install dependencies
uv sync
# Run CLI locally
uv run frappecli --helpuv run frappecli <command> # Run CLI during development
uv run pytest tests/ -v # Run tests with verbose output
uv run ruff check src/ # Lint code
uv run ruff format src/ # Format code
uv pip install -e . # Install in editable modeWe follow Test-Driven Development and GitHub best practices.
1. Pick Issue → 2. Assign → 3. Branch → 4. Write Tests → 5. Implement
→ 6. Test → 7. Commit → 8. PR → 9. Review → 10. Merge → Repeat
# View open issues
gh issue list --label "phase-1"
# Pick next issue (e.g., #2 - Configuration Management)
gh issue view 2gh issue edit 2 --add-assignee @me# Branch naming: feature/{issue-number}-{short-description}
git checkout -b feature/2-config-managementTest-Driven Development:
- Write the test (it will fail - RED)
- Implement the code (make it pass - GREEN)
- Refactor (improve code - REFACTOR)
# Create test file first
touch tests/test_config.py
# Write failing tests
cat > tests/test_config.py << 'EOF'
import pytest
from frappecli.config import Config
def test_load_config():
"""Test loading configuration from file."""
config = Config("tests/fixtures/test_config.yaml")
assert config.data is not None
assert "sites" in config.data
def test_env_var_substitution():
"""Test environment variable substitution."""
import os
os.environ["TEST_API_KEY"] = "test_key"
config = Config("tests/fixtures/test_config.yaml")
site_config = config.get_site_config("test")
assert site_config["api_key"] == "test_key"
EOF
# Run tests - they should FAIL (RED)
uv run pytest tests/test_config.py -v# Now create the actual implementation
touch src/frappecli/config.py
# Implement until tests pass
# ... write code ...
# Run tests - they should PASS (GREEN)
uv run pytest tests/test_config.py -v# Run all tests to ensure nothing broke
uv run pytest tests/ -v
# Check coverage
uv run pytest --cov=src/frappecli --cov-report=term-missing
# Lint and format
uv run ruff format .
uv run ruff check --fix .
# Type check
uv run mypy src/# Stage changes
git add .
# Commit with conventional format
git commit -m "feat(config): implement configuration management with env var substitution
- Add Config class for loading YAML config
- Support ${VAR} environment variable substitution
- Add get_site_config() and list_sites() methods
- Add comprehensive tests with fixtures
Closes #2"# Push branch
git push origin feature/2-config-management
# Create PR via GitHub CLI
gh pr create \
--title "feat(config): implement configuration management" \
--body "Implements #2
## Changes
- Configuration loading with YAML
- Environment variable substitution
- Multi-site support
- Tests with 100% coverage
## Checklist
- [x] Tests written first (TDD)
- [x] All tests pass
- [x] Code formatted (ruff)
- [x] Type hints added
- [x] Documentation updated" \
--base main- GitHub Actions runs automatically
- CI checks: lint, test, security
- Self-review or wait for maintainer review
- Address any feedback
# After approval, merge PR
gh pr merge --squash
# Switch back to main
git checkout main
git pull origin main
# Delete feature branch
git branch -D feature/2-config-management# Pick next issue from the list
gh issue list --label "phase-1"
# Start from step 1 againRed-Green-Refactor Cycle:
-
🔴 RED: Write a failing test
# Test doesn't pass yet uv run pytest tests/test_client.py::test_get_doctype # FAILED - method not implemented
-
🟢 GREEN: Write minimal code to pass
def get_doctype(self, name): # Simplest implementation that passes return {"name": name}
-
🔵 REFACTOR: Improve code quality
def get_doctype(self, name: str) -> dict: """Get doctype information. Args: name: Doctype name Returns: Doctype metadata """ response = self._make_request("GET", f"/api/resource/DocType/{name}") return response.json()["message"]
TDD Benefits:
- ✅ Confidence in code (tests prove it works)
- ✅ Better design (testable code is good code)
- ✅ Documentation (tests show how to use it)
- ✅ Regression prevention (tests catch breaks)
- ✅ Easier refactoring (tests verify behavior)
Labels for Issues:
phase-1,phase-2,phase-3,phase-4,phase-5config,client,cli,crud,files,reportstdd- Test-Driven Developmentbug- Bug fixesenhancement- New featuresdocumentation- Docs updates
Milestones:
- Phase 1: Foundation
- Phase 2: CRUD Operations
- Phase 3: File Management
- Phase 4: Reports & RPC
- Phase 5: Polish & Release
feature/{issue-number}-{short-description}
fix/{issue-number}-{short-description}
docs/{issue-number}-{short-description}
test/{issue-number}-{short-description}
Examples:
feature/2-config-managementfeature/3-api-clientfix/15-file-upload-spacesdocs/23-update-readme
- One issue per PR - Keep PRs focused
- Reference issue - Use "Closes #X" in description
- Add checklist - Tests, linting, docs
- Squash merge - Keep main branch clean
- Delete branch - After merge
# 1. Pick issue
gh issue view 3
gh issue edit 3 --add-assignee @me
# 2. Create branch
git checkout -b feature/3-api-client
# 3. TDD: Write tests first
cat > tests/test_client.py << 'EOF'
def test_client_initialization():
client = FrappeClient("https://example.com", "key", "secret")
assert client.base_url == "https://example.com"
assert "Authorization" in client.session.headers
EOF
# 4. Run tests (RED)
uv run pytest tests/test_client.py -v
# FAILED
# 5. Implement
cat > src/frappecli/client.py << 'EOF'
class FrappeClient:
def __init__(self, base_url, api_key, api_secret):
self.base_url = base_url
self.session = requests.Session()
self.session.headers["Authorization"] = f"token {api_key}:{api_secret}"
EOF
# 6. Run tests (GREEN)
uv run pytest tests/test_client.py -v
# PASSED
# 7. Run all tests
uv run pytest tests/ -v --cov
# 8. Commit
git add .
git commit -m "feat(client): implement Frappe API client
- Add FrappeClient class with authentication
- Support for token-based auth header
- Session management for all requests
- Add comprehensive tests
Closes #3"
# 9. Push and PR
git push origin feature/3-api-client
gh pr create --title "feat(client): implement Frappe API client" --base main
# 10. After merge
git checkout main
git pull origin main
git branch -D feature/3-api-client
# 11. Next issue
gh issue list --label "phase-1"frappecli/
├── AGENTS.md # This file
├── README.md # User documentation
├── pyproject.toml # UV project config
├── .python-version # Python version (3.12+)
├── src/
│ └── frappecli/
│ ├── __init__.py
│ ├── cli.py # Main CLI entry point (Click)
│ ├── client.py # Frappe API client
│ ├── config.py # Config file management
│ └── commands/
│ ├── __init__.py
│ ├── site.py # Site info, doctypes list, status
│ ├── doctypes.py # CRUD commands (list, get, create, update, delete)
│ ├── files.py # File upload/download/management
│ ├── reports.py # Report execution
│ └── rpc.py # RPC method calls
└── tests/
├── __init__.py
├── test_client.py
├── test_commands.py
└── fixtures/
- Framework: pytest
- Coverage: Target 80%+
- Mocking: Use
responsesorhttpx-mockfor API calls - Test locations: All tests in
/tests/directory - Run before committing:
uv run pytest tests/ -v
- Spaces in doctype names and document names (e.g., "Production Order", "Service Contract")
- Special characters in names (Umlauts, ampersands, slashes)
- File uploads (multipart/form-data) with various file types
- Private vs public file handling
- Multi-site configuration
- Error responses from API (404, 403, 500)
- Large file uploads and pagination
- Python 3.12+ features allowed
- Type hints required on all functions
- Use
rufffor linting and formatting (configured inpyproject.toml) - Follow PEP 8 naming conventions
- Prefer functional patterns over classes where appropriate
- Functions and variables:
snake_case - Classes:
PascalCase - Constants:
UPPER_SNAKE_CASE - Private functions:
_leading_underscore
- Use Rich for user-facing error messages
- Provide actionable error messages
- Include HTTP status codes in API errors
- Guide users to fix configuration issues
- Never log API keys or secrets
- Validate file paths (prevent path traversal)
- Warn users when uploading public files
- Support environment variables for credentials
All uploads are private by default. Use --public flag explicitly for public files.
frappecli upload file.pdf # → Private (default)
frappecli upload logo.png --public # → Public (explicit)All doctype and docname arguments must be quoted to handle spaces and special characters.
# Correct ✅
frappecli get "Customer" "CUST-001"
frappecli get "Customer" "Müller GmbH & Co. KG"
frappecli upload file.pdf --attach "Legal Case" "Case 2026-001"
# Wrong ❌ (will fail with spaces)
frappecli get Customer Acme CorpConfig file supports multiple Frappe sites:
# ~/.config/frappecli/config.yaml
sites:
production:
url: https://erp-prod.company.com
api_key: ${FRAPPE_PROD_API_KEY}
api_secret: ${FRAPPE_PROD_API_SECRET}
staging:
url: https://erp-staging.company.com
api_key: ${FRAPPE_STAGING_API_KEY}
api_secret: ${FRAPPE_STAGING_API_SECRET}
default_site: productionSupport both JSON and pretty-printed table output:
frappecli list "Customer" --json # JSON for piping
frappecli list "Customer" --table # Pretty table (default)Support @file.json syntax for complex data:
frappecli create "Customer" --data '{"customer_name": "Test"}'
frappecli create "Customer" --data @customer.jsonMethod: API Key + API Secret in Authorization header
Authorization: token api_key:api_secret
# List
GET /api/resource/{doctype}?filters={"status":"Active"}&limit=20
# Get
GET /api/resource/{doctype}/{name}
# Create
POST /api/resource/{doctype}
Body: {"customer_name": "Acme Corp", ...}
# Update
PUT /api/resource/{doctype}/{name}
Body: {"mobile": "+43 123 456", ...}
# Delete
DELETE /api/resource/{doctype}/{name}POST /api/method/upload_file
Content-Type: multipart/form-data
Authorization: token api_key:api_secret
Form fields:
- file: <binary>
- is_private: "1" (default) or "0"
- folder: "Home" (optional)
- doctype: "Customer" (optional, for attachment)
- docname: "CUST-001" (optional, for attachment)
- fieldname: "image" (optional, for attachment)
- optimize: "1" (optional, for images)# Get list of all doctypes
GET /api/method/frappe.client.get_list
POST /api/method/frappe.client.get_list
Body: {"doctype": "DocType", "fields": ["name", "module", "custom", "issingle"]}
# Alternative: Get meta information
GET /api/resource/DocType
GET /api/resource/DocType/{doctype_name}{
"message": {
"name": "CUST-001",
"customer_name": "Acme Corp",
...
}
}# List all available doctypes on site
frappecli doctypes
frappecli doctypes --json # JSON output
frappecli doctypes --module "Stock" # Filter by module
frappecli doctypes --custom # Only custom doctypes
frappecli doctypes --standard # Only standard doctypes
# Show doctype details (fields, permissions, etc.)
frappecli doctype-info "Customer"
frappecli doctype-info "Production Order" --fields # List all fields
# List configured sites
frappecli sites
frappecli sites --show-urls # Include URLs
# Site status and version
frappecli status
frappecli status --detailed # Include modules, users count# List documents
frappecli list "Customer"
frappecli list "Customer" --filters '{"customer_type": "Company"}' --limit 50
# Get single document
frappecli get "Customer" "CUST-001"
# Create document
frappecli create "Customer" --data '{"customer_name": "Acme Corp"}'
# Update document
frappecli update "Customer" "CUST-001" --data '{"mobile": "+43 123"}'
# Delete document
frappecli delete "Customer" "CUST-001"# Upload (private by default)
frappecli upload technical-spec.pdf
frappecli upload blueprint.jpg --attach "Production Order" "PROD-ORD-2026-001"
frappecli upload company-logo.png --public --folder "Assets"
# Download
frappecli download /files/technical-spec.pdf -o local.pdf
# List files
frappecli files list --folder "Home"
frappecli files search "specification"
# Bulk upload
frappecli bulk-upload ./quality-reports/*.pdf --folder "QA Documents"- Frappe uses spaces in doctype names:
"User","Email Queue","Custom Field" - Apps add their own doctypes with spaces:
"Sales Invoice","ToDo","Web Page" - Document names can contain spaces, special chars:
"Administrator","Project-2026" - Always quote arguments in CLI to prevent parsing errors
- Private files:
/private/files/(requires authentication) - Public files:
/files/(no authentication) - File documents stored in
Filedoctype - Content hash used for deduplication
- Files can be organized in folders
- Files can be attached to any doctype
- Attachment fields:
attached_to_doctype,attached_to_name,attached_to_field - Multiple files per document allowed
- Attachments are versioned with document
- User Management:
User,Role,Permission,User Type - Documents:
File,Comment,Version,Activity Log - System:
DocType,Custom Field,Print Format,Report - Communication:
Email Account,Email Queue,Notification - Workflow:
Workflow,Workflow State,Workflow Action
Frappe apps (ERPNext, Healthcare, Education, etc.) add their own doctypes:
- Use
frappecli doctypes --module "ModuleName"to filter by app/module - Example modules: Core, Desk, Website, Setup, Integrations
- Create custom doctypes for your specific business needs
- Use
frappecli doctypes --customto list only custom doctypes - Custom workflows, forms, and business logic
- Integration points with external systems
Key dependencies in pyproject.toml:
[project]
dependencies = [
"click>=8.1.7", # CLI framework
"requests>=2.31.0", # HTTP client (or httpx)
"pyyaml>=6.0.1", # Config file parsing
"rich>=13.7.0", # Terminal output
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0", # Testing
"ruff>=0.6.0", # Linting and formatting
"responses>=0.25.0", # HTTP mocking
]# Site credentials (used if not in config.yaml)
FRAPPE_URL=https://erp.example.com
FRAPPE_API_KEY=your_api_key
FRAPPE_API_SECRET=your_api_secret
# Config file location (optional override)
FRAPPECLI_CONFIG=~/.config/frappecli/config.yaml- Run tests:
uv run pytest tests/ -v - Run linter:
uv run ruff check src/ - Format code:
uv run ruff format src/ - Update AGENTS.md and README.md if needed
Follow conventional commits:
feat(files): add bulk upload command
fix(client): handle connection timeout gracefully
docs: update installation instructions
test(commands): add tests for quoted arguments
[component] Description
Examples:
[files] Add bulk upload support[client] Fix multipart form data encoding[docs] Update CLI examples in README
- frappe-bench skill (
/Users/pascal/projects/agent-scripts/skills/frappe-bench/):- For Frappe installation and development operations
- Commands:
bench init,bench get-app,bench migrate - Separate scope: frappecli = business ops, frappe-bench = dev/admin
README.md- User-facing documentation and installation guideAGENTS.md- This file, for AI agents- Main workspace
CLAUDE.md-/Users/pascal/projects/CLAUDE.md - Frappe Framework repository - https://github.com/frappe/frappe
- Frappe documentation - https://docs.frappe.io/framework
- Frappe API docs - https://docs.frappe.io/framework/user/en/api
- Frappe REST API reference - https://docs.frappe.io/framework/user/en/api/rest
Potential features to consider:
- Interactive REPL mode (
frappecli shell) - Watch mode for live updates (
frappecli watch "Customer") - Bulk operations via CSV import/export
- Workflow action commands
- Bash/Zsh completion scripts
- Plugin system for custom commands
- Sync with local SQLite for offline work