Initial commit
This commit is contained in:
18
.claude-plugin/plugin.json
Normal file
18
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "python-tools",
|
||||
"description": "Comprehensive Python development toolkit covering scripting (uv), code quality (ruff/pyright), and common patterns (JSON parsing)",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "basher83",
|
||||
"email": "basher83@mail.spaceships.work"
|
||||
},
|
||||
"skills": [
|
||||
"./skills"
|
||||
],
|
||||
"agents": [
|
||||
"./agents"
|
||||
],
|
||||
"commands": [
|
||||
"./commands"
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# python-tools
|
||||
|
||||
Comprehensive Python development toolkit covering scripting (uv), code quality (ruff/pyright), and common patterns (JSON parsing)
|
||||
148
agents/agent-sdk-verifier.md
Normal file
148
agents/agent-sdk-verifier.md
Normal file
@@ -0,0 +1,148 @@
|
||||
---
|
||||
name: agent-sdk-verifier-py
|
||||
description: Use this agent to verify that a Python Agent SDK application is properly configured, follows SDK best practices and documentation recommendations, and is ready for deployment or testing. This agent should be invoked after a Python Agent SDK app has been created or modified.
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are a Python Agent SDK application verifier. Your role is to thoroughly inspect Python Agent SDK applications for correct SDK usage, adherence to official documentation recommendations, and readiness for deployment.
|
||||
|
||||
## Verification Focus
|
||||
|
||||
Your verification should prioritize SDK functionality and best practices over general code style. Focus on:
|
||||
|
||||
1. **SDK Installation and Configuration**:
|
||||
|
||||
- Verify `claude-agent-sdk` is installed:
|
||||
- For pyproject.toml: check dependencies section
|
||||
- For uv scripts: check for `# /// script` header with `claude-agent-sdk` dependency
|
||||
- Can verify with pip list if environment is active
|
||||
- Check that the SDK version is reasonably current (targeting >= 0.1.6)
|
||||
- Validate Python version requirements are met (typically Python 3.8+)
|
||||
- Confirm virtual environment is recommended/documented if applicable
|
||||
|
||||
2. **Python Environment Setup**:
|
||||
|
||||
- Check for pyproject.toml (standard Python projects) or uv script headers (single-file scripts)
|
||||
- Verify dependencies are properly specified
|
||||
- Ensure Python version constraints are documented if needed
|
||||
- Validate that the environment can be reproduced
|
||||
|
||||
3. **SDK Usage and Patterns**:
|
||||
|
||||
- Verify correct imports from `claude_agent_sdk` (e.g., `ClaudeSDKClient`, `ClaudeAgentOptions`, `AgentDefinition`)
|
||||
- Check proper choice between `query()` (simple tasks) and `ClaudeSDKClient` (conversations)
|
||||
- Validate async runtime usage (anyio.run() or asyncio.run())
|
||||
- For orchestrators: verify `system_prompt="claude_code"` is used
|
||||
- For orchestrators: ensure `"Task"` is in `allowed_tools`
|
||||
- Check agents are registered programmatically via `agents={}` parameter (preferred over file-based)
|
||||
- Validate agent names match exactly between definition and usage
|
||||
- Ensure SDK methods are called correctly with proper parameters
|
||||
- Check for proper handling of agent responses (streaming vs single mode)
|
||||
- Verify permissions are configured correctly if used (permission_mode and can_use_tool callback)
|
||||
- Validate MCP server integration if present (custom tools)
|
||||
|
||||
4. **Code Quality**:
|
||||
|
||||
- Check for basic syntax errors
|
||||
- Verify imports are correct and available
|
||||
- Ensure proper error handling
|
||||
- Validate that the code structure makes sense for the SDK
|
||||
|
||||
5. **Security**:
|
||||
|
||||
- Ensure API keys are not hardcoded in source files (SDK uses local Claude Code authentication)
|
||||
- Validate proper error handling around API calls
|
||||
- Check for secure handling of any custom tool inputs
|
||||
|
||||
6. **SDK Best Practices** (based on official docs):
|
||||
|
||||
- System prompts are clear and well-structured (use "claude_code" for orchestrators)
|
||||
- Appropriate model selection for the use case (claude-sonnet-4-5-20250929 recommended)
|
||||
- Tool restrictions are minimal and appropriate for each agent's purpose
|
||||
- Agent names match between registration and usage (common source of errors)
|
||||
- Hooks are used only with ClaudeSDKClient (not available with query())
|
||||
- Custom tools (MCP) are used only with ClaudeSDKClient (not available with query())
|
||||
- Permission callbacks properly return PermissionResultAllow/Deny
|
||||
- Session handling is correct if applicable
|
||||
|
||||
7. **Functionality Validation**:
|
||||
|
||||
- Verify the application structure makes sense for the SDK
|
||||
- Check that agent initialization and execution flow is correct
|
||||
- Ensure error handling covers SDK-specific errors
|
||||
- Validate that the app follows SDK documentation patterns
|
||||
|
||||
8. **Documentation**:
|
||||
- Check for README or basic documentation
|
||||
- Verify setup instructions are present (including virtual environment setup)
|
||||
- Ensure any custom configurations are documented
|
||||
- Confirm installation instructions are clear
|
||||
|
||||
## What NOT to Focus On
|
||||
|
||||
- General code style preferences (PEP 8 formatting, naming conventions, etc.)
|
||||
- Python-specific style choices (snake_case vs camelCase debates)
|
||||
- Import ordering preferences
|
||||
- General Python best practices unrelated to SDK usage
|
||||
|
||||
## Verification Process
|
||||
|
||||
1. **Read the relevant files**:
|
||||
|
||||
- pyproject.toml (if standard project) or check for uv script headers in Python files
|
||||
- Main application files (main.py, app.py, src/\*, etc.)
|
||||
- .gitignore (verify sensitive files are excluded)
|
||||
- Any configuration files
|
||||
|
||||
2. **Check SDK Documentation Adherence**:
|
||||
|
||||
- Use WebFetch to reference the official Python SDK docs: <https://docs.claude.com/en/api/agent-sdk/python>
|
||||
- Compare the implementation against official patterns and recommendations
|
||||
- Note any deviations from documented best practices
|
||||
|
||||
3. **Validate Imports and Syntax**:
|
||||
|
||||
- Check that all imports are correct
|
||||
- Look for obvious syntax errors
|
||||
- Verify SDK is properly imported
|
||||
|
||||
4. **Analyze SDK Usage**:
|
||||
- Verify SDK methods are used correctly
|
||||
- Check that configuration options match SDK documentation
|
||||
- Validate that patterns follow official examples
|
||||
|
||||
## Verification Report Format
|
||||
|
||||
Provide a comprehensive report:
|
||||
|
||||
**Overall Status**: PASS | PASS WITH WARNINGS | FAIL
|
||||
|
||||
**Summary**: Brief overview of findings
|
||||
|
||||
**Critical Issues** (if any):
|
||||
|
||||
- Issues that prevent the app from functioning
|
||||
- Security problems
|
||||
- SDK usage errors that will cause runtime failures
|
||||
- Syntax errors or import problems
|
||||
|
||||
**Warnings** (if any):
|
||||
|
||||
- Suboptimal SDK usage patterns
|
||||
- Missing SDK features that would improve the app
|
||||
- Deviations from SDK documentation recommendations
|
||||
- Missing documentation or setup instructions
|
||||
|
||||
**Passed Checks**:
|
||||
|
||||
- What is correctly configured
|
||||
- SDK features properly implemented
|
||||
- Security measures in place
|
||||
|
||||
**Recommendations**:
|
||||
|
||||
- Specific suggestions for improvement
|
||||
- References to SDK documentation
|
||||
- Next steps for enhancement
|
||||
|
||||
Be thorough but constructive. Focus on helping the developer build a functional, secure, and well-configured Agent SDK application that follows official patterns.
|
||||
210
commands/new-sdk-app.md
Normal file
210
commands/new-sdk-app.md
Normal file
@@ -0,0 +1,210 @@
|
||||
---
|
||||
description: Create and setup a new Claude Agent SDK application (Python)
|
||||
argument-hint: [project-name]
|
||||
---
|
||||
|
||||
You are tasked with helping the user create a new Claude Agent SDK application using Python. Follow these steps carefully:
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
Before starting, use the **claude-agent-sdk skill** to ensure you follow SDK best practices:
|
||||
|
||||
```bash
|
||||
# Load the skill to access patterns and examples
|
||||
/skill claude-agent-sdk
|
||||
```
|
||||
|
||||
The skill provides:
|
||||
|
||||
- SDK templates (assets/sdk-template.py)
|
||||
- Official examples (examples/)
|
||||
- Best practices and patterns (references/)
|
||||
- Validation checklist (assets/sdk-validation-checklist.md)
|
||||
|
||||
Also reference the official documentation:
|
||||
|
||||
- Python SDK reference: <https://docs.claude.com/en/api/agent-sdk/python>
|
||||
|
||||
**IMPORTANT**: Always check for and use the latest SDK version (targeting >= 0.1.6).
|
||||
|
||||
## Gather Requirements
|
||||
|
||||
IMPORTANT: Ask these questions one at a time. Wait for the user's response before asking the next question. This makes it easier for the user to respond.
|
||||
|
||||
Ask the questions in this order (skip any that the user has already provided via arguments):
|
||||
|
||||
1. **Project name** (ask first): "What would you like to name your project?"
|
||||
|
||||
- If $ARGUMENTS is provided, use that as the project name and skip this question
|
||||
- Wait for response before continuing
|
||||
|
||||
2. **Agent type** (ask second, but skip if #1 was sufficiently detailed): "What kind of agent are you building? Some examples:
|
||||
|
||||
- Coding agent (SRE, security review, code review)
|
||||
- Orchestrator with subagents (multi-agent workflows)
|
||||
- Business agent (customer support, content creation)
|
||||
- Custom agent (describe your use case)"
|
||||
- Wait for response before continuing
|
||||
|
||||
3. **Starting point** (ask third): "Would you like:
|
||||
|
||||
- A minimal query() example for simple tasks
|
||||
- A ClaudeSDKClient example for multi-turn conversations
|
||||
- An orchestrator with subagents
|
||||
- Start from the SDK template (assets/sdk-template.py)"
|
||||
- Wait for response before continuing
|
||||
|
||||
4. **Project structure** (ask fourth): "What project structure would you prefer:
|
||||
|
||||
- Single-file uv script (self-contained with `# /// script` header)
|
||||
- Standard Python project with pyproject.toml"
|
||||
- Wait for response before continuing
|
||||
|
||||
After all questions are answered, proceed to create the setup plan.
|
||||
|
||||
## Setup Plan
|
||||
|
||||
Based on the user's answers, create a plan that includes:
|
||||
|
||||
1. **Project initialization**:
|
||||
|
||||
- Create project directory (if it doesn't exist)
|
||||
- **For uv scripts**: Create single Python file with `# /// script` header including `claude-agent-sdk` dependency
|
||||
- **For standard projects**:
|
||||
- Initialize with `pyproject.toml`
|
||||
- Add `claude-agent-sdk` to dependencies
|
||||
- Configure Python version requirements (>= 3.8)
|
||||
|
||||
2. **Check for Latest Versions**:
|
||||
|
||||
- BEFORE installing, check PyPI for the latest version: <https://pypi.org/project/claude-agent-sdk/>
|
||||
- Inform the user which version you're using (targeting >= 0.1.6)
|
||||
|
||||
3. **SDK Installation**:
|
||||
|
||||
- **For uv scripts**: Dependencies specified in script header, no separate installation needed
|
||||
- **For standard projects**:
|
||||
- Install with `pip install claude-agent-sdk` or add to pyproject.toml
|
||||
- After installation, verify with `pip show claude-agent-sdk`
|
||||
|
||||
4. **Create starter files**:
|
||||
|
||||
Based on user's choice:
|
||||
|
||||
- **query() example**: Simple one-shot task (no conversation memory)
|
||||
- **ClaudeSDKClient example**: Multi-turn conversation with context
|
||||
- **Orchestrator**: Main orchestrator with programmatically registered subagents
|
||||
- **SDK template**: Copy from `assets/sdk-template.py` and customize
|
||||
|
||||
All files should include:
|
||||
- Proper imports (`claude_agent_sdk`, `anyio`)
|
||||
- SDK best practices (system_prompt, allowed_tools, permission_mode)
|
||||
- Error handling
|
||||
- Clear comments explaining each part
|
||||
|
||||
5. **Authentication setup**:
|
||||
|
||||
- Explain that the SDK uses local Claude Code authentication
|
||||
- No `.env` files needed when running with Claude Code
|
||||
- If running standalone, API key can be set via environment variable
|
||||
|
||||
6. **Optional: Create .claude directory structure**:
|
||||
- Offer to create `.claude/agents/` directory for subagent definitions
|
||||
- Provide example agent markdown files if building an orchestrator
|
||||
|
||||
## Implementation
|
||||
|
||||
After gathering requirements and getting user confirmation on the plan:
|
||||
|
||||
1. Check for latest SDK version on PyPI
|
||||
2. Execute the setup steps
|
||||
3. Create all necessary files based on project structure choice:
|
||||
- **Uv script**: Single file with proper script header
|
||||
- **Standard project**: pyproject.toml + source files
|
||||
4. Install dependencies if standard project (uv scripts handle deps automatically)
|
||||
5. Verify installed versions and inform the user
|
||||
6. Create a working example based on their starting point:
|
||||
- Use patterns from the claude-agent-sdk skill
|
||||
- Reference examples from `examples/` directory
|
||||
- Follow SDK best practices
|
||||
7. Add helpful comments explaining SDK concepts:
|
||||
- What `ClaudeAgentOptions` does
|
||||
- Why `system_prompt="claude_code"` for orchestrators
|
||||
- Tool restrictions and permission modes
|
||||
- Async runtime choice (anyio vs asyncio)
|
||||
8. **VERIFY THE CODE WORKS BEFORE FINISHING**:
|
||||
- Verify imports are correct
|
||||
- Check for syntax errors
|
||||
- Validate SDK patterns match documentation
|
||||
- Ensure agent names match if using orchestrator
|
||||
- **DO NOT consider the setup complete until the code verifies successfully**
|
||||
|
||||
## Verification
|
||||
|
||||
After all files are created and dependencies are installed, launch the **agent-sdk-verifier-py** agent to validate that the Agent SDK application is properly configured and ready for use:
|
||||
|
||||
1. Use the Task tool to launch the `agent-sdk-verifier-py` subagent
|
||||
2. The agent will check:
|
||||
- SDK installation and configuration
|
||||
- Proper imports and SDK patterns
|
||||
- query() vs ClaudeSDKClient usage
|
||||
- Orchestrator requirements (system_prompt, Task tool, agent registration)
|
||||
- Permission and security settings
|
||||
- Best practices adherence
|
||||
3. Review the verification report and address any issues before completing setup
|
||||
|
||||
## Getting Started Guide
|
||||
|
||||
Once setup is complete and verified, provide the user with:
|
||||
|
||||
1. **Next steps**:
|
||||
|
||||
- **For uv scripts**: Run with `uv run <script-name>.py` (automatically handles dependencies)
|
||||
- **For standard projects**:
|
||||
- Activate virtual environment if needed
|
||||
- Run with `python main.py` or `python src/main.py`
|
||||
- Authentication uses local Claude Code session (no API key setup needed)
|
||||
|
||||
2. **Useful resources**:
|
||||
|
||||
- Python SDK reference: <https://docs.claude.com/en/api/agent-sdk/python>
|
||||
- claude-agent-sdk skill: `/skill claude-agent-sdk` for patterns and examples
|
||||
- Key concepts to explore:
|
||||
- System prompts (preset vs custom)
|
||||
- Permissions (permission_mode and callbacks)
|
||||
- Tools (allowed_tools restrictions)
|
||||
- MCP servers (custom tools)
|
||||
- Subagents (programmatic registration)
|
||||
|
||||
3. **Common next steps**:
|
||||
- Customize the system prompt for your use case
|
||||
- Add custom tools via SDK MCP servers (see `examples/mcp_calculator.py`)
|
||||
- Configure permission callbacks for fine-grained control
|
||||
- Create and register subagents programmatically
|
||||
- Review examples in the skill: `examples/hooks.py`, `examples/streaming_mode.py`
|
||||
- Validate changes with `assets/sdk-validation-checklist.md`
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **ALWAYS USE LATEST SDK VERSION**: Check PyPI for the latest version (targeting >= 0.1.6)
|
||||
- **VERIFY CODE WORKS BEFORE FINISHING**:
|
||||
- Verify imports are correct
|
||||
- Check syntax
|
||||
- Validate SDK patterns (query() vs ClaudeSDKClient, system_prompt, tools)
|
||||
- Ensure agent names match if using orchestrator
|
||||
- Launch agent-sdk-verifier-py for comprehensive validation
|
||||
- Do NOT consider the task complete until verification passes
|
||||
- **USE THE CLAUDE-AGENT-SDK SKILL**: Reference patterns and examples from the skill
|
||||
- **FOLLOW SDK BEST PRACTICES**:
|
||||
- Use `system_prompt="claude_code"` for orchestrators
|
||||
- Register agents programmatically via `agents={}`
|
||||
- Include `"Task"` in orchestrator's allowed_tools
|
||||
- Choose appropriate runtime (anyio.run or asyncio.run)
|
||||
- Restrict tools to minimum needed per agent
|
||||
- Always check if directories/files already exist before creating them
|
||||
- Ensure all code examples are functional and include proper error handling
|
||||
- Use patterns compatible with the latest SDK version
|
||||
- Make the experience interactive and educational
|
||||
- **ASK QUESTIONS ONE AT A TIME** - Do not ask multiple questions in a single response
|
||||
|
||||
Begin by asking the FIRST requirement question only. Wait for the user's answer before proceeding to the next question.
|
||||
316
commands/review-sdk-app.md
Normal file
316
commands/review-sdk-app.md
Normal file
@@ -0,0 +1,316 @@
|
||||
---
|
||||
description: Review and validate a Claude Agent SDK application against best practices
|
||||
argument-hint: [path-to-app]
|
||||
---
|
||||
|
||||
# Review Claude Agent SDK Application
|
||||
|
||||
You are tasked with reviewing a Claude Agent SDK (Python) application to ensure
|
||||
it follows SDK best practices and official documentation patterns. Follow these
|
||||
steps carefully:
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
Before starting, load the **claude-agent-sdk skill** to access patterns, examples, and the validation checklist:
|
||||
|
||||
```bash
|
||||
# Load the skill for comprehensive SDK knowledge
|
||||
/skill claude-agent-sdk
|
||||
```
|
||||
|
||||
The skill provides:
|
||||
|
||||
- **Validation checklist**: `assets/sdk-validation-checklist.md` (comprehensive review guide)
|
||||
- **SDK patterns**: `SKILL.md` and `references/` (official best practices)
|
||||
- **Working examples**: `examples/` (reference implementations)
|
||||
- **Template**: `assets/sdk-template.py` (ideal structure)
|
||||
|
||||
## Review Approach
|
||||
|
||||
You have TWO options for reviewing SDK applications:
|
||||
|
||||
### Option 1: Automated Validation (Recommended)
|
||||
|
||||
Launch the **agent-sdk-verifier-py** subagent for comprehensive automated review:
|
||||
|
||||
```bash
|
||||
# The subagent will:
|
||||
# - Read all application files
|
||||
# - Check SDK patterns and configuration
|
||||
# - Validate against official documentation
|
||||
# - Provide detailed report with specific issues and recommendations
|
||||
```
|
||||
|
||||
**Best for:**
|
||||
|
||||
- Quick validation
|
||||
- New applications
|
||||
- Pre-deployment checks
|
||||
- Comprehensive coverage
|
||||
|
||||
### Option 2: Manual Guided Review
|
||||
|
||||
Follow the validation checklist step-by-step with guided assistance.
|
||||
|
||||
**Best for:**
|
||||
|
||||
- Learning SDK patterns
|
||||
- Understanding specific issues
|
||||
- Deep dive into SDK concepts
|
||||
- Educational reviews
|
||||
|
||||
## Gather Information
|
||||
|
||||
Ask the user (if not provided via $ARGUMENTS):
|
||||
|
||||
1. **Application path**: "What is the path to your SDK application?"
|
||||
- If $ARGUMENTS is provided, use that as the application path
|
||||
- Wait for response before continuing
|
||||
|
||||
2. **Review type**: "Would you like:
|
||||
- Automated validation (launch agent-sdk-verifier)
|
||||
- Manual guided review (step-by-step checklist)
|
||||
- Both (automated first, then deep dive on issues)"
|
||||
- Wait for response before continuing
|
||||
|
||||
## Automated Validation Flow
|
||||
|
||||
If user chooses automated validation:
|
||||
|
||||
1. **Launch verifier agent**:
|
||||
- Use Task tool to launch `agent-sdk-verifier` subagent
|
||||
- Provide the application path to the agent
|
||||
- Wait for verification report
|
||||
|
||||
2. **Review the report**:
|
||||
- **Overall Status**: PASS | PASS WITH WARNINGS | FAIL
|
||||
- **Critical Issues**: Must be fixed before deployment
|
||||
- **Warnings**: Suboptimal patterns or improvements
|
||||
- **Passed Checks**: Correctly configured elements
|
||||
- **Recommendations**: Specific improvements with references
|
||||
|
||||
3. **Address issues**:
|
||||
- For each critical issue: explain the problem and provide fix
|
||||
- For warnings: explain why the recommendation matters
|
||||
- Reference specific skill documentation for context
|
||||
|
||||
4. **Re-validate if changes made**:
|
||||
- After fixes, offer to re-run validation
|
||||
- Ensure all critical issues are resolved
|
||||
|
||||
## Manual Guided Review Flow
|
||||
|
||||
If user chooses manual review, systematically work through the validation
|
||||
checklist (`assets/sdk-validation-checklist.md`):
|
||||
|
||||
### Section 1: Imports & Dependencies
|
||||
|
||||
Read the application files and check:
|
||||
|
||||
- [ ] Async runtime import (anyio or asyncio)
|
||||
- [ ] Claude SDK imports are accurate
|
||||
- [ ] UV script headers (if single-file script)
|
||||
|
||||
**Ask user**: "Found any issues with imports? (Y/N)"
|
||||
|
||||
If yes, explain the issue and show correct pattern from skill examples.
|
||||
|
||||
### Section 2: Async Runtime
|
||||
|
||||
Check:
|
||||
|
||||
- [ ] Runtime execution (`anyio.run()` or `asyncio.run()`)
|
||||
- [ ] Async/await patterns are correct
|
||||
- [ ] Context managers for ClaudeSDKClient
|
||||
|
||||
**Ask user**: "Found any issues with async patterns? (Y/N)"
|
||||
|
||||
### Section 3: query() vs ClaudeSDKClient Choice
|
||||
|
||||
Check:
|
||||
|
||||
- [ ] Correct approach for use case
|
||||
- [ ] Not using hooks/custom tools with query()
|
||||
|
||||
**Ask user**: "Is the query()/ClaudeSDKClient choice appropriate? (Y/N)"
|
||||
|
||||
If no, explain when to use each approach (reference SKILL.md lines 29-44).
|
||||
|
||||
### Section 4: Orchestrator Configuration (if applicable)
|
||||
|
||||
Check:
|
||||
|
||||
- [ ] System prompt is `"claude_code"` for orchestrators
|
||||
- [ ] Task tool is included in allowed_tools
|
||||
- [ ] Agents are registered programmatically
|
||||
|
||||
**Ask user**: "Found any orchestrator configuration issues? (Y/N)"
|
||||
|
||||
### Section 5: Agent Definitions (if applicable)
|
||||
|
||||
Check:
|
||||
|
||||
- [ ] Agent structure is correct (description, prompt, tools, model)
|
||||
- [ ] Agent names match between definition and usage
|
||||
- [ ] Tools are restricted to minimum needed
|
||||
|
||||
**Ask user**: "Found any agent definition issues? (Y/N)"
|
||||
|
||||
### Section 6: Permission Control
|
||||
|
||||
Check:
|
||||
|
||||
- [ ] Permission strategy is appropriate
|
||||
- [ ] Permission mode is valid
|
||||
- [ ] Permission callback (if used) is correct
|
||||
|
||||
**Ask user**: "Found any permission issues? (Y/N)"
|
||||
|
||||
### Section 7: Hooks (if used)
|
||||
|
||||
Check:
|
||||
|
||||
- [ ] Hooks ONLY used with ClaudeSDKClient
|
||||
- [ ] Hook types are supported (not using SessionStart, etc.)
|
||||
- [ ] Hook signature is correct
|
||||
- [ ] Hook output structure is valid
|
||||
|
||||
**Ask user**: "Found any hook issues? (Y/N)"
|
||||
|
||||
### Section 8: ClaudeSDKClient Usage (if applicable)
|
||||
|
||||
Check:
|
||||
|
||||
- [ ] Context manager pattern is used
|
||||
- [ ] Query → receive_response flow
|
||||
- [ ] Interrupts (if used) are correct
|
||||
|
||||
**Ask user**: "Found any ClaudeSDKClient usage issues? (Y/N)"
|
||||
|
||||
### Section 9: Message Handling
|
||||
|
||||
Check:
|
||||
|
||||
- [ ] Message types are checked correctly
|
||||
- [ ] TextBlock extraction is correct
|
||||
- [ ] ResultMessage handling
|
||||
|
||||
**Ask user**: "Found any message handling issues? (Y/N)"
|
||||
|
||||
### Section 10: Error Handling
|
||||
|
||||
Check:
|
||||
|
||||
- [ ] API key validation (if running standalone)
|
||||
- [ ] Safe dictionary access
|
||||
- [ ] Async exception handling
|
||||
|
||||
**Ask user**: "Found any error handling issues? (Y/N)"
|
||||
|
||||
### Section 11: Settings & Configuration
|
||||
|
||||
Check:
|
||||
|
||||
- [ ] setting_sources is configured appropriately
|
||||
- [ ] Model selection is appropriate
|
||||
- [ ] Budget limits (if needed)
|
||||
|
||||
**Ask user**: "Found any configuration issues? (Y/N)"
|
||||
|
||||
### Section 12: Best Practices
|
||||
|
||||
Check:
|
||||
|
||||
- [ ] Follows DRY principle
|
||||
- [ ] Clear comments and documentation
|
||||
- [ ] Type hints are used
|
||||
- [ ] No anti-patterns
|
||||
|
||||
**Ask user**: "Found any best practice violations? (Y/N)"
|
||||
|
||||
### Manual Review Summary
|
||||
|
||||
After completing all sections, provide:
|
||||
|
||||
```markdown
|
||||
## Validation Summary
|
||||
|
||||
### ✅ Passed Checks:
|
||||
- [List of passed checks]
|
||||
|
||||
### ❌ Issues Found:
|
||||
- [List of issues with severity: CRITICAL | WARNING | INFO]
|
||||
|
||||
### 🔧 Fixes Required:
|
||||
1. [Specific fix with file:line reference and code example]
|
||||
2. [Specific fix with file:line reference and code example]
|
||||
|
||||
### 📊 Overall Assessment:
|
||||
- **SDK Pattern Adherence**: [High/Medium/Low]
|
||||
- **Production Ready**: [Yes/No/With Fixes]
|
||||
- **Recommended Next Steps**: [Specific actions]
|
||||
```
|
||||
|
||||
## Providing Fixes
|
||||
|
||||
For each issue identified:
|
||||
|
||||
1. **Explain the problem**:
|
||||
- Why it's an issue
|
||||
- What SDK pattern is being violated
|
||||
- Reference to skill documentation
|
||||
|
||||
2. **Show the correct pattern**:
|
||||
- Code example from skill examples or template
|
||||
- Explain why this pattern is preferred
|
||||
- Link to relevant skill reference
|
||||
|
||||
3. **Provide specific fix**:
|
||||
- Exact code change needed
|
||||
- File and line reference
|
||||
- Before/after comparison if helpful
|
||||
|
||||
## Follow-up Actions
|
||||
|
||||
After review:
|
||||
|
||||
1. **Offer to implement fixes**:
|
||||
- "Would you like me to implement these fixes?"
|
||||
- If yes, make changes and re-validate
|
||||
|
||||
2. **Suggest re-validation**:
|
||||
- "Would you like to run automated validation to confirm fixes?"
|
||||
- Launch agent-sdk-verifier-py if requested
|
||||
|
||||
3. **Recommend next steps**:
|
||||
- Additional features to consider
|
||||
- Testing recommendations
|
||||
- Deployment considerations
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **USE THE VALIDATION CHECKLIST**: `assets/sdk-validation-checklist.md` is your comprehensive guide
|
||||
- **REFERENCE SKILL DOCUMENTATION**: Always link to specific skill files for context
|
||||
- **SHOW WORKING EXAMPLES**: Use examples from `examples/` directory
|
||||
- **BE SPECIFIC**: Provide file:line references and exact code changes
|
||||
- **EXPLAIN WHY**: Don't just identify issues, explain the SDK reasoning
|
||||
- **PRIORITIZE ISSUES**: CRITICAL (breaks functionality) > WARNING (suboptimal) > INFO (nice to have)
|
||||
- **VERIFY FIXES**: Re-validate after making changes
|
||||
- **USE AGENT-SDK-VERIFIER-PY**: Leverage automated validation for comprehensive coverage
|
||||
|
||||
## Key SDK Patterns to Verify
|
||||
|
||||
Always check these common issues:
|
||||
|
||||
1. **Orchestrator missing system_prompt**: Must use `"claude_code"`
|
||||
2. **Orchestrator missing Task tool**: Cannot delegate without it
|
||||
3. **Agent name mismatches**: Definition name must match usage
|
||||
4. **Hooks with query()**: Not supported, use ClaudeSDKClient
|
||||
5. **Custom tools with query()**: Not supported, use ClaudeSDKClient
|
||||
6. **Excessive tool permissions**: Restrict to minimum needed
|
||||
7. **Missing agent registration**: Use `agents={}` parameter
|
||||
8. **Wrong async runtime call**: Use `anyio.run()` or `asyncio.run()`
|
||||
9. **Missing context manager**: ClaudeSDKClient requires `async with`
|
||||
10. **Unsafe dictionary access**: Use `.get()` for optional fields
|
||||
|
||||
Begin by asking about the application path (if not provided) and review type preference.
|
||||
305
plugin.lock.json
Normal file
305
plugin.lock.json
Normal file
@@ -0,0 +1,305 @@
|
||||
{
|
||||
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||
"pluginId": "gh:basher83/lunar-claude:plugins/devops/python-tools",
|
||||
"normalized": {
|
||||
"repo": null,
|
||||
"ref": "refs/tags/v20251128.0",
|
||||
"commit": "11d9ecfb1ddf201c7123dae21e282738dc62376b",
|
||||
"treeHash": "615eb4886cac28c4065441216bbe4e12a7a578663b1676c5e74eda947033bb39",
|
||||
"generatedAt": "2025-11-28T10:14:12.499982Z",
|
||||
"toolVersion": "publish_plugins.py@0.2.0"
|
||||
},
|
||||
"origin": {
|
||||
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||
"branch": "master",
|
||||
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||
},
|
||||
"manifest": {
|
||||
"name": "python-tools",
|
||||
"description": "Comprehensive Python development toolkit covering scripting (uv), code quality (ruff/pyright), and common patterns (JSON parsing)",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"content": {
|
||||
"files": [
|
||||
{
|
||||
"path": "README.md",
|
||||
"sha256": "ba2ee420552e3a63b97c46154655d69edfdbbc1569986ccd9db50bb52cbb4c51"
|
||||
},
|
||||
{
|
||||
"path": "agents/agent-sdk-verifier.md",
|
||||
"sha256": "e6d55a9c6c550c0b4256f2d665cd401cc9c0d3dab6cf10751357eacf24cf72a7"
|
||||
},
|
||||
{
|
||||
"path": ".claude-plugin/plugin.json",
|
||||
"sha256": "a6d68d84532da2ddff8281820a26918d7c02c3307028228ac34f5d425498d89a"
|
||||
},
|
||||
{
|
||||
"path": "commands/review-sdk-app.md",
|
||||
"sha256": "2004215a97500ca6a401f731c8a9af7cc69bc5574f4b60c24ba9ee4d3f9980ab"
|
||||
},
|
||||
{
|
||||
"path": "commands/new-sdk-app.md",
|
||||
"sha256": "18dd8a44046084819ad332b5d4b19ad6ee4ef73ff7950a5516da1fd3df0eb53f"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-code-quality/SKILL.md",
|
||||
"sha256": "51cf9f60e619c0f3e0155e1a2736fd59249ac65b4b89a980dd8edc4e324eeb60"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-code-quality/tools/python_ruff_checker.py",
|
||||
"sha256": "351666e095ca5e19d644c188b3ede49544e4e5dd6c02aedb1dade66a43acc8a2"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-code-quality/tools/python_formatter.py",
|
||||
"sha256": "4c8b9753aee96880c6339af16b73e0b5aa1cbe41cb6cbfa21865f8b70b1678cb"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-code-quality/patterns/ci-cd-quality-gates.md",
|
||||
"sha256": "0b69db1ce925a26c2092c732445ffc2153ecbfd0b09fd3155bafc31050758742"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-code-quality/patterns/pre-commit-integration.md",
|
||||
"sha256": "426316b54cb2d4977e2329fdd54b392bfbef2f067eb4246a645f1d06dea48938"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-code-quality/examples/ruff-minimal.toml",
|
||||
"sha256": "5958ecc66ffcc4a7792e9453b06b72debb13a71e596ba307aa2c4df23fb749ed"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-code-quality/examples/ruff-comprehensive.toml",
|
||||
"sha256": "6fd330ca6ebd4c4bcf01d206df5540bdd41ebe0bef43276dbfb42a9c6a13f4d9"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-code-quality/examples/pyrightconfig-strict.json",
|
||||
"sha256": "66ef73d95b58b7a0b5d7c44976957ea077d3623869dcaaf37e0a738ad949c2e1"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-code-quality/examples/pyrightconfig-starter.json",
|
||||
"sha256": "f2458fd5f97f091b1386fd676835eb3f3ad3fa0c6070ba153b78f03434b85503"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-code-quality/reference/ruff-formatting-settings.md",
|
||||
"sha256": "7471e21219426e96fccdcda9d923ba04349e617ba833e70e595c835e82118c3b"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-code-quality/reference/ruff-linting-settings.md",
|
||||
"sha256": "d8836697aab74b1a3c10e2e1fdcd6ca2b5390c415781a61a5a920a8a3d8bc032"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-code-quality/reference/ruff-configuration.md",
|
||||
"sha256": "a8a8046395df4211adefe3b730074c4a934eafe45b5e024dd05a6f0eb864e69e"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-code-quality/reference/pyright-configuration.md",
|
||||
"sha256": "35d577110e9635b2930dfc29148c50e0707a492f7706dc5fdba0338dcd2da327"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/SKILL.md",
|
||||
"sha256": "90fc4cdd83cea3f267ff7b3ad669344ce4896e3eaa9ac0e5db81c7e6993710ca"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/references/slash-commands.md",
|
||||
"sha256": "e64c07ced6e78f3db87544b2b2823036e740700d7146208f04c91742d34a9863"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/references/best-practices.md",
|
||||
"sha256": "b712ad1aac0244a5edb868b50eb25ea7f735e948195eb2eadd91055e4d60ae06"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/references/agent-patterns.md",
|
||||
"sha256": "b1d2728e6d2fef03b737c0f815cb3aa886cad81b33c744e0072797a7631cafa6"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/references/hooks-guide.md",
|
||||
"sha256": "9a4aa0a3333a0ba03d4daa0502b45f9511f12b8aaf8bcbdf439bfd4ba0042f53"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/references/tool-permissions.md",
|
||||
"sha256": "8b0012bda45877ec3f39526d190fdd9b0977206da54e14f85b8f64f82065005a"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/references/subagents.md",
|
||||
"sha256": "96c25ebeb629bbc1785c9e0a6675a565255842c4f52c6a0c054a079e9a94778f"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/references/skills.md",
|
||||
"sha256": "d8e5e5f201d81ff4048bf24ec15221f896ee1b7cd039b5ad87b27f547fc0b7e3"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/references/custom-tools.md",
|
||||
"sha256": "53a59bf9d8aa5b6d306baa44ffa047fea045e7bc4b957d210a04fe32ed346b89"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/references/sessions.md",
|
||||
"sha256": "09555d26bf55e34551978c00d2841d620939bb5060f71151508fa2c6fa8a3cac"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/references/api-reference.md",
|
||||
"sha256": "1fcf83d91988e1b78447d402ca3d5a0e426dccc9cfc053cee86904723bb398db"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/references/system-prompts.md",
|
||||
"sha256": "d42b18f7582770cc67cb7523f8afc2db4470acde9192cc97d601d0bd01b919ed"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/examples/setting_sources.py",
|
||||
"sha256": "94d9c9b8856d60d91b2972ca06c3570aae00c9d45a55f0c78d586043fad79c74"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/examples/mcp_calculator.py",
|
||||
"sha256": "444df6810e8fcbb1a092601737723f7ebf6ea4de4ff5f1e6b94576ed864c4968"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/examples/hooks.py",
|
||||
"sha256": "de7d454a698d4f1e59e01ff13bfb18625e16091e3a639066e35ee5bbd7caa5d5"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/examples/quick_start.py",
|
||||
"sha256": "50671f3d23e583322815b35392740d71b33d5d8d81fb9b5646a5f8263160b973"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/examples/system_prompt.py",
|
||||
"sha256": "17cf2544af9d700694011dc585c85ca413f426476016d54ba9b539e3eee1e458"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/examples/agents.py",
|
||||
"sha256": "e6f026be8c386f86eee740d23ae27e86851d872de09b27c1fce14ccff685668e"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/examples/plugin_example.py",
|
||||
"sha256": "fc7fcbe59d2b80baf27f7c54f0cd380157fc65fe891b4ead67aa73ea0eee39a3"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/examples/basic-orchestrator.py",
|
||||
"sha256": "9377a88c9ab6f07b986e917a6f1cf1757865bcfda57840bb3752bab5dd06dd76"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/examples/tool_permission_callback.py",
|
||||
"sha256": "33355dd11bd6cce4860220d897b79da1993f280634684ffb870eb4cf0fc1d1dd"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/examples/streaming_mode.py",
|
||||
"sha256": "c0765c7665ccd4dd502691a95ff6710b61057e852eae092ab5cc6dd57d112cea"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/assets/sdk-template.py",
|
||||
"sha256": "f37fabd2726d968e9201487b99299be335adb1bf5789b11bc6db94dfd4064f98"
|
||||
},
|
||||
{
|
||||
"path": "skills/claude-agent-sdk/assets/sdk-validation-checklist.md",
|
||||
"sha256": "5fc3c81b0d5a096cd910238e38d4465723e70fc8439af624bb30602fcd9cdbac"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/SKILL.md",
|
||||
"sha256": "c6b804178b49ebecd5ab6710d6d20889b55df19c484bf24c1dc481d80c97233c"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/tools/validate_script.py",
|
||||
"sha256": "79c83d1931a7ef18e13bf732462402dc4cb25caacbcfab4daa3242d89ea46790"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/tools/convert_to_uv.py",
|
||||
"sha256": "53fa5f4a5d857260cd3090e3faaa64e9ce1af0ba1eecb7e649ce54cca591b2a1"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/patterns/api-clients.md",
|
||||
"sha256": "f85d943f2da6e501655dca8b27431d98dd11392a62a40e443403120a2f6c6742"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/patterns/data-processing.md",
|
||||
"sha256": "0ceebdf6844c45b1a6f79006f8974b308bcf052775bf4e0864c831d3b3c90656"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/patterns/cli-applications.md",
|
||||
"sha256": "5438d0f1865ecaf6fce45e0df3a05c4ce8bc3d63d313b44b3b73981b7d1a3076"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/patterns/security-patterns.md",
|
||||
"sha256": "c5a69109c85ed5fed02610fbc95618f9f625b1b00cc16c7d5e6a0c0134d54ca4"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/patterns/error-handling.md",
|
||||
"sha256": "09227d605daa70631eda9ca4e688c7a54a396054afa2d137f09c3fd921a2f0dc"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/patterns/system-automation.md",
|
||||
"sha256": "6f8c8f3da5feb9aa7b7fbc1ee9377464f9e7d5ab97ad2ddbea1fe11c12d3226f"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/anti-patterns/common-mistakes.md",
|
||||
"sha256": "7eddb4e883b7648be8b49d26271dd5f1e4fd31342c4d44ab10d164b03e2b6b86"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/anti-patterns/when-not-to-use.md",
|
||||
"sha256": "3cd6aef95e811a97aa840b1f31e7deb4051ce8f1a8d1c0b7868b8d38b6b2d4ea"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/workflows/ci-cd-integration.md",
|
||||
"sha256": "9fcfdcfa640878493e004e2325788be956409033b5b92886dcbc1a3f13cb1a96"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/workflows/team-adoption.md",
|
||||
"sha256": "c8450682922edf11cba92b379d6d638ac0678368b4026fcbf27db9c606811add"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/workflows/testing-strategies.md",
|
||||
"sha256": "1973bf32d32eab67b3b2329314e7de888559149cf6cd59db316259f3ba64aac2"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/examples/04-api-clients/netbox_client.py",
|
||||
"sha256": "a78cd68ac926d992e0a8140eb9f68511c6201caff4d8c6ddc3f83083ded5c96a"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/examples/03-production-ready/check_cluster_health_enhanced.py",
|
||||
"sha256": "24d4a993f889f346bd29f6359b23a23c238114333d8330e58faa1c806884bdb4"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/assets/templates/basic-script.py",
|
||||
"sha256": "269a78e8544fb38a0eab4d5a0cc5e6bcbd9231886fb75f47d9b27b73572db2a7"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/assets/templates/cli-app.py",
|
||||
"sha256": "8dcdf56c9de3ec508b79fcdf5bfa22a28944c67edaf784ec4854d7eb751e0927"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/assets/templates/data-processor.py",
|
||||
"sha256": "dc71ea76464971c3cafa094d4353fe84037be284144c51766d612ca8edf8b82f"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/assets/templates/api-client.py",
|
||||
"sha256": "66df4e1205a5a319f543a09c558c1d6ffa5c5c0c91f8a617476730cd7a9f62ea"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/reference/bash-to-python.md",
|
||||
"sha256": "0660847c66e2ec2f5fa5846eae5f36d48a6ddb8e815b35f15641ca9a34e38236"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/reference/dependency-management.md",
|
||||
"sha256": "ee3707b8129acfc14e198396f4a2e9b0baa353df4bb93eda9dfcbe018d61dfed"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/reference/security-patterns.md",
|
||||
"sha256": "a351428b18cc3d335e3224c83ca5362f846f4dcd0ab316cd671e60867afd23ae"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-uv-scripts/reference/pep-723-spec.md",
|
||||
"sha256": "104163f5053aaf9a965474fd04cb673f5d97eb5ea2bc7a13935213bc39f9c569"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-json-parsing/SKILL.md",
|
||||
"sha256": "5fd634250635fe88ac46e80c372b2d0627b1bf0eb17f2914f45804eb670e00b3"
|
||||
},
|
||||
{
|
||||
"path": "skills/python-json-parsing/reference/python-json-parsing-best-practices-2025.md",
|
||||
"sha256": "08b9dc521a91b4f098b51edc4e31c4efd04f12d380edbbaf2eb2233df2a14826"
|
||||
}
|
||||
],
|
||||
"dirSha256": "615eb4886cac28c4065441216bbe4e12a7a578663b1676c5e74eda947033bb39"
|
||||
},
|
||||
"security": {
|
||||
"scannedAt": null,
|
||||
"scannerVersion": null,
|
||||
"flags": []
|
||||
}
|
||||
}
|
||||
557
skills/claude-agent-sdk/SKILL.md
Normal file
557
skills/claude-agent-sdk/SKILL.md
Normal file
@@ -0,0 +1,557 @@
|
||||
---
|
||||
name: claude-agent-sdk
|
||||
description: This skill should be used when building applications with the Claude Agent SDK (Python). Use for creating orchestrators with subagents, configuring agents programmatically, setting up hooks and permissions, and following SDK best practices. Trigger when implementing agentic workflows, multi-agent systems, or SDK-based automation.
|
||||
---
|
||||
|
||||
# Claude Agent SDK
|
||||
|
||||
Build production-ready applications using the Claude Agent SDK for Python.
|
||||
|
||||
**SDK Version:** This skill targets `claude-agent-sdk>=0.1.6` (Python)
|
||||
|
||||
## Overview
|
||||
|
||||
This skill provides patterns, examples, and best practices for building SDK applications that orchestrate Claude agents.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Copy the template and customize:
|
||||
|
||||
```bash
|
||||
cp assets/sdk-template.py my-app.py
|
||||
# Edit my-app.py - customize agents and workflow
|
||||
chmod +x my-app.py
|
||||
./my-app.py
|
||||
```
|
||||
|
||||
The template includes proper uv script headers, agent definitions, and async patterns.
|
||||
|
||||
## Choosing Between query() and ClaudeSDKClient
|
||||
|
||||
The SDK provides two ways to interact with Claude: the `query()` function for simple one-shot tasks, and `ClaudeSDKClient` for continuous conversations.
|
||||
|
||||
### Quick Comparison
|
||||
|
||||
| Feature | `query()` | `ClaudeSDKClient` |
|
||||
|---------|-----------|-------------------|
|
||||
| **Conversation memory** | No - each call is independent | Yes - maintains context across queries |
|
||||
| **Use case** | One-off tasks, single questions | Multi-turn conversations, complex workflows |
|
||||
| **Complexity** | Simple - one function call | More setup - context manager pattern |
|
||||
| **Hooks support** | No | Yes |
|
||||
| **Custom tools** | No | Yes |
|
||||
| **Interrupts** | No | Yes - can interrupt ongoing operations |
|
||||
| **Session control** | New session each time | Single persistent session |
|
||||
|
||||
> **Important:** Hooks and custom tools (SDK MCP servers) are **only supported with `ClaudeSDKClient`**, not with `query()`. If you need hooks or custom tools, you must use `ClaudeSDKClient`.
|
||||
>
|
||||
> **Note on Async Runtimes:** The SDK works with both `asyncio` and `anyio`. The official SDK examples prefer `anyio.run()` for better async library compatibility, but `asyncio.run()` works equally well. Use whichever fits your project's async runtime.
|
||||
|
||||
### When to Use query()
|
||||
|
||||
Use `query()` for simple, independent tasks where you don't need conversation history:
|
||||
|
||||
```python
|
||||
import anyio # or: import asyncio
|
||||
from claude_agent_sdk import query, ClaudeAgentOptions
|
||||
|
||||
async def analyze_file():
|
||||
"""One-shot file analysis - no conversation needed."""
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt="You are a code analyzer",
|
||||
allowed_tools=["Read", "Grep", "Glob"],
|
||||
permission_mode="acceptEdits"
|
||||
)
|
||||
|
||||
async for message in query(
|
||||
prompt="Analyze /path/to/file.py for bugs",
|
||||
options=options
|
||||
):
|
||||
print(message)
|
||||
|
||||
anyio.run(analyze_file) # or: asyncio.run(analyze_file())
|
||||
```
|
||||
|
||||
**Best for:**
|
||||
|
||||
- Single analysis tasks
|
||||
- Independent file operations
|
||||
- Quick questions without follow-up
|
||||
- Scripts that run once and exit
|
||||
|
||||
**Key limitation:** Each `query()` call creates a new session with no memory of previous calls.
|
||||
|
||||
### When to Use ClaudeSDKClient
|
||||
|
||||
Use `ClaudeSDKClient` when you need conversation context across multiple interactions:
|
||||
|
||||
```python
|
||||
import anyio # or: import asyncio
|
||||
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, AssistantMessage, TextBlock
|
||||
|
||||
async def interactive_debugging():
|
||||
"""Multi-turn debugging conversation with context."""
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt="You are a debugging assistant",
|
||||
allowed_tools=["Read", "Grep", "Bash"],
|
||||
permission_mode="acceptEdits"
|
||||
)
|
||||
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
# First query
|
||||
await client.query("Find all TODO comments in /path/to/project")
|
||||
async for message in client.receive_response():
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
|
||||
# Follow-up - Claude remembers the TODOs found above
|
||||
await client.query("Now prioritize them by complexity")
|
||||
async for message in client.receive_response():
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
|
||||
# Another follow-up - still in same conversation
|
||||
await client.query("Create a plan to address the top 3")
|
||||
async for message in client.receive_response():
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
|
||||
anyio.run(interactive_debugging) # or: asyncio.run(interactive_debugging())
|
||||
```
|
||||
|
||||
**Best for:**
|
||||
|
||||
- Multi-turn conversations
|
||||
- Interactive workflows
|
||||
- Tasks requiring context from previous responses
|
||||
- Applications with interrupt capability
|
||||
- Orchestrators managing complex workflows
|
||||
|
||||
**Key advantage:** Claude remembers all previous queries and responses in the session.
|
||||
|
||||
**See:** `examples/streaming_mode.py` - Comprehensive ClaudeSDKClient examples with all patterns
|
||||
|
||||
### Advanced: Interrupts with ClaudeSDKClient
|
||||
|
||||
Only `ClaudeSDKClient` supports interrupting ongoing operations:
|
||||
|
||||
```python
|
||||
import anyio # or: import asyncio
|
||||
from claude_agent_sdk import ClaudeSDKClient
|
||||
|
||||
async def interruptible_task():
|
||||
async with ClaudeSDKClient() as client:
|
||||
await client.query("Run a long analysis on /large/codebase")
|
||||
|
||||
# Start processing in background
|
||||
async with anyio.create_task_group() as tg:
|
||||
tg.start_soon(process_messages, client)
|
||||
|
||||
# Simulate user interrupt after 5 seconds
|
||||
await anyio.sleep(5)
|
||||
await client.interrupt()
|
||||
|
||||
async def process_messages(client):
|
||||
async for message in client.receive_response():
|
||||
print(message)
|
||||
|
||||
anyio.run(interruptible_task) # or: asyncio.run(interruptible_task())
|
||||
```
|
||||
|
||||
### Quick Decision Guide
|
||||
|
||||
**Use `query()` if:**
|
||||
|
||||
- Task is self-contained
|
||||
- No follow-up questions needed
|
||||
- Each execution is independent
|
||||
- Simpler code is preferred
|
||||
|
||||
**Use `ClaudeSDKClient` if:**
|
||||
|
||||
- Need conversation memory
|
||||
- Building interactive workflows
|
||||
- Require interrupt capability
|
||||
- Managing complex multi-step processes
|
||||
- Working with orchestrators and subagents
|
||||
|
||||
## Core Patterns
|
||||
|
||||
### 1. Orchestrator with Subagents
|
||||
|
||||
Define a main orchestrator that delegates work to specialized subagents.
|
||||
|
||||
**Critical requirements:**
|
||||
|
||||
- Orchestrator must use `system_prompt={"type": "preset", "preset": "claude_code"}` (provides Task tool knowledge)
|
||||
- Register agents programmatically via `agents={}` parameter (SDK best practice)
|
||||
- Orchestrator must include `"Task"` in `allowed_tools`
|
||||
- Match agent names exactly between definition and usage
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
from claude_agent_sdk import AgentDefinition, ClaudeAgentOptions
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt={"type": "preset", "preset": "claude_code"}, # REQUIRED for orchestrators
|
||||
allowed_tools=["Bash", "Task", "Read", "Write"],
|
||||
agents={
|
||||
"analyzer": AgentDefinition(
|
||||
description="Analyzes code structure and patterns",
|
||||
prompt="You are a code analyzer...",
|
||||
tools=["Read", "Grep", "Glob"],
|
||||
model="sonnet"
|
||||
),
|
||||
"fixer": AgentDefinition(
|
||||
description="Fixes identified issues",
|
||||
prompt="You are a code fixer...",
|
||||
tools=["Read", "Edit", "Bash"],
|
||||
model="sonnet"
|
||||
)
|
||||
},
|
||||
permission_mode="acceptEdits",
|
||||
model="claude-sonnet-4-5"
|
||||
)
|
||||
```
|
||||
|
||||
**See:**
|
||||
|
||||
- `references/agent-patterns.md` - Complete agent definition patterns
|
||||
- `examples/agents.py` - Official SDK agent examples with different agent types
|
||||
|
||||
### 2. System Prompt Configuration
|
||||
|
||||
Choose the appropriate system prompt pattern:
|
||||
|
||||
```python
|
||||
# Orchestrator (use claude_code preset) - dict format (official examples prefer this)
|
||||
system_prompt={"type": "preset", "preset": "claude_code"}
|
||||
|
||||
# Shorthand format (equivalent, but less explicit)
|
||||
system_prompt="claude_code"
|
||||
|
||||
# Custom behavior
|
||||
system_prompt="You are a Python expert..."
|
||||
|
||||
# Extend preset with additional instructions
|
||||
system_prompt={
|
||||
"type": "preset",
|
||||
"preset": "claude_code",
|
||||
"append": "Additional domain-specific instructions"
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The shorthand `system_prompt="claude_code"` is equivalent to `{"type": "preset", "preset": "claude_code"}`. Both are valid. Official examples prefer the dict format for explicitness.
|
||||
|
||||
**See:**
|
||||
|
||||
- `references/system-prompts.md` - Complete system prompt documentation
|
||||
- `examples/system_prompt.py` - Official SDK system prompt examples
|
||||
|
||||
### 3. Tool Restrictions
|
||||
|
||||
Limit subagent tools to minimum needed:
|
||||
|
||||
```python
|
||||
# Read-only analyzer
|
||||
tools=["Read", "Grep", "Glob"]
|
||||
|
||||
# Code modifier
|
||||
tools=["Read", "Edit", "Bash"]
|
||||
|
||||
# Test runner
|
||||
tools=["Bash", "Read"]
|
||||
```
|
||||
|
||||
**See:** `references/agent-patterns.md` for common tool combinations
|
||||
|
||||
### 4. Hooks
|
||||
|
||||
Intercept SDK events to control behavior:
|
||||
|
||||
```python
|
||||
from claude_agent_sdk import HookMatcher
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
hooks={
|
||||
"PreToolUse": [
|
||||
HookMatcher(matcher="Bash", hooks=[check_bash_command])
|
||||
],
|
||||
"PostToolUse": [
|
||||
HookMatcher(matcher="Bash", hooks=[review_output])
|
||||
]
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**See:**
|
||||
|
||||
- `references/hooks-guide.md` - Complete hook patterns documentation
|
||||
- `examples/hooks.py` - Official SDK hook examples with all hook types
|
||||
|
||||
### 5. Permission Callbacks
|
||||
|
||||
Fine-grained control over tool usage:
|
||||
|
||||
```python
|
||||
async def permission_callback(tool_name, input_data, context):
|
||||
# Allow read operations
|
||||
if tool_name in ["Read", "Grep", "Glob"]:
|
||||
return PermissionResultAllow()
|
||||
|
||||
# Block dangerous commands
|
||||
if tool_name == "Bash" and "rm -rf" in input_data.get("command", ""):
|
||||
return PermissionResultDeny(message="Dangerous command")
|
||||
|
||||
return PermissionResultAllow()
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
can_use_tool=permission_callback,
|
||||
permission_mode="default"
|
||||
)
|
||||
```
|
||||
|
||||
**See:**
|
||||
|
||||
- `references/tool-permissions.md` - Complete permission patterns and decision guide
|
||||
- `examples/tool_permission_callback.py` - Official SDK permission callback example
|
||||
|
||||
## Workflow Templates
|
||||
|
||||
### Building an Orchestrator
|
||||
|
||||
Follow these steps to build an effective orchestrator:
|
||||
|
||||
**1. Define agent purposes**
|
||||
|
||||
- What specialized tasks need delegation?
|
||||
- What tools does each agent need?
|
||||
- What constraints should apply?
|
||||
|
||||
**2. Create agent definitions**
|
||||
|
||||
```python
|
||||
agents={
|
||||
"agent-name": AgentDefinition(
|
||||
description="When to use this agent",
|
||||
prompt="Agent's role and behavior",
|
||||
tools=["Tool1", "Tool2"],
|
||||
model="sonnet"
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**3. Configure orchestrator**
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt={"type": "preset", "preset": "claude_code"}, # CRITICAL
|
||||
allowed_tools=["Bash", "Task", "Read", "Write"],
|
||||
agents=agents,
|
||||
permission_mode="acceptEdits"
|
||||
)
|
||||
```
|
||||
|
||||
**4. Implement workflow**
|
||||
|
||||
```python
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
await client.query("Use 'agent-name' to perform task")
|
||||
|
||||
async for message in client.receive_response():
|
||||
# Process responses
|
||||
pass
|
||||
```
|
||||
|
||||
**See:** `examples/basic-orchestrator.py` for complete working example
|
||||
|
||||
### Loading Agents from Files
|
||||
|
||||
While programmatic registration is recommended, agent content can be stored in markdown files:
|
||||
|
||||
```python
|
||||
import yaml
|
||||
|
||||
def load_agent_definition(path: str) -> AgentDefinition:
|
||||
"""Load agent from markdown file with YAML frontmatter."""
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
|
||||
parts = content.split("---")
|
||||
frontmatter = yaml.safe_load(parts[1])
|
||||
prompt = parts[2].strip()
|
||||
|
||||
# Parse tools (comma-separated string or array)
|
||||
tools = frontmatter.get("tools", [])
|
||||
if isinstance(tools, str):
|
||||
tools = [t.strip() for t in tools.split(",")]
|
||||
|
||||
return AgentDefinition(
|
||||
description=frontmatter["description"],
|
||||
prompt=prompt,
|
||||
tools=tools,
|
||||
model=frontmatter.get("model", "inherit")
|
||||
)
|
||||
|
||||
# Load and register programmatically
|
||||
agent = load_agent_definition(".claude/agents/my-agent.md")
|
||||
options = ClaudeAgentOptions(agents={"my-agent": agent})
|
||||
```
|
||||
|
||||
**See:** `references/agent-patterns.md` for complete loading pattern
|
||||
|
||||
## Common Anti-Patterns
|
||||
|
||||
Avoid these common mistakes:
|
||||
|
||||
**❌ Missing orchestrator system prompt**
|
||||
|
||||
```python
|
||||
# Orchestrator won't know how to use Task tool
|
||||
options = ClaudeAgentOptions(agents={...})
|
||||
```
|
||||
|
||||
**✅ Correct orchestrator configuration**
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt="claude_code",
|
||||
agents={...}
|
||||
)
|
||||
```
|
||||
|
||||
**❌ Mismatched agent names**
|
||||
|
||||
```python
|
||||
agents={"investigator": AgentDefinition(...)}
|
||||
await client.query("Use 'markdown-investigator'...") # Wrong name
|
||||
```
|
||||
|
||||
**✅ Exact name matching**
|
||||
|
||||
```python
|
||||
agents={"investigator": AgentDefinition(...)}
|
||||
await client.query("Use 'investigator'...") # Matches
|
||||
```
|
||||
|
||||
**❌ Tool/prompt mismatch**
|
||||
|
||||
```python
|
||||
system_prompt="Fix bugs you find"
|
||||
allowed_tools=["Read", "Grep"] # Can't fix, only read
|
||||
```
|
||||
|
||||
**✅ Aligned tools and behavior**
|
||||
|
||||
```python
|
||||
system_prompt="Analyze code for bugs"
|
||||
allowed_tools=["Read", "Grep", "Glob"]
|
||||
```
|
||||
|
||||
**See:** `references/best-practices.md` for complete anti-patterns list
|
||||
|
||||
## Resources
|
||||
|
||||
### references/
|
||||
|
||||
In-depth documentation loaded as needed:
|
||||
|
||||
- `api-reference.md` - Complete Python SDK API reference (types, functions, examples)
|
||||
- `agent-patterns.md` - Agent definition patterns, tool restrictions, best practices
|
||||
- `subagents.md` - Comprehensive subagent patterns and SDK integration
|
||||
- `system-prompts.md` - System prompt configuration (preset, custom, append)
|
||||
- `hooks-guide.md` - Hook patterns for all hook types with examples
|
||||
- `tool-permissions.md` - Permission callback patterns and examples
|
||||
- `best-practices.md` - SDK best practices, anti-patterns, debugging tips
|
||||
- `custom-tools.md` - Creating custom tools with SDK MCP servers (Python-only)
|
||||
- `sessions.md` - Session management and resumption patterns (Python-only)
|
||||
- `skills.md` - Using Agent Skills with the SDK (Python-only)
|
||||
- `slash-commands.md` - Slash commands and custom command creation (Python-only)
|
||||
|
||||
### examples/
|
||||
|
||||
Ready-to-run code examples from official SDK:
|
||||
|
||||
**Getting Started:**
|
||||
|
||||
- `quick_start.py` - Basic query() usage and message handling (start here!)
|
||||
- `basic-orchestrator.py` - Complete orchestrator with analyzer and fixer subagents
|
||||
|
||||
**Core Patterns:**
|
||||
|
||||
- `agents.py` - Programmatic agent definitions with different agent types
|
||||
- `hooks.py` - Comprehensive hook patterns (PreToolUse, PostToolUse, UserPromptSubmit, etc.)
|
||||
- `system_prompt.py` - System prompt patterns (preset, custom, append)
|
||||
- `streaming_mode.py` - Complete ClaudeSDKClient patterns with multi-turn conversations
|
||||
|
||||
**Advanced Features:**
|
||||
|
||||
- `mcp_calculator.py` - Custom tools with SDK MCP server (in-process tool server)
|
||||
- `tool_permission_callback.py` - Permission callbacks with logging and control
|
||||
- `setting_sources.py` - Settings isolation and loading (user/project/local)
|
||||
- `plugin_example.py` - Using plugins with the SDK (relevant for plugin marketplace!)
|
||||
|
||||
### assets/
|
||||
|
||||
Templates and validation tools:
|
||||
|
||||
- `sdk-template.py` - Project template with uv script headers and agent structure
|
||||
- `sdk-validation-checklist.md` - Comprehensive checklist for validating SDK applications against best practices
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Creating new Claude Agent SDK applications
|
||||
- Building orchestrators with multiple subagents
|
||||
- Implementing programmatic agent definitions
|
||||
- Configuring hooks or permission callbacks
|
||||
- Validating/reviewing SDK code (use `assets/sdk-validation-checklist.md`)
|
||||
- Migrating from filesystem agent discovery to programmatic registration
|
||||
- Debugging SDK applications (agent not found, Task tool not working)
|
||||
- Following SDK best practices
|
||||
|
||||
Do not use for:
|
||||
|
||||
- Claude Code slash commands or skills (different system)
|
||||
- Direct API usage without SDK
|
||||
- Non-Python implementations (TypeScript SDK has different patterns)
|
||||
|
||||
## Next Steps
|
||||
|
||||
### For Beginners
|
||||
|
||||
1. Start with `examples/quick_start.py` - Learn basic query() usage
|
||||
2. Try `assets/sdk-template.py` - Template for new projects
|
||||
3. Review `examples/basic-orchestrator.py` - See orchestrator pattern
|
||||
|
||||
### For Intermediate Users
|
||||
|
||||
1. Explore core patterns:
|
||||
- `examples/agents.py` - Agent definitions
|
||||
- `examples/system_prompt.py` - System prompt patterns
|
||||
- `examples/streaming_mode.py` - Multi-turn conversations
|
||||
- `examples/hooks.py` - Hook patterns
|
||||
|
||||
### For Advanced Users
|
||||
|
||||
1. Study advanced features:
|
||||
- `examples/tool_permission_callback.py` - Permission control
|
||||
- `examples/mcp_calculator.py` - Custom tools
|
||||
- `examples/setting_sources.py` - Settings management
|
||||
- `examples/plugin_example.py` - Plugin integration
|
||||
|
||||
### Validation & Quality
|
||||
|
||||
1. Validate your code with `assets/sdk-validation-checklist.md`
|
||||
2. Review against best practices in `references/best-practices.md`
|
||||
|
||||
### Reference Documentation
|
||||
|
||||
1. Consult `references/` as needed for detailed patterns
|
||||
96
skills/claude-agent-sdk/assets/sdk-template.py
Executable file
96
skills/claude-agent-sdk/assets/sdk-template.py
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env -S uv run --script --quiet
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "claude-agent-sdk>=0.1.6",
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
Claude Agent SDK Project Template
|
||||
|
||||
This template provides a starting point for building SDK applications.
|
||||
|
||||
Usage:
|
||||
1. Copy this file to your project
|
||||
2. Customize the agent definitions
|
||||
3. Update the prompt and workflow
|
||||
4. Run: ./your-script.py
|
||||
|
||||
Note: Uses anyio for async runtime (official SDK examples use anyio).
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import anyio # Official SDK examples use anyio
|
||||
from claude_agent_sdk import (
|
||||
AgentDefinition,
|
||||
AssistantMessage,
|
||||
ClaudeAgentOptions,
|
||||
ClaudeSDKClient,
|
||||
ResultMessage,
|
||||
TextBlock,
|
||||
)
|
||||
|
||||
|
||||
def get_sdk_options() -> ClaudeAgentOptions:
|
||||
"""Configure SDK options with agents."""
|
||||
return ClaudeAgentOptions(
|
||||
# Use claude_code preset for orchestrators
|
||||
system_prompt={"type": "preset", "preset": "claude_code"},
|
||||
# Allow orchestrator to use Task tool for delegation
|
||||
allowed_tools=["Bash", "Task", "Read", "Write"],
|
||||
# Permission mode: "default", "acceptEdits", or "rejectEdits"
|
||||
permission_mode="acceptEdits",
|
||||
# Define subagents programmatically
|
||||
agents={
|
||||
"example-agent": AgentDefinition(
|
||||
description="Replace with agent purpose/when to use",
|
||||
prompt="Replace with agent's system prompt and instructions",
|
||||
tools=["Read", "Grep"], # Limit to needed tools
|
||||
model="sonnet", # or "opus", "haiku", "inherit"
|
||||
),
|
||||
},
|
||||
model="claude-sonnet-4-5",
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main workflow."""
|
||||
# Verify API key is set
|
||||
if not os.getenv("ANTHROPIC_API_KEY"):
|
||||
print("Error: ANTHROPIC_API_KEY environment variable not set")
|
||||
return
|
||||
|
||||
print("🚀 Starting SDK Application")
|
||||
print("=" * 60)
|
||||
|
||||
# Get SDK configuration
|
||||
options = get_sdk_options()
|
||||
|
||||
# Create client and send query
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
# Replace with your actual prompt
|
||||
prompt = "Your task description here"
|
||||
|
||||
print(f"\n📨 Query: {prompt}\n")
|
||||
await client.query(prompt)
|
||||
|
||||
# Stream responses
|
||||
async for message in client.receive_response():
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}\n")
|
||||
|
||||
elif isinstance(message, ResultMessage):
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ Complete")
|
||||
if message.duration_ms:
|
||||
print(f"Duration: {message.duration_ms}ms")
|
||||
if message.total_cost_usd:
|
||||
print(f"Cost: ${message.total_cost_usd:.4f}")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
anyio.run(main) # Official SDK examples use anyio.run()
|
||||
362
skills/claude-agent-sdk/assets/sdk-validation-checklist.md
Normal file
362
skills/claude-agent-sdk/assets/sdk-validation-checklist.md
Normal file
@@ -0,0 +1,362 @@
|
||||
# Claude Agent SDK Validation Checklist
|
||||
|
||||
This checklist helps validate SDK applications against official patterns and best practices from the claude-agent-sdk skill documentation.
|
||||
|
||||
## Quick Validation
|
||||
|
||||
Use this checklist when:
|
||||
|
||||
- Creating new SDK applications
|
||||
- Reviewing SDK code
|
||||
- Debugging SDK issues
|
||||
- Ensuring alignment with best practices
|
||||
|
||||
---
|
||||
|
||||
## 1. Imports & Dependencies
|
||||
|
||||
### Required Checks
|
||||
|
||||
- [ ] **Async runtime import**
|
||||
- Uses `import anyio` (official SDK examples use anyio)
|
||||
- Comment reflects official preference: `# Official SDK examples use anyio`
|
||||
- **Reference:** Official examples consistently use anyio
|
||||
|
||||
- [ ] **Claude SDK imports are accurate**
|
||||
- `ClaudeAgentOptions` for configuration
|
||||
- `ClaudeSDKClient` for continuous conversations OR `query` for one-shot tasks
|
||||
- `AgentDefinition` if using programmatic agents
|
||||
- Message types: `AssistantMessage`, `ResultMessage`, `TextBlock`
|
||||
- Permission types if using callbacks: `PermissionResultAllow`, `PermissionResultDeny`
|
||||
- **Reference:** `references/api-reference.md`
|
||||
|
||||
- [ ] **UV script headers (if applicable)**
|
||||
- Uses `#!/usr/bin/env -S uv run --script --quiet`
|
||||
- Has PEP 723 dependencies block with `claude-agent-sdk>=0.1.6`
|
||||
- **Reference:** `assets/sdk-template.py` lines 1-7
|
||||
|
||||
---
|
||||
|
||||
## 2. Async Runtime
|
||||
|
||||
### Required Checks
|
||||
|
||||
- [ ] **Runtime execution is correct**
|
||||
- Uses `anyio.run(main)` (official SDK examples use anyio.run())
|
||||
- Comment reflects official preference: `# Official SDK examples use anyio.run()`
|
||||
- **Reference:** Official examples consistently use anyio.run()
|
||||
|
||||
- [ ] **Async/await patterns are correct**
|
||||
- Functions marked as `async def`
|
||||
- Uses `await` for SDK calls
|
||||
- Uses `async for` for message streaming
|
||||
- Uses `async with` for ClaudeSDKClient context manager
|
||||
- **Reference:** `references/best-practices.md` lines 82-94
|
||||
|
||||
---
|
||||
|
||||
## 3. Choosing query() vs ClaudeSDKClient
|
||||
|
||||
### Required Checks
|
||||
|
||||
- [ ] **Correct approach for use case**
|
||||
- `query()`: One-shot tasks, no conversation memory
|
||||
- `ClaudeSDKClient`: Multi-turn conversations, context retention
|
||||
- **Reference:** SKILL.md lines 29-44
|
||||
|
||||
- [ ] **Hooks/Custom tools only with ClaudeSDKClient**
|
||||
- NOT using hooks with `query()` (not supported)
|
||||
- NOT using custom tools with `query()` (not supported)
|
||||
- **Reference:** SKILL.md line 45 (important warning)
|
||||
|
||||
---
|
||||
|
||||
## 4. Orchestrator Configuration
|
||||
|
||||
### Required Checks
|
||||
|
||||
- [ ] **System prompt is set correctly**
|
||||
- Uses `system_prompt={"type": "preset", "preset": "claude_code"}` for orchestrators
|
||||
(official examples use dict format)
|
||||
- OR uses `system_prompt="claude_code"` (string shorthand, equivalent but less explicit)
|
||||
- Custom prompts only for non-orchestrators
|
||||
- **Reference:** Official examples use dict format, SKILL.md lines 226-242,
|
||||
`references/system-prompts.md`
|
||||
|
||||
- [ ] **Task tool is included**
|
||||
- `allowed_tools` includes `"Task"` for orchestrators
|
||||
- Orchestrators cannot delegate without Task tool
|
||||
- **Reference:** SKILL.md line 39, `references/best-practices.md` lines 72-80
|
||||
|
||||
- [ ] **Agent definitions are programmatic**
|
||||
- Agents defined in `agents={}` parameter (preferred)
|
||||
- Clear `description` (when to use agent)
|
||||
- Specific `prompt` (agent instructions)
|
||||
- Minimal `tools` list (principle of least privilege)
|
||||
- **Reference:** SKILL.md lines 195-217, `references/agent-patterns.md`
|
||||
|
||||
---
|
||||
|
||||
## 5. Agent Definitions
|
||||
|
||||
### Required Checks
|
||||
|
||||
- [ ] **Agent definition structure is correct**
|
||||
|
||||
```python
|
||||
AgentDefinition(
|
||||
description="...", # When to use this agent
|
||||
prompt="...", # Agent instructions
|
||||
tools=[...], # Minimal tool set
|
||||
model="sonnet" # or "opus", "haiku", "inherit"
|
||||
)
|
||||
```
|
||||
|
||||
- **Reference:** SKILL.md lines 195-217
|
||||
|
||||
- [ ] **Agent names match references**
|
||||
- Names in `agents={}` match Task tool usage
|
||||
- No naming mismatches between definition and invocation
|
||||
- **Reference:** `references/best-practices.md` lines 43-52
|
||||
|
||||
- [ ] **Tools are restricted to minimum needed**
|
||||
- Read-only agents: `["Read", "Grep", "Glob"]`
|
||||
- Code modifiers: `["Read", "Edit", "Bash"]`
|
||||
- No excessive tool permissions
|
||||
- **Reference:** SKILL.md lines 248-256, `references/best-practices.md` lines 54-71
|
||||
|
||||
---
|
||||
|
||||
## 6. Permission Control
|
||||
|
||||
### Required Checks
|
||||
|
||||
- [ ] **Permission strategy is appropriate**
|
||||
- Simple use case → `permission_mode` only
|
||||
- Complex logic → `can_use_tool` callback
|
||||
- **Reference:** `references/tool-permissions.md` lines 13-114
|
||||
|
||||
- [ ] **Permission mode is valid**
|
||||
- One of: `"acceptEdits"`, `"rejectEdits"`, `"plan"`, `"bypassPermissions"`, `"default"`
|
||||
- Appropriate for use case (e.g., CI/CD uses `"acceptEdits"`)
|
||||
- **Reference:** `references/tool-permissions.md` lines 64-70
|
||||
|
||||
- [ ] **Permission callback (if used) is correct**
|
||||
- Signature: `async def(tool_name, input_data, context) -> PermissionResultAllow | PermissionResultDeny`
|
||||
- Returns early for unmatched tools
|
||||
- Uses `.get()` for safe input_data access
|
||||
- Clear denial messages
|
||||
- **Reference:** `references/tool-permissions.md` lines 120-344, `examples/tool_permission_callback.py`
|
||||
|
||||
---
|
||||
|
||||
## 7. Hooks (if used)
|
||||
|
||||
### Required Checks
|
||||
|
||||
- [ ] **Hooks ONLY used with ClaudeSDKClient**
|
||||
- NOT using hooks with `query()` function
|
||||
- **Reference:** SKILL.md line 45 (critical warning)
|
||||
|
||||
- [ ] **Hook types are supported**
|
||||
- Using ONLY: `PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `Stop`, `SubagentStop`, `PreCompact`
|
||||
- NOT using unsupported: `SessionStart`, `SessionEnd`, `Notification`
|
||||
- **Reference:** `references/hooks-guide.md` line 14 (important warning)
|
||||
|
||||
- [ ] **Hook signature is correct**
|
||||
- `async def(input_data, tool_use_id, context) -> HookJSONOutput`
|
||||
- Returns empty `{}` when hook doesn't apply
|
||||
- Uses `HookMatcher` for tool filtering
|
||||
- **Reference:** `references/hooks-guide.md` lines 46-68, `examples/hooks.py`
|
||||
|
||||
- [ ] **Hook output structure is valid**
|
||||
- Includes `hookEventName` in `hookSpecificOutput`
|
||||
- PreToolUse: includes `permissionDecision` ("allow" or "deny")
|
||||
- Includes clear `reason` and `systemMessage` fields
|
||||
- **Reference:** `references/hooks-guide.md` lines 70-144
|
||||
|
||||
---
|
||||
|
||||
## 8. ClaudeSDKClient Usage
|
||||
|
||||
### Required Checks
|
||||
|
||||
- [ ] **Context manager pattern is used**
|
||||
|
||||
```python
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
await client.query(...)
|
||||
async for message in client.receive_response():
|
||||
...
|
||||
```
|
||||
|
||||
- **Reference:** SKILL.md lines 88-124
|
||||
|
||||
- [ ] **Query → receive_response flow**
|
||||
- Calls `await client.query(prompt)` first
|
||||
- Then iterates `async for message in client.receive_response()`
|
||||
- Does NOT interleave queries and receives incorrectly
|
||||
- **Reference:** `examples/streaming_mode.py`
|
||||
|
||||
- [ ] **Interrupts (if used) are correct**
|
||||
- Uses `await client.interrupt()` to stop execution
|
||||
- Only available with ClaudeSDKClient (not query())
|
||||
- **Reference:** SKILL.md lines 139-162
|
||||
|
||||
---
|
||||
|
||||
## 9. Message Handling
|
||||
|
||||
### Required Checks
|
||||
|
||||
- [ ] **Message types are checked correctly**
|
||||
|
||||
```python
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(block.text)
|
||||
elif isinstance(message, ResultMessage):
|
||||
# Handle completion
|
||||
```
|
||||
|
||||
- **Reference:** SKILL.md lines 77-91, `examples/streaming_mode.py`
|
||||
|
||||
- [ ] **TextBlock extraction is correct**
|
||||
- Iterates through `message.content`
|
||||
- Checks `isinstance(block, TextBlock)` before accessing `.text`
|
||||
- **Reference:** `references/best-practices.md` lines 95-113
|
||||
|
||||
- [ ] **ResultMessage handling**
|
||||
- Checks for `message.duration_ms`, `message.total_cost_usd`
|
||||
- Uses optional access (fields may be None)
|
||||
- **Reference:** `assets/sdk-template.py` lines 86-93
|
||||
|
||||
---
|
||||
|
||||
## 10. Error Handling
|
||||
|
||||
### Required Checks
|
||||
|
||||
- [ ] **API key validation**
|
||||
- Checks `os.getenv("ANTHROPIC_API_KEY")` before SDK calls
|
||||
- Provides clear error message if missing
|
||||
- **Reference:** `assets/sdk-template.py` lines 58-63
|
||||
|
||||
- [ ] **Safe dictionary access**
|
||||
- Uses `.get()` for input_data, tool_response fields
|
||||
- Handles missing/None values gracefully
|
||||
- **Reference:** `references/tool-permissions.md` lines 297-344
|
||||
|
||||
- [ ] **Async exception handling**
|
||||
- Try/except blocks for critical sections
|
||||
- Proper cleanup in exception cases
|
||||
- **Reference:** `references/best-practices.md`
|
||||
|
||||
---
|
||||
|
||||
## 11. Settings & Configuration
|
||||
|
||||
### Required Checks
|
||||
|
||||
- [ ] **setting_sources is configured (if needed)**
|
||||
- Default behavior: NO settings loaded (isolated environment)
|
||||
- Explicitly set to load: `["user"]`, `["project"]`, `["local"]`, or combinations like `["user", "project"]`
|
||||
- Understands isolation vs loading tradeoff
|
||||
- **Reference:** `examples/setting_sources.py` (official example shows user, project, local options)
|
||||
|
||||
- [ ] **Model selection is appropriate**
|
||||
- Orchestrator: `"claude-sonnet-4-5"` (simplified, official examples prefer this)
|
||||
or `"claude-sonnet-4-5-20250929"` (dated version)
|
||||
- Subagents: `"sonnet"`, `"opus"`, `"haiku"`, or `"inherit"`
|
||||
- **Reference:** Official examples use `claude-sonnet-4-5`, SKILL.md line 51,
|
||||
`references/agent-patterns.md`
|
||||
|
||||
- [ ] **Budget limits (if needed)**
|
||||
- Uses `max_budget_usd` for cost control
|
||||
- Appropriate for CI/CD and automated workflows
|
||||
- **Reference:** `examples/max_budget_usd.py`
|
||||
|
||||
---
|
||||
|
||||
## 12. Best Practices Compliance
|
||||
|
||||
### Required Checks
|
||||
|
||||
- [ ] **Follows DRY principle**
|
||||
- Options extracted to function (e.g., `get_sdk_options()`)
|
||||
- Reusable patterns not duplicated
|
||||
- **Reference:** `assets/sdk-template.py` lines 33-55
|
||||
|
||||
- [ ] **Clear comments and documentation**
|
||||
- Docstrings for functions
|
||||
- Inline comments for complex logic
|
||||
- Usage notes in module docstring
|
||||
- **Reference:** `assets/sdk-template.py` lines 8-17
|
||||
|
||||
- [ ] **Type hints are used**
|
||||
- Function return types specified
|
||||
- Parameter types for clarity
|
||||
- **Reference:** `assets/sdk-template.py` line 36
|
||||
|
||||
- [ ] **No anti-patterns**
|
||||
- Not using agents for simple tasks (use query() instead)
|
||||
- Not giving excessive tool permissions
|
||||
- Not bypassing permissions without reason
|
||||
- **Reference:** `references/best-practices.md`, skill SKILL.md
|
||||
|
||||
---
|
||||
|
||||
## Validation Summary Template
|
||||
|
||||
After reviewing, fill out this summary:
|
||||
|
||||
### ✅ Passed Checks
|
||||
|
||||
- [ ] Imports & Dependencies
|
||||
- [ ] Async Runtime
|
||||
- [ ] Orchestrator Configuration
|
||||
- [ ] Agent Definitions
|
||||
- [ ] Permission Control
|
||||
- [ ] Message Handling
|
||||
- [ ] Best Practices
|
||||
|
||||
### ❌ Issues Found
|
||||
|
||||
- Issue 1: [Description]
|
||||
- Issue 2: [Description]
|
||||
|
||||
### 🔧 Fixes Required
|
||||
|
||||
1. [Specific fix with line reference]
|
||||
2. [Specific fix with line reference]
|
||||
|
||||
### 📊 Overall Assessment
|
||||
|
||||
- **Accuracy:** [%]
|
||||
- **Alignment with docs:** [High/Medium/Low]
|
||||
- **Production ready:** [Yes/No]
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Links
|
||||
|
||||
**Core Documentation:**
|
||||
|
||||
- Main skill: `SKILL.md`
|
||||
- API reference: `references/api-reference.md`
|
||||
- Best practices: `references/best-practices.md`
|
||||
|
||||
**Pattern Guides:**
|
||||
|
||||
- Agent patterns: `references/agent-patterns.md`
|
||||
- Hooks: `references/hooks-guide.md`
|
||||
- Permissions: `references/tool-permissions.md`
|
||||
- System prompts: `references/system-prompts.md`
|
||||
- Subagents: `references/subagents.md`
|
||||
|
||||
**Examples:**
|
||||
|
||||
- Quick start: `examples/quick_start.py`
|
||||
- Template: `assets/sdk-template.py`
|
||||
- Complete examples: `examples/*.py`
|
||||
124
skills/claude-agent-sdk/examples/agents.py
Executable file
124
skills/claude-agent-sdk/examples/agents.py
Executable file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example of using custom agents with Claude Code SDK.
|
||||
|
||||
This example demonstrates how to define and use custom agents with specific
|
||||
tools, prompts, and models.
|
||||
|
||||
Usage:
|
||||
./examples/agents.py - Run the example
|
||||
"""
|
||||
|
||||
import anyio
|
||||
|
||||
from claude_agent_sdk import (
|
||||
AgentDefinition,
|
||||
AssistantMessage,
|
||||
ClaudeAgentOptions,
|
||||
ResultMessage,
|
||||
TextBlock,
|
||||
query,
|
||||
)
|
||||
|
||||
|
||||
async def code_reviewer_example():
|
||||
"""Example using a custom code reviewer agent."""
|
||||
print("=== Code Reviewer Agent Example ===")
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
agents={
|
||||
"code-reviewer": AgentDefinition(
|
||||
description="Reviews code for best practices and potential issues",
|
||||
prompt="You are a code reviewer. Analyze code for bugs, performance issues, "
|
||||
"security vulnerabilities, and adherence to best practices. "
|
||||
"Provide constructive feedback.",
|
||||
tools=["Read", "Grep"],
|
||||
model="sonnet",
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
async for message in query(
|
||||
prompt="Use the code-reviewer agent to review the code in src/claude_agent_sdk/types.py",
|
||||
options=options,
|
||||
):
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
elif isinstance(message, ResultMessage) and message.total_cost_usd and message.total_cost_usd > 0:
|
||||
print(f"\nCost: ${message.total_cost_usd:.4f}")
|
||||
print()
|
||||
|
||||
|
||||
async def documentation_writer_example():
|
||||
"""Example using a documentation writer agent."""
|
||||
print("=== Documentation Writer Agent Example ===")
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
agents={
|
||||
"doc-writer": AgentDefinition(
|
||||
description="Writes comprehensive documentation",
|
||||
prompt="You are a technical documentation expert. Write clear, comprehensive "
|
||||
"documentation with examples. Focus on clarity and completeness.",
|
||||
tools=["Read", "Write", "Edit"],
|
||||
model="sonnet",
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
async for message in query(
|
||||
prompt="Use the doc-writer agent to explain what AgentDefinition is used for",
|
||||
options=options,
|
||||
):
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
elif isinstance(message, ResultMessage) and message.total_cost_usd and message.total_cost_usd > 0:
|
||||
print(f"\nCost: ${message.total_cost_usd:.4f}")
|
||||
print()
|
||||
|
||||
|
||||
async def multiple_agents_example():
|
||||
"""Example with multiple custom agents."""
|
||||
print("=== Multiple Agents Example ===")
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
agents={
|
||||
"analyzer": AgentDefinition(
|
||||
description="Analyzes code structure and patterns",
|
||||
prompt="You are a code analyzer. Examine code structure, patterns, and architecture.",
|
||||
tools=["Read", "Grep", "Glob"],
|
||||
),
|
||||
"tester": AgentDefinition(
|
||||
description="Creates and runs tests",
|
||||
prompt="You are a testing expert. Write comprehensive tests and ensure code quality.",
|
||||
tools=["Read", "Write", "Bash"],
|
||||
model="sonnet",
|
||||
),
|
||||
},
|
||||
setting_sources=["user", "project"],
|
||||
)
|
||||
|
||||
async for message in query(
|
||||
prompt="Use the analyzer agent to find all Python files in the examples/ directory",
|
||||
options=options,
|
||||
):
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
elif isinstance(message, ResultMessage) and message.total_cost_usd and message.total_cost_usd > 0:
|
||||
print(f"\nCost: ${message.total_cost_usd:.4f}")
|
||||
print()
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run all agent examples."""
|
||||
await code_reviewer_example()
|
||||
await documentation_writer_example()
|
||||
await multiple_agents_example()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
anyio.run(main)
|
||||
128
skills/claude-agent-sdk/examples/basic-orchestrator.py
Executable file
128
skills/claude-agent-sdk/examples/basic-orchestrator.py
Executable file
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env -S uv run --script --quiet
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "claude-agent-sdk>=0.1.6",
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
Basic Orchestrator Pattern with Subagents
|
||||
|
||||
This example demonstrates the recommended pattern for building an
|
||||
orchestrator that delegates work to specialized subagents.
|
||||
|
||||
Key patterns:
|
||||
- Programmatic agent registration
|
||||
- Orchestrator with claude_code system prompt
|
||||
- Subagents with restricted tools
|
||||
- Async/await streaming
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import anyio
|
||||
|
||||
from claude_agent_sdk import (
|
||||
AgentDefinition,
|
||||
AssistantMessage,
|
||||
ClaudeAgentOptions,
|
||||
ClaudeSDKClient,
|
||||
ResultMessage,
|
||||
TextBlock,
|
||||
)
|
||||
|
||||
|
||||
def get_sdk_options() -> ClaudeAgentOptions:
|
||||
"""
|
||||
Create ClaudeAgentOptions with programmatically defined subagents.
|
||||
|
||||
Returns:
|
||||
Configured options for ClaudeSDKClient
|
||||
"""
|
||||
return ClaudeAgentOptions(
|
||||
# CRITICAL: Orchestrator needs claude_code preset to use Task tool
|
||||
system_prompt={"type": "preset", "preset": "claude_code"},
|
||||
# Orchestrator tools - include Task for delegating to subagents
|
||||
allowed_tools=["Bash", "Task", "Read", "Write", "Edit"],
|
||||
# Auto-accept file edits for automated workflow
|
||||
permission_mode="acceptEdits",
|
||||
# Programmatically register subagents (SDK best practice)
|
||||
agents={
|
||||
"analyzer": AgentDefinition(
|
||||
description="Analyzes code structure and identifies patterns",
|
||||
prompt="""You are a code analyzer.
|
||||
|
||||
Your role:
|
||||
- Examine code structure and architecture
|
||||
- Identify patterns and anti-patterns
|
||||
- Suggest improvements
|
||||
|
||||
Use Read, Grep, and Glob to explore the codebase.
|
||||
Return analysis in structured format.""",
|
||||
tools=["Read", "Grep", "Glob"],
|
||||
model="sonnet",
|
||||
),
|
||||
"fixer": AgentDefinition(
|
||||
description="Fixes identified code issues",
|
||||
prompt="""You are a code fixer.
|
||||
|
||||
Your role:
|
||||
- Apply fixes based on analysis
|
||||
- Follow project conventions
|
||||
- Test changes after applying
|
||||
|
||||
Use Read, Edit, and Bash to implement and verify fixes.""",
|
||||
tools=["Read", "Edit", "Bash"],
|
||||
model="sonnet",
|
||||
),
|
||||
},
|
||||
model="claude-sonnet-4-5",
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run orchestrator workflow."""
|
||||
if not os.getenv("ANTHROPIC_API_KEY"):
|
||||
print("Error: ANTHROPIC_API_KEY environment variable not set")
|
||||
return
|
||||
|
||||
print("🚀 Starting Basic Orchestrator")
|
||||
print("=" * 60)
|
||||
|
||||
options = get_sdk_options()
|
||||
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
# Orchestrator delegates to analyzer subagent
|
||||
prompt = """Use the 'analyzer' subagent to examine the code in this directory.
|
||||
|
||||
The analyzer should:
|
||||
1. Find all Python files
|
||||
2. Identify code patterns
|
||||
3. Return a structured analysis report
|
||||
|
||||
Wait for the analyzer to complete its work."""
|
||||
|
||||
print("\n📨 Sending query to orchestrator...")
|
||||
await client.query(prompt)
|
||||
|
||||
print("\n💬 Receiving response...\n")
|
||||
|
||||
async for message in client.receive_response():
|
||||
if isinstance(message, AssistantMessage):
|
||||
# Print Claude's responses
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}\n")
|
||||
|
||||
elif isinstance(message, ResultMessage):
|
||||
# Print execution summary
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ Workflow Complete")
|
||||
print(f"Duration: {message.duration_ms}ms")
|
||||
if message.total_cost_usd:
|
||||
print(f"Cost: ${message.total_cost_usd:.4f}")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
anyio.run(main)
|
||||
350
skills/claude-agent-sdk/examples/hooks.py
Executable file
350
skills/claude-agent-sdk/examples/hooks.py
Executable file
@@ -0,0 +1,350 @@
|
||||
#!/usr/bin/env python
|
||||
"""Example of using hooks with Claude Code SDK via ClaudeAgentOptions.
|
||||
|
||||
This file demonstrates various hook patterns using the hooks parameter
|
||||
in ClaudeAgentOptions instead of decorator-based hooks.
|
||||
|
||||
Usage:
|
||||
./examples/hooks.py - List the examples
|
||||
./examples/hooks.py all - Run all examples
|
||||
./examples/hooks.py PreToolUse - Run a specific example
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
||||
from claude_agent_sdk.types import (
|
||||
AssistantMessage,
|
||||
HookContext,
|
||||
HookInput,
|
||||
HookJSONOutput,
|
||||
HookMatcher,
|
||||
Message,
|
||||
ResultMessage,
|
||||
TextBlock,
|
||||
)
|
||||
|
||||
# Set up logging to see what's happening
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def display_message(msg: Message) -> None:
|
||||
"""Standardized message display function."""
|
||||
if isinstance(msg, AssistantMessage):
|
||||
for block in msg.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
elif isinstance(msg, ResultMessage):
|
||||
print("Result ended")
|
||||
|
||||
|
||||
##### Hook callback functions
|
||||
async def check_bash_command(
|
||||
input_data: HookInput, tool_use_id: str | None, context: HookContext
|
||||
) -> HookJSONOutput:
|
||||
"""Prevent certain bash commands from being executed."""
|
||||
tool_name = input_data["tool_name"]
|
||||
tool_input = input_data["tool_input"]
|
||||
|
||||
if tool_name != "Bash":
|
||||
return {}
|
||||
|
||||
command = tool_input.get("command", "")
|
||||
block_patterns = ["foo.sh"]
|
||||
|
||||
for pattern in block_patterns:
|
||||
if pattern in command:
|
||||
logger.warning(f"Blocked command: {command}")
|
||||
return {
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": f"Command contains invalid pattern: {pattern}",
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
async def add_custom_instructions(
|
||||
input_data: HookInput, tool_use_id: str | None, context: HookContext
|
||||
) -> HookJSONOutput:
|
||||
"""Add custom instructions when a session starts."""
|
||||
return {
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": "My favorite color is hot pink",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async def review_tool_output(
|
||||
input_data: HookInput, tool_use_id: str | None, context: HookContext
|
||||
) -> HookJSONOutput:
|
||||
"""Review tool output and provide additional context or warnings."""
|
||||
tool_response = input_data.get("tool_response", "")
|
||||
|
||||
# If the tool produced an error, add helpful context
|
||||
if "error" in str(tool_response).lower():
|
||||
return {
|
||||
"systemMessage": "⚠️ The command produced an error",
|
||||
"reason": "Tool execution failed - consider checking the command syntax",
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PostToolUse",
|
||||
"additionalContext": "The command encountered an error. You may want to try a different approach.",
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
async def strict_approval_hook(
|
||||
input_data: HookInput, tool_use_id: str | None, context: HookContext
|
||||
) -> HookJSONOutput:
|
||||
"""Demonstrates using permissionDecision to control tool execution."""
|
||||
tool_name = input_data.get("tool_name")
|
||||
tool_input = input_data.get("tool_input", {})
|
||||
|
||||
# Block any Write operations to specific files
|
||||
if tool_name == "Write":
|
||||
file_path = tool_input.get("file_path", "")
|
||||
if "important" in file_path.lower():
|
||||
logger.warning(f"Blocked Write to: {file_path}")
|
||||
return {
|
||||
"reason": "Writes to files containing 'important' in the name are not allowed for safety",
|
||||
"systemMessage": "🚫 Write operation blocked by security policy",
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": "Security policy blocks writes to important files",
|
||||
},
|
||||
}
|
||||
|
||||
# Allow everything else explicitly
|
||||
return {
|
||||
"reason": "Tool use approved after security review",
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "allow",
|
||||
"permissionDecisionReason": "Tool passed security checks",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def stop_on_error_hook(
|
||||
input_data: HookInput, tool_use_id: str | None, context: HookContext
|
||||
) -> HookJSONOutput:
|
||||
"""Demonstrates using continue=False to stop execution on certain conditions."""
|
||||
tool_response = input_data.get("tool_response", "")
|
||||
|
||||
# Stop execution if we see a critical error
|
||||
if "critical" in str(tool_response).lower():
|
||||
logger.error("Critical error detected - stopping execution")
|
||||
return {
|
||||
"continue_": False,
|
||||
"stopReason": "Critical error detected in tool output - execution halted for safety",
|
||||
"systemMessage": "🛑 Execution stopped due to critical error",
|
||||
}
|
||||
|
||||
return {"continue_": True}
|
||||
|
||||
|
||||
async def example_pretooluse() -> None:
|
||||
"""Basic example demonstrating hook protection."""
|
||||
print("=== PreToolUse Example ===")
|
||||
print("This example demonstrates how PreToolUse can block some bash commands but not others.\n")
|
||||
|
||||
# Configure hooks using ClaudeAgentOptions
|
||||
options = ClaudeAgentOptions(
|
||||
allowed_tools=["Bash"],
|
||||
hooks={
|
||||
"PreToolUse": [
|
||||
HookMatcher(matcher="Bash", hooks=[check_bash_command]),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
# Test 1: Command with forbidden pattern (will be blocked)
|
||||
print("Test 1: Trying a command that our PreToolUse hook should block...")
|
||||
print("User: Run the bash command: ./foo.sh --help")
|
||||
await client.query("Run the bash command: ./foo.sh --help")
|
||||
|
||||
async for msg in client.receive_response():
|
||||
display_message(msg)
|
||||
|
||||
print("\n" + "=" * 50 + "\n")
|
||||
|
||||
# Test 2: Safe command that should work
|
||||
print("Test 2: Trying a command that our PreToolUse hook should allow...")
|
||||
print("User: Run the bash command: echo 'Hello from hooks example!'")
|
||||
await client.query("Run the bash command: echo 'Hello from hooks example!'")
|
||||
|
||||
async for msg in client.receive_response():
|
||||
display_message(msg)
|
||||
|
||||
print("\n" + "=" * 50 + "\n")
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
async def example_userpromptsubmit() -> None:
|
||||
"""Demonstrate context retention across conversation."""
|
||||
print("=== UserPromptSubmit Example ===")
|
||||
print("This example shows how a UserPromptSubmit hook can add context.\n")
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
hooks={
|
||||
"UserPromptSubmit": [
|
||||
HookMatcher(matcher=None, hooks=[add_custom_instructions]),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
print("User: What's my favorite color?")
|
||||
await client.query("What's my favorite color?")
|
||||
|
||||
async for msg in client.receive_response():
|
||||
display_message(msg)
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
async def example_posttooluse() -> None:
|
||||
"""Demonstrate PostToolUse hook with reason and systemMessage fields."""
|
||||
print("=== PostToolUse Example ===")
|
||||
print("This example shows how PostToolUse can provide feedback with reason and systemMessage.\n")
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
allowed_tools=["Bash"],
|
||||
hooks={
|
||||
"PostToolUse": [
|
||||
HookMatcher(matcher="Bash", hooks=[review_tool_output]),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
print("User: Run a command that will produce an error: ls /nonexistent_directory")
|
||||
await client.query("Run this command: ls /nonexistent_directory")
|
||||
|
||||
async for msg in client.receive_response():
|
||||
display_message(msg)
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
async def example_decision_fields() -> None:
|
||||
"""Demonstrate permissionDecision, reason, and systemMessage fields."""
|
||||
print("=== Permission Decision Example ===")
|
||||
print("This example shows how to use permissionDecision='allow'/'deny' with reason and systemMessage.\n")
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
allowed_tools=["Write", "Bash"],
|
||||
model="claude-sonnet-4-5-20250929",
|
||||
hooks={
|
||||
"PreToolUse": [
|
||||
HookMatcher(matcher="Write", hooks=[strict_approval_hook]),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
# Test 1: Try to write to a file with "important" in the name (should be blocked)
|
||||
print("Test 1: Trying to write to important_config.txt (should be blocked)...")
|
||||
print("User: Write 'test' to important_config.txt")
|
||||
await client.query("Write the text 'test data' to a file called important_config.txt")
|
||||
|
||||
async for msg in client.receive_response():
|
||||
display_message(msg)
|
||||
|
||||
print("\n" + "=" * 50 + "\n")
|
||||
|
||||
# Test 2: Write to a regular file (should be approved)
|
||||
print("Test 2: Trying to write to regular_file.txt (should be approved)...")
|
||||
print("User: Write 'test' to regular_file.txt")
|
||||
await client.query("Write the text 'test data' to a file called regular_file.txt")
|
||||
|
||||
async for msg in client.receive_response():
|
||||
display_message(msg)
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
async def example_continue_control() -> None:
|
||||
"""Demonstrate continue and stopReason fields for execution control."""
|
||||
print("=== Continue/Stop Control Example ===")
|
||||
print("This example shows how to use continue_=False with stopReason to halt execution.\n")
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
allowed_tools=["Bash"],
|
||||
hooks={
|
||||
"PostToolUse": [
|
||||
HookMatcher(matcher="Bash", hooks=[stop_on_error_hook]),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
print("User: Run a command that outputs 'CRITICAL ERROR'")
|
||||
await client.query("Run this bash command: echo 'CRITICAL ERROR: system failure'")
|
||||
|
||||
async for msg in client.receive_response():
|
||||
display_message(msg)
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run all examples or a specific example based on command line argument."""
|
||||
examples = {
|
||||
"PreToolUse": example_pretooluse,
|
||||
"UserPromptSubmit": example_userpromptsubmit,
|
||||
"PostToolUse": example_posttooluse,
|
||||
"DecisionFields": example_decision_fields,
|
||||
"ContinueControl": example_continue_control,
|
||||
}
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
# List available examples
|
||||
print("Usage: python hooks.py <example_name>")
|
||||
print("\nAvailable examples:")
|
||||
print(" all - Run all examples")
|
||||
for name in examples:
|
||||
print(f" {name}")
|
||||
print("\nExample descriptions:")
|
||||
print(" PreToolUse - Block commands using PreToolUse hook")
|
||||
print(" UserPromptSubmit - Add context at prompt submission")
|
||||
print(" PostToolUse - Review tool output with reason and systemMessage")
|
||||
print(" DecisionFields - Use permissionDecision='allow'/'deny' with reason")
|
||||
print(" ContinueControl - Control execution with continue_ and stopReason")
|
||||
sys.exit(0)
|
||||
|
||||
example_name = sys.argv[1]
|
||||
|
||||
if example_name == "all":
|
||||
# Run all examples
|
||||
for example in examples.values():
|
||||
await example()
|
||||
print("-" * 50 + "\n")
|
||||
elif example_name in examples:
|
||||
# Run specific example
|
||||
await examples[example_name]()
|
||||
else:
|
||||
print(f"Error: Unknown example '{example_name}'")
|
||||
print("\nAvailable examples:")
|
||||
print(" all - Run all examples")
|
||||
for name in examples:
|
||||
print(f" {name}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting Claude SDK Hooks Examples...")
|
||||
print("=" * 50 + "\n")
|
||||
asyncio.run(main())
|
||||
193
skills/claude-agent-sdk/examples/mcp_calculator.py
Executable file
193
skills/claude-agent-sdk/examples/mcp_calculator.py
Executable file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example: Calculator MCP Server.
|
||||
|
||||
This example demonstrates how to create an in-process MCP server with
|
||||
calculator tools using the Claude Code Python SDK.
|
||||
|
||||
Unlike external MCP servers that require separate processes, this server
|
||||
runs directly within your Python application, providing better performance
|
||||
and simpler deployment.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from claude_agent_sdk import (
|
||||
ClaudeAgentOptions,
|
||||
create_sdk_mcp_server,
|
||||
tool,
|
||||
)
|
||||
|
||||
# Define calculator tools using the @tool decorator
|
||||
|
||||
|
||||
@tool("add", "Add two numbers", {"a": float, "b": float})
|
||||
async def add_numbers(args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Add two numbers together."""
|
||||
result = args["a"] + args["b"]
|
||||
return {
|
||||
"content": [{"type": "text", "text": f"{args['a']} + {args['b']} = {result}"}]
|
||||
}
|
||||
|
||||
|
||||
@tool("subtract", "Subtract one number from another", {"a": float, "b": float})
|
||||
async def subtract_numbers(args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Subtract b from a."""
|
||||
result = args["a"] - args["b"]
|
||||
return {
|
||||
"content": [{"type": "text", "text": f"{args['a']} - {args['b']} = {result}"}]
|
||||
}
|
||||
|
||||
|
||||
@tool("multiply", "Multiply two numbers", {"a": float, "b": float})
|
||||
async def multiply_numbers(args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Multiply two numbers."""
|
||||
result = args["a"] * args["b"]
|
||||
return {
|
||||
"content": [{"type": "text", "text": f"{args['a']} × {args['b']} = {result}"}]
|
||||
}
|
||||
|
||||
|
||||
@tool("divide", "Divide one number by another", {"a": float, "b": float})
|
||||
async def divide_numbers(args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Divide a by b."""
|
||||
if args["b"] == 0:
|
||||
return {
|
||||
"content": [
|
||||
{"type": "text", "text": "Error: Division by zero is not allowed"}
|
||||
],
|
||||
"is_error": True,
|
||||
}
|
||||
|
||||
result = args["a"] / args["b"]
|
||||
return {
|
||||
"content": [{"type": "text", "text": f"{args['a']} ÷ {args['b']} = {result}"}]
|
||||
}
|
||||
|
||||
|
||||
@tool("sqrt", "Calculate square root", {"n": float})
|
||||
async def square_root(args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Calculate the square root of a number."""
|
||||
n = args["n"]
|
||||
if n < 0:
|
||||
return {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": f"Error: Cannot calculate square root of negative number {n}",
|
||||
}
|
||||
],
|
||||
"is_error": True,
|
||||
}
|
||||
|
||||
import math
|
||||
|
||||
result = math.sqrt(n)
|
||||
return {"content": [{"type": "text", "text": f"√{n} = {result}"}]}
|
||||
|
||||
|
||||
@tool("power", "Raise a number to a power", {"base": float, "exponent": float})
|
||||
async def power(args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Raise base to the exponent power."""
|
||||
result = args["base"] ** args["exponent"]
|
||||
return {
|
||||
"content": [
|
||||
{"type": "text", "text": f"{args['base']}^{args['exponent']} = {result}"}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def display_message(msg):
|
||||
"""Display message content in a clean format."""
|
||||
from claude_agent_sdk import (
|
||||
AssistantMessage,
|
||||
ResultMessage,
|
||||
SystemMessage,
|
||||
TextBlock,
|
||||
ToolResultBlock,
|
||||
ToolUseBlock,
|
||||
UserMessage,
|
||||
)
|
||||
|
||||
if isinstance(msg, UserMessage):
|
||||
for block in msg.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"User: {block.text}")
|
||||
elif isinstance(block, ToolResultBlock):
|
||||
print(
|
||||
f"Tool Result: {block.content[:100] if block.content else 'None'}..."
|
||||
)
|
||||
elif isinstance(msg, AssistantMessage):
|
||||
for block in msg.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
elif isinstance(block, ToolUseBlock):
|
||||
print(f"Using tool: {block.name}")
|
||||
# Show tool inputs for calculator
|
||||
if block.input:
|
||||
print(f" Input: {block.input}")
|
||||
elif isinstance(msg, SystemMessage):
|
||||
# Ignore system messages
|
||||
pass
|
||||
elif isinstance(msg, ResultMessage):
|
||||
print("Result ended")
|
||||
if msg.total_cost_usd:
|
||||
print(f"Cost: ${msg.total_cost_usd:.6f}")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run example calculations using the SDK MCP server with streaming client."""
|
||||
from claude_agent_sdk import ClaudeSDKClient
|
||||
|
||||
# Create the calculator server with all tools
|
||||
calculator = create_sdk_mcp_server(
|
||||
name="calculator",
|
||||
version="2.0.0",
|
||||
tools=[
|
||||
add_numbers,
|
||||
subtract_numbers,
|
||||
multiply_numbers,
|
||||
divide_numbers,
|
||||
square_root,
|
||||
power,
|
||||
],
|
||||
)
|
||||
|
||||
# Configure Claude to use the calculator server with allowed tools
|
||||
# Pre-approve all calculator MCP tools so they can be used without permission prompts
|
||||
options = ClaudeAgentOptions(
|
||||
mcp_servers={"calc": calculator},
|
||||
allowed_tools=[
|
||||
"mcp__calc__add",
|
||||
"mcp__calc__subtract",
|
||||
"mcp__calc__multiply",
|
||||
"mcp__calc__divide",
|
||||
"mcp__calc__sqrt",
|
||||
"mcp__calc__power",
|
||||
],
|
||||
)
|
||||
|
||||
# Example prompts to demonstrate calculator usage
|
||||
prompts = [
|
||||
"List your tools",
|
||||
"Calculate 15 + 27",
|
||||
"What is 100 divided by 7?",
|
||||
"Calculate the square root of 144",
|
||||
"What is 2 raised to the power of 8?",
|
||||
"Calculate (12 + 8) * 3 - 10", # Complex calculation
|
||||
]
|
||||
|
||||
for prompt in prompts:
|
||||
print(f"\n{'=' * 50}")
|
||||
print(f"Prompt: {prompt}")
|
||||
print(f"{'=' * 50}")
|
||||
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
await client.query(prompt)
|
||||
|
||||
async for message in client.receive_response():
|
||||
display_message(message)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
71
skills/claude-agent-sdk/examples/plugin_example.py
Executable file
71
skills/claude-agent-sdk/examples/plugin_example.py
Executable file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example demonstrating how to use plugins with Claude Code SDK.
|
||||
|
||||
Plugins allow you to extend Claude Code with custom commands, agents, skills,
|
||||
and hooks. This example shows how to load a local plugin and verify it's
|
||||
loaded by checking the system message.
|
||||
|
||||
The demo plugin is located in examples/plugins/demo-plugin/ and provides
|
||||
a custom /greet command.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import anyio
|
||||
|
||||
from claude_agent_sdk import (
|
||||
ClaudeAgentOptions,
|
||||
SystemMessage,
|
||||
query,
|
||||
)
|
||||
|
||||
|
||||
async def plugin_example():
|
||||
"""Example showing plugins being loaded in the system message."""
|
||||
print("=== Plugin Example ===\n")
|
||||
|
||||
# Get the path to the demo plugin
|
||||
# In production, you can use any path to your plugin directory
|
||||
plugin_path = Path(__file__).parent / "plugins" / "demo-plugin"
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
plugins=[
|
||||
{
|
||||
"type": "local",
|
||||
"path": str(plugin_path),
|
||||
}
|
||||
],
|
||||
max_turns=1, # Limit to one turn for quick demo
|
||||
)
|
||||
|
||||
print(f"Loading plugin from: {plugin_path}\n")
|
||||
|
||||
found_plugins = False
|
||||
async for message in query(prompt="Hello!", options=options):
|
||||
if isinstance(message, SystemMessage) and message.subtype == "init":
|
||||
print("System initialized!")
|
||||
print(f"System message data keys: {list(message.data.keys())}\n")
|
||||
|
||||
# Check for plugins in the system message
|
||||
plugins_data = message.data.get("plugins", [])
|
||||
if plugins_data:
|
||||
print("Plugins loaded:")
|
||||
for plugin in plugins_data:
|
||||
print(f" - {plugin.get('name')} (path: {plugin.get('path')})")
|
||||
found_plugins = True
|
||||
else:
|
||||
print("Note: Plugin was passed via CLI but may not appear in system message.")
|
||||
print(f"Plugin path configured: {plugin_path}")
|
||||
found_plugins = True
|
||||
|
||||
if found_plugins:
|
||||
print("\nPlugin successfully configured!\n")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run all plugin examples."""
|
||||
await plugin_example()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
anyio.run(main)
|
||||
76
skills/claude-agent-sdk/examples/quick_start.py
Executable file
76
skills/claude-agent-sdk/examples/quick_start.py
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Quick start example for Claude Code SDK."""
|
||||
|
||||
import anyio
|
||||
|
||||
from claude_agent_sdk import (
|
||||
AssistantMessage,
|
||||
ClaudeAgentOptions,
|
||||
ResultMessage,
|
||||
TextBlock,
|
||||
query,
|
||||
)
|
||||
|
||||
|
||||
async def basic_example():
|
||||
"""Basic example - simple question."""
|
||||
print("=== Basic Example ===")
|
||||
|
||||
async for message in query(prompt="What is 2 + 2?"):
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
print()
|
||||
|
||||
|
||||
async def with_options_example():
|
||||
"""Example with custom options."""
|
||||
print("=== With Options Example ===")
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt="You are a helpful assistant that explains things simply.",
|
||||
max_turns=1,
|
||||
)
|
||||
|
||||
async for message in query(
|
||||
prompt="Explain what Python is in one sentence.", options=options
|
||||
):
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
print()
|
||||
|
||||
|
||||
async def with_tools_example():
|
||||
"""Example using tools."""
|
||||
print("=== With Tools Example ===")
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
allowed_tools=["Read", "Write"],
|
||||
system_prompt="You are a helpful file assistant.",
|
||||
)
|
||||
|
||||
async for message in query(
|
||||
prompt="Create a file called hello.txt with 'Hello, World!' in it",
|
||||
options=options,
|
||||
):
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
elif isinstance(message, ResultMessage) and message.total_cost_usd > 0:
|
||||
print(f"\nCost: ${message.total_cost_usd:.4f}")
|
||||
print()
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run all examples."""
|
||||
await basic_example()
|
||||
await with_options_example()
|
||||
await with_tools_example()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
anyio.run(main)
|
||||
174
skills/claude-agent-sdk/examples/setting_sources.py
Executable file
174
skills/claude-agent-sdk/examples/setting_sources.py
Executable file
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example demonstrating setting sources control.
|
||||
|
||||
This example shows how to use the setting_sources option to control which
|
||||
settings are loaded, including custom slash commands, agents, and other
|
||||
configurations.
|
||||
|
||||
Setting sources determine where Claude Code loads configurations from:
|
||||
- "user": Global user settings (~/.claude/)
|
||||
- "project": Project-level settings (.claude/ in project)
|
||||
- "local": Local gitignored settings (.claude-local/)
|
||||
|
||||
IMPORTANT: When setting_sources is not provided (None), NO settings are loaded
|
||||
by default. This creates an isolated environment. To load settings, explicitly
|
||||
specify which sources to use.
|
||||
|
||||
By controlling which sources are loaded, you can:
|
||||
- Create isolated environments with no custom settings (default)
|
||||
- Load only user settings, excluding project-specific configurations
|
||||
- Combine multiple sources as needed
|
||||
|
||||
Usage:
|
||||
./examples/setting_sources.py - List the examples
|
||||
./examples/setting_sources.py all - Run all examples
|
||||
./examples/setting_sources.py default - Run a specific example
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from claude_agent_sdk import (
|
||||
ClaudeAgentOptions,
|
||||
ClaudeSDKClient,
|
||||
SystemMessage,
|
||||
)
|
||||
|
||||
|
||||
def extract_slash_commands(msg: SystemMessage) -> list[str]:
|
||||
"""Extract slash command names from system message."""
|
||||
if msg.subtype == "init":
|
||||
commands = msg.data.get("slash_commands", [])
|
||||
return commands
|
||||
return []
|
||||
|
||||
|
||||
async def example_default():
|
||||
"""Default behavior - no settings loaded."""
|
||||
print("=== Default Behavior Example ===")
|
||||
print("Setting sources: None (default)")
|
||||
print("Expected: No custom slash commands will be available\n")
|
||||
|
||||
sdk_dir = Path(__file__).parent.parent
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
cwd=sdk_dir,
|
||||
)
|
||||
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
await client.query("What is 2 + 2?")
|
||||
|
||||
async for msg in client.receive_response():
|
||||
if isinstance(msg, SystemMessage) and msg.subtype == "init":
|
||||
commands = extract_slash_commands(msg)
|
||||
print(f"Available slash commands: {commands}")
|
||||
if "commit" in commands:
|
||||
print("❌ /commit is available (unexpected)")
|
||||
else:
|
||||
print("✓ /commit is NOT available (expected - no settings loaded)")
|
||||
break
|
||||
|
||||
print()
|
||||
|
||||
|
||||
async def example_user_only():
|
||||
"""Load only user-level settings, excluding project settings."""
|
||||
print("=== User Settings Only Example ===")
|
||||
print("Setting sources: ['user']")
|
||||
print("Expected: Project slash commands (like /commit) will NOT be available\n")
|
||||
|
||||
# Use the SDK repo directory which has .claude/commands/commit.md
|
||||
sdk_dir = Path(__file__).parent.parent
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
setting_sources=["user"],
|
||||
cwd=sdk_dir,
|
||||
)
|
||||
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
# Send a simple query
|
||||
await client.query("What is 2 + 2?")
|
||||
|
||||
# Check the initialize message for available commands
|
||||
async for msg in client.receive_response():
|
||||
if isinstance(msg, SystemMessage) and msg.subtype == "init":
|
||||
commands = extract_slash_commands(msg)
|
||||
print(f"Available slash commands: {commands}")
|
||||
if "commit" in commands:
|
||||
print("❌ /commit is available (unexpected)")
|
||||
else:
|
||||
print("✓ /commit is NOT available (expected)")
|
||||
break
|
||||
|
||||
print()
|
||||
|
||||
|
||||
async def example_project_and_user():
|
||||
"""Load both project and user settings."""
|
||||
print("=== Project + User Settings Example ===")
|
||||
print("Setting sources: ['user', 'project']")
|
||||
print("Expected: Project slash commands (like /commit) WILL be available\n")
|
||||
|
||||
sdk_dir = Path(__file__).parent.parent
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
setting_sources=["user", "project"],
|
||||
cwd=sdk_dir,
|
||||
)
|
||||
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
await client.query("What is 2 + 2?")
|
||||
|
||||
async for msg in client.receive_response():
|
||||
if isinstance(msg, SystemMessage) and msg.subtype == "init":
|
||||
commands = extract_slash_commands(msg)
|
||||
print(f"Available slash commands: {commands}")
|
||||
if "commit" in commands:
|
||||
print("✓ /commit is available (expected)")
|
||||
else:
|
||||
print("❌ /commit is NOT available (unexpected)")
|
||||
break
|
||||
|
||||
print()
|
||||
|
||||
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run all examples or a specific example based on command line argument."""
|
||||
examples = {
|
||||
"default": example_default,
|
||||
"user_only": example_user_only,
|
||||
"project_and_user": example_project_and_user,
|
||||
}
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python setting_sources.py <example_name>")
|
||||
print("\nAvailable examples:")
|
||||
print(" all - Run all examples")
|
||||
for name in examples:
|
||||
print(f" {name}")
|
||||
sys.exit(0)
|
||||
|
||||
example_name = sys.argv[1]
|
||||
|
||||
if example_name == "all":
|
||||
for example in examples.values():
|
||||
await example()
|
||||
print("-" * 50 + "\n")
|
||||
elif example_name in examples:
|
||||
await examples[example_name]()
|
||||
else:
|
||||
print(f"Error: Unknown example '{example_name}'")
|
||||
print("\nAvailable examples:")
|
||||
print(" all - Run all examples")
|
||||
for name in examples:
|
||||
print(f" {name}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting Claude SDK Setting Sources Examples...")
|
||||
print("=" * 50 + "\n")
|
||||
asyncio.run(main())
|
||||
511
skills/claude-agent-sdk/examples/streaming_mode.py
Executable file
511
skills/claude-agent-sdk/examples/streaming_mode.py
Executable file
@@ -0,0 +1,511 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive examples of using ClaudeSDKClient for streaming mode.
|
||||
|
||||
This file demonstrates various patterns for building applications with
|
||||
the ClaudeSDKClient streaming interface.
|
||||
|
||||
The queries are intentionally simplistic. In reality, a query can be a more
|
||||
complex task that Claude SDK uses its agentic capabilities and tools (e.g. run
|
||||
bash commands, edit files, search the web, fetch web content) to accomplish.
|
||||
|
||||
Usage:
|
||||
./examples/streaming_mode.py - List the examples
|
||||
./examples/streaming_mode.py all - Run all examples
|
||||
./examples/streaming_mode.py basic_streaming - Run a specific example
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import sys
|
||||
|
||||
from claude_agent_sdk import (
|
||||
AssistantMessage,
|
||||
ClaudeAgentOptions,
|
||||
ClaudeSDKClient,
|
||||
CLIConnectionError,
|
||||
ResultMessage,
|
||||
SystemMessage,
|
||||
TextBlock,
|
||||
ToolResultBlock,
|
||||
ToolUseBlock,
|
||||
UserMessage,
|
||||
)
|
||||
|
||||
|
||||
def display_message(msg):
|
||||
"""Standardized message display function.
|
||||
|
||||
- UserMessage: "User: <content>"
|
||||
- AssistantMessage: "Claude: <content>"
|
||||
- SystemMessage: ignored
|
||||
- ResultMessage: "Result ended" + cost if available
|
||||
"""
|
||||
if isinstance(msg, UserMessage):
|
||||
for block in msg.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"User: {block.text}")
|
||||
elif isinstance(msg, AssistantMessage):
|
||||
for block in msg.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
elif isinstance(msg, SystemMessage):
|
||||
# Ignore system messages
|
||||
pass
|
||||
elif isinstance(msg, ResultMessage):
|
||||
print("Result ended")
|
||||
|
||||
|
||||
async def example_basic_streaming():
|
||||
"""Basic streaming with context manager."""
|
||||
print("=== Basic Streaming Example ===")
|
||||
|
||||
async with ClaudeSDKClient() as client:
|
||||
print("User: What is 2+2?")
|
||||
await client.query("What is 2+2?")
|
||||
|
||||
# Receive complete response using the helper method
|
||||
async for msg in client.receive_response():
|
||||
display_message(msg)
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
async def example_multi_turn_conversation():
|
||||
"""Multi-turn conversation using receive_response helper."""
|
||||
print("=== Multi-Turn Conversation Example ===")
|
||||
|
||||
async with ClaudeSDKClient() as client:
|
||||
# First turn
|
||||
print("User: What's the capital of France?")
|
||||
await client.query("What's the capital of France?")
|
||||
|
||||
# Extract and print response
|
||||
async for msg in client.receive_response():
|
||||
display_message(msg)
|
||||
|
||||
# Second turn - follow-up
|
||||
print("\nUser: What's the population of that city?")
|
||||
await client.query("What's the population of that city?")
|
||||
|
||||
async for msg in client.receive_response():
|
||||
display_message(msg)
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
async def example_concurrent_responses():
|
||||
"""Handle responses while sending new messages."""
|
||||
print("=== Concurrent Send/Receive Example ===")
|
||||
|
||||
async with ClaudeSDKClient() as client:
|
||||
# Background task to continuously receive messages
|
||||
async def receive_messages():
|
||||
async for message in client.receive_messages():
|
||||
display_message(message)
|
||||
|
||||
# Start receiving in background
|
||||
receive_task = asyncio.create_task(receive_messages())
|
||||
|
||||
# Send multiple messages with delays
|
||||
questions = [
|
||||
"What is 2 + 2?",
|
||||
"What is the square root of 144?",
|
||||
"What is 10% of 80?",
|
||||
]
|
||||
|
||||
for question in questions:
|
||||
print(f"\nUser: {question}")
|
||||
await client.query(question)
|
||||
await asyncio.sleep(3) # Wait between messages
|
||||
|
||||
# Give time for final responses
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Clean up
|
||||
receive_task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await receive_task
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
async def example_with_interrupt():
|
||||
"""Demonstrate interrupt capability."""
|
||||
print("=== Interrupt Example ===")
|
||||
print("IMPORTANT: Interrupts require active message consumption.")
|
||||
|
||||
async with ClaudeSDKClient() as client:
|
||||
# Start a long-running task
|
||||
print("\nUser: Count from 1 to 100 slowly")
|
||||
await client.query(
|
||||
"Count from 1 to 100 slowly, with a brief pause between each number"
|
||||
)
|
||||
|
||||
# Create a background task to consume messages
|
||||
messages_received = []
|
||||
|
||||
async def consume_messages():
|
||||
"""Consume messages in the background to enable interrupt processing."""
|
||||
async for message in client.receive_response():
|
||||
messages_received.append(message)
|
||||
display_message(message)
|
||||
|
||||
# Start consuming messages in the background
|
||||
consume_task = asyncio.create_task(consume_messages())
|
||||
|
||||
# Wait 2 seconds then send interrupt
|
||||
await asyncio.sleep(2)
|
||||
print("\n[After 2 seconds, sending interrupt...]")
|
||||
await client.interrupt()
|
||||
|
||||
# Wait for the consume task to finish processing the interrupt
|
||||
await consume_task
|
||||
|
||||
# Send new instruction after interrupt
|
||||
print("\nUser: Never mind, just tell me a quick joke")
|
||||
await client.query("Never mind, just tell me a quick joke")
|
||||
|
||||
# Get the joke
|
||||
async for msg in client.receive_response():
|
||||
display_message(msg)
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
async def example_manual_message_handling():
|
||||
"""Manually handle message stream for custom logic."""
|
||||
print("=== Manual Message Handling Example ===")
|
||||
|
||||
async with ClaudeSDKClient() as client:
|
||||
await client.query("List 5 programming languages and their main use cases")
|
||||
|
||||
# Manually process messages with custom logic
|
||||
languages_found = []
|
||||
|
||||
async for message in client.receive_messages():
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
text = block.text
|
||||
print(f"Claude: {text}")
|
||||
# Custom logic: extract language names
|
||||
for lang in [
|
||||
"Python",
|
||||
"JavaScript",
|
||||
"Java",
|
||||
"C++",
|
||||
"Go",
|
||||
"Rust",
|
||||
"Ruby",
|
||||
]:
|
||||
if lang in text and lang not in languages_found:
|
||||
languages_found.append(lang)
|
||||
print(f"Found language: {lang}")
|
||||
elif isinstance(message, ResultMessage):
|
||||
display_message(message)
|
||||
print(f"Total languages mentioned: {len(languages_found)}")
|
||||
break
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
async def example_with_options():
|
||||
"""Use ClaudeAgentOptions to configure the client."""
|
||||
print("=== Custom Options Example ===")
|
||||
|
||||
# Configure options
|
||||
options = ClaudeAgentOptions(
|
||||
allowed_tools=["Read", "Write"], # Allow file operations
|
||||
system_prompt="You are a helpful coding assistant.",
|
||||
env={
|
||||
"ANTHROPIC_MODEL": "claude-sonnet-4-5",
|
||||
},
|
||||
)
|
||||
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
print("User: Create a simple hello.txt file with a greeting message")
|
||||
await client.query("Create a simple hello.txt file with a greeting message")
|
||||
|
||||
tool_uses = []
|
||||
async for msg in client.receive_response():
|
||||
if isinstance(msg, AssistantMessage):
|
||||
display_message(msg)
|
||||
for block in msg.content:
|
||||
if hasattr(block, "name") and not isinstance(
|
||||
block, TextBlock
|
||||
): # ToolUseBlock
|
||||
tool_uses.append(getattr(block, "name", ""))
|
||||
else:
|
||||
display_message(msg)
|
||||
|
||||
if tool_uses:
|
||||
print(f"Tools used: {', '.join(tool_uses)}")
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
async def example_async_iterable_prompt():
|
||||
"""Demonstrate send_message with async iterable."""
|
||||
print("=== Async Iterable Prompt Example ===")
|
||||
|
||||
async def create_message_stream():
|
||||
"""Generate a stream of messages."""
|
||||
print("User: Hello! I have multiple questions.")
|
||||
yield {
|
||||
"type": "user",
|
||||
"message": {"role": "user", "content": "Hello! I have multiple questions."},
|
||||
"parent_tool_use_id": None,
|
||||
"session_id": "qa-session",
|
||||
}
|
||||
|
||||
print("User: First, what's the capital of Japan?")
|
||||
yield {
|
||||
"type": "user",
|
||||
"message": {
|
||||
"role": "user",
|
||||
"content": "First, what's the capital of Japan?",
|
||||
},
|
||||
"parent_tool_use_id": None,
|
||||
"session_id": "qa-session",
|
||||
}
|
||||
|
||||
print("User: Second, what's 15% of 200?")
|
||||
yield {
|
||||
"type": "user",
|
||||
"message": {"role": "user", "content": "Second, what's 15% of 200?"},
|
||||
"parent_tool_use_id": None,
|
||||
"session_id": "qa-session",
|
||||
}
|
||||
|
||||
async with ClaudeSDKClient() as client:
|
||||
# Send async iterable of messages
|
||||
await client.query(create_message_stream())
|
||||
|
||||
# Receive the three responses
|
||||
async for msg in client.receive_response():
|
||||
display_message(msg)
|
||||
async for msg in client.receive_response():
|
||||
display_message(msg)
|
||||
async for msg in client.receive_response():
|
||||
display_message(msg)
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
async def example_bash_command():
|
||||
"""Example showing tool use blocks when running bash commands."""
|
||||
print("=== Bash Command Example ===")
|
||||
|
||||
async with ClaudeSDKClient() as client:
|
||||
print("User: Run a bash echo command")
|
||||
await client.query("Run a bash echo command that says 'Hello from bash!'")
|
||||
|
||||
# Track all message types received
|
||||
message_types = []
|
||||
|
||||
async for msg in client.receive_messages():
|
||||
message_types.append(type(msg).__name__)
|
||||
|
||||
if isinstance(msg, UserMessage):
|
||||
# User messages can contain tool results
|
||||
for block in msg.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"User: {block.text}")
|
||||
elif isinstance(block, ToolResultBlock):
|
||||
print(
|
||||
f"Tool Result (id: {block.tool_use_id}): {block.content[:100] if block.content else 'None'}..."
|
||||
)
|
||||
|
||||
elif isinstance(msg, AssistantMessage):
|
||||
# Assistant messages can contain tool use blocks
|
||||
for block in msg.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
elif isinstance(block, ToolUseBlock):
|
||||
print(f"Tool Use: {block.name} (id: {block.id})")
|
||||
if block.name == "Bash":
|
||||
command = block.input.get("command", "")
|
||||
print(f" Command: {command}")
|
||||
|
||||
elif isinstance(msg, ResultMessage):
|
||||
print("Result ended")
|
||||
if msg.total_cost_usd:
|
||||
print(f"Cost: ${msg.total_cost_usd:.4f}")
|
||||
break
|
||||
|
||||
print(f"\nMessage types received: {', '.join(set(message_types))}")
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
async def example_control_protocol():
|
||||
"""Demonstrate server info and interrupt capabilities."""
|
||||
print("=== Control Protocol Example ===")
|
||||
print("Shows server info retrieval and interrupt capability\n")
|
||||
|
||||
async with ClaudeSDKClient() as client:
|
||||
# 1. Get server initialization info
|
||||
print("1. Getting server info...")
|
||||
server_info = await client.get_server_info()
|
||||
|
||||
if server_info:
|
||||
print("✓ Server info retrieved successfully!")
|
||||
print(f" - Available commands: {len(server_info.get('commands', []))}")
|
||||
print(f" - Output style: {server_info.get('output_style', 'unknown')}")
|
||||
|
||||
# Show available output styles if present
|
||||
styles = server_info.get('available_output_styles', [])
|
||||
if styles:
|
||||
print(f" - Available output styles: {', '.join(styles)}")
|
||||
|
||||
# Show a few example commands
|
||||
commands = server_info.get('commands', [])[:5]
|
||||
if commands:
|
||||
print(" - Example commands:")
|
||||
for cmd in commands:
|
||||
if isinstance(cmd, dict):
|
||||
print(f" • {cmd.get('name', 'unknown')}")
|
||||
else:
|
||||
print("✗ No server info available (may not be in streaming mode)")
|
||||
|
||||
print("\n2. Testing interrupt capability...")
|
||||
|
||||
# Start a long-running task
|
||||
print("User: Count from 1 to 20 slowly")
|
||||
await client.query("Count from 1 to 20 slowly, pausing between each number")
|
||||
|
||||
# Start consuming messages in background to enable interrupt
|
||||
messages = []
|
||||
async def consume():
|
||||
async for msg in client.receive_response():
|
||||
messages.append(msg)
|
||||
if isinstance(msg, AssistantMessage):
|
||||
for block in msg.content:
|
||||
if isinstance(block, TextBlock):
|
||||
# Print first 50 chars to show progress
|
||||
print(f"Claude: {block.text[:50]}...")
|
||||
break
|
||||
if isinstance(msg, ResultMessage):
|
||||
break
|
||||
|
||||
consume_task = asyncio.create_task(consume())
|
||||
|
||||
# Wait a moment then interrupt
|
||||
await asyncio.sleep(2)
|
||||
print("\n[Sending interrupt after 2 seconds...]")
|
||||
|
||||
try:
|
||||
await client.interrupt()
|
||||
print("✓ Interrupt sent successfully")
|
||||
except Exception as e:
|
||||
print(f"✗ Interrupt failed: {e}")
|
||||
|
||||
# Wait for task to complete
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await consume_task
|
||||
|
||||
# Send new query after interrupt
|
||||
print("\nUser: Just say 'Hello!'")
|
||||
await client.query("Just say 'Hello!'")
|
||||
|
||||
async for msg in client.receive_response():
|
||||
if isinstance(msg, AssistantMessage):
|
||||
for block in msg.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
async def example_error_handling():
|
||||
"""Demonstrate proper error handling."""
|
||||
print("=== Error Handling Example ===")
|
||||
|
||||
client = ClaudeSDKClient()
|
||||
|
||||
try:
|
||||
await client.connect()
|
||||
|
||||
# Send a message that will take time to process
|
||||
print("User: Run a bash sleep command for 60 seconds not in the background")
|
||||
await client.query("Run a bash sleep command for 60 seconds not in the background")
|
||||
|
||||
# Try to receive response with a short timeout
|
||||
try:
|
||||
messages = []
|
||||
async with asyncio.timeout(10.0):
|
||||
async for msg in client.receive_response():
|
||||
messages.append(msg)
|
||||
if isinstance(msg, AssistantMessage):
|
||||
for block in msg.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text[:50]}...")
|
||||
elif isinstance(msg, ResultMessage):
|
||||
display_message(msg)
|
||||
break
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
print(
|
||||
"\nResponse timeout after 10 seconds - demonstrating graceful handling"
|
||||
)
|
||||
print(f"Received {len(messages)} messages before timeout")
|
||||
|
||||
except CLIConnectionError as e:
|
||||
print(f"Connection error: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}")
|
||||
|
||||
finally:
|
||||
# Always disconnect
|
||||
await client.disconnect()
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run all examples or a specific example based on command line argument."""
|
||||
examples = {
|
||||
"basic_streaming": example_basic_streaming,
|
||||
"multi_turn_conversation": example_multi_turn_conversation,
|
||||
"concurrent_responses": example_concurrent_responses,
|
||||
"with_interrupt": example_with_interrupt,
|
||||
"manual_message_handling": example_manual_message_handling,
|
||||
"with_options": example_with_options,
|
||||
"async_iterable_prompt": example_async_iterable_prompt,
|
||||
"bash_command": example_bash_command,
|
||||
"control_protocol": example_control_protocol,
|
||||
"error_handling": example_error_handling,
|
||||
}
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
# List available examples
|
||||
print("Usage: python streaming_mode.py <example_name>")
|
||||
print("\nAvailable examples:")
|
||||
print(" all - Run all examples")
|
||||
for name in examples:
|
||||
print(f" {name}")
|
||||
sys.exit(0)
|
||||
|
||||
example_name = sys.argv[1]
|
||||
|
||||
if example_name == "all":
|
||||
# Run all examples
|
||||
for example in examples.values():
|
||||
await example()
|
||||
print("-" * 50 + "\n")
|
||||
elif example_name in examples:
|
||||
# Run specific example
|
||||
await examples[example_name]()
|
||||
else:
|
||||
print(f"Error: Unknown example '{example_name}'")
|
||||
print("\nAvailable examples:")
|
||||
print(" all - Run all examples")
|
||||
for name in examples:
|
||||
print(f" {name}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
87
skills/claude-agent-sdk/examples/system_prompt.py
Executable file
87
skills/claude-agent-sdk/examples/system_prompt.py
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example demonstrating different system_prompt configurations."""
|
||||
|
||||
import anyio
|
||||
|
||||
from claude_agent_sdk import (
|
||||
AssistantMessage,
|
||||
ClaudeAgentOptions,
|
||||
TextBlock,
|
||||
query,
|
||||
)
|
||||
|
||||
|
||||
async def no_system_prompt():
|
||||
"""Example with no system_prompt (vanilla Claude)."""
|
||||
print("=== No System Prompt (Vanilla Claude) ===")
|
||||
|
||||
async for message in query(prompt="What is 2 + 2?"):
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
print()
|
||||
|
||||
|
||||
async def string_system_prompt():
|
||||
"""Example with system_prompt as a string."""
|
||||
print("=== String System Prompt ===")
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt="You are a pirate assistant. Respond in pirate speak.",
|
||||
)
|
||||
|
||||
async for message in query(prompt="What is 2 + 2?", options=options):
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
print()
|
||||
|
||||
|
||||
async def preset_system_prompt():
|
||||
"""Example with system_prompt preset (uses default Claude Code prompt)."""
|
||||
print("=== Preset System Prompt (Default) ===")
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt={"type": "preset", "preset": "claude_code"},
|
||||
)
|
||||
|
||||
async for message in query(prompt="What is 2 + 2?", options=options):
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
print()
|
||||
|
||||
|
||||
async def preset_with_append():
|
||||
"""Example with system_prompt preset and append."""
|
||||
print("=== Preset System Prompt with Append ===")
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt={
|
||||
"type": "preset",
|
||||
"preset": "claude_code",
|
||||
"append": "Always end your response with a fun fact.",
|
||||
},
|
||||
)
|
||||
|
||||
async for message in query(prompt="What is 2 + 2?", options=options):
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"Claude: {block.text}")
|
||||
print()
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run all examples."""
|
||||
await no_system_prompt()
|
||||
await string_system_prompt()
|
||||
await preset_system_prompt()
|
||||
await preset_with_append()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
anyio.run(main)
|
||||
158
skills/claude-agent-sdk/examples/tool_permission_callback.py
Executable file
158
skills/claude-agent-sdk/examples/tool_permission_callback.py
Executable file
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example: Tool Permission Callbacks.
|
||||
|
||||
This example demonstrates how to use tool permission callbacks to control
|
||||
which tools Claude can use and modify their inputs.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from claude_agent_sdk import (
|
||||
AssistantMessage,
|
||||
ClaudeAgentOptions,
|
||||
ClaudeSDKClient,
|
||||
PermissionResultAllow,
|
||||
PermissionResultDeny,
|
||||
ResultMessage,
|
||||
TextBlock,
|
||||
ToolPermissionContext,
|
||||
)
|
||||
|
||||
# Track tool usage for demonstration
|
||||
tool_usage_log = []
|
||||
|
||||
|
||||
async def my_permission_callback(
|
||||
tool_name: str,
|
||||
input_data: dict,
|
||||
context: ToolPermissionContext
|
||||
) -> PermissionResultAllow | PermissionResultDeny:
|
||||
"""Control tool permissions based on tool type and input."""
|
||||
|
||||
# Log the tool request
|
||||
tool_usage_log.append({
|
||||
"tool": tool_name,
|
||||
"input": input_data,
|
||||
"suggestions": context.suggestions
|
||||
})
|
||||
|
||||
print(f"\n🔧 Tool Permission Request: {tool_name}")
|
||||
print(f" Input: {json.dumps(input_data, indent=2)}")
|
||||
|
||||
# Always allow read operations
|
||||
if tool_name in ["Read", "Glob", "Grep"]:
|
||||
print(f" ✅ Automatically allowing {tool_name} (read-only operation)")
|
||||
return PermissionResultAllow()
|
||||
|
||||
# Deny write operations to system directories
|
||||
if tool_name in ["Write", "Edit", "MultiEdit"]:
|
||||
file_path = input_data.get("file_path", "")
|
||||
if file_path.startswith("/etc/") or file_path.startswith("/usr/"):
|
||||
print(f" ❌ Denying write to system directory: {file_path}")
|
||||
return PermissionResultDeny(
|
||||
message=f"Cannot write to system directory: {file_path}"
|
||||
)
|
||||
|
||||
# Redirect writes to a safe directory
|
||||
if not file_path.startswith("/tmp/") and not file_path.startswith("./"):
|
||||
safe_path = f"./safe_output/{file_path.split('/')[-1]}"
|
||||
print(f" ⚠️ Redirecting write from {file_path} to {safe_path}")
|
||||
modified_input = input_data.copy()
|
||||
modified_input["file_path"] = safe_path
|
||||
return PermissionResultAllow(
|
||||
updated_input=modified_input
|
||||
)
|
||||
|
||||
# Check dangerous bash commands
|
||||
if tool_name == "Bash":
|
||||
command = input_data.get("command", "")
|
||||
dangerous_commands = ["rm -rf", "sudo", "chmod 777", "dd if=", "mkfs"]
|
||||
|
||||
for dangerous in dangerous_commands:
|
||||
if dangerous in command:
|
||||
print(f" ❌ Denying dangerous command: {command}")
|
||||
return PermissionResultDeny(
|
||||
message=f"Dangerous command pattern detected: {dangerous}"
|
||||
)
|
||||
|
||||
# Allow but log the command
|
||||
print(f" ✅ Allowing bash command: {command}")
|
||||
return PermissionResultAllow()
|
||||
|
||||
# For all other tools, ask the user
|
||||
print(f" ❓ Unknown tool: {tool_name}")
|
||||
print(f" Input: {json.dumps(input_data, indent=6)}")
|
||||
user_input = input(" Allow this tool? (y/N): ").strip().lower()
|
||||
|
||||
if user_input in ("y", "yes"):
|
||||
return PermissionResultAllow()
|
||||
else:
|
||||
return PermissionResultDeny(
|
||||
message="User denied permission"
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run example with tool permission callbacks."""
|
||||
|
||||
print("=" * 60)
|
||||
print("Tool Permission Callback Example")
|
||||
print("=" * 60)
|
||||
print("\nThis example demonstrates how to:")
|
||||
print("1. Allow/deny tools based on type")
|
||||
print("2. Modify tool inputs for safety")
|
||||
print("3. Log tool usage")
|
||||
print("4. Prompt for unknown tools")
|
||||
print("=" * 60)
|
||||
|
||||
# Configure options with our callback
|
||||
options = ClaudeAgentOptions(
|
||||
can_use_tool=my_permission_callback,
|
||||
# Use default permission mode to ensure callbacks are invoked
|
||||
permission_mode="default",
|
||||
cwd="." # Set working directory
|
||||
)
|
||||
|
||||
# Create client and send a query that will use multiple tools
|
||||
async with ClaudeSDKClient(options) as client:
|
||||
print("\n📝 Sending query to Claude...")
|
||||
await client.query(
|
||||
"Please do the following:\n"
|
||||
"1. List the files in the current directory\n"
|
||||
"2. Create a simple Python hello world script at hello.py\n"
|
||||
"3. Run the script to test it"
|
||||
)
|
||||
|
||||
print("\n📨 Receiving response...")
|
||||
message_count = 0
|
||||
|
||||
async for message in client.receive_response():
|
||||
message_count += 1
|
||||
|
||||
if isinstance(message, AssistantMessage):
|
||||
# Print Claude's text responses
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(f"\n💬 Claude: {block.text}")
|
||||
|
||||
elif isinstance(message, ResultMessage):
|
||||
print("\n✅ Task completed!")
|
||||
print(f" Duration: {message.duration_ms}ms")
|
||||
if message.total_cost_usd:
|
||||
print(f" Cost: ${message.total_cost_usd:.4f}")
|
||||
print(f" Messages processed: {message_count}")
|
||||
|
||||
# Print tool usage summary
|
||||
print("\n" + "=" * 60)
|
||||
print("Tool Usage Summary")
|
||||
print("=" * 60)
|
||||
for i, usage in enumerate(tool_usage_log, 1):
|
||||
print(f"\n{i}. Tool: {usage['tool']}")
|
||||
print(f" Input: {json.dumps(usage['input'], indent=6)}")
|
||||
if usage['suggestions']:
|
||||
print(f" Suggestions: {usage['suggestions']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
191
skills/claude-agent-sdk/references/agent-patterns.md
Normal file
191
skills/claude-agent-sdk/references/agent-patterns.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Agent and Subagent Definition Patterns
|
||||
|
||||
This guide covers how to define agents and subagents programmatically using the Claude Agent SDK.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
**AgentDefinition** - Defines a specialized agent with specific tools, prompts, and model configuration.
|
||||
|
||||
**Programmatic Definition** - SDK best practice is to define agents programmatically using the `agents` parameter in `ClaudeAgentOptions`, not filesystem auto-discovery.
|
||||
|
||||
## Basic Agent Definition
|
||||
|
||||
```python
|
||||
from claude_agent_sdk import AgentDefinition, ClaudeAgentOptions
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
agents={
|
||||
"agent-name": AgentDefinition(
|
||||
description="When to use this agent",
|
||||
prompt="System prompt defining role and behavior",
|
||||
tools=["Read", "Grep", "Glob"],
|
||||
model="sonnet" # or "opus", "haiku", "inherit"
|
||||
)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## AgentDefinition Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `description` | `str` | Yes | Natural language description of when to use this agent |
|
||||
| `prompt` | `str` | Yes | Agent's system prompt defining role and behavior |
|
||||
| `tools` | `list[str]` | No | Array of allowed tool names. If omitted, inherits all tools |
|
||||
| `model` | `str` | No | Model override: "sonnet", "opus", "haiku", or "inherit" |
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Read-Only Analyzer Agent
|
||||
|
||||
For code review, architecture analysis, or documentation review:
|
||||
|
||||
```python
|
||||
"code-reviewer": AgentDefinition(
|
||||
description="Reviews code for best practices, security, and performance",
|
||||
prompt="""You are a code reviewer. Analyze code for:
|
||||
- Security vulnerabilities
|
||||
- Performance issues
|
||||
- Best practice adherence
|
||||
- Potential bugs
|
||||
|
||||
Provide constructive, specific feedback.""",
|
||||
tools=["Read", "Grep", "Glob"],
|
||||
model="sonnet"
|
||||
)
|
||||
```
|
||||
|
||||
### Code Modification Agent
|
||||
|
||||
For implementing features or fixing bugs:
|
||||
|
||||
```python
|
||||
"code-writer": AgentDefinition(
|
||||
description="Implements features and fixes bugs",
|
||||
prompt="""You are a code implementation specialist.
|
||||
Write clean, tested, well-documented code.
|
||||
Follow project conventions and best practices.""",
|
||||
tools=["Read", "Write", "Edit", "Grep", "Glob"],
|
||||
model="sonnet"
|
||||
)
|
||||
```
|
||||
|
||||
### Test Execution Agent
|
||||
|
||||
For running tests and analyzing results:
|
||||
|
||||
```python
|
||||
"test-runner": AgentDefinition(
|
||||
description="Runs tests and analyzes results",
|
||||
prompt="""You are a testing specialist.
|
||||
Execute tests, analyze failures, and provide clear diagnostics.
|
||||
Focus on actionable feedback.""",
|
||||
tools=["Bash", "Read", "Grep"],
|
||||
model="sonnet"
|
||||
)
|
||||
```
|
||||
|
||||
### Multiple Agents Pattern
|
||||
|
||||
Orchestrator with specialized subagents:
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt="claude_code", # Orchestrator needs Task tool knowledge
|
||||
allowed_tools=["Bash", "Task", "Read", "Write"],
|
||||
agents={
|
||||
"analyzer": AgentDefinition(
|
||||
description="Analyzes code structure and patterns",
|
||||
prompt="You are a code analyzer. Examine structure, patterns, and architecture.",
|
||||
tools=["Read", "Grep", "Glob"]
|
||||
),
|
||||
"fixer": AgentDefinition(
|
||||
description="Fixes identified issues",
|
||||
prompt="You are a code fixer. Apply fixes based on analysis results.",
|
||||
tools=["Read", "Edit", "Bash"],
|
||||
model="sonnet"
|
||||
)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Loading Agent Definitions from Files
|
||||
|
||||
While programmatic definition is recommended, you can still store agent prompts in markdown files:
|
||||
|
||||
```python
|
||||
import yaml
|
||||
|
||||
def load_agent_definition(path: str) -> AgentDefinition:
|
||||
"""Load agent definition from markdown file with YAML frontmatter."""
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
|
||||
parts = content.split("---")
|
||||
frontmatter = yaml.safe_load(parts[1])
|
||||
system_prompt = parts[2].strip()
|
||||
|
||||
# Parse tools (can be comma-separated string or array)
|
||||
tools_value = frontmatter.get("tools", [])
|
||||
if isinstance(tools_value, str):
|
||||
tools = [t.strip() for t in tools_value.split(",")]
|
||||
else:
|
||||
tools = tools_value
|
||||
|
||||
return AgentDefinition(
|
||||
description=frontmatter["description"],
|
||||
prompt=system_prompt,
|
||||
tools=tools,
|
||||
model=frontmatter.get("model", "inherit")
|
||||
)
|
||||
|
||||
# Load and register programmatically
|
||||
investigator = load_agent_definition(".claude/agents/investigator.md")
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
agents={"investigator": investigator}
|
||||
)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use programmatic registration** - Define agents via `agents` parameter, not filesystem auto-discovery
|
||||
2. **Set orchestrator system_prompt** - Main agent needs `system_prompt="claude_code"` to use Task tool
|
||||
3. **Specific descriptions** - Agent descriptions determine when they're used
|
||||
4. **Restrict tools** - Limit agent tools to minimum needed for safety and clarity
|
||||
5. **Match agent names** - Ensure agent names in `agents={}` match what orchestrator references
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
❌ **Relying on filesystem auto-discovery**
|
||||
|
||||
```python
|
||||
# SDK will auto-discover .claude/agents/*.md but this is NOT recommended
|
||||
options = ClaudeAgentOptions() # Missing explicit agents={}
|
||||
```
|
||||
|
||||
✅ **Programmatic registration**
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
agents={"agent-name": AgentDefinition(...)}
|
||||
)
|
||||
```
|
||||
|
||||
❌ **Missing orchestrator system prompt**
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
agents={...}
|
||||
# Missing system_prompt="claude_code"
|
||||
)
|
||||
```
|
||||
|
||||
✅ **Proper orchestrator configuration**
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt="claude_code", # Orchestrator knows how to use Task tool
|
||||
agents={...}
|
||||
)
|
||||
```
|
||||
1848
skills/claude-agent-sdk/references/api-reference.md
Normal file
1848
skills/claude-agent-sdk/references/api-reference.md
Normal file
File diff suppressed because it is too large
Load Diff
406
skills/claude-agent-sdk/references/best-practices.md
Normal file
406
skills/claude-agent-sdk/references/best-practices.md
Normal file
@@ -0,0 +1,406 @@
|
||||
# Claude Agent SDK Best Practices
|
||||
|
||||
This guide captures best practices and common patterns for building effective SDK applications.
|
||||
|
||||
## Agent Definition
|
||||
|
||||
### ✅ Use Programmatic Registration
|
||||
|
||||
**Recommended:** Define agents via `agents` parameter
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
agents={
|
||||
"investigator": AgentDefinition(
|
||||
description="Analyzes errors autonomously",
|
||||
prompt="You are an error investigator...",
|
||||
tools=["Read", "Grep", "Glob", "Bash"]
|
||||
)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**Not Recommended:** Relying on filesystem auto-discovery
|
||||
|
||||
```python
|
||||
# SDK can auto-discover .claude/agents/*.md
|
||||
# but programmatic registration is clearer and more maintainable
|
||||
options = ClaudeAgentOptions()
|
||||
```
|
||||
|
||||
### ✅ Set Orchestrator System Prompt
|
||||
|
||||
**Critical:** Orchestrators must use `system_prompt="claude_code"`
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt="claude_code", # Knows how to use Task tool
|
||||
allowed_tools=["Bash", "Task", "Read", "Write"],
|
||||
agents={...}
|
||||
)
|
||||
```
|
||||
|
||||
**Why:** The claude_code preset includes knowledge of the Task tool for delegating to subagents.
|
||||
|
||||
### ✅ Match Agent Names
|
||||
|
||||
Ensure agent names in `agents={}` match references in prompts:
|
||||
|
||||
```python
|
||||
# Define agent
|
||||
options = ClaudeAgentOptions(
|
||||
agents={"markdown-investigator": AgentDefinition(...)}
|
||||
)
|
||||
|
||||
# Reference in prompt
|
||||
await client.query("Use the 'markdown-investigator' subagent...")
|
||||
```
|
||||
|
||||
## Tool Configuration
|
||||
|
||||
### ✅ Restrict Subagent Tools
|
||||
|
||||
Limit subagent tools to minimum needed:
|
||||
|
||||
```python
|
||||
# Read-only analyzer
|
||||
"analyzer": AgentDefinition(
|
||||
tools=["Read", "Grep", "Glob"]
|
||||
)
|
||||
|
||||
# Code modifier
|
||||
"fixer": AgentDefinition(
|
||||
tools=["Read", "Edit", "Bash"]
|
||||
)
|
||||
```
|
||||
|
||||
### ✅ Give Orchestrator Task Tool
|
||||
|
||||
Orchestrators need Task tool to delegate:
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
allowed_tools=["Bash", "Task", "Read", "Write"], # Include Task
|
||||
agents={...}
|
||||
)
|
||||
```
|
||||
|
||||
## Async/Await Patterns
|
||||
|
||||
### ✅ Use async with for Streaming
|
||||
|
||||
```python
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
await client.query(prompt)
|
||||
|
||||
async for message in client.receive_response():
|
||||
if isinstance(message, AssistantMessage):
|
||||
# Process messages
|
||||
pass
|
||||
```
|
||||
|
||||
### ✅ Handle Multiple Message Types
|
||||
|
||||
```python
|
||||
async for message in client.receive_response():
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
text = block.text
|
||||
|
||||
elif isinstance(message, ResultMessage):
|
||||
print(f"Cost: ${message.total_cost_usd:.4f}")
|
||||
print(f"Duration: {message.duration_ms}ms")
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### ✅ Validate Agent Responses
|
||||
|
||||
Don't assume agents return expected format:
|
||||
|
||||
```python
|
||||
investigation_report = None
|
||||
|
||||
async for message in client.receive_response():
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
# Try to extract JSON
|
||||
try:
|
||||
investigation_report = json.loads(block.text)
|
||||
except json.JSONDecodeError:
|
||||
# Handle non-JSON response
|
||||
continue
|
||||
|
||||
if not investigation_report:
|
||||
raise RuntimeError("Agent did not return valid report")
|
||||
```
|
||||
|
||||
### ✅ Use uv Script Headers
|
||||
|
||||
For standalone SDK scripts, use uv inline script metadata:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script --quiet
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "claude-agent-sdk>=0.1.6",
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
### ✅ Organize Agent Definitions
|
||||
|
||||
Option 1: Store in markdown files, load programmatically
|
||||
|
||||
```text
|
||||
project/
|
||||
├── .claude/
|
||||
│ └── agents/
|
||||
│ ├── investigator.md
|
||||
│ └── fixer.md
|
||||
├── main.py
|
||||
```
|
||||
|
||||
```python
|
||||
def load_agent_definition(path: str) -> AgentDefinition:
|
||||
# Parse frontmatter and content
|
||||
# Return AgentDefinition
|
||||
|
||||
investigator = load_agent_definition(".claude/agents/investigator.md")
|
||||
options = ClaudeAgentOptions(agents={"investigator": investigator})
|
||||
```
|
||||
|
||||
Option 2: Define inline
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
agents={
|
||||
"investigator": AgentDefinition(
|
||||
description="...",
|
||||
prompt="...",
|
||||
tools=[...]
|
||||
)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Permission Management
|
||||
|
||||
### ✅ Choose Appropriate Permission Mode
|
||||
|
||||
```python
|
||||
# Automated workflows (auto-approve edits)
|
||||
options = ClaudeAgentOptions(
|
||||
permission_mode="acceptEdits"
|
||||
)
|
||||
|
||||
# Interactive development (ask for approval)
|
||||
options = ClaudeAgentOptions(
|
||||
permission_mode="default",
|
||||
can_use_tool=permission_callback
|
||||
)
|
||||
|
||||
# Read-only mode (use tool restrictions)
|
||||
options = ClaudeAgentOptions(
|
||||
allowed_tools=["Read", "Grep", "Glob"] # Only read tools
|
||||
)
|
||||
```
|
||||
|
||||
### ✅ Use Hooks for Complex Logic
|
||||
|
||||
Prefer hooks over permission callbacks for:
|
||||
|
||||
- Adding context
|
||||
- Reviewing outputs
|
||||
- Stopping on errors
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
hooks={
|
||||
"PreToolUse": [
|
||||
HookMatcher(matcher="Bash", hooks=[check_command])
|
||||
],
|
||||
"PostToolUse": [
|
||||
HookMatcher(matcher="Bash", hooks=[review_output])
|
||||
]
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Common Anti-Patterns
|
||||
|
||||
### ❌ Missing System Prompt on Orchestrator
|
||||
|
||||
```python
|
||||
# Orchestrator won't know how to use Task tool
|
||||
options = ClaudeAgentOptions(
|
||||
agents={...}
|
||||
# Missing system_prompt="claude_code"
|
||||
)
|
||||
```
|
||||
|
||||
### ❌ Tool/Prompt Mismatch
|
||||
|
||||
```python
|
||||
# Tells agent to modify files but only allows read tools
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt="Fix any bugs you find",
|
||||
allowed_tools=["Read", "Grep"] # Can't actually fix
|
||||
)
|
||||
```
|
||||
|
||||
### ❌ Assuming Agent Output Format
|
||||
|
||||
```python
|
||||
# Assumes agent returns JSON
|
||||
json_data = json.loads(message.content[0].text) # May crash
|
||||
```
|
||||
|
||||
### ❌ Not Validating Agent Names
|
||||
|
||||
```python
|
||||
# Define as "investigator" but reference as "markdown-investigator"
|
||||
options = ClaudeAgentOptions(
|
||||
agents={"investigator": AgentDefinition(...)}
|
||||
)
|
||||
await client.query("Use 'markdown-investigator'...") # Won't work
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### ✅ Use Appropriate Models
|
||||
|
||||
```python
|
||||
# Fast, cheap tasks
|
||||
"simple-agent": AgentDefinition(model="haiku", ...)
|
||||
|
||||
# Complex reasoning
|
||||
"complex-agent": AgentDefinition(model="sonnet", ...)
|
||||
|
||||
# Inherit from main agent
|
||||
"helper-agent": AgentDefinition(model="inherit", ...)
|
||||
```
|
||||
|
||||
### ✅ Set Budget Limits
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
max_budget_usd=1.00 # Stop after $1
|
||||
)
|
||||
```
|
||||
|
||||
### ✅ Limit Turns for Simple Tasks
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
max_turns=3 # Prevent infinite loops
|
||||
)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### ✅ Validate Agent Definitions
|
||||
|
||||
```python
|
||||
def test_agent_configuration():
|
||||
"""Ensure agent definitions are valid."""
|
||||
options = get_sdk_options()
|
||||
|
||||
# Check orchestrator has claude_code preset
|
||||
# Note: Can be string "claude_code" or dict {"type": "preset", "preset": "claude_code"}
|
||||
assert options.system_prompt in ("claude_code", {"type": "preset", "preset": "claude_code"})
|
||||
|
||||
# Check orchestrator has Task tool
|
||||
assert "Task" in options.allowed_tools
|
||||
|
||||
# Check agents are registered
|
||||
assert "investigator" in options.agents
|
||||
assert "fixer" in options.agents
|
||||
```
|
||||
|
||||
### ✅ Test Tool Restrictions
|
||||
|
||||
```python
|
||||
def test_subagent_tools():
|
||||
"""Ensure subagents have correct tools."""
|
||||
options = get_sdk_options()
|
||||
|
||||
investigator = options.agents["investigator"]
|
||||
assert "Read" in investigator.tools
|
||||
assert "Write" not in investigator.tools # Read-only
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### ✅ Document Agent Purposes
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
agents={
|
||||
"investigator": AgentDefinition(
|
||||
# Clear, specific description
|
||||
description=(
|
||||
"Autonomous analyzer that determines if markdown errors "
|
||||
"are fixable or false positives"
|
||||
),
|
||||
prompt="...",
|
||||
tools=[...]
|
||||
)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### ✅ Document Workflow
|
||||
|
||||
```python
|
||||
"""
|
||||
Intelligent Markdown Linting Orchestrator
|
||||
|
||||
Architecture:
|
||||
- Orchestrator (main): Strategic coordination
|
||||
- Investigator subagent: Autonomous error analysis
|
||||
- Fixer subagent: Execute fixes with context
|
||||
|
||||
Workflow:
|
||||
1. Discovery: Run linter, parse output
|
||||
2. Triage: Classify errors (simple vs ambiguous)
|
||||
3. Investigation: Investigator analyzes ambiguous errors
|
||||
4. Fixing: Fixer applies fixes based on investigation
|
||||
5. Verification: Re-run linter to confirm fixes
|
||||
"""
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### ✅ Log Agent Communication
|
||||
|
||||
```python
|
||||
all_response_text = []
|
||||
|
||||
async for message in client.receive_response():
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
all_response_text.append(block.text)
|
||||
print(f"Agent: {block.text}")
|
||||
|
||||
# Save full transcript for debugging
|
||||
with open("debug_transcript.txt", "w") as f:
|
||||
f.write("\n\n".join(all_response_text))
|
||||
```
|
||||
|
||||
### ✅ Track Costs
|
||||
|
||||
```python
|
||||
async for message in client.receive_response():
|
||||
if isinstance(message, ResultMessage):
|
||||
if message.total_cost_usd:
|
||||
print(f"Total cost: ${message.total_cost_usd:.4f}")
|
||||
if message.duration_ms:
|
||||
print(f"Duration: {message.duration_ms}ms")
|
||||
```
|
||||
715
skills/claude-agent-sdk/references/custom-tools.md
Normal file
715
skills/claude-agent-sdk/references/custom-tools.md
Normal file
@@ -0,0 +1,715 @@
|
||||
# Custom Tools
|
||||
|
||||
> Build and integrate custom tools to extend Claude Agent SDK functionality
|
||||
|
||||
Custom tools allow you to extend Claude Code's capabilities with your own
|
||||
functionality through in-process MCP servers, enabling Claude to interact with
|
||||
external services, APIs, or perform specialized operations.
|
||||
|
||||
## Creating Custom Tools
|
||||
|
||||
Use the `createSdkMcpServer` and `tool` helper functions to define type-safe custom tools:
|
||||
|
||||
```typescript
|
||||
const customServer = createSdkMcpServer({
|
||||
name: "my-custom-tools",
|
||||
version: "1.0.0",
|
||||
tools: [
|
||||
tool(
|
||||
"get_weather",
|
||||
"Get current weather for a location",
|
||||
{
|
||||
location: z.string().describe("City name or coordinates"),
|
||||
units: z.enum(["celsius", "fahrenheit"]).default("celsius").describe("Temperature units")
|
||||
},
|
||||
async (args) => {
|
||||
// Call weather API
|
||||
const response = await fetch(
|
||||
`https://api.weather.com/v1/current?q=${args.location}&units=${args.units}`
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Temperature: ${data.temp}°\nConditions: ${data.conditions}\nHumidity: ${data.humidity}%`
|
||||
}]
|
||||
};
|
||||
}
|
||||
)
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
```python
|
||||
from claude_agent_sdk import tool, create_sdk_mcp_server, ClaudeSDKClient, ClaudeAgentOptions
|
||||
from typing import Any
|
||||
import aiohttp
|
||||
|
||||
# Define a custom tool using the @tool decorator
|
||||
@tool("get_weather", "Get current weather for a location", {"location": str, "units": str})
|
||||
async def get_weather(args: dict[str, Any]) -> dict[str, Any]:
|
||||
# Call weather API
|
||||
units = args.get('units', 'celsius')
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
f"https://api.weather.com/v1/current?q={args['location']}&units={units}"
|
||||
) as response:
|
||||
data = await response.json()
|
||||
|
||||
return {
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": f"Temperature: {data['temp']}°\nConditions: {data['conditions']}\nHumidity: {data['humidity']}%"
|
||||
}]
|
||||
}
|
||||
|
||||
# Create an SDK MCP server with the custom tool
|
||||
custom_server = create_sdk_mcp_server(
|
||||
name="my-custom-tools",
|
||||
version="1.0.0",
|
||||
tools=[get_weather] # Pass the decorated function
|
||||
)
|
||||
```
|
||||
|
||||
## Using Custom Tools
|
||||
|
||||
Pass the custom server to the `query` function via the `mcpServers` option as a dictionary/object.
|
||||
|
||||
**Important:** Custom MCP tools require streaming input mode. You must use an
|
||||
async generator/iterable for the `prompt` parameter - a simple string will not
|
||||
work with MCP servers.
|
||||
|
||||
### Tool Name Format
|
||||
|
||||
When MCP tools are exposed to Claude, their names follow a specific format:
|
||||
|
||||
* Pattern: `mcp__{server_name}__{tool_name}`
|
||||
* Example: A tool named `get_weather` in server `my-custom-tools` becomes `mcp__my-custom-tools__get_weather`
|
||||
|
||||
### Configuring Allowed Tools
|
||||
|
||||
You can control which tools Claude can use via the `allowedTools` option:
|
||||
|
||||
```typescript
|
||||
async function* generateMessages() {
|
||||
yield {
|
||||
type: "user",
|
||||
message: {
|
||||
content: "What's the weather in San Francisco?"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for await (const message of query({
|
||||
prompt: generateMessages(), // Use async generator for streaming input
|
||||
options: {
|
||||
mcpServers: {
|
||||
"my-custom-tools": customServer // Pass as object/dictionary, not array
|
||||
},
|
||||
// Optionally specify which tools Claude can use
|
||||
allowedTools: [
|
||||
"mcp__my-custom-tools__get_weather", // Allow the weather tool
|
||||
// Add other tools as needed
|
||||
],
|
||||
maxTurns: 3
|
||||
}
|
||||
})) {
|
||||
if (message.type === "result" && message.subtype === "success") {
|
||||
console.log(message.result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
|
||||
import asyncio
|
||||
|
||||
# Use the custom tools with Claude
|
||||
options = ClaudeAgentOptions(
|
||||
mcp_servers={"my-custom-tools": custom_server},
|
||||
allowed_tools=[
|
||||
"mcp__my-custom-tools__get_weather", # Allow the weather tool
|
||||
# Add other tools as needed
|
||||
]
|
||||
)
|
||||
|
||||
async def main():
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
await client.query("What's the weather in San Francisco?")
|
||||
|
||||
# Extract and print response
|
||||
async for msg in client.receive_response():
|
||||
print(msg)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Multiple Tools Example
|
||||
|
||||
When your MCP server has multiple tools, you can selectively allow them:
|
||||
|
||||
```typescript
|
||||
const multiToolServer = createSdkMcpServer({
|
||||
name: "utilities",
|
||||
version: "1.0.0",
|
||||
tools: [
|
||||
tool("calculate", "Perform calculations", { /* ... */ }, async (args) => { /* ... */ }),
|
||||
tool("translate", "Translate text", { /* ... */ }, async (args) => { /* ... */ }),
|
||||
tool("search_web", "Search the web", { /* ... */ }, async (args) => { /* ... */ })
|
||||
]
|
||||
});
|
||||
|
||||
// Allow only specific tools with streaming input
|
||||
async function* generateMessages() {
|
||||
yield {
|
||||
type: "user",
|
||||
message: {
|
||||
content: "Calculate 5 + 3 and translate 'hello' to Spanish"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for await (const message of query({
|
||||
prompt: generateMessages(), // Use async generator for streaming input
|
||||
options: {
|
||||
mcpServers: {
|
||||
utilities: multiToolServer
|
||||
},
|
||||
allowedTools: [
|
||||
"mcp__utilities__calculate", // Allow calculator
|
||||
"mcp__utilities__translate", // Allow translator
|
||||
// "mcp__utilities__search_web" is NOT allowed
|
||||
]
|
||||
}
|
||||
})) {
|
||||
// Process messages
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, tool, create_sdk_mcp_server
|
||||
from typing import Any
|
||||
import asyncio
|
||||
|
||||
# Define multiple tools using the @tool decorator
|
||||
@tool("calculate", "Perform calculations", {"expression": str})
|
||||
async def calculate(args: dict[str, Any]) -> dict[str, Any]:
|
||||
result = eval(args["expression"]) # Use safe eval in production
|
||||
return {"content": [{"type": "text", "text": f"Result: {result}"}]}
|
||||
|
||||
@tool("translate", "Translate text", {"text": str, "target_lang": str})
|
||||
async def translate(args: dict[str, Any]) -> dict[str, Any]:
|
||||
# Translation logic here
|
||||
return {"content": [{"type": "text", "text": f"Translated: {args['text']}"}]}
|
||||
|
||||
@tool("search_web", "Search the web", {"query": str})
|
||||
async def search_web(args: dict[str, Any]) -> dict[str, Any]:
|
||||
# Search logic here
|
||||
return {"content": [{"type": "text", "text": f"Search results for: {args['query']}"}]}
|
||||
|
||||
multi_tool_server = create_sdk_mcp_server(
|
||||
name="utilities",
|
||||
version="1.0.0",
|
||||
tools=[calculate, translate, search_web] # Pass decorated functions
|
||||
)
|
||||
|
||||
# Allow only specific tools with streaming input
|
||||
async def message_generator():
|
||||
yield {
|
||||
"type": "user",
|
||||
"message": {
|
||||
"role": "user",
|
||||
"content": "Calculate 5 + 3 and translate 'hello' to Spanish"
|
||||
}
|
||||
}
|
||||
|
||||
async for message in query(
|
||||
prompt=message_generator(), # Use async generator for streaming input
|
||||
options=ClaudeAgentOptions(
|
||||
mcp_servers={"utilities": multi_tool_server},
|
||||
allowed_tools=[
|
||||
"mcp__utilities__calculate", # Allow calculator
|
||||
"mcp__utilities__translate", # Allow translator
|
||||
# "mcp__utilities__search_web" is NOT allowed
|
||||
]
|
||||
)
|
||||
):
|
||||
if hasattr(message, 'result'):
|
||||
print(message.result)
|
||||
```
|
||||
|
||||
## Type Safety with Python
|
||||
|
||||
The `@tool` decorator supports various schema definition approaches for type safety:
|
||||
|
||||
```typescript
|
||||
tool(
|
||||
"process_data",
|
||||
"Process structured data with type safety",
|
||||
{
|
||||
// Zod schema defines both runtime validation and TypeScript types
|
||||
data: z.object({
|
||||
name: z.string(),
|
||||
age: z.number().min(0).max(150),
|
||||
email: z.string().email(),
|
||||
preferences: z.array(z.string()).optional()
|
||||
}),
|
||||
format: z.enum(["json", "csv", "xml"]).default("json")
|
||||
},
|
||||
async (args) => {
|
||||
// args is fully typed based on the schema
|
||||
// TypeScript knows: args.data.name is string, args.data.age is number, etc.
|
||||
console.log(`Processing ${args.data.name}'s data as ${args.format}`);
|
||||
|
||||
// Your processing logic here
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Processed data for ${args.data.name}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
```python
|
||||
from typing import Any
|
||||
|
||||
# Simple type mapping - recommended for most cases
|
||||
@tool(
|
||||
"process_data",
|
||||
"Process structured data with type safety",
|
||||
{
|
||||
"name": str,
|
||||
"age": int,
|
||||
"email": str,
|
||||
"preferences": list # Optional parameters can be handled in the function
|
||||
}
|
||||
)
|
||||
async def process_data(args: dict[str, Any]) -> dict[str, Any]:
|
||||
# Access arguments with type hints for IDE support
|
||||
name = args["name"]
|
||||
age = args["age"]
|
||||
email = args["email"]
|
||||
preferences = args.get("preferences", [])
|
||||
|
||||
print(f"Processing {name}'s data (age: {age})")
|
||||
|
||||
return {
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": f"Processed data for {name}"
|
||||
}]
|
||||
}
|
||||
|
||||
# For more complex schemas, you can use JSON Schema format
|
||||
@tool(
|
||||
"advanced_process",
|
||||
"Process data with advanced validation",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "integer", "minimum": 0, "maximum": 150},
|
||||
"email": {"type": "string", "format": "email"},
|
||||
"format": {"type": "string", "enum": ["json", "csv", "xml"], "default": "json"}
|
||||
},
|
||||
"required": ["name", "age", "email"]
|
||||
}
|
||||
)
|
||||
async def advanced_process(args: dict[str, Any]) -> dict[str, Any]:
|
||||
# Process with advanced schema validation
|
||||
return {
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": f"Advanced processing for {args['name']}"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Handle errors gracefully to provide meaningful feedback:
|
||||
|
||||
```typescript
|
||||
tool(
|
||||
"fetch_data",
|
||||
"Fetch data from an API",
|
||||
{
|
||||
endpoint: z.string().url().describe("API endpoint URL")
|
||||
},
|
||||
async (args) => {
|
||||
try {
|
||||
const response = await fetch(args.endpoint);
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `API error: ${response.status} ${response.statusText}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(data, null, 2)
|
||||
}]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Failed to fetch data: ${error.message}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
```python
|
||||
import json
|
||||
import aiohttp
|
||||
from typing import Any
|
||||
|
||||
@tool(
|
||||
"fetch_data",
|
||||
"Fetch data from an API",
|
||||
{"endpoint": str} # Simple schema
|
||||
)
|
||||
async def fetch_data(args: dict[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(args["endpoint"]) as response:
|
||||
if response.status != 200:
|
||||
return {
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": f"API error: {response.status} {response.reason}"
|
||||
}]
|
||||
}
|
||||
|
||||
data = await response.json()
|
||||
return {
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": json.dumps(data, indent=2)
|
||||
}]
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": f"Failed to fetch data: {str(e)}"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## Example Tools
|
||||
|
||||
### Database Query Tool
|
||||
|
||||
```typescript
|
||||
const databaseServer = createSdkMcpServer({
|
||||
name: "database-tools",
|
||||
version: "1.0.0",
|
||||
tools: [
|
||||
tool(
|
||||
"query_database",
|
||||
"Execute a database query",
|
||||
{
|
||||
query: z.string().describe("SQL query to execute"),
|
||||
params: z.array(z.any()).optional().describe("Query parameters")
|
||||
},
|
||||
async (args) => {
|
||||
const results = await db.query(args.query, args.params || []);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Found ${results.length} rows:\n${JSON.stringify(results, null, 2)}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
)
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
```python
|
||||
from typing import Any
|
||||
import json
|
||||
|
||||
@tool(
|
||||
"query_database",
|
||||
"Execute a database query",
|
||||
{"query": str, "params": list} # Simple schema with list type
|
||||
)
|
||||
async def query_database(args: dict[str, Any]) -> dict[str, Any]:
|
||||
results = await db.query(args["query"], args.get("params", []))
|
||||
return {
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": f"Found {len(results)} rows:\n{json.dumps(results, indent=2)}"
|
||||
}]
|
||||
}
|
||||
|
||||
database_server = create_sdk_mcp_server(
|
||||
name="database-tools",
|
||||
version="1.0.0",
|
||||
tools=[query_database] # Pass the decorated function
|
||||
)
|
||||
```
|
||||
|
||||
### API Gateway Tool
|
||||
|
||||
```typescript
|
||||
const apiGatewayServer = createSdkMcpServer({
|
||||
name: "api-gateway",
|
||||
version: "1.0.0",
|
||||
tools: [
|
||||
tool(
|
||||
"api_request",
|
||||
"Make authenticated API requests to external services",
|
||||
{
|
||||
service: z.enum(["stripe", "github", "openai", "slack"]).describe("Service to call"),
|
||||
endpoint: z.string().describe("API endpoint path"),
|
||||
method: z.enum(["GET", "POST", "PUT", "DELETE"]).describe("HTTP method"),
|
||||
body: z.record(z.any()).optional().describe("Request body"),
|
||||
query: z.record(z.string()).optional().describe("Query parameters")
|
||||
},
|
||||
async (args) => {
|
||||
const config = {
|
||||
stripe: { baseUrl: "<https://api.stripe.com/v1>", key: process.env.STRIPE_KEY },
|
||||
github: { baseUrl: "<https://api.github.com>", key: process.env.GITHUB_TOKEN },
|
||||
openai: { baseUrl: "<https://api.openai.com/v1>", key: process.env.OPENAI_KEY },
|
||||
slack: { baseUrl: "<https://slack.com/api>", key: process.env.SLACK_TOKEN }
|
||||
};
|
||||
|
||||
const { baseUrl, key } = config[args.service];
|
||||
const url = new URL(`${baseUrl}${args.endpoint}`);
|
||||
|
||||
if (args.query) {
|
||||
Object.entries(args.query).forEach(([k, v]) => url.searchParams.set(k, v));
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: args.method,
|
||||
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
|
||||
body: args.body ? JSON.stringify(args.body) : undefined
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(data, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
)
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
```python
|
||||
import os
|
||||
import json
|
||||
import aiohttp
|
||||
from typing import Any
|
||||
|
||||
# For complex schemas with enums, use JSON Schema format
|
||||
@tool(
|
||||
"api_request",
|
||||
"Make authenticated API requests to external services",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"service": {"type": "string", "enum": ["stripe", "github", "openai", "slack"]},
|
||||
"endpoint": {"type": "string"},
|
||||
"method": {"type": "string", "enum": ["GET", "POST", "PUT", "DELETE"]},
|
||||
"body": {"type": "object"},
|
||||
"query": {"type": "object"}
|
||||
},
|
||||
"required": ["service", "endpoint", "method"]
|
||||
}
|
||||
)
|
||||
async def api_request(args: dict[str, Any]) -> dict[str, Any]:
|
||||
config = {
|
||||
"stripe": {"base_url": "<https://api.stripe.com/v1>", "key": os.environ["STRIPE_KEY"]},
|
||||
"github": {"base_url": "<https://api.github.com>", "key": os.environ["GITHUB_TOKEN"]},
|
||||
"openai": {"base_url": "<https://api.openai.com/v1>", "key": os.environ["OPENAI_KEY"]},
|
||||
"slack": {"base_url": "<https://slack.com/api>", "key": os.environ["SLACK_TOKEN"]}
|
||||
}
|
||||
|
||||
service_config = config[args["service"]]
|
||||
url = f"{service_config['base_url']}{args['endpoint']}"
|
||||
|
||||
if args.get("query"):
|
||||
params = "&".join([f"{k}={v}" for k, v in args["query"].items()])
|
||||
url += f"?{params}"
|
||||
|
||||
headers = {"Authorization": f"Bearer {service_config['key']}", "Content-Type": "application/json"}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.request(
|
||||
args["method"], url, headers=headers, json=args.get("body")
|
||||
) as response:
|
||||
data = await response.json()
|
||||
return {
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": json.dumps(data, indent=2)
|
||||
}]
|
||||
}
|
||||
|
||||
api_gateway_server = create_sdk_mcp_server(
|
||||
name="api-gateway",
|
||||
version="1.0.0",
|
||||
tools=[api_request] # Pass the decorated function
|
||||
)
|
||||
```
|
||||
|
||||
## Calculator Tool
|
||||
|
||||
```typescript
|
||||
const calculatorServer = createSdkMcpServer({
|
||||
name: "calculator",
|
||||
version: "1.0.0",
|
||||
tools: [
|
||||
tool(
|
||||
"calculate",
|
||||
"Perform mathematical calculations",
|
||||
{
|
||||
expression: z.string().describe("Mathematical expression to evaluate"),
|
||||
precision: z.number().optional().default(2).describe("Decimal precision")
|
||||
},
|
||||
async (args) => {
|
||||
try {
|
||||
// Use a safe math evaluation library in production
|
||||
const result = eval(args.expression); // Example only!
|
||||
const formatted = Number(result).toFixed(args.precision);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `${args.expression} = ${formatted}`
|
||||
}]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Error: Invalid expression - ${error.message}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
),
|
||||
tool(
|
||||
"compound_interest",
|
||||
"Calculate compound interest for an investment",
|
||||
{
|
||||
principal: z.number().positive().describe("Initial investment amount"),
|
||||
rate: z.number().describe("Annual interest rate (as decimal, e.g., 0.05 for 5%)"),
|
||||
time: z.number().positive().describe("Investment period in years"),
|
||||
n: z.number().positive().default(12).describe("Compounding frequency per year")
|
||||
},
|
||||
async (args) => {
|
||||
const amount = args.principal * Math.pow(1 + args.rate / args.n, args.n * args.time);
|
||||
const interest = amount - args.principal;
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Investment Analysis:\n` +
|
||||
`Principal: $${args.principal.toFixed(2)}\n` +
|
||||
`Rate: ${(args.rate * 100).toFixed(2)}%\n` +
|
||||
`Time: ${args.time} years\n` +
|
||||
`Compounding: ${args.n} times per year\n\n` +
|
||||
`Final Amount: $${amount.toFixed(2)}\n` +
|
||||
`Interest Earned: $${interest.toFixed(2)}\n` +
|
||||
`Return: ${((interest / args.principal) * 100).toFixed(2)}%`
|
||||
}]
|
||||
};
|
||||
}
|
||||
)
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
```python
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
@tool(
|
||||
"calculate",
|
||||
"Perform mathematical calculations",
|
||||
{"expression": str, "precision": int} # Simple schema
|
||||
)
|
||||
async def calculate(args: dict[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
# Use a safe math evaluation library in production
|
||||
result = eval(args["expression"], {"__builtins__": {}})
|
||||
precision = args.get("precision", 2)
|
||||
formatted = round(result, precision)
|
||||
|
||||
return {
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": f"{args['expression']} = {formatted}"
|
||||
}]
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": f"Error: Invalid expression - {str(e)}"
|
||||
}]
|
||||
}
|
||||
|
||||
@tool(
|
||||
"compound_interest",
|
||||
"Calculate compound interest for an investment",
|
||||
{"principal": float, "rate": float, "time": float, "n": int}
|
||||
)
|
||||
async def compound_interest(args: dict[str, Any]) -> dict[str, Any]:
|
||||
principal = args["principal"]
|
||||
rate = args["rate"]
|
||||
time = args["time"]
|
||||
n = args.get("n", 12)
|
||||
|
||||
amount = principal * (1 + rate / n) ** (n * time)
|
||||
interest = amount - principal
|
||||
|
||||
return {
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": f"""Investment Analysis:
|
||||
Principal: ${principal:.2f}
|
||||
Rate: {rate * 100:.2f}%
|
||||
Time: {time} years
|
||||
Compounding: {n} times per year
|
||||
|
||||
Final Amount: ${amount:.2f}
|
||||
Interest Earned: ${interest:.2f}
|
||||
Return: {(interest / principal) * 100:.2f}%"""
|
||||
}]
|
||||
}
|
||||
|
||||
calculator_server = create_sdk_mcp_server(
|
||||
name="calculator",
|
||||
version="1.0.0",
|
||||
tools=[calculate, compound_interest] # Pass decorated functions
|
||||
)
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [TypeScript SDK Reference](/en/api/agent-sdk/typescript)
|
||||
* [Python SDK Reference](/en/api/agent-sdk/python)
|
||||
* [MCP Documentation](https://modelcontextprotocol.io)
|
||||
* [SDK Overview](/en/api/agent-sdk/overview)
|
||||
374
skills/claude-agent-sdk/references/hooks-guide.md
Normal file
374
skills/claude-agent-sdk/references/hooks-guide.md
Normal file
@@ -0,0 +1,374 @@
|
||||
# Hook Patterns and Configuration
|
||||
|
||||
This guide covers hook patterns for intercepting and modifying Claude Agent SDK behavior.
|
||||
|
||||
## Overview
|
||||
|
||||
Hooks allow you to intercept SDK events and modify behavior at key points in execution. Common uses:
|
||||
|
||||
- Control tool execution (approve/deny/modify)
|
||||
- Add context to prompts
|
||||
- Review tool outputs
|
||||
- Stop execution on errors
|
||||
- Log activity
|
||||
|
||||
> **⚠️ IMPORTANT:** The Python SDK does **NOT** support `SessionStart`, `SessionEnd`, or `Notification` hooks due to setup limitations. Only the 6 hook types listed below are supported. Attempting to use unsupported hooks will result in them never firing.
|
||||
|
||||
## Hook Types
|
||||
|
||||
| Hook | When It Fires | Common Uses |
|
||||
|------|---------------|-------------|
|
||||
| `PreToolUse` | Before tool execution | Approve/deny/modify tool calls |
|
||||
| `PostToolUse` | After tool execution | Review output, add context, stop on errors |
|
||||
| `UserPromptSubmit` | Before processing user prompt | Add context, modify prompt |
|
||||
| `Stop` | When execution stops | Cleanup, final logging |
|
||||
| `SubagentStop` | When a subagent stops | Capture subagent results, cleanup |
|
||||
| `PreCompact` | Before message compaction | Review/modify messages before compaction |
|
||||
|
||||
## Hook Configuration
|
||||
|
||||
Hooks are configured via the `hooks` parameter in `ClaudeAgentOptions`:
|
||||
|
||||
```python
|
||||
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
hooks={
|
||||
"PreToolUse": [
|
||||
HookMatcher(matcher="Bash", hooks=[check_bash_command]),
|
||||
],
|
||||
"PostToolUse": [
|
||||
HookMatcher(matcher="Bash", hooks=[review_output]),
|
||||
],
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Hook Function Signature
|
||||
|
||||
All hooks have the same signature:
|
||||
|
||||
```python
|
||||
from claude_agent_sdk.types import HookInput, HookContext, HookJSONOutput
|
||||
|
||||
async def hook_function(
|
||||
input_data: HookInput,
|
||||
tool_use_id: str | None,
|
||||
context: HookContext
|
||||
) -> HookJSONOutput:
|
||||
"""
|
||||
Args:
|
||||
input_data: Hook-specific data (tool_name, tool_input, etc.)
|
||||
tool_use_id: Unique ID for this tool use (if applicable)
|
||||
context: Additional context about the execution
|
||||
|
||||
Returns:
|
||||
HookJSONOutput: Dict with hook-specific fields
|
||||
"""
|
||||
return {} # Empty dict = no action
|
||||
```
|
||||
|
||||
## HookJSONOutput Fields
|
||||
|
||||
| Field | Type | Use Case | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `continue_` | `bool` | Stop execution | Set to `False` to halt execution |
|
||||
| `stopReason` | `str` | Stop execution | Explanation for why execution stopped |
|
||||
| `reason` | `str` | Logging/debugging | Explanation of hook decision |
|
||||
| `systemMessage` | `str` | User feedback | Message shown to user |
|
||||
| `hookSpecificOutput` | `dict` | Hook-specific data | Additional hook-specific fields |
|
||||
|
||||
### PreToolUse Hook-Specific Fields
|
||||
|
||||
```python
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "allow" | "deny", # Control tool execution
|
||||
"permissionDecisionReason": "Explanation for decision",
|
||||
"modifiedInput": {...} # Optional: Modified tool input
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PostToolUse Hook-Specific Fields
|
||||
|
||||
```python
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PostToolUse",
|
||||
"additionalContext": "Extra context based on tool output"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UserPromptSubmit Hook-Specific Fields
|
||||
|
||||
```python
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "UserPromptSubmit",
|
||||
"updatedPrompt": "Modified prompt text"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Stop Hook-Specific Fields
|
||||
|
||||
```python
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "Stop"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SubagentStop Hook-Specific Fields
|
||||
|
||||
```python
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SubagentStop"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PreCompact Hook-Specific Fields
|
||||
|
||||
```python
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreCompact",
|
||||
"additionalContext": "Context to preserve during compaction"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### 1. Block Dangerous Commands (PreToolUse)
|
||||
|
||||
```python
|
||||
async def check_bash_command(
|
||||
input_data: HookInput,
|
||||
tool_use_id: str | None,
|
||||
context: HookContext
|
||||
) -> HookJSONOutput:
|
||||
"""Prevent dangerous bash commands."""
|
||||
if input_data["tool_name"] != "Bash":
|
||||
return {}
|
||||
|
||||
command = input_data["tool_input"].get("command", "")
|
||||
dangerous_patterns = ["rm -rf", "sudo", "chmod 777", "dd if="]
|
||||
|
||||
for pattern in dangerous_patterns:
|
||||
if pattern in command:
|
||||
return {
|
||||
"reason": f"Blocked dangerous command pattern: {pattern}",
|
||||
"systemMessage": f"🚫 Blocked: {pattern}",
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": f"Command contains: {pattern}"
|
||||
}
|
||||
}
|
||||
|
||||
return {} # Allow by default
|
||||
```
|
||||
|
||||
### 2. Review Tool Output (PostToolUse)
|
||||
|
||||
```python
|
||||
async def review_tool_output(
|
||||
input_data: HookInput,
|
||||
tool_use_id: str | None,
|
||||
context: HookContext
|
||||
) -> HookJSONOutput:
|
||||
"""Add context based on tool output."""
|
||||
tool_response = input_data.get("tool_response", "")
|
||||
|
||||
if "error" in str(tool_response).lower():
|
||||
return {
|
||||
"systemMessage": "⚠️ Command produced an error",
|
||||
"reason": "Tool execution failed",
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PostToolUse",
|
||||
"additionalContext": "Consider checking command syntax or permissions."
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
```
|
||||
|
||||
### 4. Stop on Critical Errors (PostToolUse)
|
||||
|
||||
```python
|
||||
async def stop_on_critical_error(
|
||||
input_data: HookInput,
|
||||
tool_use_id: str | None,
|
||||
context: HookContext
|
||||
) -> HookJSONOutput:
|
||||
"""Halt execution on critical errors."""
|
||||
tool_response = input_data.get("tool_response", "")
|
||||
|
||||
if "critical" in str(tool_response).lower():
|
||||
return {
|
||||
"continue_": False,
|
||||
"stopReason": "Critical error detected - halting for safety",
|
||||
"systemMessage": "🛑 Execution stopped: critical error"
|
||||
}
|
||||
|
||||
return {"continue_": True}
|
||||
```
|
||||
|
||||
### 5. Redirect File Writes (PreToolUse)
|
||||
|
||||
```python
|
||||
async def safe_file_writes(
|
||||
input_data: HookInput,
|
||||
tool_use_id: str | None,
|
||||
context: HookContext
|
||||
) -> HookJSONOutput:
|
||||
"""Redirect writes to safe directory."""
|
||||
if input_data["tool_name"] not in ["Write", "Edit"]:
|
||||
return {}
|
||||
|
||||
file_path = input_data["tool_input"].get("file_path", "")
|
||||
|
||||
# Block writes to system directories
|
||||
if file_path.startswith("/etc/") or file_path.startswith("/usr/"):
|
||||
return {
|
||||
"reason": f"Blocked write to system directory: {file_path}",
|
||||
"systemMessage": "🚫 Cannot write to system directories",
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": "System directory protection"
|
||||
}
|
||||
}
|
||||
|
||||
# Redirect to safe directory
|
||||
if not file_path.startswith("./safe/"):
|
||||
safe_path = f"./safe/{file_path.split('/')[-1]}"
|
||||
modified_input = input_data["tool_input"].copy()
|
||||
modified_input["file_path"] = safe_path
|
||||
|
||||
return {
|
||||
"reason": f"Redirected write from {file_path} to {safe_path}",
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "allow",
|
||||
"permissionDecisionReason": "Redirected to safe directory",
|
||||
"modifiedInput": modified_input
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
```
|
||||
|
||||
## Hook Matcher Patterns
|
||||
|
||||
`HookMatcher` determines when hooks fire:
|
||||
|
||||
```python
|
||||
# Match specific tool
|
||||
HookMatcher(matcher="Bash", hooks=[check_bash])
|
||||
|
||||
# Match all tools
|
||||
HookMatcher(matcher=None, hooks=[log_all_tools])
|
||||
|
||||
# Multiple hooks for same matcher
|
||||
HookMatcher(
|
||||
matcher="Write",
|
||||
hooks=[check_permissions, log_write, backup_file]
|
||||
)
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```python
|
||||
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, HookMatcher
|
||||
|
||||
async def block_dangerous_bash(input_data, tool_use_id, context):
|
||||
if input_data["tool_name"] != "Bash":
|
||||
return {}
|
||||
|
||||
command = input_data["tool_input"].get("command", "")
|
||||
if "rm -rf" in command:
|
||||
return {
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": "Dangerous rm -rf detected"
|
||||
}
|
||||
}
|
||||
return {}
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
allowed_tools=["Bash", "Read", "Write"],
|
||||
hooks={
|
||||
"PreToolUse": [
|
||||
HookMatcher(matcher="Bash", hooks=[block_dangerous_bash])
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
async with ClaudeSDKClient(options=options) as client:
|
||||
await client.query("Run a safe bash command")
|
||||
async for message in client.receive_response():
|
||||
# Process messages
|
||||
pass
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Return early** - Return empty dict `{}` when hook doesn't apply
|
||||
2. **Be specific** - Clear `reason` and `systemMessage` fields help debugging
|
||||
3. **Use matchers** - Filter hooks to relevant tools with `matcher` parameter
|
||||
4. **Chain hooks** - Multiple hooks can process same event
|
||||
5. **Handle errors** - Hooks should be defensive and handle missing data
|
||||
6. **Log decisions** - Use `reason` field to explain hook decisions
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
❌ **Blocking without explanation**
|
||||
|
||||
```python
|
||||
return {
|
||||
"hookSpecificOutput": {
|
||||
"permissionDecision": "deny"
|
||||
# Missing permissionDecisionReason
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
✅ **Clear explanations**
|
||||
|
||||
```python
|
||||
return {
|
||||
"reason": "Command contains dangerous pattern: rm -rf",
|
||||
"systemMessage": "🚫 Blocked dangerous command",
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": "Safety: rm -rf detected"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
❌ **Ignoring tool_name in PreToolUse**
|
||||
|
||||
```python
|
||||
# This will fire for ALL tools
|
||||
async def hook(input_data, tool_use_id, context):
|
||||
command = input_data["tool_input"]["command"] # Crashes on non-Bash tools
|
||||
```
|
||||
|
||||
✅ **Filter by tool_name**
|
||||
|
||||
```python
|
||||
async def hook(input_data, tool_use_id, context):
|
||||
if input_data["tool_name"] != "Bash":
|
||||
return {}
|
||||
command = input_data["tool_input"].get("command", "")
|
||||
```
|
||||
247
skills/claude-agent-sdk/references/sessions.md
Normal file
247
skills/claude-agent-sdk/references/sessions.md
Normal file
@@ -0,0 +1,247 @@
|
||||
# Session Management
|
||||
|
||||
> Understanding how the Claude Agent SDK handles sessions and session resumption
|
||||
|
||||
## Session Management
|
||||
|
||||
The Claude Agent SDK provides session management capabilities for handling conversation state and resumption. Sessions allow you to continue conversations across multiple interactions while maintaining full context.
|
||||
|
||||
## How Sessions Work
|
||||
|
||||
When you start a new query, the SDK automatically creates a session and returns a session ID in the initial system message. You can capture this ID to resume the session later.
|
||||
|
||||
### Getting the Session ID
|
||||
|
||||
options: { options: {
|
||||
model: "claude-sonnet-4-5"
|
||||
}
|
||||
})
|
||||
|
||||
for await (const message of response) {
|
||||
// The first message is a system init message with the session ID
|
||||
|
||||
```python
|
||||
if (message.type === 'system' && message.subtype === 'init') { if (message.type === 'system' && message.subtype === 'init') {
|
||||
sessionId = message.session_id
|
||||
console.log(`Session started with ID: ${sessionId}`)
|
||||
// You can save this ID for later resumption
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
// Process other messages... // Process other messages...
|
||||
console.log(message)
|
||||
}
|
||||
|
||||
// Later, you can use the saved sessionId to resume
|
||||
if (sessionId) {
|
||||
const resumedResponse = query({
|
||||
|
||||
```python
|
||||
prompt: "Continue where we left off",
|
||||
options: {
|
||||
resume: sessionId
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
```python Python theme={null}
|
||||
from claude_agent_sdk import query, ClaudeAgentOptions
|
||||
|
||||
session_id = None
|
||||
|
||||
async for message in query(
|
||||
prompt="Help me build a web application",
|
||||
```
|
||||
|
||||
```python
|
||||
options=ClaudeAgentOptions( options=ClaudeAgentOptions(
|
||||
model="claude-sonnet-4-5"
|
||||
)
|
||||
):
|
||||
|
||||
## The first message is a system init message with the session ID
|
||||
```
|
||||
|
||||
```python
|
||||
if hasattr(message, 'subtype') and message.subtype == 'init': if hasattr(message, 'subtype') and message.subtype == 'init':
|
||||
session_id = message.data.get('session_id')
|
||||
print(f"Session started with ID: {session_id}")
|
||||
|
||||
# You can save this ID for later resumption
|
||||
|
||||
```
|
||||
|
||||
## Process other messages... # Process other messages
|
||||
|
||||
print(message)
|
||||
|
||||
## Later, you can use the saved session_id to resume
|
||||
|
||||
if session_id:
|
||||
async for message in query(
|
||||
|
||||
```python
|
||||
prompt="Continue where we left off", prompt="Continue where we left off",
|
||||
options=ClaudeAgentOptions(
|
||||
resume=session_id
|
||||
)
|
||||
):
|
||||
print(message)
|
||||
```
|
||||
|
||||
## Resuming Sessions
|
||||
|
||||
The SDK supports resuming sessions from previous conversation states, enabling continuous development workflows. Use the `resume` option with a session ID to continue a previous conversation.
|
||||
|
||||
options: { options: {
|
||||
resume: "session-xyz", // Session ID from previous conversation
|
||||
model: "claude-sonnet-4-5",
|
||||
allowedTools: ["Read", "Edit", "Write", "Glob", "Grep", "Bash"]
|
||||
}
|
||||
})
|
||||
|
||||
// The conversation continues with full context from the previous session
|
||||
for await (const message of response) {
|
||||
console.log(message)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```python Python theme={null}
|
||||
from claude_agent_sdk import query, ClaudeAgentOptions
|
||||
|
||||
## Resume a previous session using its ID
|
||||
|
||||
async for message in query(
|
||||
prompt="Continue implementing the authentication system from where we left off",
|
||||
```
|
||||
|
||||
```python
|
||||
options=ClaudeAgentOptions( options=ClaudeAgentOptions(
|
||||
resume="session-xyz", # Session ID from previous conversation
|
||||
model="claude-sonnet-4-5",
|
||||
allowed_tools=["Read", "Edit", "Write", "Glob", "Grep", "Bash"]
|
||||
)
|
||||
):
|
||||
print(message)
|
||||
|
||||
## The conversation continues with full context from the previous session
|
||||
```
|
||||
|
||||
The SDK automatically handles loading the conversation history and context when you resume a session, allowing Claude to continue exactly where it left off.
|
||||
|
||||
## Forking Sessions
|
||||
|
||||
When resuming a session, you can choose to either continue the original session or fork it into a new branch. By default, resuming continues the original session. Use the `forkSession` option (TypeScript) or `fork_session` option (Python) to create a new session ID that starts from the resumed state.
|
||||
|
||||
### When to Fork a Session
|
||||
|
||||
Forking is useful when you want to:
|
||||
|
||||
* Explore different approaches from the same starting point
|
||||
* Create multiple conversation branches without modifying the original
|
||||
* Test changes without affecting the original session history
|
||||
* Maintain separate conversation paths for different experiments
|
||||
|
||||
### Forking vs Continuing
|
||||
|
||||
| Behavior | `forkSession: false` (default) | `forkSession: true` |
|
||||
| -------------------- | ------------------------------ | ------------------------------------ |
|
||||
| **Session ID** | Same as original | New session ID generated |
|
||||
| **History** | Appends to original session | Creates new branch from resume point |
|
||||
| **Original Session** | Modified | Preserved unchanged |
|
||||
| **Use Case** | Continue linear conversation | Branch to explore alternatives |
|
||||
|
||||
### Example: Forking a Session
|
||||
|
||||
options: { model: "claude-sonnet-4-5" } options: { model: "claude-sonnet-4-5" }
|
||||
})
|
||||
|
||||
for await (const message of response) {
|
||||
if (message.type === 'system' && message.subtype === 'init') {
|
||||
|
||||
```python
|
||||
sessionId = message.session_id sessionId = message.session_id
|
||||
console.log(`Original session: ${sessionId}`)
|
||||
}
|
||||
|
||||
// Fork the session to try a different approach
|
||||
const forkedResponse = query({
|
||||
prompt: "Now let's redesign this as a GraphQL API instead",
|
||||
```python
|
||||
options: { options: {
|
||||
resume: sessionId,
|
||||
forkSession: true, // Creates a new session ID
|
||||
model: "claude-sonnet-4-5"
|
||||
}
|
||||
})
|
||||
|
||||
for await (const message of forkedResponse) {
|
||||
if (message.type === 'system' && message.subtype === 'init') {
|
||||
```python
|
||||
console.log(`Forked session: ${message.session_id}`) console.log(`Forked session: ${message.session_id}`)
|
||||
// This will be a different session ID
|
||||
}
|
||||
|
||||
// The original session remains unchanged and can still be resumed
|
||||
const originalContinued = query({
|
||||
prompt: "Add authentication to the REST API",
|
||||
```python
|
||||
options: { options: {
|
||||
resume: sessionId,
|
||||
forkSession: false, // Continue original session (default)
|
||||
model: "claude-sonnet-4-5"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```python Python theme={null}
|
||||
from claude_agent_sdk import query, ClaudeAgentOptions
|
||||
|
||||
## First, capture the session ID
|
||||
|
||||
session_id = None
|
||||
|
||||
async for message in query(
|
||||
prompt="Help me design a REST API",
|
||||
```python
|
||||
options=ClaudeAgentOptions(model="claude-sonnet-4-5") options=ClaudeAgentOptions(model="claude-sonnet-4-5")
|
||||
):
|
||||
if hasattr(message, 'subtype') and message.subtype == 'init':
|
||||
```python
|
||||
session_id = message.data.get('session_id') session_id = message.data.get('session_id')
|
||||
print(f"Original session: {session_id}")
|
||||
|
||||
## Fork the session to try a different approach
|
||||
|
||||
async for message in query(
|
||||
prompt="Now let's redesign this as a GraphQL API instead",
|
||||
```python
|
||||
options=ClaudeAgentOptions( options=ClaudeAgentOptions(
|
||||
resume=session_id,
|
||||
fork_session=True, # Creates a new session ID
|
||||
model="claude-sonnet-4-5"
|
||||
)
|
||||
):
|
||||
if hasattr(message, 'subtype') and message.subtype == 'init':
|
||||
```python
|
||||
forked_id = message.data.get('session_id') forked_id = message.data.get('session_id')
|
||||
print(f"Forked session: {forked_id}")
|
||||
|
||||
## This will be a different session ID
|
||||
|
||||
## The original session remains unchanged and can still be resumed
|
||||
|
||||
async for message in query(
|
||||
prompt="Add authentication to the REST API",
|
||||
```python
|
||||
options=ClaudeAgentOptions( options=ClaudeAgentOptions(
|
||||
resume=session_id,
|
||||
fork_session=False, # Continue original session (default)
|
||||
model="claude-sonnet-4-5"
|
||||
)
|
||||
):
|
||||
print(message)
|
||||
```
|
||||
286
skills/claude-agent-sdk/references/skills.md
Normal file
286
skills/claude-agent-sdk/references/skills.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# Agent Skills in the SDK
|
||||
|
||||
> Extend Claude with specialized capabilities using Agent Skills in the Claude Agent SDK
|
||||
|
||||
## Overview
|
||||
|
||||
Agent Skills extend Claude with specialized capabilities that Claude autonomously invokes when relevant. Skills are packaged as `SKILL.md` files containing instructions, descriptions, and optional supporting resources.
|
||||
|
||||
For comprehensive information about Skills, including benefits, architecture, and authoring guidelines, see the [Agent Skills overview](/en/docs/agents-and-tools/agent-skills/overview).
|
||||
|
||||
## How Skills Work with the SDK
|
||||
|
||||
When using the Claude Agent SDK, Skills are:
|
||||
|
||||
1. **Defined as filesystem artifacts**: Created as `SKILL.md` files in specific directories (`.claude/skills/`)
|
||||
2. **Loaded from filesystem**: Skills are loaded from configured filesystem locations. You must specify `settingSources` (TypeScript) or `setting_sources` (Python) to load Skills from the filesystem
|
||||
3. **Automatically discovered**: Once filesystem settings are loaded, Skill metadata is discovered at startup from user and project directories; full content loaded when triggered
|
||||
4. **Model-invoked**: Claude autonomously chooses when to use them based on context
|
||||
5. **Enabled via allowed\_tools**: Add `"Skill"` to your `allowed_tools` to enable Skills
|
||||
|
||||
Unlike subagents (which can be defined programmatically), Skills must be created as filesystem artifacts. The SDK does not provide a programmatic API for registering Skills.
|
||||
|
||||
**Default behavior**: By default, the SDK does not load any filesystem settings. To use Skills, you must explicitly configure `settingSources: ['user', 'project']` (TypeScript) or `setting_sources=["user", "project"]` (Python) in your options.
|
||||
|
||||
## Using Skills with the SDK
|
||||
|
||||
To use Skills with the SDK, you need to:
|
||||
|
||||
1. Include `"Skill"` in your `allowed_tools` configuration
|
||||
2. Configure `settingSources`/`setting_sources` to load Skills from the filesystem
|
||||
|
||||
Once configured, Claude automatically discovers Skills from the specified directories and invokes them when relevant to the user's request.
|
||||
|
||||
```python Python theme={null}
|
||||
import asyncio
|
||||
from claude_agent_sdk import query, ClaudeAgentOptions
|
||||
|
||||
async def main():
|
||||
options = ClaudeAgentOptions(
|
||||
```
|
||||
|
||||
```python
|
||||
cwd="/path/to/project", # Project with .claude/skills/ cwd="/path/to/project", # Project with .claude/skills/
|
||||
setting_sources=["user", "project"], # Load Skills from filesystem
|
||||
allowed_tools=["Skill", "Read", "Write", "Bash"] # Enable Skill tool
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
```python
|
||||
async for message in query( async for message in query(
|
||||
prompt="Help me process this PDF document",
|
||||
options=options
|
||||
):
|
||||
print(message)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
options: { options: {
|
||||
cwd: "/path/to/project", // Project with .claude/skills/
|
||||
settingSources: ["user", "project"], // Load Skills from filesystem
|
||||
allowedTools: ["Skill", "Read", "Write", "Bash"] // Enable Skill tool
|
||||
}
|
||||
})) {
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
```text
|
||||
|
||||
## Skill Locations
|
||||
|
||||
Skills are loaded from filesystem directories based on your `settingSources`/`setting_sources` configuration:
|
||||
|
||||
* **Project Skills** (`.claude/skills/`): Shared with your team via git - loaded when `setting_sources` includes `"project"`
|
||||
* **User Skills** (`~/.claude/skills/`): Personal Skills across all projects - loaded when `setting_sources` includes `"user"`
|
||||
* **Plugin Skills**: Bundled with installed Claude Code plugins
|
||||
|
||||
## Creating Skills
|
||||
|
||||
Skills are defined as directories containing a `SKILL.md` file with YAML frontmatter and Markdown content. The `description` field determines when Claude invokes your Skill.
|
||||
|
||||
**Example directory structure**:
|
||||
|
||||
```bash theme={null}
|
||||
.claude/skills/processing-pdfs/
|
||||
└── SKILL.md
|
||||
```
|
||||
|
||||
For complete guidance on creating Skills, including SKILL.md structure, multi-file Skills, and examples, see:
|
||||
|
||||
* [Agent Skills in Claude Code](/en/docs/claude-code/skills): Complete guide with examples
|
||||
* [Agent Skills Best Practices](/en/docs/agents-and-tools/agent-skills/best-practices): Authoring guidelines and naming conventions
|
||||
|
||||
## Tool Restrictions
|
||||
|
||||
The `allowed-tools` frontmatter field in SKILL.md is only supported when using Claude Code CLI directly. **It does not apply when using Skills through the SDK**.
|
||||
|
||||
When using the SDK, control tool access through the main `allowedTools` option in your query configuration.
|
||||
|
||||
To restrict tools for Skills in SDK applications, use the `allowedTools` option:
|
||||
|
||||
Import statements from the first example are assumed in the following code snippets.
|
||||
|
||||
```python Python theme={null}
|
||||
options = ClaudeAgentOptions(
|
||||
setting_sources=["user", "project"], # Load Skills from filesystem
|
||||
```
|
||||
|
||||
```python
|
||||
allowed_tools=["Skill", "Read", "Grep", "Glob"] # Restricted toolset allowed_tools=["Skill", "Read", "Grep", "Glob"] # Restricted toolset
|
||||
)
|
||||
|
||||
async for message in query(
|
||||
prompt="Analyze the codebase structure",
|
||||
```python
|
||||
options=options options=options
|
||||
):
|
||||
print(message)
|
||||
```
|
||||
|
||||
options: { options: {
|
||||
settingSources: ["user", "project"], // Load Skills from filesystem
|
||||
allowedTools: ["Skill", "Read", "Grep", "Glob"] // Restricted toolset
|
||||
}
|
||||
})) {
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
```text
|
||||
|
||||
## Discovering Available Skills
|
||||
|
||||
To see which Skills are available in your SDK application, simply ask Claude:
|
||||
|
||||
```python Python theme={null}
|
||||
options = ClaudeAgentOptions(
|
||||
setting_sources=["user", "project"], # Load Skills from filesystem
|
||||
```python
|
||||
allowed_tools=["Skill"] allowed_tools=["Skill"]
|
||||
)
|
||||
|
||||
async for message in query(
|
||||
prompt="What Skills are available?",
|
||||
```python
|
||||
options=options options=options
|
||||
):
|
||||
print(message)
|
||||
```
|
||||
|
||||
options: { options: {
|
||||
settingSources: ["user", "project"], // Load Skills from filesystem
|
||||
allowedTools: ["Skill"]
|
||||
}
|
||||
})) {
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
```text
|
||||
|
||||
Claude will list the available Skills based on your current working directory and installed plugins.
|
||||
|
||||
## Testing Skills
|
||||
|
||||
Test Skills by asking questions that match their descriptions:
|
||||
|
||||
```python Python theme={null}
|
||||
options = ClaudeAgentOptions(
|
||||
cwd="/path/to/project",
|
||||
```python
|
||||
setting_sources=["user", "project"], # Load Skills from filesystem setting_sources=["user", "project"], # Load Skills from filesystem
|
||||
allowed_tools=["Skill", "Read", "Bash"]
|
||||
)
|
||||
|
||||
async for message in query(
|
||||
prompt="Extract text from invoice.pdf",
|
||||
```python
|
||||
options=options options=options
|
||||
):
|
||||
print(message)
|
||||
```
|
||||
|
||||
options: { options: {
|
||||
cwd: "/path/to/project",
|
||||
settingSources: ["user", "project"], // Load Skills from filesystem
|
||||
allowedTools: ["Skill", "Read", "Bash"]
|
||||
}
|
||||
})) {
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
```text
|
||||
|
||||
Claude automatically invokes the relevant Skill if the description matches your request.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Skills Not Found
|
||||
|
||||
**Check settingSources configuration**: Skills are only loaded when you explicitly configure `settingSources`/`setting_sources`. This is the most common issue:
|
||||
|
||||
```python Python theme={null}
|
||||
|
||||
## Wrong - Skills won't be loaded
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
allowed_tools=["Skill"]
|
||||
)
|
||||
|
||||
## Correct - Skills will be loaded
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
setting_sources=["user", "project"], # Required to load Skills
|
||||
```python
|
||||
allowed_tools=["Skill"] allowed_tools=["Skill"]
|
||||
)
|
||||
```
|
||||
|
||||
allowedTools: ["Skill"] allowedTools: ["Skill"]
|
||||
};
|
||||
|
||||
```text
|
||||
|
||||
For more details on `settingSources`/`setting_sources`, see the [TypeScript SDK reference](/en/api/agent-sdk/typescript#settingsource) or [Python SDK reference](/en/api/agent-sdk/python#settingsource).
|
||||
|
||||
**Check working directory**: The SDK loads Skills relative to the `cwd` option. Ensure it points to a directory containing `.claude/skills/`:
|
||||
|
||||
```python Python theme={null}
|
||||
|
||||
## Ensure your cwd points to the directory containing .claude/skills/
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
cwd="/path/to/project", # Must contain .claude/skills/
|
||||
```python
|
||||
setting_sources=["user", "project"], # Required to load Skills setting_sources=["user", "project"], # Required to load Skills
|
||||
allowed_tools=["Skill"]
|
||||
)
|
||||
```
|
||||
|
||||
settingSources: ["user", "project"], // Required to load Skills settingSources: ["user", "project"], // Required to load Skills
|
||||
allowedTools: ["Skill"]
|
||||
};
|
||||
|
||||
```text
|
||||
|
||||
See the "Using Skills with the SDK" section above for the complete pattern.
|
||||
|
||||
**Verify filesystem location**:
|
||||
|
||||
```bash theme={null}
|
||||
|
||||
## Check project Skills
|
||||
|
||||
ls .claude/skills/*/SKILL.md
|
||||
|
||||
## Check personal Skills
|
||||
|
||||
ls ~/.claude/skills/*/SKILL.md
|
||||
```
|
||||
|
||||
### Skill Not Being Used
|
||||
|
||||
**Check the Skill tool is enabled**: Confirm `"Skill"` is in your `allowedTools`.
|
||||
|
||||
**Check the description**: Ensure it's specific and includes relevant keywords. See [Agent Skills Best Practices](/en/docs/agents-and-tools/agent-skills/best-practices#writing-effective-descriptions) for guidance on writing effective descriptions.
|
||||
|
||||
### Additional Troubleshooting
|
||||
|
||||
For general Skills troubleshooting (YAML syntax, debugging, etc.), see the [Claude Code Skills troubleshooting section](/en/docs/claude-code/skills#troubleshooting).
|
||||
|
||||
## Related Documentation
|
||||
|
||||
### Skills Guides
|
||||
|
||||
* [Agent Skills in Claude Code](/en/docs/claude-code/skills): Complete Skills guide with creation, examples, and troubleshooting
|
||||
* [Agent Skills Overview](/en/docs/agents-and-tools/agent-skills/overview): Conceptual overview, benefits, and architecture
|
||||
* [Agent Skills Best Practices](/en/docs/agents-and-tools/agent-skills/best-practices): Authoring guidelines for effective Skills
|
||||
* [Agent Skills Cookbook](https://github.com/anthropics/claude-cookbooks/tree/main/skills): Example Skills and templates
|
||||
|
||||
### SDK Resources
|
||||
|
||||
* [Subagents in the SDK](/en/api/agent-sdk/subagents): Similar filesystem-based agents with programmatic options
|
||||
* [Slash Commands in the SDK](/en/api/agent-sdk/slash-commands): User-invoked commands
|
||||
* [SDK Overview](/en/api/agent-sdk/overview): General SDK concepts
|
||||
* [TypeScript SDK Reference](/en/api/agent-sdk/typescript): Complete API documentation
|
||||
* [Python SDK Reference](/en/api/agent-sdk/python): Complete API documentation
|
||||
465
skills/claude-agent-sdk/references/slash-commands.md
Normal file
465
skills/claude-agent-sdk/references/slash-commands.md
Normal file
@@ -0,0 +1,465 @@
|
||||
# Slash Commands in the SDK
|
||||
|
||||
> Learn how to use slash commands to control Claude Code sessions through the SDK
|
||||
|
||||
Slash commands provide a way to control Claude Code sessions with special commands that start with `/`. These commands can be sent through the SDK to perform actions like clearing conversation history, compacting messages, or getting help.
|
||||
|
||||
## Discovering Available Slash Commands
|
||||
|
||||
The Claude Agent SDK provides information about available slash commands in the system initialization message. Access this information when your session starts:
|
||||
|
||||
options: { maxTurns: 1 } options: { maxTurns: 1 }
|
||||
})) {
|
||||
if (message.type === "system" && message.subtype === "init") {
|
||||
|
||||
```python
|
||||
console.log("Available slash commands:", message.slash_commands); console.log("Available slash commands:", message.slash_commands);
|
||||
// Example output: ["/compact", "/clear", "/help"]
|
||||
}
|
||||
```
|
||||
|
||||
```python Python theme={null}
|
||||
import asyncio
|
||||
from claude_agent_sdk import query
|
||||
|
||||
async def main():
|
||||
async for message in query(
|
||||
```
|
||||
|
||||
```python
|
||||
prompt="Hello Claude", prompt="Hello Claude",
|
||||
options={"max_turns": 1}
|
||||
):
|
||||
if message.type == "system" and message.subtype == "init":
|
||||
print("Available slash commands:", message.slash_commands)
|
||||
|
||||
# Example output: ["/compact", "/clear", "/help"]
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Sending Slash Commands
|
||||
|
||||
Send slash commands by including them in your prompt string, just like regular text:
|
||||
|
||||
options: { maxTurns: 1 } options: { maxTurns: 1 }
|
||||
})) {
|
||||
if (message.type === "result") {
|
||||
|
||||
```python
|
||||
console.log("Command executed:", message.result); console.log("Command executed:", message.result);
|
||||
}
|
||||
```
|
||||
|
||||
```python Python theme={null}
|
||||
import asyncio
|
||||
from claude_agent_sdk import query
|
||||
|
||||
async def main():
|
||||
|
||||
# Send a slash command
|
||||
```
|
||||
|
||||
```python
|
||||
async for message in query( async for message in query(
|
||||
prompt="/compact",
|
||||
options={"max_turns": 1}
|
||||
):
|
||||
if message.type == "result":
|
||||
print("Command executed:", message.result)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Common Slash Commands
|
||||
|
||||
### `/compact` - Compact Conversation History
|
||||
|
||||
The `/compact` command reduces the size of your conversation history by summarizing older messages while preserving important context:
|
||||
|
||||
options: { maxTurns: 1 } options: { maxTurns: 1 }
|
||||
})) {
|
||||
if (message.type === "system" && message.subtype === "compact_boundary") {
|
||||
|
||||
```python
|
||||
console.log("Compaction completed"); console.log("Compaction completed");
|
||||
console.log("Pre-compaction tokens:", message.compact_metadata.pre_tokens);
|
||||
console.log("Trigger:", message.compact_metadata.trigger);
|
||||
}
|
||||
```
|
||||
|
||||
```python Python theme={null}
|
||||
import asyncio
|
||||
from claude_agent_sdk import query
|
||||
|
||||
async def main():
|
||||
async for message in query(
|
||||
```
|
||||
|
||||
```python
|
||||
prompt="/compact", prompt="/compact",
|
||||
options={"max_turns": 1}
|
||||
):
|
||||
if (message.type == "system" and
|
||||
message.subtype == "compact_boundary"):
|
||||
print("Compaction completed")
|
||||
print("Pre-compaction tokens:",
|
||||
message.compact_metadata.pre_tokens)
|
||||
print("Trigger:", message.compact_metadata.trigger)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### `/clear` - Clear Conversation
|
||||
|
||||
The `/clear` command starts a fresh conversation by clearing all previous history:
|
||||
|
||||
options: { maxTurns: 1 } options: { maxTurns: 1 }
|
||||
})) {
|
||||
if (message.type === "system" && message.subtype === "init") {
|
||||
|
||||
```python
|
||||
console.log("Conversation cleared, new session started"); console.log("Conversation cleared, new session started");
|
||||
console.log("Session ID:", message.session_id);
|
||||
}
|
||||
```
|
||||
|
||||
```python Python theme={null}
|
||||
import asyncio
|
||||
from claude_agent_sdk import query
|
||||
|
||||
async def main():
|
||||
|
||||
# Clear conversation and start fresh
|
||||
```python
|
||||
async for message in query( async for message in query(
|
||||
prompt="/clear",
|
||||
options={"max_turns": 1}
|
||||
):
|
||||
if message.type == "system" and message.subtype == "init":
|
||||
print("Conversation cleared, new session started")
|
||||
print("Session ID:", message.session_id)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Creating Custom Slash Commands
|
||||
|
||||
In addition to using built-in slash commands, you can create your own custom commands that are available through the SDK. Custom commands are defined as markdown files in specific directories, similar to how subagents are configured.
|
||||
|
||||
### File Locations
|
||||
|
||||
Custom slash commands are stored in designated directories based on their scope:
|
||||
|
||||
* **Project commands**: `.claude/commands/` - Available only in the current project
|
||||
* **Personal commands**: `~/.claude/commands/` - Available across all your projects
|
||||
|
||||
### File Format
|
||||
|
||||
Each custom command is a markdown file where:
|
||||
|
||||
* The filename (without `.md` extension) becomes the command name
|
||||
* The file content defines what the command does
|
||||
* Optional YAML frontmatter provides configuration
|
||||
|
||||
#### Basic Example
|
||||
|
||||
Create `.claude/commands/refactor.md`:
|
||||
|
||||
```markdown theme={null}
|
||||
Refactor the selected code to improve readability and maintainability.
|
||||
Focus on clean code principles and best practices.
|
||||
```
|
||||
|
||||
This creates the `/refactor` command that you can use through the SDK.
|
||||
|
||||
#### With Frontmatter
|
||||
|
||||
Create `.claude/commands/security-check.md`:
|
||||
|
||||
```markdown theme={null}
|
||||
---
|
||||
allowed-tools: Read, Grep, Glob
|
||||
description: Run security vulnerability scan
|
||||
model: claude-sonnet-4-5
|
||||
---
|
||||
|
||||
Analyze the codebase for security vulnerabilities including:
|
||||
|
||||
- SQL injection risks
|
||||
- XSS vulnerabilities
|
||||
- Exposed credentials
|
||||
- Insecure configurations
|
||||
```
|
||||
|
||||
### Using Custom Commands in the SDK
|
||||
|
||||
Once defined in the filesystem, custom commands are automatically available through the SDK:
|
||||
|
||||
options: { maxTurns: 3 } options: { maxTurns: 3 }
|
||||
})) {
|
||||
if (message.type === "assistant") {
|
||||
|
||||
```python
|
||||
console.log("Refactoring suggestions:", message.message); console.log("Refactoring suggestions:", message.message);
|
||||
}
|
||||
|
||||
// Custom commands appear in the slash_commands list
|
||||
for await (const message of query({
|
||||
prompt: "Hello",
|
||||
```python
|
||||
options: { maxTurns: 1 } options: { maxTurns: 1 }
|
||||
})) {
|
||||
if (message.type === "system" && message.subtype === "init") {
|
||||
```
|
||||
|
||||
// Will include both built-in and custom commands // Will include both built-in and custom commands
|
||||
console.log("Available commands:", message.slash_commands);
|
||||
// Example: ["/compact", "/clear", "/help", "/refactor", "/security-check"]
|
||||
}
|
||||
|
||||
```text
|
||||
|
||||
```python Python theme={null}
|
||||
import asyncio
|
||||
from claude_agent_sdk import query
|
||||
|
||||
async def main():
|
||||
|
||||
# Use a custom command
|
||||
```python
|
||||
async for message in query( async for message in query(
|
||||
prompt="/refactor src/auth/login.py",
|
||||
options={"max_turns": 3}
|
||||
):
|
||||
if message.type == "assistant":
|
||||
print("Refactoring suggestions:", message.message)
|
||||
|
||||
# Custom commands appear in the slash_commands list
|
||||
async for message in query(
|
||||
prompt="Hello",
|
||||
options={"max_turns": 1}
|
||||
):
|
||||
if message.type == "system" and message.subtype == "init":
|
||||
# Will include both built-in and custom commands
|
||||
print("Available commands:", message.slash_commands)
|
||||
# Example: ["/compact", "/clear", "/help", "/refactor", "/security-check"]
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Arguments and Placeholders
|
||||
|
||||
Custom commands support dynamic arguments using placeholders:
|
||||
|
||||
Create `.claude/commands/fix-issue.md`:
|
||||
|
||||
```markdown theme={null}
|
||||
---
|
||||
argument-hint: [issue-number] [priority]
|
||||
description: Fix a GitHub issue
|
||||
---
|
||||
|
||||
Fix issue #$1 with priority $2.
|
||||
Check the issue description and implement the necessary changes.
|
||||
```
|
||||
|
||||
Use in SDK:
|
||||
|
||||
options: { maxTurns: 5 } options: { maxTurns: 5 }
|
||||
})) {
|
||||
// Command will process with $1="123" and $2="high"
|
||||
|
||||
```python
|
||||
if (message.type === "result") { if (message.type === "result") {
|
||||
console.log("Issue fixed:", message.result);
|
||||
}
|
||||
```
|
||||
|
||||
```python Python theme={null}
|
||||
import asyncio
|
||||
from claude_agent_sdk import query
|
||||
|
||||
async def main():
|
||||
|
||||
# Pass arguments to custom command
|
||||
```python
|
||||
async for message in query( async for message in query(
|
||||
prompt="/fix-issue 123 high",
|
||||
options={"max_turns": 5}
|
||||
):
|
||||
|
||||
# Command will process with $1="123" and $2="high"
|
||||
|
||||
if message.type == "result":
|
||||
print("Issue fixed:", message.result)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Bash Command Execution
|
||||
|
||||
Custom commands can execute bash commands and include their output:
|
||||
|
||||
Create `.claude/commands/git-commit.md`:
|
||||
|
||||
```markdown theme={null}
|
||||
---
|
||||
allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*)
|
||||
description: Create a git commit
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
- Current status: !`git status`
|
||||
- Current diff: !`git diff HEAD`
|
||||
|
||||
## Task
|
||||
|
||||
Create a git commit with appropriate message based on the changes.
|
||||
```
|
||||
|
||||
### File References
|
||||
|
||||
Include file contents using the `@` prefix:
|
||||
|
||||
Create `.claude/commands/review-config.md`:
|
||||
|
||||
```markdown theme={null}
|
||||
---
|
||||
description: Review configuration files
|
||||
---
|
||||
|
||||
Review the following configuration files for issues:
|
||||
|
||||
- Package config: @package.json
|
||||
- TypeScript config: @tsconfig.json
|
||||
- Environment config: @.env
|
||||
|
||||
Check for security issues, outdated dependencies, and misconfigurations.
|
||||
```
|
||||
|
||||
### Organization with Namespacing
|
||||
|
||||
Organize commands in subdirectories for better structure:
|
||||
|
||||
```bash theme={null}
|
||||
.claude/commands/
|
||||
├── frontend/
|
||||
│ ├── component.md # Creates /component (project:frontend)
|
||||
│ └── style-check.md # Creates /style-check (project:frontend)
|
||||
├── backend/
|
||||
│ ├── api-test.md # Creates /api-test (project:backend)
|
||||
│ └── db-migrate.md # Creates /db-migrate (project:backend)
|
||||
└── review.md # Creates /review (project)
|
||||
```
|
||||
|
||||
The subdirectory appears in the command description but doesn't affect the command name itself.
|
||||
|
||||
### Practical Examples
|
||||
|
||||
#### Code Review Command
|
||||
|
||||
Create `.claude/commands/code-review.md`:
|
||||
|
||||
```markdown theme={null}
|
||||
---
|
||||
allowed-tools: Read, Grep, Glob, Bash(git diff:*)
|
||||
description: Comprehensive code review
|
||||
---
|
||||
|
||||
## Changed Files
|
||||
|
||||
!`git diff --name-only HEAD~1`
|
||||
|
||||
## Detailed Changes
|
||||
|
||||
!`git diff HEAD~1`
|
||||
|
||||
## Review Checklist
|
||||
|
||||
Review the above changes for:
|
||||
|
||||
1. Code quality and readability
|
||||
2. Security vulnerabilities
|
||||
3. Performance implications
|
||||
4. Test coverage
|
||||
5. Documentation completeness
|
||||
|
||||
Provide specific, actionable feedback organized by priority.
|
||||
```
|
||||
|
||||
#### Test Runner Command
|
||||
|
||||
Create `.claude/commands/test.md`:
|
||||
|
||||
```markdown theme={null}
|
||||
---
|
||||
allowed-tools: Bash, Read, Edit
|
||||
argument-hint: [test-pattern]
|
||||
description: Run tests with optional pattern
|
||||
---
|
||||
|
||||
Run tests matching pattern: $ARGUMENTS
|
||||
|
||||
1. Detect the test framework (Jest, pytest, etc.)
|
||||
2. Run tests with the provided pattern
|
||||
3. If tests fail, analyze and fix them
|
||||
4. Re-run to verify fixes
|
||||
```
|
||||
|
||||
Use these commands through the SDK:
|
||||
|
||||
options: { maxTurns: 3 } options: { maxTurns: 3 }
|
||||
})) {
|
||||
// Process review feedback
|
||||
}
|
||||
|
||||
// Run specific tests
|
||||
for await (const message of query({
|
||||
prompt: "/test auth",
|
||||
|
||||
```python
|
||||
options: { maxTurns: 5 } options: { maxTurns: 5 }
|
||||
})) {
|
||||
// Handle test results
|
||||
}
|
||||
```
|
||||
|
||||
```python Python theme={null}
|
||||
import asyncio
|
||||
from claude_agent_sdk import query
|
||||
|
||||
async def main():
|
||||
|
||||
# Run code review
|
||||
```python
|
||||
async for message in query( async for message in query(
|
||||
prompt="/code-review",
|
||||
options={"max_turns": 3}
|
||||
):
|
||||
|
||||
# Process review feedback
|
||||
|
||||
pass
|
||||
|
||||
# Run specific tests
|
||||
async for message in query(
|
||||
prompt="/test auth",
|
||||
options={"max_turns": 5}
|
||||
):
|
||||
# Handle test results
|
||||
pass
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [Slash Commands](/en/docs/claude-code/slash-commands) - Complete slash command documentation
|
||||
- [Subagents in the SDK](/en/api/agent-sdk/subagents) - Similar filesystem-based configuration for subagents
|
||||
- [TypeScript SDK reference](/en/docs/claude-code/typescript-sdk-reference) - Complete API documentation
|
||||
- [SDK overview](/en/api/agent-sdk/overview) - General SDK concepts
|
||||
- [CLI reference](/en/docs/claude-code/cli-reference) - Command-line interface
|
||||
302
skills/claude-agent-sdk/references/subagents.md
Normal file
302
skills/claude-agent-sdk/references/subagents.md
Normal file
@@ -0,0 +1,302 @@
|
||||
# Subagents in the SDK
|
||||
|
||||
> Working with subagents in the Claude Agent SDK
|
||||
|
||||
Subagents in the Claude Agent SDK are specialized AIs that are orchestrated by the main agent.
|
||||
Use subagents for context management and parallelization.
|
||||
|
||||
This guide explains how to define and use subagents in the SDK using the `agents` parameter.
|
||||
|
||||
## Overview
|
||||
|
||||
Subagents can be defined in two ways when using the SDK:
|
||||
|
||||
1. **Programmatically** - Using the `agents` parameter in your `query()` options (recommended for SDK applications)
|
||||
2. **Filesystem-based** - Placing markdown files with YAML frontmatter in designated directories (`.claude/agents/`)
|
||||
|
||||
This guide primarily focuses on the programmatic approach using the `agents` parameter, which provides a more integrated development experience for SDK applications.
|
||||
|
||||
## Benefits of Using Subagents
|
||||
|
||||
### Context Management
|
||||
|
||||
Subagents maintain separate context from the main agent, preventing information overload and keeping interactions focused. This isolation ensures that specialized tasks don't pollute the main conversation context with irrelevant details.
|
||||
|
||||
**Example**: A `research-assistant` subagent can explore dozens of files and documentation pages without cluttering the main conversation with all the intermediate search results - only returning the relevant findings.
|
||||
|
||||
### Parallelization
|
||||
|
||||
Multiple subagents can run concurrently, dramatically speeding up complex workflows.
|
||||
|
||||
**Example**: During a code review, you can run `style-checker`, `security-scanner`, and `test-coverage` subagents simultaneously, reducing review time from minutes to seconds.
|
||||
|
||||
### Specialized Instructions and Knowledge
|
||||
|
||||
Each subagent can have tailored system prompts with specific expertise, best practices, and constraints.
|
||||
|
||||
**Example**: A `database-migration` subagent can have detailed knowledge about SQL best practices, rollback strategies, and data integrity checks that would be unnecessary noise in the main agent's instructions.
|
||||
|
||||
### Tool Restrictions
|
||||
|
||||
Subagents can be limited to specific tools, reducing the risk of unintended actions.
|
||||
|
||||
**Example**: A `doc-reviewer` subagent might only have access to Read and Grep tools, ensuring it can analyze but never accidentally modify your documentation files.
|
||||
|
||||
## Creating Subagents
|
||||
|
||||
### Programmatic Definition (Recommended)
|
||||
|
||||
Define subagents directly in your code using the `agents` parameter:
|
||||
|
||||
```python
|
||||
import anyio
|
||||
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
|
||||
|
||||
async def main():
|
||||
options = ClaudeAgentOptions(
|
||||
agents={
|
||||
"code-reviewer": AgentDefinition(
|
||||
description="Expert code review specialist. Use for quality, security, and maintainability reviews.",
|
||||
prompt="""You are a code review specialist with expertise in security, performance, and best practices.
|
||||
|
||||
When reviewing code:
|
||||
- Identify security vulnerabilities
|
||||
- Check for performance issues
|
||||
- Verify adherence to coding standards
|
||||
- Suggest specific improvements
|
||||
|
||||
Be thorough but concise in your feedback.""",
|
||||
tools=["Read", "Grep", "Glob"],
|
||||
model="sonnet"
|
||||
),
|
||||
"test-runner": AgentDefinition(
|
||||
description="Runs and analyzes test suites. Use for test execution and coverage analysis.",
|
||||
prompt="""You are a test execution specialist. Run tests and provide clear analysis of results.
|
||||
|
||||
Focus on:
|
||||
- Running test commands
|
||||
- Analyzing test output
|
||||
- Identifying failing tests
|
||||
- Suggesting fixes for failures""",
|
||||
tools=["Bash", "Read", "Grep"]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
async for message in query(
|
||||
prompt="Review the authentication module for security issues",
|
||||
options=options
|
||||
):
|
||||
print(message)
|
||||
|
||||
anyio.run(main)
|
||||
```
|
||||
|
||||
### AgentDefinition Configuration
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| :------------ | :------------------------------------------- | :------- | :--------------------------------------------------------------- |
|
||||
| `description` | `string` | Yes | Natural language description of when to use this agent |
|
||||
| `prompt` | `string` | Yes | The agent's system prompt defining its role and behavior |
|
||||
| `tools` | `string[]` | No | Array of allowed tool names. If omitted, inherits all tools |
|
||||
| `model` | `'sonnet' \| 'opus' \| 'haiku' \| 'inherit'` | No | Model override for this agent. Defaults to main model if omitted |
|
||||
|
||||
### Filesystem-Based Definition (Alternative)
|
||||
|
||||
You can also define subagents as markdown files in specific directories:
|
||||
|
||||
* **Project-level**: `.claude/agents/*.md` - Available only in the current project
|
||||
* **User-level**: `~/.claude/agents/*.md` - Available across all projects
|
||||
|
||||
Each subagent is a markdown file with YAML frontmatter:
|
||||
|
||||
```markdown theme={null}
|
||||
---
|
||||
name: code-reviewer
|
||||
description: Expert code review specialist. Use for quality, security, and maintainability reviews.
|
||||
tools: Read, Grep, Glob, Bash
|
||||
---
|
||||
|
||||
Your subagent's system prompt goes here. This defines the subagent's
|
||||
role, capabilities, and approach to solving problems.
|
||||
```
|
||||
|
||||
**Note:** Programmatically defined agents (via the `agents` parameter) take precedence over filesystem-based agents with the same name.
|
||||
|
||||
## How the SDK Uses Subagents
|
||||
|
||||
When using the Claude Agent SDK, subagents can be defined programmatically or loaded from the filesystem. Claude will:
|
||||
|
||||
1. **Load programmatic agents** from the `agents` parameter in your options
|
||||
2. **Auto-detect filesystem agents** from `.claude/agents/` directories (if not overridden)
|
||||
3. **Invoke them automatically** based on task matching and the agent's `description`
|
||||
4. **Use their specialized prompts** and tool restrictions
|
||||
5. **Maintain separate context** for each subagent invocation
|
||||
|
||||
Programmatically defined agents (via `agents` parameter) take precedence over filesystem-based agents with the same name.
|
||||
|
||||
## Example Subagents
|
||||
|
||||
For comprehensive examples of subagents including code reviewers, test runners, debuggers, and security auditors, see the [main Subagents guide](/en/docs/claude-code/sub-agents#example-subagents). The guide includes detailed configurations and best practices for creating effective subagents.
|
||||
|
||||
## SDK Integration Patterns
|
||||
|
||||
### Automatic Invocation
|
||||
|
||||
The SDK will automatically invoke appropriate subagents based on the task context. Ensure your agent's `description` field clearly indicates when it should be used:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
|
||||
|
||||
async def main():
|
||||
options = ClaudeAgentOptions(
|
||||
agents={
|
||||
"performance-optimizer": AgentDefinition(
|
||||
description="Use PROACTIVELY when code changes might impact performance. MUST BE USED for optimization tasks.",
|
||||
prompt="You are a performance optimization specialist...",
|
||||
tools=["Read", "Edit", "Bash", "Grep"],
|
||||
model="sonnet"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
async for message in query(
|
||||
prompt="Optimize the database queries in the API layer",
|
||||
options=options
|
||||
):
|
||||
# Process messages
|
||||
pass
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### Explicit Invocation
|
||||
|
||||
Users can request specific subagents in their prompts:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
|
||||
|
||||
async def main():
|
||||
options = ClaudeAgentOptions(
|
||||
agents={
|
||||
"code-reviewer": AgentDefinition(
|
||||
description="Expert code review specialist",
|
||||
prompt="You are a security-focused code reviewer...",
|
||||
tools=["Read", "Grep", "Glob"]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
async for message in query(
|
||||
prompt="Use the code-reviewer agent to check the authentication module",
|
||||
options=options
|
||||
):
|
||||
# Process messages
|
||||
pass
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### Dynamic Agent Configuration
|
||||
|
||||
You can dynamically configure agents based on your application's needs:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from typing import Literal
|
||||
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
|
||||
|
||||
def create_security_agent(
|
||||
security_level: Literal["basic", "strict"]
|
||||
) -> AgentDefinition:
|
||||
"""Create a security agent with configurable strictness."""
|
||||
strictness = "strict" if security_level == "strict" else "balanced"
|
||||
|
||||
return AgentDefinition(
|
||||
description="Security code reviewer",
|
||||
prompt=f"You are a {strictness} security reviewer...",
|
||||
tools=["Read", "Grep", "Glob"],
|
||||
model="opus" if security_level == "strict" else "sonnet"
|
||||
)
|
||||
|
||||
async def main():
|
||||
options = ClaudeAgentOptions(
|
||||
agents={
|
||||
"security-reviewer": create_security_agent("strict")
|
||||
}
|
||||
)
|
||||
|
||||
async for message in query(
|
||||
prompt="Review this PR for security issues",
|
||||
options=options
|
||||
):
|
||||
# Process messages
|
||||
pass
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Tool Restrictions
|
||||
|
||||
Subagents can have restricted tool access via the `tools` field:
|
||||
|
||||
* **Omit the field** - Agent inherits all available tools (default)
|
||||
* **Specify tools** - Agent can only use listed tools
|
||||
|
||||
Example of a read-only analysis agent:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
|
||||
|
||||
async def main():
|
||||
options = ClaudeAgentOptions(
|
||||
agents={
|
||||
"code-analyzer": AgentDefinition(
|
||||
description="Static code analysis and architecture review",
|
||||
prompt="""You are a code architecture analyst. Analyze code structure,
|
||||
identify patterns, and suggest improvements without making changes.""",
|
||||
tools=["Read", "Grep", "Glob"] # No write or execute permissions
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
async for message in query(
|
||||
prompt="Analyze the architecture of this codebase",
|
||||
options=options
|
||||
):
|
||||
# Process messages
|
||||
pass
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### Common Tool Combinations
|
||||
|
||||
**Read-only agents** (analysis, review):
|
||||
|
||||
```python
|
||||
tools=["Read", "Grep", "Glob"]
|
||||
```
|
||||
|
||||
**Test execution agents**:
|
||||
|
||||
```python
|
||||
tools=["Bash", "Read", "Grep"]
|
||||
```
|
||||
|
||||
**Code modification agents**:
|
||||
|
||||
```python
|
||||
tools=["Read", "Edit", "Write", "Grep", "Glob"]
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Main Subagents Guide](/en/docs/claude-code/sub-agents) - Comprehensive subagent documentation
|
||||
* [SDK Overview](/en/api/agent-sdk/overview) - Overview of Claude Agent SDK
|
||||
* [Settings](/en/docs/claude-code/settings) - Configuration file reference
|
||||
* [Slash Commands](/en/docs/claude-code/slash-commands) - Custom command creation
|
||||
258
skills/claude-agent-sdk/references/system-prompts.md
Normal file
258
skills/claude-agent-sdk/references/system-prompts.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# System Prompt Configuration Patterns
|
||||
|
||||
This guide covers different ways to configure system prompts in the Claude Agent SDK.
|
||||
|
||||
## Overview
|
||||
|
||||
System prompts define Claude's role, behavior, and capabilities. The SDK supports multiple configuration patterns.
|
||||
|
||||
## Configuration Types
|
||||
|
||||
### 1. No System Prompt (Vanilla Claude)
|
||||
|
||||
Claude operates without additional instructions:
|
||||
|
||||
```python
|
||||
from claude_agent_sdk import query
|
||||
|
||||
async for message in query(prompt="What is 2 + 2?"):
|
||||
# Process messages
|
||||
pass
|
||||
```
|
||||
|
||||
**Use when:** You want vanilla Claude behavior without specialized instructions.
|
||||
|
||||
### 2. String System Prompt
|
||||
|
||||
Custom instructions as a simple string:
|
||||
|
||||
```python
|
||||
from claude_agent_sdk import ClaudeAgentOptions, query
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt="You are a helpful Python expert. Explain concepts simply."
|
||||
)
|
||||
|
||||
async for message in query(prompt="Explain async/await", options=options):
|
||||
pass
|
||||
```
|
||||
|
||||
**Use when:** You need custom behavior for a specific task or domain.
|
||||
|
||||
### 3. Preset System Prompt
|
||||
|
||||
Use the official Claude Code preset:
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt={"type": "preset", "preset": "claude_code"}
|
||||
)
|
||||
```
|
||||
|
||||
The `"claude_code"` preset includes:
|
||||
|
||||
- Tool usage patterns (Bash, Read, Write, Edit, etc.)
|
||||
- Best practices for code modification
|
||||
- Knowledge of the Task tool for delegating to subagents
|
||||
- Git commit and PR workflows
|
||||
- Security guidelines
|
||||
|
||||
**Use when:** Building SDK applications that orchestrate subagents or use file tools.
|
||||
|
||||
### 4. Preset with Append
|
||||
|
||||
Extend the Claude Code preset with additional instructions:
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt={
|
||||
"type": "preset",
|
||||
"preset": "claude_code",
|
||||
"append": "Always explain your reasoning before implementing changes."
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**Use when:** You need Claude Code behavior plus domain-specific instructions.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Orchestrator Agent
|
||||
|
||||
Main agent that delegates to subagents:
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt="claude_code", # Knows how to use Task tool
|
||||
allowed_tools=["Bash", "Task", "Read", "Write"],
|
||||
agents={
|
||||
"subagent-1": AgentDefinition(...),
|
||||
"subagent-2": AgentDefinition(...)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**Critical:** Orchestrators must use `system_prompt="claude_code"` to understand how to delegate to subagents.
|
||||
|
||||
### Domain Expert
|
||||
|
||||
Specialized behavior for specific tasks:
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt={
|
||||
"type": "preset",
|
||||
"preset": "claude_code",
|
||||
"append": """You are a security auditor for Python applications.
|
||||
|
||||
Focus on:
|
||||
- SQL injection vulnerabilities
|
||||
- Command injection risks
|
||||
- Authentication/authorization flaws
|
||||
- Secrets management issues
|
||||
|
||||
Provide specific, actionable security recommendations."""
|
||||
},
|
||||
allowed_tools=["Read", "Grep", "Glob"]
|
||||
)
|
||||
```
|
||||
|
||||
### Constrained Agent
|
||||
|
||||
Agent with specific behavioral constraints:
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt="""You are a read-only code analyzer.
|
||||
|
||||
IMPORTANT:
|
||||
- Never modify files
|
||||
- Never execute code
|
||||
- Only analyze and report findings
|
||||
|
||||
Provide detailed analysis with file/line references.""",
|
||||
allowed_tools=["Read", "Grep", "Glob"]
|
||||
)
|
||||
```
|
||||
|
||||
## Shorthand vs Dict Format
|
||||
|
||||
The SDK accepts both shorthand and dictionary formats for the `claude_code` preset:
|
||||
|
||||
```python
|
||||
# Dict format (official examples prefer this)
|
||||
system_prompt={"type": "preset", "preset": "claude_code"}
|
||||
|
||||
# Shorthand format (equivalent, but less explicit)
|
||||
system_prompt="claude_code"
|
||||
|
||||
# With append (dict only)
|
||||
system_prompt={
|
||||
"type": "preset",
|
||||
"preset": "claude_code",
|
||||
"append": "Additional instructions here"
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The shorthand `system_prompt="claude_code"` is a convenience that's equivalent to the full dict format. Both are valid and produce identical behavior. Official examples prefer the dict format for explicitness, but shorthand works fine for simple cases.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use preset for orchestrators** - Orchestrators need `system_prompt="claude_code"` for Task tool knowledge
|
||||
2. **Use append for specialization** - Extend preset with domain-specific instructions
|
||||
3. **Match tools to prompt** - System prompt should align with allowed_tools
|
||||
4. **Be specific** - Clear, specific instructions produce better results
|
||||
5. **Test variations** - Experiment with different prompts for your use case
|
||||
|
||||
## Examples
|
||||
|
||||
### File Processing Agent
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt={
|
||||
"type": "preset",
|
||||
"preset": "claude_code",
|
||||
"append": """Process CSV files with these requirements:
|
||||
- Validate data types
|
||||
- Handle missing values
|
||||
- Generate summary statistics
|
||||
- Create visualizations using matplotlib"""
|
||||
},
|
||||
allowed_tools=["Read", "Write", "Bash"]
|
||||
)
|
||||
```
|
||||
|
||||
### Documentation Generator
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt="""You are a technical documentation specialist.
|
||||
|
||||
Generate clear, comprehensive documentation with:
|
||||
- Overview and purpose
|
||||
- API reference with types
|
||||
- Usage examples
|
||||
- Common pitfalls and troubleshooting
|
||||
|
||||
Use Google-style Python docstrings.""",
|
||||
allowed_tools=["Read", "Write", "Edit", "Grep"]
|
||||
)
|
||||
```
|
||||
|
||||
### Test Writer
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt={
|
||||
"type": "preset",
|
||||
"preset": "claude_code",
|
||||
"append": """Write pytest-based tests following these patterns:
|
||||
- Use fixtures for setup/teardown
|
||||
- Parametrize tests when appropriate
|
||||
- Include edge cases and error conditions
|
||||
- Aim for >90% code coverage"""
|
||||
},
|
||||
allowed_tools=["Read", "Write", "Bash"]
|
||||
)
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
❌ **Orchestrator without claude_code preset**
|
||||
|
||||
```python
|
||||
# Orchestrator won't know how to use Task tool
|
||||
options = ClaudeAgentOptions(
|
||||
agents={...}
|
||||
# Missing system_prompt="claude_code"
|
||||
)
|
||||
```
|
||||
|
||||
✅ **Proper orchestrator configuration**
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt="claude_code",
|
||||
agents={...}
|
||||
)
|
||||
```
|
||||
|
||||
❌ **Conflicting instructions**
|
||||
|
||||
```python
|
||||
# Tells agent to modify files but only allows read tools
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt="Fix any bugs you find",
|
||||
allowed_tools=["Read", "Grep"] # Can't actually fix anything
|
||||
)
|
||||
```
|
||||
|
||||
✅ **Aligned tools and instructions**
|
||||
|
||||
```python
|
||||
options = ClaudeAgentOptions(
|
||||
system_prompt="Analyze code for bugs and report findings",
|
||||
allowed_tools=["Read", "Grep", "Glob"]
|
||||
)
|
||||
```
|
||||
460
skills/claude-agent-sdk/references/tool-permissions.md
Normal file
460
skills/claude-agent-sdk/references/tool-permissions.md
Normal file
@@ -0,0 +1,460 @@
|
||||
# Tool Permission Callbacks
|
||||
|
||||
This guide covers tool permission callbacks for fine-grained control over tool usage.
|
||||
|
||||
## Overview
|
||||
|
||||
Tool permission callbacks allow you to:
|
||||
|
||||
- Approve or deny tool usage
|
||||
- Modify tool inputs before execution
|
||||
- Implement complex permission logic
|
||||
- Log tool usage
|
||||
|
||||
## Choosing Your Permission Strategy
|
||||
|
||||
The SDK provides two ways to control tool permissions: **permission modes** (simple) and **permission callbacks** (advanced).
|
||||
|
||||
### Quick Decision Guide
|
||||
|
||||
**Use `permission_mode` when:**
|
||||
|
||||
- You have simple, consistent permission policies
|
||||
- You want to auto-approve/deny all file edits
|
||||
- You don't need conditional logic
|
||||
- You want minimal code
|
||||
|
||||
**Use `can_use_tool` callback when:**
|
||||
|
||||
- You need conditional approval logic
|
||||
- You want to modify tool inputs before execution
|
||||
- You need to block specific commands or patterns
|
||||
- You want to log tool usage
|
||||
- You need fine-grained control per tool
|
||||
|
||||
### Permission Mode Options
|
||||
|
||||
```python
|
||||
from claude_agent_sdk import ClaudeAgentOptions
|
||||
|
||||
# Option 1: Auto-accept file edits (for automated workflows)
|
||||
options = ClaudeAgentOptions(
|
||||
permission_mode="acceptEdits",
|
||||
allowed_tools=["Read", "Write", "Edit", "Bash"]
|
||||
)
|
||||
|
||||
# Option 2: Require approval for edits (read-only automation)
|
||||
options = ClaudeAgentOptions(
|
||||
permission_mode="rejectEdits",
|
||||
allowed_tools=["Read", "Grep", "Glob"]
|
||||
)
|
||||
|
||||
# Option 3: Plan mode (no execution, just planning)
|
||||
options = ClaudeAgentOptions(
|
||||
permission_mode="plan",
|
||||
allowed_tools=["Read", "Write", "Bash"]
|
||||
)
|
||||
|
||||
# Option 4: Bypass all permissions (use with extreme caution)
|
||||
options = ClaudeAgentOptions(
|
||||
permission_mode="bypassPermissions",
|
||||
allowed_tools=["Read", "Write", "Bash"]
|
||||
)
|
||||
```
|
||||
|
||||
### When to Use Each Mode
|
||||
|
||||
| Mode | Behavior | Best For |
|
||||
|------|----------|----------|
|
||||
| `"acceptEdits"` | Auto-approves file edits (Write, Edit, etc.) | CI/CD pipelines, automated refactoring, code generation |
|
||||
| `"rejectEdits"` | Auto-rejects file edits, allows reads | Analysis tasks, read-only auditing, code review |
|
||||
| `"plan"` | No execution, planning only | Previewing changes, cost estimation, planning phase |
|
||||
| `"bypassPermissions"` | Bypasses all permission checks | Testing, trusted environments only (⚠️ dangerous) |
|
||||
| `"default"` | Uses `can_use_tool` callback if provided | Custom permission logic (see below) |
|
||||
|
||||
### Combining Mode + Callback
|
||||
|
||||
You can use both together - the mode provides baseline behavior and the callback adds custom logic:
|
||||
|
||||
```python
|
||||
async def custom_permissions(tool_name, input_data, context):
|
||||
"""Custom logic on top of permission mode."""
|
||||
# Block dangerous bash commands even if acceptEdits is set
|
||||
if tool_name == "Bash":
|
||||
command = input_data.get("command", "")
|
||||
if "rm -rf /" in command:
|
||||
return PermissionResultDeny(message="Dangerous command blocked")
|
||||
|
||||
return PermissionResultAllow()
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
permission_mode="acceptEdits", # Auto-approve file edits
|
||||
can_use_tool=custom_permissions, # But add custom bash validation
|
||||
allowed_tools=["Read", "Write", "Edit", "Bash"]
|
||||
)
|
||||
```
|
||||
|
||||
### Simple Use Cases: Just Use permission_mode
|
||||
|
||||
```python
|
||||
# Example 1: Automated code generation (accept all edits)
|
||||
options = ClaudeAgentOptions(
|
||||
permission_mode="acceptEdits",
|
||||
allowed_tools=["Read", "Write", "Edit"]
|
||||
)
|
||||
|
||||
# Example 2: Code analysis (read-only)
|
||||
options = ClaudeAgentOptions(
|
||||
permission_mode="rejectEdits",
|
||||
allowed_tools=["Read", "Grep", "Glob"]
|
||||
)
|
||||
|
||||
# Example 3: Planning phase (no execution)
|
||||
options = ClaudeAgentOptions(
|
||||
permission_mode="plan",
|
||||
allowed_tools=["Read", "Write", "Bash"]
|
||||
)
|
||||
```
|
||||
|
||||
## Permission Callbacks (Advanced)
|
||||
|
||||
For complex permission logic, use the `can_use_tool` callback.
|
||||
|
||||
## Callback Signature
|
||||
|
||||
```python
|
||||
from claude_agent_sdk import (
|
||||
PermissionResultAllow,
|
||||
PermissionResultDeny,
|
||||
ToolPermissionContext
|
||||
)
|
||||
|
||||
async def permission_callback(
|
||||
tool_name: str,
|
||||
input_data: dict,
|
||||
context: ToolPermissionContext
|
||||
) -> PermissionResultAllow | PermissionResultDeny:
|
||||
"""
|
||||
Args:
|
||||
tool_name: Name of the tool being used
|
||||
input_data: Tool input parameters
|
||||
context: Additional context (suggestions, etc.)
|
||||
|
||||
Returns:
|
||||
PermissionResultAllow or PermissionResultDeny
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
## Permission Results
|
||||
|
||||
### Allow
|
||||
|
||||
```python
|
||||
# Simple allow
|
||||
return PermissionResultAllow()
|
||||
|
||||
# Allow with modified input
|
||||
return PermissionResultAllow(
|
||||
updated_input={"file_path": "/safe/output.txt"}
|
||||
)
|
||||
```
|
||||
|
||||
### Deny
|
||||
|
||||
```python
|
||||
return PermissionResultDeny(
|
||||
message="Cannot write to system directories"
|
||||
)
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### 1. Allow Read-Only Tools
|
||||
|
||||
```python
|
||||
async def permission_callback(
|
||||
tool_name: str,
|
||||
input_data: dict,
|
||||
context: ToolPermissionContext
|
||||
) -> PermissionResultAllow | PermissionResultDeny:
|
||||
"""Auto-allow read-only operations."""
|
||||
|
||||
# Always allow read operations
|
||||
if tool_name in ["Read", "Glob", "Grep"]:
|
||||
return PermissionResultAllow()
|
||||
|
||||
# Deny or ask for other tools
|
||||
return PermissionResultDeny(
|
||||
message=f"Tool {tool_name} requires approval"
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Block Dangerous Commands
|
||||
|
||||
```python
|
||||
async def permission_callback(
|
||||
tool_name: str,
|
||||
input_data: dict,
|
||||
context: ToolPermissionContext
|
||||
) -> PermissionResultAllow | PermissionResultDeny:
|
||||
"""Block dangerous bash commands."""
|
||||
|
||||
if tool_name == "Bash":
|
||||
command = input_data.get("command", "")
|
||||
dangerous = ["rm -rf", "sudo", "chmod 777", "dd if=", "mkfs"]
|
||||
|
||||
for pattern in dangerous:
|
||||
if pattern in command:
|
||||
return PermissionResultDeny(
|
||||
message=f"Dangerous command pattern: {pattern}"
|
||||
)
|
||||
|
||||
return PermissionResultAllow()
|
||||
```
|
||||
|
||||
### 3. Redirect File Writes
|
||||
|
||||
```python
|
||||
async def permission_callback(
|
||||
tool_name: str,
|
||||
input_data: dict,
|
||||
context: ToolPermissionContext
|
||||
) -> PermissionResultAllow | PermissionResultDeny:
|
||||
"""Redirect writes to safe directory."""
|
||||
|
||||
if tool_name in ["Write", "Edit", "MultiEdit"]:
|
||||
file_path = input_data.get("file_path", "")
|
||||
|
||||
# Block system directories
|
||||
if file_path.startswith("/etc/") or file_path.startswith("/usr/"):
|
||||
return PermissionResultDeny(
|
||||
message=f"Cannot write to system directory: {file_path}"
|
||||
)
|
||||
|
||||
# Redirect to safe directory
|
||||
if not file_path.startswith("./safe/"):
|
||||
safe_path = f"./safe/{file_path.split('/')[-1]}"
|
||||
modified_input = input_data.copy()
|
||||
modified_input["file_path"] = safe_path
|
||||
|
||||
return PermissionResultAllow(
|
||||
updated_input=modified_input
|
||||
)
|
||||
|
||||
return PermissionResultAllow()
|
||||
```
|
||||
|
||||
### 4. Log Tool Usage
|
||||
|
||||
```python
|
||||
tool_usage_log = []
|
||||
|
||||
async def permission_callback(
|
||||
tool_name: str,
|
||||
input_data: dict,
|
||||
context: ToolPermissionContext
|
||||
) -> PermissionResultAllow | PermissionResultDeny:
|
||||
"""Log all tool usage."""
|
||||
|
||||
# Log the request
|
||||
tool_usage_log.append({
|
||||
"tool": tool_name,
|
||||
"input": input_data,
|
||||
"suggestions": context.suggestions
|
||||
})
|
||||
|
||||
print(f"Tool: {tool_name}")
|
||||
print(f"Input: {input_data}")
|
||||
|
||||
return PermissionResultAllow()
|
||||
```
|
||||
|
||||
### 5. Interactive Approval
|
||||
|
||||
```python
|
||||
async def permission_callback(
|
||||
tool_name: str,
|
||||
input_data: dict,
|
||||
context: ToolPermissionContext
|
||||
) -> PermissionResultAllow | PermissionResultDeny:
|
||||
"""Ask user for permission on unknown tools."""
|
||||
|
||||
# Auto-allow safe tools
|
||||
if tool_name in ["Read", "Grep", "Glob"]:
|
||||
return PermissionResultAllow()
|
||||
|
||||
# Auto-deny dangerous tools
|
||||
if tool_name == "Bash":
|
||||
command = input_data.get("command", "")
|
||||
if "rm -rf" in command:
|
||||
return PermissionResultDeny(message="Dangerous command")
|
||||
|
||||
# Ask user for other tools
|
||||
print(f"\nTool: {tool_name}")
|
||||
print(f"Input: {input_data}")
|
||||
user_input = input("Allow? (y/N): ").strip().lower()
|
||||
|
||||
if user_input in ("y", "yes"):
|
||||
return PermissionResultAllow()
|
||||
else:
|
||||
return PermissionResultDeny(message="User denied permission")
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the callback in `ClaudeAgentOptions`:
|
||||
|
||||
```python
|
||||
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
||||
|
||||
options = ClaudeAgentOptions(
|
||||
can_use_tool=permission_callback,
|
||||
permission_mode="default", # Ensure callbacks are invoked
|
||||
cwd="."
|
||||
)
|
||||
|
||||
async with ClaudeSDKClient(options) as client:
|
||||
await client.query("List files and create hello.py")
|
||||
async for message in client.receive_response():
|
||||
# Process messages
|
||||
pass
|
||||
```
|
||||
|
||||
## Permission Modes
|
||||
|
||||
| Mode | Behavior | Use Case |
|
||||
|------|----------|----------|
|
||||
| `"default"` | Invokes callback for every tool | Fine-grained control |
|
||||
| `"acceptEdits"` | Auto-approves file edits | Automated workflows |
|
||||
| `"rejectEdits"` | Auto-rejects file edits | Read-only mode |
|
||||
|
||||
## Complete Example
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from claude_agent_sdk import (
|
||||
ClaudeAgentOptions,
|
||||
ClaudeSDKClient,
|
||||
PermissionResultAllow,
|
||||
PermissionResultDeny,
|
||||
ToolPermissionContext,
|
||||
)
|
||||
|
||||
# Track usage
|
||||
tool_log = []
|
||||
|
||||
async def safe_permission_callback(
|
||||
tool_name: str,
|
||||
input_data: dict,
|
||||
context: ToolPermissionContext
|
||||
) -> PermissionResultAllow | PermissionResultDeny:
|
||||
"""Safe permission callback with logging."""
|
||||
|
||||
# Log usage
|
||||
tool_log.append({"tool": tool_name, "input": input_data})
|
||||
|
||||
# Always allow read operations
|
||||
if tool_name in ["Read", "Glob", "Grep"]:
|
||||
print(f"✅ Auto-allow: {tool_name}")
|
||||
return PermissionResultAllow()
|
||||
|
||||
# Check writes to system directories
|
||||
if tool_name in ["Write", "Edit"]:
|
||||
file_path = input_data.get("file_path", "")
|
||||
if file_path.startswith("/etc/"):
|
||||
print(f"❌ Blocked: write to {file_path}")
|
||||
return PermissionResultDeny(
|
||||
message=f"Cannot write to system directory"
|
||||
)
|
||||
|
||||
# Check dangerous bash commands
|
||||
if tool_name == "Bash":
|
||||
command = input_data.get("command", "")
|
||||
if "rm -rf" in command or "sudo" in command:
|
||||
print(f"❌ Blocked: dangerous command")
|
||||
return PermissionResultDeny(
|
||||
message="Dangerous command pattern detected"
|
||||
)
|
||||
|
||||
print(f"✅ Allowed: {tool_name}")
|
||||
return PermissionResultAllow()
|
||||
|
||||
async def main():
|
||||
options = ClaudeAgentOptions(
|
||||
can_use_tool=safe_permission_callback,
|
||||
permission_mode="default",
|
||||
allowed_tools=["Read", "Write", "Bash"]
|
||||
)
|
||||
|
||||
async with ClaudeSDKClient(options) as client:
|
||||
await client.query("List files and create hello.py")
|
||||
|
||||
async for message in client.receive_response():
|
||||
# Process messages
|
||||
pass
|
||||
|
||||
# Print usage summary
|
||||
print("\nTool Usage Summary:")
|
||||
for entry in tool_log:
|
||||
print(f" {entry['tool']}: {entry['input']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Return early** - Check tool_name first and return quickly for unmatched tools
|
||||
2. **Be defensive** - Use `.get()` to safely access input_data fields
|
||||
3. **Log decisions** - Track what was allowed/denied for debugging
|
||||
4. **Clear messages** - Denial messages should explain why
|
||||
5. **Test thoroughly** - Verify callback logic with different tool types
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
❌ **Assuming input structure**
|
||||
|
||||
```python
|
||||
# Crashes if command key doesn't exist
|
||||
command = input_data["command"]
|
||||
```
|
||||
|
||||
✅ **Safe access**
|
||||
|
||||
```python
|
||||
command = input_data.get("command", "")
|
||||
```
|
||||
|
||||
❌ **Silent denials**
|
||||
|
||||
```python
|
||||
return PermissionResultDeny() # No message
|
||||
```
|
||||
|
||||
✅ **Informative denials**
|
||||
|
||||
```python
|
||||
return PermissionResultDeny(
|
||||
message="Cannot write to system directories for safety"
|
||||
)
|
||||
```
|
||||
|
||||
❌ **Checking all tools for Bash-specific logic**
|
||||
|
||||
```python
|
||||
# This crashes on non-Bash tools
|
||||
async def callback(tool_name, input_data, context):
|
||||
command = input_data["command"] # Only Bash has "command"
|
||||
```
|
||||
|
||||
✅ **Filter by tool_name first**
|
||||
|
||||
```python
|
||||
async def callback(tool_name, input_data, context):
|
||||
if tool_name != "Bash":
|
||||
return PermissionResultAllow()
|
||||
|
||||
command = input_data.get("command", "")
|
||||
# Now safe to check command
|
||||
```
|
||||
151
skills/python-code-quality/SKILL.md
Normal file
151
skills/python-code-quality/SKILL.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
name: python-code-quality
|
||||
description: >
|
||||
Python code quality tooling with ruff and pyright.
|
||||
Use when setting up linting, formatting, type checking,
|
||||
configuring ruff or pyright, or establishing code quality standards.
|
||||
---
|
||||
|
||||
# Python Code Quality with Ruff and Pyright
|
||||
|
||||
Modern Python code quality tooling using ruff (linting + formatting) and pyright (type checking).
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Install Tools
|
||||
|
||||
```bash
|
||||
# Using uv (recommended)
|
||||
uv add --dev ruff pyright
|
||||
|
||||
# Using pip
|
||||
pip install ruff pyright
|
||||
```
|
||||
|
||||
### Run Quality Checks
|
||||
|
||||
```bash
|
||||
# Format and lint with ruff
|
||||
ruff check --fix .
|
||||
ruff format .
|
||||
|
||||
# Type check with pyright
|
||||
pyright
|
||||
```
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Setting up linting and formatting for a Python project
|
||||
- Configuring type checking
|
||||
- Establishing code quality standards for a team
|
||||
- Integrating quality checks into pre-commit or CI/CD
|
||||
- Migrating from black/flake8/mypy to ruff/pyright
|
||||
|
||||
## Ruff: All-in-One Linter and Formatter
|
||||
|
||||
Ruff combines the functionality of flake8, black, isort, and more:
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- 10-100x faster than alternatives
|
||||
- Drop-in replacement for black, flake8, isort
|
||||
- Single tool configuration
|
||||
- Auto-fix for many violations
|
||||
|
||||
**Configuration:** See `reference/ruff-configuration.md`
|
||||
|
||||
## Pyright: Fast Type Checker
|
||||
|
||||
Pyright provides static type checking for Python:
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Faster than mypy
|
||||
- Better editor integration (VS Code, etc.)
|
||||
- Incremental type checking
|
||||
- Configurable strictness
|
||||
|
||||
**Configuration:** See `reference/pyright-configuration.md`
|
||||
|
||||
## Recommended Workflow
|
||||
|
||||
1. **Pre-commit Hooks** - Run quality checks before each commit
|
||||
- See: `patterns/pre-commit-integration.md`
|
||||
|
||||
2. **CI/CD Quality Gates** - Block merges on quality failures
|
||||
- See: `patterns/ci-cd-quality-gates.md`
|
||||
|
||||
3. **Editor Integration** - Real-time feedback while coding
|
||||
- See: `workflows/quality-workflow.md`
|
||||
|
||||
## Configuration Templates
|
||||
|
||||
Generic starter configs in `examples/`:
|
||||
|
||||
- `pyrightconfig-starter.json` - Minimal type checking
|
||||
- `pyrightconfig-strict.json` - Strict type checking
|
||||
- `ruff-minimal.toml` - Basic linting + formatting
|
||||
- `ruff-comprehensive.toml` - Full-featured config
|
||||
|
||||
## Helper Tools
|
||||
|
||||
- `tools/python_formatter.py` - Batch format Python files
|
||||
- `tools/python_ruff_checker.py` - Check code quality
|
||||
|
||||
## Ruff vs Alternatives
|
||||
|
||||
| Feature | Ruff | Black + Flake8 + isort |
|
||||
|---------|------|------------------------|
|
||||
| Speed | ⚡⚡⚡ | ⚡ |
|
||||
| Configuration | Single file | Multiple files |
|
||||
| Auto-fix | ✅ | Partial |
|
||||
| Formatting | ✅ | Black only |
|
||||
| Import sorting | ✅ | isort only |
|
||||
|
||||
## Pyright vs mypy
|
||||
|
||||
| Feature | Pyright | mypy |
|
||||
|---------|---------|------|
|
||||
| Speed | ⚡⚡⚡ | ⚡⚡ |
|
||||
| VS Code integration | Native | Extension |
|
||||
| Configuration | JSON | INI/TOML |
|
||||
| Incremental checking | ✅ | ✅ |
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Ignore Specific Lines
|
||||
|
||||
```python
|
||||
# Ruff
|
||||
x = 1 # noqa: F841 # Unused variable
|
||||
|
||||
# Pyright
|
||||
x = 1 # type: ignore
|
||||
```
|
||||
|
||||
### Configure Per-Directory
|
||||
|
||||
```toml
|
||||
# ruff.toml
|
||||
[tool.ruff]
|
||||
exclude = ["migrations/", "scripts/"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "W"]
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Choose config template from `examples/`
|
||||
2. Set up pre-commit hooks: `patterns/pre-commit-integration.md`
|
||||
3. Add CI/CD quality gates: `patterns/ci-cd-quality-gates.md`
|
||||
4. Configure editor integration: `workflows/quality-workflow.md`
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
- `reference/ruff-configuration.md` - Complete ruff configuration guide
|
||||
- `reference/ruff-linting-settings.md` - Linting rule categories
|
||||
- `reference/ruff-formatting-settings.md` - Formatting options
|
||||
- `reference/pyright-configuration.md` - Pyright setup and configuration
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"include": ["src", "tests"],
|
||||
"exclude": [
|
||||
"**/node_modules",
|
||||
"**/__pycache__",
|
||||
".venv",
|
||||
"build",
|
||||
"dist"
|
||||
],
|
||||
"venvPath": ".",
|
||||
"venv": ".venv",
|
||||
"typeCheckingMode": "basic",
|
||||
"reportMissingTypeStubs": false,
|
||||
"reportUnknownMemberType": false,
|
||||
"reportUnknownArgumentType": false,
|
||||
"reportUnknownVariableType": false
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"**/node_modules",
|
||||
"**/__pycache__",
|
||||
".venv",
|
||||
"build",
|
||||
"dist"
|
||||
],
|
||||
"venvPath": ".",
|
||||
"venv": ".venv",
|
||||
"typeCheckingMode": "strict",
|
||||
"reportMissingTypeStubs": true,
|
||||
"reportUnknownMemberType": true,
|
||||
"reportUnknownArgumentType": true,
|
||||
"reportUnknownVariableType": true,
|
||||
"reportPrivateUsage": true,
|
||||
"reportUnusedImport": true,
|
||||
"reportUnusedVariable": true
|
||||
}
|
||||
40
skills/python-code-quality/examples/ruff-comprehensive.toml
Normal file
40
skills/python-code-quality/examples/ruff-comprehensive.toml
Normal file
@@ -0,0 +1,40 @@
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
target-version = "py311"
|
||||
exclude = [
|
||||
".git",
|
||||
".venv",
|
||||
"__pycache__",
|
||||
"build",
|
||||
"dist",
|
||||
]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"F", # Pyflakes
|
||||
"I", # isort
|
||||
"N", # pep8-naming
|
||||
"UP", # pyupgrade
|
||||
"B", # flake8-bugbear
|
||||
"C4", # flake8-comprehensions
|
||||
"SIM", # flake8-simplify
|
||||
"TCH", # flake8-type-checking
|
||||
]
|
||||
ignore = [
|
||||
"E501", # line too long (handled by formatter)
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/**/*.py" = [
|
||||
"S101", # allow assert in tests
|
||||
]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["myproject"]
|
||||
16
skills/python-code-quality/examples/ruff-minimal.toml
Normal file
16
skills/python-code-quality/examples/ruff-minimal.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"F", # Pyflakes
|
||||
"I", # isort
|
||||
]
|
||||
ignore = []
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
191
skills/python-code-quality/patterns/ci-cd-quality-gates.md
Normal file
191
skills/python-code-quality/patterns/ci-cd-quality-gates.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# CI/CD Quality Gates for Ruff and Pyright
|
||||
|
||||
Block merges when code quality fails. Run comprehensive checks in CI that catch
|
||||
issues missed locally.
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
### Basic Quality Check
|
||||
|
||||
Create `.github/workflows/quality.yml`:
|
||||
|
||||
```yaml
|
||||
name: Code Quality
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install ruff pyright
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Run ruff
|
||||
run: |
|
||||
ruff check .
|
||||
ruff format --check .
|
||||
|
||||
- name: Run pyright
|
||||
run: pyright
|
||||
```
|
||||
|
||||
### Comprehensive Check with Caching
|
||||
|
||||
```yaml
|
||||
name: Code Quality
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install ruff pyright
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Lint with ruff
|
||||
run: ruff check . --output-format=github
|
||||
|
||||
- name: Check formatting
|
||||
run: ruff format --check . --diff
|
||||
|
||||
- name: Type check with pyright
|
||||
run: pyright --outputjson > pyright-report.json
|
||||
|
||||
- name: Upload pyright report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pyright-report
|
||||
path: pyright-report.json
|
||||
```
|
||||
|
||||
## GitLab CI
|
||||
|
||||
Create `.gitlab-ci.yml`:
|
||||
|
||||
```yaml
|
||||
code-quality:
|
||||
stage: test
|
||||
image: python:3.11
|
||||
before_script:
|
||||
- pip install ruff pyright
|
||||
- pip install -r requirements.txt
|
||||
script:
|
||||
- ruff check .
|
||||
- ruff format --check .
|
||||
- pyright
|
||||
rules:
|
||||
- if: $CI_MERGE_REQUEST_IID
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
```
|
||||
|
||||
## Quality Metrics
|
||||
|
||||
### Track Quality Over Time
|
||||
|
||||
```yaml
|
||||
- name: Generate quality report
|
||||
run: |
|
||||
ruff check . --output-format=json > ruff-report.json
|
||||
pyright --outputjson > pyright-report.json
|
||||
|
||||
- name: Comment PR with quality metrics
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const ruffReport = JSON.parse(fs.readFileSync('ruff-report.json'));
|
||||
const pyrightReport = JSON.parse(fs.readFileSync('pyright-report.json'));
|
||||
|
||||
const comment = `## Code Quality Report
|
||||
|
||||
**Ruff:** ${ruffReport.length} issues
|
||||
**Pyright:** ${pyrightReport.generalDiagnostics.length} issues
|
||||
`;
|
||||
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: comment
|
||||
});
|
||||
```
|
||||
|
||||
## Branch Protection Rules
|
||||
|
||||
### GitHub
|
||||
|
||||
Settings → Branches → Branch protection rules:
|
||||
|
||||
1. Require status checks to pass before merging
|
||||
2. Select "Code Quality" workflow
|
||||
3. Require branches to be up to date before merging
|
||||
|
||||
### GitLab
|
||||
|
||||
Settings → Repository → Protected branches:
|
||||
|
||||
1. Allowed to merge: Developers + Maintainers
|
||||
2. Require approval from code owners
|
||||
3. Pipelines must succeed
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Fail fast** - Run quality checks before tests
|
||||
2. **Cache dependencies** - Speed up CI with pip caching
|
||||
3. **Parallel jobs** - Run ruff and pyright in parallel
|
||||
4. **Quality trends** - Track violations over time
|
||||
5. **Auto-fix in CI** - Create PR with ruff fixes automatically
|
||||
|
||||
## Auto-fix Bot Example
|
||||
|
||||
```yaml
|
||||
- name: Auto-fix with ruff
|
||||
run: ruff check --fix .
|
||||
|
||||
- name: Commit fixes
|
||||
run: |
|
||||
git config user.name "ruff-bot"
|
||||
git config user.email "bot@example.com"
|
||||
git add .
|
||||
git diff --staged --quiet || git commit -m "style: auto-fix ruff violations"
|
||||
git push
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**CI passes but pre-commit fails:**
|
||||
|
||||
- Ensure same ruff/pyright versions in CI and pre-commit
|
||||
- Check `.pre-commit-config.yaml` rev matches installed version
|
||||
|
||||
**CI too slow:**
|
||||
|
||||
- Use pip caching
|
||||
- Run quality checks in parallel with tests
|
||||
- Consider skipping pyright on non-Python file changes
|
||||
107
skills/python-code-quality/patterns/pre-commit-integration.md
Normal file
107
skills/python-code-quality/patterns/pre-commit-integration.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Pre-commit Integration for Ruff and Pyright
|
||||
|
||||
Run quality checks automatically before each commit to prevent bad code from
|
||||
entering the repository.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install pre-commit
|
||||
|
||||
```bash
|
||||
pip install pre-commit
|
||||
```
|
||||
|
||||
### 2. Create .pre-commit-config.yaml
|
||||
|
||||
```yaml
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/RobertCraigie/pyright-python
|
||||
rev: v1.1.380
|
||||
hooks:
|
||||
- id: pyright
|
||||
```
|
||||
|
||||
### 3. Install hooks
|
||||
|
||||
```bash
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Pre-commit hooks now run automatically:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat: add feature"
|
||||
# Hooks run automatically before commit
|
||||
```
|
||||
|
||||
### Skip hooks (when needed)
|
||||
|
||||
```bash
|
||||
git commit --no-verify -m "wip: work in progress"
|
||||
```
|
||||
|
||||
## Manual Runs
|
||||
|
||||
Run hooks on all files:
|
||||
|
||||
```bash
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
Run specific hook:
|
||||
|
||||
```bash
|
||||
pre-commit run ruff --all-files
|
||||
pre-commit run pyright --all-files
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Ruff with auto-fix
|
||||
|
||||
```yaml
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
```
|
||||
|
||||
### Pyright with specific directories
|
||||
|
||||
```yaml
|
||||
- id: pyright
|
||||
files: ^(src|tests)/
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Hook fails with "command not found":**
|
||||
|
||||
- Ensure ruff/pyright installed in environment
|
||||
- Try: `pre-commit clean` then `pre-commit install`
|
||||
|
||||
**Hooks too slow:**
|
||||
|
||||
- Run only on changed files (default behavior)
|
||||
- Skip pyright in pre-commit, run in CI instead
|
||||
|
||||
**Want to update hook versions:**
|
||||
|
||||
```bash
|
||||
pre-commit autoupdate
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep hooks fast** - Pre-commit should be < 10 seconds
|
||||
2. **Auto-fix when possible** - Use `--fix` for ruff
|
||||
3. **Document skip policy** - When is `--no-verify` acceptable?
|
||||
4. **Update regularly** - Run `pre-commit autoupdate` monthly
|
||||
466
skills/python-code-quality/reference/pyright-configuration.md
Normal file
466
skills/python-code-quality/reference/pyright-configuration.md
Normal file
@@ -0,0 +1,466 @@
|
||||
## Pyright Configuration
|
||||
|
||||
Pyright offers flexible configuration options specified in a JSON-formatted text configuration. By default, the file is called “pyrightconfig.json” and is located within the root directory of your project. Multi-root workspaces (“Add Folder to Workspace…”) are supported, and each workspace root can have its own “pyrightconfig.json” file. For a sample pyrightconfig.json file, see [below](configuration.md#sample-config-file).
|
||||
|
||||
Pyright settings can also be specified in a `[tool.pyright]` section of a “pyproject.toml” file. A “pyrightconfig.json” file always takes precedent over “pyproject.toml” if both are present. For a sample pyproject.toml file, see [below](configuration.md#sample-pyprojecttoml-file).
|
||||
|
||||
Relative paths specified within the config file are relative to the config file’s location. Paths with shell variables (including `~`) are not supported. Paths within a config file should generally be relative paths so the config file can be shared by other developers who contribute to the project.
|
||||
|
||||
## Environment Options
|
||||
|
||||
The following settings control the *environment* in which Pyright will check for diagnostics. These settings determine how Pyright finds source files, imports, and what Python version specific rules are applied.
|
||||
|
||||
- **include** [array of paths, optional]: Paths of directories or files that should be considered part of the project. If no paths are specified, pyright defaults to the directory that contains the config file. Paths may contain wildcard characters ** (a directory or multiple levels of directories), * (a sequence of zero or more characters), or ? (a single character). If no include paths are specified, the root path for the workspace is assumed.
|
||||
|
||||
- **exclude** [array of paths, optional]: Paths of directories or files that should not be considered part of the project. These override the directories and files that `include` matched, allowing specific subdirectories to be excluded. Note that files in the exclude paths may still be included in the analysis if they are referenced (imported) by source files that are not excluded. Paths may contain wildcard characters ** (a directory or multiple levels of directories), * (a sequence of zero or more characters), or ? (a single character). If no exclude paths are specified, Pyright automatically excludes the following: `**/node_modules`, `**/__pycache__`, `**/.*`. Pylance also excludes any virtual environment directories regardless of the exclude paths specified. For more detail on Python environment specification and discovery, refer to the [import resolution](import-resolution.md#configuring-your-python-environment) documentation.
|
||||
|
||||
- **strict** [array of paths, optional]: Paths of directories or files that should use “strict” analysis if they are included. This is the same as manually adding a “# pyright: strict” comment. In strict mode, most type-checking rules are enabled. Refer to [this table](configuration.md#diagnostic-settings-defaults) for details about which rules are enabled in strict mode. Paths may contain wildcard characters ** (a directory or multiple levels of directories), * (a sequence of zero or more characters), or ? (a single character).
|
||||
|
||||
- **extends** [path, optional]: Path to another `.json` or `.toml` file that is used as a “base configuration”, allowing this configuration to inherit configuration settings. Top-level keys within this configuration overwrite top-level keys in the base configuration. Multiple levels of inheritance are supported. Relative paths specified in a configuration file are resolved relative to the location of that configuration file.
|
||||
|
||||
- **defineConstant** [map of constants to values (boolean or string), optional]: Set of identifiers that should be assumed to contain a constant value wherever used within this program. For example, `{ "DEBUG": true }` indicates that pyright should assume that the identifier `DEBUG` will always be equal to `True`. If this identifier is used within a conditional expression (such as `if not DEBUG:`) pyright will use the indicated value to determine whether the guarded block is reachable or not. Member expressions that reference one of these constants (e.g. `my_module.DEBUG`) are also supported.
|
||||
|
||||
- **typeshedPath** [path, optional]: Path to a directory that contains typeshed type stub files. Pyright ships with a bundled copy of typeshed type stubs. If you want to use a different version of typeshed stubs, you can clone the [typeshed github repo](https://github.com/python/typeshed) to a local directory and reference the location with this path. This option is useful if you’re actively contributing updates to typeshed.
|
||||
|
||||
- **stubPath** [path, optional]: Path to a directory that contains custom type stubs. Each package's type stub file(s) are expected to be in its own subdirectory. The default value of this setting is "./typings". (typingsPath is now deprecated)
|
||||
|
||||
- **venvPath** [path, optional]: Path to a directory containing one or more subdirectories, each of which contains a virtual environment. When used in conjunction with a **venv** setting (see below), pyright will search for imports in the virtual environment’s site-packages directory rather than the paths specified by the default Python interpreter. If you are working on a project with other developers, it is best not to specify this setting in the config file, since this path will typically differ for each developer. Instead, it can be specified on the command line or in a per-user setting. For more details, refer to the [import resolution](import-resolution.md#configuring-your-python-environment) documentation. This setting is ignored when using Pylance. VS Code's python interpreter path is used instead.
|
||||
|
||||
- **venv** [string, optional]: Used in conjunction with the venvPath, specifies the virtual environment to use. For more details, refer to the [import resolution](import-resolution.md#configuring-your-python-environment) documentation. This setting is ignored when using Pylance.
|
||||
|
||||
- **verboseOutput** [boolean]: Specifies whether output logs should be verbose. This is useful when diagnosing certain problems like import resolution issues.
|
||||
|
||||
- **extraPaths** [array of strings, optional]: Additional search paths that will be used when searching for modules imported by files.
|
||||
|
||||
- **pythonVersion** [string, optional]: Specifies the version of Python that will be used to execute the source code. The version should be specified as a string in the format "M.m" where M is the major version and m is the minor (e.g. `"3.0"` or `"3.6"`). If a version is provided, pyright will generate errors if the source code makes use of language features that are not supported in that version. It will also tailor its use of type stub files, which conditionalizes type definitions based on the version. If no version is specified, pyright will use the version of the current python interpreter, if one is present.
|
||||
|
||||
- **pythonPlatform** [string, optional]: Specifies the target platform that will be used to execute the source code. Should be one of `"Windows"`, `"Darwin"`, `"Linux"`, or `"All"`. If specified, pyright will tailor its use of type stub files, which conditionalize type definitions based on the platform. If no platform is specified, pyright will use the current platform.
|
||||
|
||||
- **executionEnvironments** [array of objects, optional]: Specifies a list of execution environments (see [below](configuration.md#execution-environment-options)). Execution environments are searched from start to finish by comparing the path of a source file with the root path specified in the execution environment.
|
||||
|
||||
- **useLibraryCodeForTypes** [boolean]: Determines whether pyright reads, parses and analyzes library code to extract type information in the absence of type stub files. Type information will typically be incomplete. We recommend using type stubs where possible. The default value for this option is true.
|
||||
|
||||
## Type Evaluation Settings
|
||||
|
||||
The following settings determine how different types should be evaluated.
|
||||
|
||||
- **strictListInference** [boolean]: When inferring the type of a list, use strict type assumptions. For example, the expression `[1, 'a', 3.4]` could be inferred to be of type `list[Any]` or `list[int | str | float]`. If this setting is true, it will use the latter (stricter) type. The default value for this setting is `false`.
|
||||
|
||||
- **strictDictionaryInference** [boolean]: When inferring the type of a dictionary’s keys and values, use strict type assumptions. For example, the expression `{'a': 1, 'b': 'a'}` could be inferred to be of type `dict[str, Any]` or `dict[str, int | str]`. If this setting is true, it will use the latter (stricter) type. The default value for this setting is `false`.
|
||||
|
||||
- **strictSetInference** [boolean]: When inferring the type of a set, use strict type assumptions. For example, the expression `{1, 'a', 3.4}` could be inferred to be of type `set[Any]` or `set[int | str | float]`. If this setting is true, it will use the latter (stricter) type. The default value for this setting is `false`.
|
||||
|
||||
- **analyzeUnannotatedFunctions** [boolean]: Analyze and report errors for functions and methods that have no type annotations for input parameters or return types. The default value for this setting is `true`.
|
||||
|
||||
- **strictParameterNoneValue** [boolean]: PEP 484 indicates that when a function parameter is assigned a default value of None, its type should implicitly be Optional even if the explicit type is not. When enabled, this rule requires that parameter type annotations use Optional explicitly in this case. The default value for this setting is `true`.
|
||||
|
||||
- **enableTypeIgnoreComments** [boolean]: PEP 484 defines support for "# type: ignore" comments. This switch enables or disables support for these comments. The default value for this setting is `true`. This does not affect "# pyright: ignore" comments.
|
||||
|
||||
- **deprecateTypingAliases** [boolean]: PEP 585 indicates that aliases to types in standard collections that were introduced solely to support generics are deprecated as of Python 3.9. This switch controls whether these are treated as deprecated. This applies only when pythonVersion is 3.9 or newer. The default value for this setting is `false` but may be switched to `true` in the future.
|
||||
|
||||
- **enableReachabilityAnalysis** [boolean]: If enabled, code that is determined to be unreachable by type analysis is reported using a tagged hint. This setting does not affect code that is determined to be unreachable independent of type analysis; such code is always reported as unreachable using a tagged hint. This setting also has no effect when using the command-line version of pyright because it never emits tagged hints for unreachable code.
|
||||
|
||||
- **enableExperimentalFeatures** [boolean]: Enables a set of experimental (mostly undocumented) features that correspond to proposed or exploratory changes to the Python typing standard. These features will likely change or be removed, so they should not be used except for experimentation purposes. The default value for this setting is `false`.
|
||||
|
||||
- **disableBytesTypePromotions** [boolean]: Disables legacy behavior where `bytearray` and `memoryview` are considered subtypes of `bytes`. [PEP 688](https://peps.python.org/pep-0688/#no-special-meaning-for-bytes) deprecates this behavior, but this switch is provided to restore the older behavior. The default value for this setting is `true`.
|
||||
|
||||
## Type Check Diagnostics Settings
|
||||
|
||||
The following settings control pyright’s diagnostic output (warnings or errors).
|
||||
|
||||
- **typeCheckingMode** ["off", "basic", "standard", "strict"]: Specifies the default rule set to use. Some rules can be overridden using additional configuration flags documented below. The default value for this setting is "standard". If set to "off", all type-checking rules are disabled, but Python syntax and semantic errors are still reported.
|
||||
|
||||
- **ignore** [array of paths, optional]: Paths of directories or files whose diagnostic output (errors and warnings) should be suppressed even if they are an included file or within the transitive closure of an included file. Paths may contain wildcard characters ** (a directory or multiple levels of directories), * (a sequence of zero or more characters), or ? (a single character). This setting can be overridden in VS code in your settings.json.
|
||||
|
||||
### Type Check Rule Overrides
|
||||
|
||||
The following settings allow more fine grained control over the **typeCheckingMode**. Unless otherwise specified, each diagnostic setting can specify a boolean value (`false` indicating that no error is generated and `true` indicating that an error is generated). Alternatively, a string value of `"none"`, `"warning"`, `"information"`, or `"error"` can be used to specify the diagnostic level.
|
||||
|
||||
- **reportGeneralTypeIssues** [boolean or string, optional]: Generate or suppress diagnostics for general type inconsistencies, unsupported operations, argument/parameter mismatches, etc. This covers all of the basic type-checking rules not covered by other rules. It does not include syntax errors. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportPropertyTypeMismatch** [boolean or string, optional]: Generate or suppress diagnostics for properties where the type of the value passed to the setter is not assignable to the value returned by the getter. Such mismatches violate the intended use of properties, which are meant to act like variables. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportFunctionMemberAccess** [boolean or string, optional]: Generate or suppress diagnostics for non-standard member accesses for functions. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportMissingImports** [boolean or string, optional]: Generate or suppress diagnostics for imports that have no corresponding imported python file or type stub file. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportMissingModuleSource** [boolean or string, optional]: Generate or suppress diagnostics for imports that have no corresponding source file. This happens when a type stub is found, but the module source file was not found, indicating that the code may fail at runtime when using this execution environment. Type checking will be done using the type stub. The default value for this setting is `"warning"`.
|
||||
|
||||
- **reportInvalidTypeForm** [boolean or string, optional]: Generate or suppress diagnostics for type annotations that use invalid type expression forms or are semantically invalid. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportMissingTypeStubs** [boolean or string, optional]: Generate or suppress diagnostics for imports that have no corresponding type stub file (either a typeshed file or a custom type stub). The type checker requires type stubs to do its best job at analysis. The default value for this setting is `"none"`. Note that there is a corresponding quick fix for this diagnostics that let you generate custom type stub to improve editing experiences.
|
||||
|
||||
- **reportImportCycles** [boolean or string, optional]: Generate or suppress diagnostics for cyclical import chains. These are not errors in Python, but they do slow down type analysis and often hint at architectural layering issues. Generally, they should be avoided. The default value for this setting is `"none"`. Note that there are import cycles in the typeshed stdlib typestub files that are ignored by this setting.
|
||||
|
||||
- **reportUnusedImport** [boolean or string, optional]: Generate or suppress diagnostics for an imported symbol that is not referenced within that file. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUnusedClass** [boolean or string, optional]: Generate or suppress diagnostics for a class with a private name (starting with an underscore) that is not accessed. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUnusedFunction** [boolean or string, optional]: Generate or suppress diagnostics for a function or method with a private name (starting with an underscore) that is not accessed. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUnusedVariable** [boolean or string, optional]: Generate or suppress diagnostics for a variable that is not accessed. The default value for this setting is `"none"`. Variables whose names begin with an underscore are exempt from this check.
|
||||
|
||||
- **reportDuplicateImport** [boolean or string, optional]: Generate or suppress diagnostics for an imported symbol or module that is imported more than once. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportWildcardImportFromLibrary** [boolean or string, optional]: Generate or suppress diagnostics for a wildcard import from an external library. The use of this language feature is highly discouraged and can result in bugs when the library is updated. The default value for this setting is `"warning"`.
|
||||
|
||||
- **reportAbstractUsage** [boolean or string, optional]: Generate or suppress diagnostics for the attempted instantiate an abstract or protocol class or use of an abstract method. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportArgumentType** [boolean or string, optional]: Generate or suppress diagnostics for argument type incompatibilities when evaluating a call expression. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportAssertTypeFailure** [boolean or string, optional]: Generate or suppress diagnostics for a type mismatch detected by the `typing.assert_type` call. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportAssignmentType** [boolean or string, optional]: Generate or suppress diagnostics for assignment type incompatibility. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportAttributeAccessIssue** [boolean or string, optional]: Generate or suppress diagnostics related to attribute accesses. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportCallIssue** [boolean or string, optional]: Generate or suppress diagnostics related to call expressions and arguments passed to a call target. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportInconsistentOverload** [boolean or string, optional]: Generate or suppress diagnostics for an overloaded function that has overload signatures that are inconsistent with each other or with the implementation. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportIndexIssue** [boolean or string, optional]: Generate or suppress diagnostics related to index operations and expressions. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportInvalidTypeArguments** [boolean or string, optional]: Generate or suppress diagnostics for invalid type argument usage. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportNoOverloadImplementation** [boolean or string, optional]: Generate or suppress diagnostics for an overloaded function or method if the implementation is not provided. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportOperatorIssue** [boolean or string, optional]: Generate or suppress diagnostics related to the use of unary or binary operators (like `*` or `not`). The default value for this setting is `"error"`.
|
||||
|
||||
- **reportOptionalSubscript** [boolean or string, optional]: Generate or suppress diagnostics for an attempt to subscript (index) a variable with an Optional type. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportOptionalMemberAccess** [boolean or string, optional]: Generate or suppress diagnostics for an attempt to access a member of a variable with an Optional type. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportOptionalCall** [boolean or string, optional]: Generate or suppress diagnostics for an attempt to call a variable with an Optional type. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportOptionalIterable** [boolean or string, optional]: Generate or suppress diagnostics for an attempt to use an Optional type as an iterable value (e.g. within a `for` statement). The default value for this setting is `"error"`.
|
||||
|
||||
- **reportOptionalContextManager** [boolean or string, optional]: Generate or suppress diagnostics for an attempt to use an Optional type as a context manager (as a parameter to a `with` statement). The default value for this setting is `"error"`.
|
||||
|
||||
- **reportOptionalOperand** [boolean or string, optional]: Generate or suppress diagnostics for an attempt to use an Optional type as an operand to a unary operator (like `~`) or the left-hand operator of a binary operator (like `*` or `<<`). The default value for this setting is `"error"`.
|
||||
|
||||
- **reportRedeclaration** [boolean or string, optional]: Generate or suppress diagnostics for a symbol that has more than one type declaration. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportReturnType** [boolean or string, optional]: Generate or suppress diagnostics related to function return type compatibility. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportTypedDictNotRequiredAccess** [boolean or string, optional]: Generate or suppress diagnostics for an attempt to access a non-required field within a TypedDict without first checking whether it is present. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportUntypedFunctionDecorator** [boolean or string, optional]: Generate or suppress diagnostics for function decorators that have no type annotations. These obscure the function type, defeating many type analysis features. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUntypedClassDecorator** [boolean or string, optional]: Generate or suppress diagnostics for class decorators that have no type annotations. These obscure the class type, defeating many type analysis features. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUntypedBaseClass** [boolean or string, optional]: Generate or suppress diagnostics for base classes whose type cannot be determined statically. These obscure the class type, defeating many type analysis features. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUntypedNamedTuple** [boolean or string, optional]: Generate or suppress diagnostics when “namedtuple” is used rather than “NamedTuple”. The former contains no type information, whereas the latter does. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportPrivateUsage** [boolean or string, optional]: Generate or suppress diagnostics for incorrect usage of private or protected variables or functions. Protected class members begin with a single underscore (“_”) and can be accessed only by subclasses. Private class members begin with a double underscore but do not end in a double underscore and can be accessed only within the declaring class. Variables and functions declared outside of a class are considered private if their names start with either a single or double underscore, and they cannot be accessed outside of the declaring module. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportTypeCommentUsage** [boolean or string, optional]: Prior to Python 3.5, the grammar did not support type annotations, so types needed to be specified using “type comments”. Python 3.5 eliminated the need for function type comments, and Python 3.6 eliminated the need for variable type comments. Future versions of Python will likely deprecate all support for type comments. If enabled, this check will flag any type comment usage unless it is required for compatibility with the specified language version. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportPrivateImportUsage** [boolean or string, optional]: Generate or suppress diagnostics for use of a symbol from a "py.typed" module that is not meant to be exported from that module. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportConstantRedefinition** [boolean or string, optional]: Generate or suppress diagnostics for attempts to redefine variables whose names are all-caps with underscores and numerals. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportDeprecated** [boolean or string, optional]: Generate or suppress diagnostics for use of a class or function that has been marked as deprecated. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportIncompatibleMethodOverride** [boolean or string, optional]: Generate or suppress diagnostics for methods that override a method of the same name in a base class in an incompatible manner (wrong number of parameters, incompatible parameter types, or incompatible return type). The default value for this setting is `"error"`.
|
||||
|
||||
- **reportIncompatibleVariableOverride** [boolean or string, optional]: Generate or suppress diagnostics for class variable declarations that override a symbol of the same name in a base class with a type that is incompatible with the base class symbol type. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportInconsistentConstructor** [boolean or string, optional]: Generate or suppress diagnostics when an `__init__` method signature is inconsistent with a `__new__` signature. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportOverlappingOverload** [boolean or string, optional]: Generate or suppress diagnostics for function overloads that overlap in signature and obscure each other or have incompatible return types. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportPossiblyUnboundVariable** [boolean or string, optional]: Generate or suppress diagnostics for variables that are possibly unbound on some code paths. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportMissingSuperCall** [boolean or string, optional]: Generate or suppress diagnostics for `__init__`, `__init_subclass__`, `__enter__` and `__exit__` methods in a subclass that fail to call through to the same-named method on a base class. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUninitializedInstanceVariable** [boolean or string, optional]: Generate or suppress diagnostics for instance variables within a class that are not initialized or declared within the class body or the `__init__` method. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportInvalidStringEscapeSequence** [boolean or string, optional]: Generate or suppress diagnostics for invalid escape sequences used within string literals. The Python specification indicates that such sequences will generate a syntax error in future versions. The default value for this setting is `"warning"`.
|
||||
|
||||
- **reportUnknownParameterType** [boolean or string, optional]: Generate or suppress diagnostics for input or return parameters for functions or methods that have an unknown type. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUnknownArgumentType** [boolean or string, optional]: Generate or suppress diagnostics for call arguments for functions or methods that have an unknown type. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUnknownLambdaType** [boolean or string, optional]: Generate or suppress diagnostics for input or return parameters for lambdas that have an unknown type. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUnknownVariableType** [boolean or string, optional]: Generate or suppress diagnostics for variables that have an unknown type. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUnknownMemberType** [boolean or string, optional]: Generate or suppress diagnostics for class or instance variables that have an unknown type. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportMissingParameterType** [boolean or string, optional]: Generate or suppress diagnostics for input parameters for functions or methods that are missing a type annotation. The `self` and `cls` parameters used within methods are exempt from this check. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportMissingTypeArgument** [boolean or string, optional]: Generate or suppress diagnostics when a generic class is used without providing explicit or implicit type arguments. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportInvalidTypeVarUse** [boolean or string, optional]: Generate or suppress diagnostics when a TypeVar is used inappropriately (e.g. if a TypeVar appears only once) within a generic function signature. The default value for this setting is `"warning"`.
|
||||
|
||||
- **reportCallInDefaultInitializer** [boolean or string, optional]: Generate or suppress diagnostics for function calls, list expressions, set expressions, or dictionary expressions within a default value initialization expression. Such calls can mask expensive operations that are performed at module initialization time. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUnnecessaryIsInstance** [boolean or string, optional]: Generate or suppress diagnostics for `isinstance` or `issubclass` calls where the result is statically determined to be always true or always false. Such calls are often indicative of a programming error. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUnnecessaryCast** [boolean or string, optional]: Generate or suppress diagnostics for `cast` calls that are statically determined to be unnecessary. Such calls are sometimes indicative of a programming error. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUnnecessaryComparison** [boolean or string, optional]: Generate or suppress diagnostics for `==` or `!=` comparisons or other conditional expressions that are statically determined to always evaluate to False or True. Such comparisons are sometimes indicative of a programming error. The default value for this setting is `"none"`. Also reports `case` clauses in a `match` statement that can be statically determined to never match (with exception of the `_` wildcard pattern, which is exempted).
|
||||
|
||||
- **reportUnnecessaryContains** [boolean or string, optional]: Generate or suppress diagnostics for `in` operations that are statically determined to always evaluate to False or True. Such operations are sometimes indicative of a programming error. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportAssertAlwaysTrue** [boolean or string, optional]: Generate or suppress diagnostics for `assert` statement that will provably always assert because its first argument is a parenthesized tuple (for example, `assert (v > 0, "Bad value")` when the intent was probably `assert v > 0, "Bad value"`). This is a common programming error. The default value for this setting is `"warning"`.
|
||||
|
||||
- **reportSelfClsParameterName** [boolean or string, optional]: Generate or suppress diagnostics for a missing or misnamed “self” parameter in instance methods and “cls” parameter in class methods. Instance methods in metaclasses (classes that derive from “type”) are allowed to use “cls” for instance methods. The default value for this setting is `"warning"`.
|
||||
|
||||
- **reportImplicitStringConcatenation** [boolean or string, optional]: Generate or suppress diagnostics for two or more string literals that follow each other, indicating an implicit concatenation. This is considered a bad practice and often masks bugs such as missing commas. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUndefinedVariable** [boolean or string, optional]: Generate or suppress diagnostics for undefined variables. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportUnboundVariable** [boolean or string, optional]: Generate or suppress diagnostics for unbound variables. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportUnhashable** [boolean or string, optional]: Generate or suppress diagnostics for the use of an unhashable object in a container that requires hashability. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportInvalidStubStatement** [boolean or string, optional]: Generate or suppress diagnostics for statements that are syntactically correct but have no purpose within a type stub file. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportIncompleteStub** [boolean or string, optional]: Generate or suppress diagnostics for a module-level `__getattr__` call in a type stub file, indicating that it is incomplete. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUnsupportedDunderAll** [boolean or string, optional]: Generate or suppress diagnostics for statements that define or manipulate `__all__` in a way that is not allowed by a static type checker, thus rendering the contents of `__all__` to be unknown or incorrect. Also reports names within the `__all__` list that are not present in the module namespace. The default value for this setting is `"warning"`.
|
||||
|
||||
- **reportUnusedCallResult** [boolean or string, optional]: Generate or suppress diagnostics for call statements whose return value is not used in any way and is not None. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUnusedCoroutine** [boolean or string, optional]: Generate or suppress diagnostics for call statements whose return value is not used in any way and is a Coroutine. This identifies a common error where an `await` keyword is mistakenly omitted. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportUnusedExcept** [boolean or string, optional]: Generate or suppress diagnostics for an `except` clause that will never be reached. The default value for this setting is `"error"`.
|
||||
|
||||
- **reportUnusedExpression** [boolean or string, optional]: Generate or suppress diagnostics for simple expressions whose results are not used in any way. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUnnecessaryTypeIgnoreComment** [boolean or string, optional]: Generate or suppress diagnostics for a `# type: ignore` or `# pyright: ignore` comment that would have no effect if removed. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportMatchNotExhaustive** [boolean or string, optional]: Generate or suppress diagnostics for a `match` statement that does not provide cases that exhaustively match against all potential types of the target expression. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportUnreachable** [boolean or string, optional]: Generate or suppress diagnostics for code that is determined to be structurally unreachable or unreachable by type analysis. The default value for this setting is `"none"`.
|
||||
|
||||
- **reportImplicitOverride** [boolean or string, optional]: Generate or suppress diagnostics for overridden methods in a class that are missing an explicit `@override` decorator. The default value for this setting is `"none"`.
|
||||
|
||||
## Execution Environment Options
|
||||
|
||||
Pyright allows multiple “execution environments” to be defined for different portions of your source tree. For example, a subtree may be designed to run with different import search paths or a different version of the python interpreter than the rest of the source base.
|
||||
|
||||
The following settings can be specified for each execution environment. Each source file within a project is associated with at most one execution environment -- the first one whose root directory contains that file.
|
||||
|
||||
- **root** [string, required]: Root path for the code that will execute within this execution environment.
|
||||
|
||||
- **extraPaths** [array of strings, optional]: Additional search paths (in addition to the root path) that will be used when searching for modules imported by files within this execution environment. If specified, this overrides the default extraPaths setting when resolving imports for files within this execution environment. Note that each file’s execution environment mapping is independent, so if file A is in one execution environment and imports a second file B within a second execution environment, any imports from B will use the extraPaths in the second execution environment.
|
||||
|
||||
- **pythonVersion** [string, optional]: The version of Python used for this execution environment. If not specified, the global `pythonVersion` setting is used instead.
|
||||
|
||||
- **pythonPlatform** [string, optional]: Specifies the target platform that will be used for this execution environment. If not specified, the global `pythonPlatform` setting is used instead.
|
||||
|
||||
In addition, any of the [type check diagnostics settings](configuration.md#type-check-diagnostics-settings) listed above can be specified. These settings act as overrides for the files in this execution environment.
|
||||
|
||||
## Sample Config File
|
||||
|
||||
The following is an example of a pyright config file:
|
||||
|
||||
```json
|
||||
{
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
|
||||
"exclude": [
|
||||
"**/node_modules",
|
||||
"**/__pycache__",
|
||||
"src/experimental",
|
||||
"src/typestubs"
|
||||
],
|
||||
|
||||
"ignore": [
|
||||
"src/oldstuff"
|
||||
],
|
||||
|
||||
"defineConstant": {
|
||||
"DEBUG": true
|
||||
},
|
||||
|
||||
"stubPath": "src/stubs",
|
||||
|
||||
"reportMissingImports": "error",
|
||||
"reportMissingTypeStubs": false,
|
||||
|
||||
"pythonVersion": "3.6",
|
||||
"pythonPlatform": "Linux",
|
||||
|
||||
"executionEnvironments": [
|
||||
{
|
||||
"root": "src/web",
|
||||
"pythonVersion": "3.5",
|
||||
"pythonPlatform": "Windows",
|
||||
"extraPaths": [
|
||||
"src/service_libs"
|
||||
],
|
||||
"reportMissingImports": "warning"
|
||||
},
|
||||
{
|
||||
"root": "src/sdk",
|
||||
"pythonVersion": "3.0",
|
||||
"extraPaths": [
|
||||
"src/backend"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "src/tests",
|
||||
"extraPaths": [
|
||||
"src/tests/e2e",
|
||||
"src/sdk"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "src"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Sample pyproject.toml File
|
||||
|
||||
```toml
|
||||
[tool.pyright]
|
||||
include = ["src"]
|
||||
exclude = ["**/node_modules",
|
||||
"**/__pycache__",
|
||||
"src/experimental",
|
||||
"src/typestubs"
|
||||
]
|
||||
ignore = ["src/oldstuff"]
|
||||
defineConstant = { DEBUG = true }
|
||||
stubPath = "src/stubs"
|
||||
|
||||
reportMissingImports = "error"
|
||||
reportMissingTypeStubs = false
|
||||
|
||||
pythonVersion = "3.6"
|
||||
pythonPlatform = "Linux"
|
||||
|
||||
executionEnvironments = [
|
||||
{ root = "src/web", pythonVersion = "3.5", pythonPlatform = "Windows", extraPaths = [ "src/service_libs" ], reportMissingImports = "warning" },
|
||||
{ root = "src/sdk", pythonVersion = "3.0", extraPaths = [ "src/backend" ] },
|
||||
{ root = "src/tests", extraPaths = ["src/tests/e2e", "src/sdk" ]},
|
||||
{ root = "src" }
|
||||
]
|
||||
```
|
||||
|
||||
## Diagnostic Settings Defaults
|
||||
|
||||
Each diagnostic setting has a default that is dictated by the specified type checking mode. The default for each rule can be overridden in the configuration file or settings. In strict type checking mode, overrides may only increase the strictness (e.g. increase the severity level from `"warning"` to `"error"`).
|
||||
|
||||
The following table lists the default severity levels for each diagnostic rule within each type checking mode (`"off"`, `"basic"`, `"standard"` and `"strict"`).
|
||||
|
||||
| Diagnostic Rule | Off | Basic | Standard | Strict |
|
||||
| :---------------------------------------- | :--------- | :--------- | :--------- | :--------- |
|
||||
| analyzeUnannotatedFunctions | true | true | true | true |
|
||||
| disableBytesTypePromotions | true | true | true | true |
|
||||
| strictParameterNoneValue | true | true | true | true |
|
||||
| enableTypeIgnoreComments | true | true | true | true |
|
||||
| enableReachabilityAnalysis | false | true | true | true |
|
||||
| strictListInference | false | false | false | true |
|
||||
| strictDictionaryInference | false | false | false | true |
|
||||
| strictSetInference | false | false | false | true |
|
||||
| deprecateTypingAliases | false | false | false | false |
|
||||
| enableExperimentalFeatures | false | false | false | false |
|
||||
| reportMissingTypeStubs | "none" | "none" | "none" | "error" |
|
||||
| reportMissingModuleSource | "warning" | "warning" | "warning" | "warning" |
|
||||
| reportInvalidTypeForm | "warning" | "error" | "error" | "error" |
|
||||
| reportMissingImports | "warning" | "error" | "error" | "error" |
|
||||
| reportUndefinedVariable | "warning" | "error" | "error" | "error" |
|
||||
| reportAssertAlwaysTrue | "none" | "warning" | "warning" | "error" |
|
||||
| reportInvalidStringEscapeSequence | "none" | "warning" | "warning" | "error" |
|
||||
| reportInvalidTypeVarUse | "none" | "warning" | "warning" | "error" |
|
||||
| reportSelfClsParameterName | "none" | "warning" | "warning" | "error" |
|
||||
| reportUnsupportedDunderAll | "none" | "warning" | "warning" | "error" |
|
||||
| reportUnusedExpression | "none" | "warning" | "warning" | "error" |
|
||||
| reportWildcardImportFromLibrary | "none" | "warning" | "warning" | "error" |
|
||||
| reportAbstractUsage | "none" | "error" | "error" | "error" |
|
||||
| reportArgumentType | "none" | "error" | "error" | "error" |
|
||||
| reportAssertTypeFailure | "none" | "error" | "error" | "error" |
|
||||
| reportAssignmentType | "none" | "error" | "error" | "error" |
|
||||
| reportAttributeAccessIssue | "none" | "error" | "error" | "error" |
|
||||
| reportCallIssue | "none" | "error" | "error" | "error" |
|
||||
| reportGeneralTypeIssues | "none" | "error" | "error" | "error" |
|
||||
| reportInconsistentOverload | "none" | "error" | "error" | "error" |
|
||||
| reportIndexIssue | "none" | "error" | "error" | "error" |
|
||||
| reportInvalidTypeArguments | "none" | "error" | "error" | "error" |
|
||||
| reportNoOverloadImplementation | "none" | "error" | "error" | "error" |
|
||||
| reportOperatorIssue | "none" | "error" | "error" | "error" |
|
||||
| reportOptionalSubscript | "none" | "error" | "error" | "error" |
|
||||
| reportOptionalMemberAccess | "none" | "error" | "error" | "error" |
|
||||
| reportOptionalCall | "none" | "error" | "error" | "error" |
|
||||
| reportOptionalIterable | "none" | "error" | "error" | "error" |
|
||||
| reportOptionalContextManager | "none" | "error" | "error" | "error" |
|
||||
| reportOptionalOperand | "none" | "error" | "error" | "error" |
|
||||
| reportRedeclaration | "none" | "error" | "error" | "error" |
|
||||
| reportReturnType | "none" | "error" | "error" | "error" |
|
||||
| reportTypedDictNotRequiredAccess | "none" | "error" | "error" | "error" |
|
||||
| reportPrivateImportUsage | "none" | "error" | "error" | "error" |
|
||||
| reportUnboundVariable | "none" | "error" | "error" | "error" |
|
||||
| reportUnhashable | "none" | "error" | "error" | "error" |
|
||||
| reportUnusedCoroutine | "none" | "error" | "error" | "error" |
|
||||
| reportUnusedExcept | "none" | "error" | "error" | "error" |
|
||||
| reportFunctionMemberAccess | "none" | "none" | "error" | "error" |
|
||||
| reportIncompatibleMethodOverride | "none" | "none" | "error" | "error" |
|
||||
| reportIncompatibleVariableOverride | "none" | "none" | "error" | "error" |
|
||||
| reportOverlappingOverload | "none" | "none" | "error" | "error" |
|
||||
| reportPossiblyUnboundVariable | "none" | "none" | "error" | "error" |
|
||||
| reportConstantRedefinition | "none" | "none" | "none" | "error" |
|
||||
| reportDeprecated | "none" | "none" | "none" | "error" |
|
||||
| reportDuplicateImport | "none" | "none" | "none" | "error" |
|
||||
| reportIncompleteStub | "none" | "none" | "none" | "error" |
|
||||
| reportInconsistentConstructor | "none" | "none" | "none" | "error" |
|
||||
| reportInvalidStubStatement | "none" | "none" | "none" | "error" |
|
||||
| reportMatchNotExhaustive | "none" | "none" | "none" | "error" |
|
||||
| reportMissingParameterType | "none" | "none" | "none" | "error" |
|
||||
| reportMissingTypeArgument | "none" | "none" | "none" | "error" |
|
||||
| reportPrivateUsage | "none" | "none" | "none" | "error" |
|
||||
| reportTypeCommentUsage | "none" | "none" | "none" | "error" |
|
||||
| reportUnknownArgumentType | "none" | "none" | "none" | "error" |
|
||||
| reportUnknownLambdaType | "none" | "none" | "none" | "error" |
|
||||
| reportUnknownMemberType | "none" | "none" | "none" | "error" |
|
||||
| reportUnknownParameterType | "none" | "none" | "none" | "error" |
|
||||
| reportUnknownVariableType | "none" | "none" | "none" | "error" |
|
||||
| reportUnnecessaryCast | "none" | "none" | "none" | "error" |
|
||||
| reportUnnecessaryComparison | "none" | "none" | "none" | "error" |
|
||||
| reportUnnecessaryContains | "none" | "none" | "none" | "error" |
|
||||
| reportUnnecessaryIsInstance | "none" | "none" | "none" | "error" |
|
||||
| reportUnusedClass | "none" | "none" | "none" | "error" |
|
||||
| reportUnusedImport | "none" | "none" | "none" | "error" |
|
||||
| reportUnusedFunction | "none" | "none" | "none" | "error" |
|
||||
| reportUnusedVariable | "none" | "none" | "none" | "error" |
|
||||
| reportUntypedBaseClass | "none" | "none" | "none" | "error" |
|
||||
| reportUntypedClassDecorator | "none" | "none" | "none" | "error" |
|
||||
| reportUntypedFunctionDecorator | "none" | "none" | "none" | "error" |
|
||||
| reportUntypedNamedTuple | "none" | "none" | "none" | "error" |
|
||||
| reportCallInDefaultInitializer | "none" | "none" | "none" | "none" |
|
||||
| reportImplicitOverride | "none" | "none" | "none" | "none" |
|
||||
| reportImplicitStringConcatenation | "none" | "none" | "none" | "none" |
|
||||
| reportImportCycles | "none" | "none" | "none" | "none" |
|
||||
| reportMissingSuperCall | "none" | "none" | "none" | "none" |
|
||||
| reportPropertyTypeMismatch | "none" | "none" | "none" | "none" |
|
||||
| reportUninitializedInstanceVariable | "none" | "none" | "none" | "none" |
|
||||
| reportUnnecessaryTypeIgnoreComment | "none" | "none" | "none" | "none" |
|
||||
| reportUnreachable | "none" | "none" | "none" | "none" |
|
||||
| reportUnusedCallResult | "none" | "none" | "none" | "none" |
|
||||
|
||||
## Overriding settings (in VS Code)
|
||||
|
||||
If a pyproject.toml (with a pyright section) or a pyrightconfig.json exists, any pyright settings in a VS Code settings.json will be ignored. Pyrightconfig.json is prescribing the environment to be used for a particular project. Changing the environment configuration options per user is not supported.
|
||||
|
||||
If a pyproject.toml (with a pyright section) or a pyrightconfig.json does not exist, then the VS Code settings.json settings apply.
|
||||
|
||||
## Locale Configuration
|
||||
|
||||
Pyright provides diagnostic messages that are translated to multiple languages. By default, pyright uses the default locale of the operating system. You can override the desired locale through the use of one of the following environment variables, listed in priority order.
|
||||
|
||||
```text
|
||||
LC_ALL="de"
|
||||
LC_MESSAGES="en-us"
|
||||
LANG="zh-cn"
|
||||
LANGUAGE="fr"
|
||||
```
|
||||
|
||||
When running in VS Code, the editor's locale takes precedence. Setting these environment variables applies only when using pyright outside of VS Code.
|
||||
895
skills/python-code-quality/reference/ruff-configuration.md
Normal file
895
skills/python-code-quality/reference/ruff-configuration.md
Normal file
@@ -0,0 +1,895 @@
|
||||
# Configuring Ruff
|
||||
|
||||
Ruff can be configured through a `pyproject.toml`, `ruff.toml`, or `.ruff.toml` file.
|
||||
|
||||
Whether you're using Ruff as a linter, formatter, or both, the underlying configuration strategy and
|
||||
semantics are the same.
|
||||
|
||||
For a complete enumeration of the available configuration options, see [_Settings_](settings.md).
|
||||
|
||||
If left unspecified, Ruff's default configuration is equivalent to:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
# Exclude a variety of commonly ignored directories.
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".ipynb_checkpoints",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pyenv",
|
||||
".pytest_cache",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
".vscode",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"site-packages",
|
||||
"venv",
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
## Same as Black. # Same as Black
|
||||
|
||||
line-length = 88
|
||||
indent-width = 4
|
||||
|
||||
```text
|
||||
# Assume Python 3.9 # Assume Python 3.9
|
||||
target-version = "py39"
|
||||
|
||||
```
|
||||
|
||||
[tool.ruff.lint] [tool.ruff.lint]
|
||||
|
||||
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||
|
||||
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
|
||||
|
||||
# McCabe complexity (`C901`) by default.
|
||||
|
||||
select = ["E4", "E7", "E9", "F"]
|
||||
ignore = []
|
||||
|
||||
```text
|
||||
# Allow fix for all enabled rules (when `--fix`) is provided. # Allow fix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
```
|
||||
|
||||
## Allow unused variables when underscore-prefixed. # Allow unused variables when underscore-prefixed
|
||||
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
```text
|
||||
[tool.ruff.format] [tool.ruff.format]
|
||||
# Like Black, use double quotes for strings.
|
||||
quote-style = "double"
|
||||
|
||||
```
|
||||
|
||||
## Like Black, indent with spaces, rather than tabs. # Like Black, indent with spaces, rather than tabs
|
||||
|
||||
indent-style = "space"
|
||||
|
||||
```text
|
||||
# Like Black, respect magic trailing commas. # Like Black, respect magic trailing commas.
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
```
|
||||
|
||||
## Like Black, automatically detect the appropriate line ending. # Like Black, automatically detect the appropriate line ending
|
||||
|
||||
line-ending = "auto"
|
||||
|
||||
```text
|
||||
# Enable auto-formatting of code examples in docstrings. Markdown, # Enable auto-formatting of code examples in docstrings. Markdown,
|
||||
# reStructuredText code/literal blocks and doctests are all supported.
|
||||
#
|
||||
# This is currently disabled by default, but it is planned for this
|
||||
# to be opt-out in the future.
|
||||
docstring-code-format = false
|
||||
|
||||
```
|
||||
|
||||
## Set the line length limit used when formatting code snippets in # Set the line length limit used when formatting code snippets in
|
||||
|
||||
# docstrings.
|
||||
|
||||
# This only has an effect when the `docstring-code-format` setting is
|
||||
# enabled.
|
||||
docstring-code-line-length = "dynamic"
|
||||
|
||||
```text
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```
|
||||
|
||||
```toml
|
||||
# Exclude a variety of commonly ignored directories.
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".ipynb_checkpoints",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pyenv",
|
||||
".pytest_cache",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
".vscode",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"site-packages",
|
||||
"venv",
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
# Same as Black. # Same as Black.
|
||||
line-length = 88
|
||||
indent-width = 4
|
||||
|
||||
```text
|
||||
|
||||
## Assume Python 3.9 # Assume Python 3.9
|
||||
|
||||
target-version = "py39"
|
||||
|
||||
```
|
||||
|
||||
[lint] [lint]
|
||||
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
|
||||
# McCabe complexity (`C901`) by default.
|
||||
select = ["E4", "E7", "E9", "F"]
|
||||
ignore = []
|
||||
|
||||
```text
|
||||
|
||||
## Allow fix for all enabled rules (when `--fix`) is provided. # Allow fix for all enabled rules (when `--fix`) is provided
|
||||
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
```
|
||||
|
||||
# Allow unused variables when underscore-prefixed. # Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
```text
|
||||
[format] [format]
|
||||
|
||||
# Like Black, use double quotes for strings.
|
||||
|
||||
quote-style = "double"
|
||||
|
||||
```
|
||||
|
||||
# Like Black, indent with spaces, rather than tabs. # Like Black, indent with spaces, rather than tabs.
|
||||
indent-style = "space"
|
||||
|
||||
```text
|
||||
|
||||
## Like Black, respect magic trailing commas. # Like Black, respect magic trailing commas
|
||||
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
```
|
||||
|
||||
# Like Black, automatically detect the appropriate line ending. # Like Black, automatically detect the appropriate line ending.
|
||||
line-ending = "auto"
|
||||
|
||||
```text
|
||||
|
||||
## Enable auto-formatting of code examples in docstrings. Markdown, # Enable auto-formatting of code examples in docstrings. Markdown
|
||||
|
||||
# reStructuredText code/literal blocks and doctests are all supported.
|
||||
|
||||
# This is currently disabled by default, but it is planned for this
|
||||
# to be opt-out in the future.
|
||||
docstring-code-format = false
|
||||
|
||||
```
|
||||
|
||||
# Set the line length limit used when formatting code snippets in # Set the line length limit used when formatting code snippets in
|
||||
# docstrings.
|
||||
#
|
||||
# This only has an effect when the `docstring-code-format` setting is
|
||||
# enabled.
|
||||
docstring-code-line-length = "dynamic"
|
||||
|
||||
```text
|
||||
|
||||
As an example, the following would configure Ruff to:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff.lint]
|
||||
# 1. Enable flake8-bugbear (`B`) rules, in addition to the defaults.
|
||||
select = ["E4", "E7", "E9", "F", "B"]
|
||||
|
||||
```
|
||||
|
||||
## 2. Avoid enforcing line-length violations (`E501`) # 2. Avoid enforcing line-length violations (`E501`)
|
||||
|
||||
ignore = ["E501"]
|
||||
|
||||
```text
|
||||
# 3. Avoid trying to fix flake8-bugbear (`B`) violations. # 3. Avoid trying to fix flake8-bugbear (`B`) violations.
|
||||
unfixable = ["B"]
|
||||
|
||||
```
|
||||
|
||||
## 4. Ignore `E402` (import violations) in all `__init__.py` files, and in selected subdirectories. # 4. Ignore `E402` (import violations) in all `__init__.py` files, and in selected subdirectories
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"__init__.py" = ["E402"]
|
||||
"**/{tests,docs,tools}/*" = ["E402"]
|
||||
|
||||
```text
|
||||
[tool.ruff.format] [tool.ruff.format]
|
||||
# 5. Use single quotes in `ruff format`.
|
||||
quote-style = "single"
|
||||
```
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```toml
|
||||
[lint]
|
||||
# 1. Enable flake8-bugbear (`B`) rules, in addition to the defaults.
|
||||
select = ["E4", "E7", "E9", "F", "B"]
|
||||
|
||||
```
|
||||
|
||||
## 2. Avoid enforcing line-length violations (`E501`) # 2. Avoid enforcing line-length violations (`E501`)
|
||||
|
||||
ignore = ["E501"]
|
||||
|
||||
```text
|
||||
# 3. Avoid trying to fix flake8-bugbear (`B`) violations. # 3. Avoid trying to fix flake8-bugbear (`B`) violations.
|
||||
unfixable = ["B"]
|
||||
|
||||
```
|
||||
|
||||
## 4. Ignore `E402` (import violations) in all `__init__.py` files, and in selected subdirectories. # 4. Ignore `E402` (import violations) in all `__init__.py` files, and in selected subdirectories
|
||||
|
||||
[lint.per-file-ignores]
|
||||
"__init__.py" = ["E402"]
|
||||
"**/{tests,docs,tools}/*" = ["E402"]
|
||||
|
||||
```text
|
||||
[format] [format]
|
||||
# 5. Use single quotes in `ruff format`.
|
||||
quote-style = "single"
|
||||
```
|
||||
|
||||
Linter plugin configurations are expressed as subsections, e.g.:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff.lint]
|
||||
# Add "Q" to the list of enabled codes.
|
||||
select = ["E4", "E7", "E9", "F", "Q"]
|
||||
|
||||
```
|
||||
|
||||
[tool.ruff.lint.flake8-quotes] [tool.ruff.lint.flake8-quotes]
|
||||
docstring-quotes = "double"
|
||||
|
||||
```text
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```toml
|
||||
[lint]
|
||||
# Add "Q" to the list of enabled codes.
|
||||
select = ["E4", "E7", "E9", "F", "Q"]
|
||||
|
||||
```
|
||||
|
||||
[lint.flake8-quotes] [lint.flake8-quotes]
|
||||
docstring-quotes = "double"
|
||||
|
||||
```text
|
||||
|
||||
Ruff respects `pyproject.toml`, `ruff.toml`, and `.ruff.toml` files. All three implement an
|
||||
equivalent schema (though in the `ruff.toml` and `.ruff.toml` versions, the `[tool.ruff]` header and
|
||||
`tool.ruff` section prefix is omitted).
|
||||
|
||||
For a complete enumeration of the available configuration options, see [_Settings_](settings.md).
|
||||
|
||||
## Config file discovery
|
||||
|
||||
Similar to [ESLint](https://eslint.org/docs/latest/use/configure/configuration-files#cascading-configuration-objects),
|
||||
Ruff supports hierarchical configuration, such that the "closest" config file in the
|
||||
directory hierarchy is used for every individual file, with all paths in the config file
|
||||
(e.g., `exclude` globs, `src` paths) being resolved relative to the directory containing that
|
||||
config file.
|
||||
|
||||
There are a few exceptions to these rules:
|
||||
|
||||
1. In locating the "closest" `pyproject.toml` file for a given path, Ruff ignores any
|
||||
`pyproject.toml` files that lack a `[tool.ruff]` section.
|
||||
1. If a configuration file is passed directly via `--config`, those settings are used for _all_
|
||||
analyzed files, and any relative paths in that configuration file (like `exclude` globs or
|
||||
`src` paths) are resolved relative to the _current_ working directory.
|
||||
1. If no config file is found in the filesystem hierarchy, Ruff will fall back to using
|
||||
a default configuration. If a user-specific configuration file exists
|
||||
at `${config_dir}/ruff/pyproject.toml`, that file will be used instead of the default
|
||||
configuration, with `${config_dir}` being determined via [`etcetera`'s base strategy](https://docs.rs/etcetera/latest/etcetera/#native-strategy),
|
||||
and all relative paths being again resolved relative to the _current working directory_.
|
||||
1. Any config-file-supported settings that are provided on the command-line (e.g., via
|
||||
`--select`) will override the settings in _every_ resolved configuration file.
|
||||
|
||||
Unlike [ESLint](https://eslint.org/docs/latest/use/configure/configuration-files#cascading-configuration-objects),
|
||||
Ruff does not merge settings across configuration files; instead, the "closest" configuration file
|
||||
is used, and any parent configuration files are ignored. In lieu of this implicit cascade, Ruff
|
||||
supports an [`extend`](settings.md#extend) field, which allows you to inherit the settings from another
|
||||
config file, like so:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
# Extend the `pyproject.toml` file in the parent directory...
|
||||
extend = "../pyproject.toml"
|
||||
|
||||
```
|
||||
|
||||
## ...but use a different line length. # ...but use a different line length
|
||||
|
||||
line-length = 100
|
||||
|
||||
```text
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```toml
|
||||
# Extend the `ruff.toml` file in the parent directory...
|
||||
extend = "../ruff.toml"
|
||||
|
||||
```
|
||||
|
||||
# ...but use a different line length. # ...but use a different line length.
|
||||
line-length = 100
|
||||
|
||||
```text
|
||||
|
||||
All of the above rules apply equivalently to `pyproject.toml`, `ruff.toml`, and `.ruff.toml` files.
|
||||
If Ruff detects multiple configuration files in the same directory, the `.ruff.toml` file will take
|
||||
precedence over the `ruff.toml` file, and the `ruff.toml` file will take precedence over
|
||||
the `pyproject.toml` file.
|
||||
|
||||
## Inferring the Python version
|
||||
|
||||
When no discovered configuration specifies a [`target-version`](settings.md#target-version), Ruff will attempt to fall back to the minimum version compatible with the `requires-python` field in a nearby `pyproject.toml`.
|
||||
The rules for this behavior are as follows:
|
||||
|
||||
1. If a configuration file is passed directly, Ruff does not attempt to infer a missing `target-version`.
|
||||
1. If a configuration file is found in the filesystem hierarchy, Ruff will infer a missing `target-version` from the `requires-python` field in a `pyproject.toml` file in the same directory as the found configuration.
|
||||
1. If we are using a user-level configuration from `${config_dir}/ruff/pyproject.toml`, the `requires-python` field in the first `pyproject.toml` file found in an ancestor of the current working directory takes precedence over the `target-version` in the user-level configuration.
|
||||
1. If no configuration files are found, Ruff will infer the `target-version` from the `requires-python` field in the first `pyproject.toml` file found in an ancestor of the current working directory.
|
||||
|
||||
Note that in these last two cases, the behavior of Ruff may differ depending on the working directory from which it is invoked.
|
||||
|
||||
## Python file discovery
|
||||
|
||||
When passed a path on the command-line, Ruff will automatically discover all Python files in that
|
||||
path, taking into account the [`exclude`](settings.md#exclude) and [`extend-exclude`](settings.md#extend-exclude)
|
||||
settings in each directory's configuration file.
|
||||
|
||||
Files can also be selectively excluded from linting or formatting by scoping the `exclude` setting
|
||||
to the tool-specific configuration tables. For example, the following would prevent `ruff` from
|
||||
formatting `.pyi` files, but would continue to include them in linting:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff.format]
|
||||
exclude = ["*.pyi"]
|
||||
```
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```toml
|
||||
[format]
|
||||
exclude = ["*.pyi"]
|
||||
```
|
||||
|
||||
By default, Ruff will also skip any files that are omitted via `.ignore`, `.gitignore`,
|
||||
`.git/info/exclude`, and global `gitignore` files (see: [`respect-gitignore`](settings.md#respect-gitignore)).
|
||||
|
||||
Files that are passed to `ruff` directly are always analyzed, regardless of the above criteria.
|
||||
For example, `ruff check /path/to/excluded/file.py` will always lint `file.py`.
|
||||
|
||||
### Default inclusions
|
||||
|
||||
By default, Ruff will discover files matching `*.py`, `*.pyi`, `*.ipynb`, or `pyproject.toml`.
|
||||
|
||||
To lint or format files with additional file extensions, use the [`extend-include`](settings.md#extend-include) setting.
|
||||
You can also change the default selection using the [`include`](settings.md#include) setting.
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
include = ["pyproject.toml", "src/**/*.py", "scripts/**/*.py"]
|
||||
```
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```toml
|
||||
include = ["pyproject.toml", "src/**/*.py", "scripts/**/*.py"]
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Paths provided to `include` _must_ match files. For example, `include = ["src"]` will fail since it
|
||||
|
||||
```text
|
||||
matches a directory. matches a directory.
|
||||
|
||||
## Jupyter Notebook discovery
|
||||
|
||||
Ruff has built-in support for linting and formatting [Jupyter Notebooks](https://jupyter.org/),
|
||||
which are linted and formatted by default on version `0.6.0` and higher.
|
||||
|
||||
If you'd prefer to either only lint or only format Jupyter Notebook files, you can use the
|
||||
section-specific `exclude` option to do so. For example, the following would only lint Jupyter
|
||||
Notebook files and not format them:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff.format]
|
||||
exclude = ["*.ipynb"]
|
||||
```
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```toml
|
||||
[format]
|
||||
exclude = ["*.ipynb"]
|
||||
```
|
||||
|
||||
And, conversely, the following would only format Jupyter Notebook files and not lint them:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff.lint]
|
||||
exclude = ["*.ipynb"]
|
||||
```
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```toml
|
||||
[lint]
|
||||
exclude = ["*.ipynb"]
|
||||
```
|
||||
|
||||
You can completely disable Jupyter Notebook support by updating the
|
||||
[`extend-exclude`](settings.md#extend-exclude) setting:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
extend-exclude = ["*.ipynb"]
|
||||
```
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```toml
|
||||
extend-exclude = ["*.ipynb"]
|
||||
```
|
||||
|
||||
If you'd like to ignore certain rules specifically for Jupyter Notebook files, you can do so by
|
||||
using the [`per-file-ignores`](settings.md#per-file-ignores) setting:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"*.ipynb" = ["T20"]
|
||||
```
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```toml
|
||||
[lint.per-file-ignores]
|
||||
"*.ipynb" = ["T20"]
|
||||
```
|
||||
|
||||
Some rules have different behavior when applied to Jupyter Notebook files. For
|
||||
example, when applied to `.py` files the
|
||||
[`module-import-not-at-top-of-file` (`E402`)](rules/module-import-not-at-top-of-file.md)
|
||||
rule detect imports at the top of a file, but for notebooks it detects imports at the top of a
|
||||
**cell**. For a given rule, the rule's documentation will always specify if it has different
|
||||
behavior when applied to Jupyter Notebook files.
|
||||
|
||||
## Command-line interface
|
||||
|
||||
Some configuration options can be provided or overridden via dedicated flags on the command line.
|
||||
This includes those related to rule enablement and disablement,
|
||||
file discovery, logging level, and more:
|
||||
|
||||
```console
|
||||
ruff check path/to/code/ --select F401 --select F403 --quiet
|
||||
```
|
||||
|
||||
All other configuration options can be set via the command line
|
||||
using the `--config` flag, detailed below.
|
||||
|
||||
### The `--config` CLI flag
|
||||
|
||||
The `--config` flag has two uses. It is most often used to point to the
|
||||
configuration file that you would like Ruff to use, for example:
|
||||
|
||||
```console
|
||||
ruff check path/to/directory --config path/to/ruff.toml
|
||||
```
|
||||
|
||||
However, the `--config` flag can also be used to provide arbitrary
|
||||
overrides of configuration settings using TOML `<KEY> = <VALUE>` pairs.
|
||||
This is mostly useful in situations where you wish to override a configuration setting
|
||||
that does not have a dedicated command-line flag.
|
||||
|
||||
In the below example, the `--config` flag is the only way of overriding the
|
||||
`dummy-variable-rgx` configuration setting from the command line,
|
||||
since this setting has no dedicated CLI flag. The `per-file-ignores` setting
|
||||
could also have been overridden via the `--per-file-ignores` dedicated flag,
|
||||
but using `--config` to override the setting is also fine:
|
||||
|
||||
```console
|
||||
ruff check path/to/file --config path/to/ruff.toml --config "lint.dummy-variable-rgx = '__.*'" --config "lint.per-file-ignores = {'some_file.py' = ['F841']}"
|
||||
```
|
||||
|
||||
Configuration options passed to `--config` are parsed in the same way
|
||||
as configuration options in a `ruff.toml` file.
|
||||
As such, options specific to the Ruff linter need to be prefixed with `lint.`
|
||||
(`--config "lint.dummy-variable-rgx = '__.*'"` rather than simply
|
||||
`--config "dummy-variable-rgx = '__.*'"`), and options specific to the Ruff formatter
|
||||
need to be prefixed with `format.`.
|
||||
|
||||
If a specific configuration option is simultaneously overridden by
|
||||
a dedicated flag and by the `--config` flag, the dedicated flag
|
||||
takes priority. In this example, the maximum permitted line length
|
||||
will be set to 90, not 100:
|
||||
|
||||
```console
|
||||
ruff format path/to/file --line-length=90 --config "line-length=100"
|
||||
```
|
||||
|
||||
Specifying `--config "line-length=90"` will override the `line-length`
|
||||
setting from _all_ configuration files detected by Ruff,
|
||||
including configuration files discovered in subdirectories.
|
||||
In this respect, specifying `--config "line-length=90"` has
|
||||
the same effect as specifying `--line-length=90`,
|
||||
which will similarly override the `line-length` setting from
|
||||
all configuration files detected by Ruff, regardless of where
|
||||
a specific configuration file is located.
|
||||
|
||||
### Full command-line interface
|
||||
|
||||
See `ruff help` for the full list of Ruff's top-level commands:
|
||||
|
||||
<!-- Begin auto-generated command help. -->
|
||||
|
||||
```text
|
||||
Ruff: An extremely fast Python linter and code formatter.
|
||||
|
||||
Usage: ruff [OPTIONS] <COMMAND>
|
||||
|
||||
Commands:
|
||||
check Run Ruff on the given files or directories
|
||||
rule Explain a rule (or all rules)
|
||||
config List or describe the available configuration options
|
||||
linter List all supported upstream linters
|
||||
clean Clear any caches in the current directory and any subdirectories
|
||||
format Run the Ruff formatter on the given files or directories
|
||||
server Run the language server
|
||||
analyze Run analysis over Python source code
|
||||
version Display Ruff's version
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
|
||||
Log levels:
|
||||
-v, --verbose Enable verbose logging
|
||||
-q, --quiet Print diagnostics, but nothing else
|
||||
-s, --silent Disable all logging (but still exit with status code "1" upon
|
||||
detecting diagnostics)
|
||||
|
||||
Global options:
|
||||
--config <CONFIG_OPTION>
|
||||
Either a path to a TOML configuration file (`pyproject.toml` or
|
||||
`ruff.toml`), or a TOML `<KEY> = <VALUE>` pair (such as you might
|
||||
find in a `ruff.toml` configuration file) overriding a specific
|
||||
configuration option. Overrides of individual settings using this
|
||||
option always take precedence over all configuration files, including
|
||||
configuration files that were also specified using `--config`
|
||||
--isolated
|
||||
Ignore all configuration files
|
||||
|
||||
For help with a specific command, see: `ruff help <command>`.
|
||||
```
|
||||
|
||||
<!-- End auto-generated command help. -->
|
||||
|
||||
Or `ruff help check` for more on the linting command:
|
||||
|
||||
<!-- Begin auto-generated check help. -->
|
||||
|
||||
```text
|
||||
Run Ruff on the given files or directories
|
||||
|
||||
Usage: ruff check [OPTIONS] [FILES]...
|
||||
|
||||
Arguments:
|
||||
[FILES]... List of files or directories to check [default: .]
|
||||
|
||||
Options:
|
||||
--fix
|
||||
Apply fixes to resolve lint violations. Use `--no-fix` to disable or
|
||||
`--unsafe-fixes` to include unsafe fixes
|
||||
--unsafe-fixes
|
||||
Include fixes that may not retain the original intent of the code.
|
||||
Use `--no-unsafe-fixes` to disable
|
||||
--show-fixes
|
||||
Show an enumeration of all fixed lint violations. Use
|
||||
`--no-show-fixes` to disable
|
||||
--diff
|
||||
Avoid writing any fixed files back; instead, output a diff for each
|
||||
changed file to stdout, and exit 0 if there are no diffs. Implies
|
||||
`--fix-only`
|
||||
-w, --watch
|
||||
Run in watch mode by re-running whenever files change
|
||||
--fix-only
|
||||
Apply fixes to resolve lint violations, but don't report on, or exit
|
||||
non-zero for, leftover violations. Implies `--fix`. Use
|
||||
`--no-fix-only` to disable or `--unsafe-fixes` to include unsafe
|
||||
fixes
|
||||
--ignore-noqa
|
||||
Ignore any `# noqa` comments
|
||||
--output-format <OUTPUT_FORMAT>
|
||||
Output serialization format for violations. The default serialization
|
||||
format is "full" [env: RUFF_OUTPUT_FORMAT=] [possible values:
|
||||
concise, full, json, json-lines, junit, grouped, github, gitlab,
|
||||
pylint, rdjson, azure, sarif]
|
||||
-o, --output-file <OUTPUT_FILE>
|
||||
Specify file to write the linter output to (default: stdout) [env:
|
||||
RUFF_OUTPUT_FILE=]
|
||||
--target-version <TARGET_VERSION>
|
||||
The minimum Python version that should be supported [possible values:
|
||||
py37, py38, py39, py310, py311, py312, py313, py314]
|
||||
--preview
|
||||
Enable preview mode; checks will include unstable rules and fixes.
|
||||
Use `--no-preview` to disable
|
||||
--extension <EXTENSION>
|
||||
List of mappings from file extension to language (one of `python`,
|
||||
`ipynb`, `pyi`). For example, to treat `.ipy` files as IPython
|
||||
notebooks, use `--extension ipy:ipynb`
|
||||
--statistics
|
||||
Show counts for every rule with at least one violation
|
||||
--add-noqa
|
||||
Enable automatic additions of `noqa` directives to failing lines
|
||||
--show-files
|
||||
See the files Ruff will be run against with the current settings
|
||||
--show-settings
|
||||
See the settings Ruff will use to lint a given Python file
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
Rule selection:
|
||||
--select <RULE_CODE>
|
||||
Comma-separated list of rule codes to enable (or ALL, to enable all
|
||||
rules)
|
||||
--ignore <RULE_CODE>
|
||||
Comma-separated list of rule codes to disable
|
||||
--extend-select <RULE_CODE>
|
||||
Like --select, but adds additional rule codes on top of those already
|
||||
specified
|
||||
--per-file-ignores <PER_FILE_IGNORES>
|
||||
List of mappings from file pattern to code to exclude
|
||||
--extend-per-file-ignores <EXTEND_PER_FILE_IGNORES>
|
||||
Like `--per-file-ignores`, but adds additional ignores on top of
|
||||
those already specified
|
||||
--fixable <RULE_CODE>
|
||||
List of rule codes to treat as eligible for fix. Only applicable when
|
||||
fix itself is enabled (e.g., via `--fix`)
|
||||
--unfixable <RULE_CODE>
|
||||
List of rule codes to treat as ineligible for fix. Only applicable
|
||||
when fix itself is enabled (e.g., via `--fix`)
|
||||
--extend-fixable <RULE_CODE>
|
||||
Like --fixable, but adds additional rule codes on top of those
|
||||
already specified
|
||||
|
||||
File selection:
|
||||
--exclude <FILE_PATTERN>
|
||||
List of paths, used to omit files and/or directories from analysis
|
||||
--extend-exclude <FILE_PATTERN>
|
||||
Like --exclude, but adds additional files and directories on top of
|
||||
those already excluded
|
||||
--respect-gitignore
|
||||
Respect file exclusions via `.gitignore` and other standard ignore
|
||||
files. Use `--no-respect-gitignore` to disable
|
||||
--force-exclude
|
||||
Enforce exclusions, even for paths passed to Ruff directly on the
|
||||
command-line. Use `--no-force-exclude` to disable
|
||||
|
||||
Miscellaneous:
|
||||
-n, --no-cache
|
||||
Disable cache reads [env: RUFF_NO_CACHE=]
|
||||
--cache-dir <CACHE_DIR>
|
||||
Path to the cache directory [env: RUFF_CACHE_DIR=]
|
||||
--stdin-filename <STDIN_FILENAME>
|
||||
The name of the file when passing it through stdin
|
||||
-e, --exit-zero
|
||||
Exit with status code "0", even upon detecting lint violations
|
||||
--exit-non-zero-on-fix
|
||||
Exit with a non-zero status code if any files were modified via fix,
|
||||
even if no lint violations remain
|
||||
|
||||
Log levels:
|
||||
-v, --verbose Enable verbose logging
|
||||
-q, --quiet Print diagnostics, but nothing else
|
||||
-s, --silent Disable all logging (but still exit with status code "1" upon
|
||||
detecting diagnostics)
|
||||
|
||||
Global options:
|
||||
--config <CONFIG_OPTION>
|
||||
Either a path to a TOML configuration file (`pyproject.toml` or
|
||||
`ruff.toml`), or a TOML `<KEY> = <VALUE>` pair (such as you might
|
||||
find in a `ruff.toml` configuration file) overriding a specific
|
||||
configuration option. Overrides of individual settings using this
|
||||
option always take precedence over all configuration files, including
|
||||
configuration files that were also specified using `--config`
|
||||
--isolated
|
||||
Ignore all configuration files
|
||||
```
|
||||
|
||||
<!-- End auto-generated check help. -->
|
||||
|
||||
Or `ruff help format` for more on the formatting command:
|
||||
|
||||
<!-- Begin auto-generated format help. -->
|
||||
|
||||
```text
|
||||
Run the Ruff formatter on the given files or directories
|
||||
|
||||
Usage: ruff format [OPTIONS] [FILES]...
|
||||
|
||||
Arguments:
|
||||
[FILES]... List of files or directories to format [default: .]
|
||||
|
||||
Options:
|
||||
--check
|
||||
Avoid writing any formatted files back; instead, exit with a non-zero
|
||||
status code if any files would have been modified, and zero otherwise
|
||||
--diff
|
||||
Avoid writing any formatted files back; instead, exit with a non-zero
|
||||
status code and the difference between the current file and how the
|
||||
formatted file would look like
|
||||
--extension <EXTENSION>
|
||||
List of mappings from file extension to language (one of `python`,
|
||||
`ipynb`, `pyi`). For example, to treat `.ipy` files as IPython
|
||||
notebooks, use `--extension ipy:ipynb`
|
||||
--target-version <TARGET_VERSION>
|
||||
The minimum Python version that should be supported [possible values:
|
||||
py37, py38, py39, py310, py311, py312, py313, py314]
|
||||
--preview
|
||||
Enable preview mode; enables unstable formatting. Use `--no-preview`
|
||||
to disable
|
||||
--output-format <OUTPUT_FORMAT>
|
||||
Output serialization format for violations, when used with `--check`.
|
||||
The default serialization format is "full" [env: RUFF_OUTPUT_FORMAT=]
|
||||
[possible values: concise, full, json, json-lines, junit, grouped,
|
||||
github, gitlab, pylint, rdjson, azure, sarif]
|
||||
-h, --help
|
||||
Print help (see more with '--help')
|
||||
|
||||
Miscellaneous:
|
||||
-n, --no-cache
|
||||
Disable cache reads [env: RUFF_NO_CACHE=]
|
||||
--cache-dir <CACHE_DIR>
|
||||
Path to the cache directory [env: RUFF_CACHE_DIR=]
|
||||
--stdin-filename <STDIN_FILENAME>
|
||||
The name of the file when passing it through stdin
|
||||
--exit-non-zero-on-format
|
||||
Exit with a non-zero status code if any files were modified via
|
||||
format, even if all files were formatted successfully
|
||||
|
||||
File selection:
|
||||
--respect-gitignore
|
||||
Respect file exclusions via `.gitignore` and other standard ignore
|
||||
files. Use `--no-respect-gitignore` to disable
|
||||
--exclude <FILE_PATTERN>
|
||||
List of paths, used to omit files and/or directories from analysis
|
||||
--force-exclude
|
||||
Enforce exclusions, even for paths passed to Ruff directly on the
|
||||
command-line. Use `--no-force-exclude` to disable
|
||||
|
||||
Format configuration:
|
||||
--line-length <LINE_LENGTH> Set the line-length
|
||||
|
||||
Editor options:
|
||||
--range <RANGE> When specified, Ruff will try to only format the code in
|
||||
the given range.
|
||||
It might be necessary to extend the start backwards or
|
||||
the end forwards, to fully enclose a logical line.
|
||||
The `<RANGE>` uses the format
|
||||
`<start_line>:<start_column>-<end_line>:<end_column>`.
|
||||
|
||||
Log levels:
|
||||
-v, --verbose Enable verbose logging
|
||||
-q, --quiet Print diagnostics, but nothing else
|
||||
-s, --silent Disable all logging (but still exit with status code "1" upon
|
||||
detecting diagnostics)
|
||||
|
||||
Global options:
|
||||
--config <CONFIG_OPTION>
|
||||
Either a path to a TOML configuration file (`pyproject.toml` or
|
||||
`ruff.toml`), or a TOML `<KEY> = <VALUE>` pair (such as you might
|
||||
find in a `ruff.toml` configuration file) overriding a specific
|
||||
configuration option. Overrides of individual settings using this
|
||||
option always take precedence over all configuration files, including
|
||||
configuration files that were also specified using `--config`
|
||||
--isolated
|
||||
Ignore all configuration files
|
||||
```
|
||||
|
||||
<!-- End auto-generated format help. -->
|
||||
|
||||
## Shell autocompletion
|
||||
|
||||
Ruff supports autocompletion for most shells. A shell-specific completion script can be generated
|
||||
by `ruff generate-shell-completion <SHELL>`, where `<SHELL>` is one of `bash`, `elvish`, `fig`, `fish`,
|
||||
`powershell`, or `zsh`.
|
||||
|
||||
The exact steps required to enable autocompletion will vary by shell. For example instructions,
|
||||
see the [Poetry](https://python-poetry.org/docs/#enable-tab-completion-for-bash-fish-or-zsh) or
|
||||
[ripgrep FAQ](https://github.com/BurntSushi/ripgrep/blob/master/FAQ.md) documentation (see the "Does ripgrep have support for shell auto-completion?" section).
|
||||
|
||||
As an example: to enable autocompletion for Zsh, run
|
||||
`ruff generate-shell-completion zsh > ~/.zfunc/_ruff`. Then add the following line to your
|
||||
`~/.zshrc` file, if they're not already present:
|
||||
|
||||
```zsh
|
||||
fpath+=~/.zfunc
|
||||
autoload -Uz compinit && compinit
|
||||
```
|
||||
524
skills/python-code-quality/reference/ruff-formatting-settings.md
Normal file
524
skills/python-code-quality/reference/ruff-formatting-settings.md
Normal file
@@ -0,0 +1,524 @@
|
||||
# The Ruff Formatter
|
||||
|
||||
The Ruff formatter is an extremely fast Python code formatter designed as a drop-in replacement for
|
||||
[Black](https://pypi.org/project/black/), available as part of the `ruff` CLI via `ruff format`.
|
||||
|
||||
## `ruff format`
|
||||
|
||||
`ruff format` is the primary entrypoint to the formatter. It accepts a list of files or
|
||||
directories, and formats all discovered Python files:
|
||||
|
||||
```shell
|
||||
ruff format # Format all files in the current directory.
|
||||
ruff format path/to/code/ # Format all files in `path/to/code` (and any subdirectories).
|
||||
ruff format path/to/file.py # Format a single file.
|
||||
```
|
||||
|
||||
Similar to Black, running `ruff format /path/to/file.py` will format the given file or directory
|
||||
in-place, while `ruff format --check /path/to/file.py` will avoid writing any formatted files back,
|
||||
and instead exit with a non-zero status code upon detecting any unformatted files.
|
||||
|
||||
For the full list of supported options, run `ruff format --help`.
|
||||
|
||||
## Philosophy
|
||||
|
||||
The initial goal of the Ruff formatter is _not_ to innovate on code style, but rather, to innovate
|
||||
on performance, and provide a unified toolchain across Ruff's linter, formatter, and any and all
|
||||
future tools.
|
||||
|
||||
As such, the formatter is designed as a drop-in replacement for [Black](https://github.com/psf/black),
|
||||
but with an excessive focus on performance and direct integration with Ruff. Given Black's
|
||||
popularity within the Python ecosystem, targeting Black compatibility ensures that formatter
|
||||
adoption is minimally disruptive for the vast majority of projects.
|
||||
|
||||
Specifically, the formatter is intended to emit near-identical output when run over existing
|
||||
Black-formatted code. When run over extensive Black-formatted projects like Django and Zulip, > 99.9%
|
||||
of lines are formatted identically. (See: [_Style Guide_](#style-guide).)
|
||||
|
||||
Given this focus on Black compatibility, the formatter thus adheres to [Black's (stable) code style](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html),
|
||||
which aims for "consistency, generality, readability and reducing git diffs". To give you a sense
|
||||
for the enforced code style, here's an example:
|
||||
|
||||
```python
|
||||
# Input
|
||||
def _make_ssl_transport(
|
||||
rawsock, protocol, sslcontext, waiter=None,
|
||||
*, server_side=False, server_hostname=None,
|
||||
extra=None, server=None,
|
||||
ssl_handshake_timeout=None,
|
||||
call_connection_made=True):
|
||||
'''Make an SSL transport.'''
|
||||
if waiter is None:
|
||||
waiter = Future(loop=loop)
|
||||
|
||||
if extra is None:
|
||||
extra = {}
|
||||
|
||||
...
|
||||
|
||||
# Ruff
|
||||
def _make_ssl_transport(
|
||||
rawsock,
|
||||
protocol,
|
||||
sslcontext,
|
||||
waiter=None,
|
||||
*,
|
||||
server_side=False,
|
||||
server_hostname=None,
|
||||
extra=None,
|
||||
server=None,
|
||||
ssl_handshake_timeout=None,
|
||||
call_connection_made=True,
|
||||
):
|
||||
"""Make an SSL transport."""
|
||||
if waiter is None:
|
||||
waiter = Future(loop=loop)
|
||||
|
||||
if extra is None:
|
||||
extra = {}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Like Black, the Ruff formatter does _not_ support extensive code style configuration; however,
|
||||
unlike Black, it _does_ support configuring the desired quote style, indent style, line endings,
|
||||
and more. (See: [_Configuration_](#configuration).)
|
||||
|
||||
While the formatter is designed to be a drop-in replacement for Black, it is not intended to be
|
||||
used interchangeably with Black on an ongoing basis, as the formatter _does_ differ from
|
||||
Black in a few conscious ways (see: [_Known deviations_](formatter/black.md)). In general,
|
||||
deviations are limited to cases in which Ruff's behavior was deemed more consistent, or
|
||||
significantly simpler to support (with negligible end-user impact) given the differences in the
|
||||
underlying implementations between Black and Ruff.
|
||||
|
||||
Going forward, the Ruff Formatter will support Black's preview style under Ruff's own
|
||||
[preview](preview.md) mode.
|
||||
|
||||
## Configuration
|
||||
|
||||
The Ruff Formatter exposes a small set of configuration options, some of which are also supported
|
||||
by Black (like line width), some of which are unique to Ruff (like quote, indentation style and
|
||||
formatting code examples in docstrings).
|
||||
|
||||
For example, to configure the formatter to use single quotes, format code
|
||||
examples in docstrings, a line width of 100, and tab indentation, add the
|
||||
following to your configuration file:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
|
||||
```
|
||||
|
||||
[tool.ruff.format] [tool.ruff.format]
|
||||
quote-style = "single"
|
||||
indent-style = "tab"
|
||||
docstring-code-format = true
|
||||
|
||||
```text
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```
|
||||
|
||||
```toml
|
||||
line-length = 100
|
||||
|
||||
```
|
||||
|
||||
[format] [format]
|
||||
quote-style = "single"
|
||||
indent-style = "tab"
|
||||
docstring-code-format = true
|
||||
|
||||
```text
|
||||
|
||||
For the full list of supported settings, see [_Settings_](settings.md#format). For more on
|
||||
configuring Ruff via `pyproject.toml`, see [_Configuring Ruff_](configuration.md).
|
||||
|
||||
Given the focus on Black compatibility (and unlike formatters like [YAPF](https://github.com/google/yapf)),
|
||||
Ruff does not currently expose any other configuration options.
|
||||
|
||||
## Docstring formatting
|
||||
|
||||
The Ruff formatter provides an opt-in feature for automatically formatting
|
||||
Python code examples in docstrings. The Ruff formatter currently recognizes
|
||||
code examples in the following formats:
|
||||
|
||||
* The Python [doctest] format.
|
||||
* CommonMark [fenced code blocks] with the following info strings: `python`,
|
||||
`py`, `python3`, or `py3`. Fenced code blocks without an info string are
|
||||
assumed to be Python code examples and also formatted.
|
||||
* reStructuredText [literal blocks]. While literal blocks may contain things
|
||||
other than Python, this is meant to reflect a long-standing convention in the
|
||||
|
||||
Python ecosystem where literal blocks often contain Python code.
|
||||
|
||||
* reStructuredText [`code-block` and `sourcecode` directives]. As with
|
||||
|
||||
Markdown, the language names recognized for Python are `python`, `py`,
|
||||
`python3`, or `py3`.
|
||||
|
||||
If a code example is recognized and treated as Python, the Ruff formatter will
|
||||
automatically skip it if the code does not parse as valid Python or if the
|
||||
reformatted code would produce an invalid Python program.
|
||||
|
||||
Users may also configure the line length limit used for reformatting Python
|
||||
code examples in docstrings. The default is a special value, `dynamic`, which
|
||||
instructs the formatter to respect the line length limit setting for the
|
||||
surrounding Python code. The `dynamic` setting ensures that even when code
|
||||
examples are found inside indented docstrings, the line length limit configured
|
||||
for the surrounding Python code will not be exceeded. Users may also configure
|
||||
a fixed line length limit for code examples in docstrings.
|
||||
|
||||
For example, this configuration shows how to enable docstring code formatting
|
||||
with a fixed line length limit:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff.format]
|
||||
docstring-code-format = true
|
||||
docstring-code-line-length = 20
|
||||
```
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```toml
|
||||
[format]
|
||||
docstring-code-format = true
|
||||
docstring-code-line-length = 20
|
||||
```
|
||||
|
||||
With the above configuration, this code:
|
||||
|
||||
```python
|
||||
def f(x):
|
||||
'''
|
||||
Something about `f`. And an example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear)
|
||||
'''
|
||||
pass
|
||||
```
|
||||
|
||||
... will be reformatted (assuming the rest of the options are set
|
||||
to their defaults) as:
|
||||
|
||||
```python
|
||||
def f(x):
|
||||
"""
|
||||
Something about `f`. And an example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
(
|
||||
foo,
|
||||
bar,
|
||||
quux,
|
||||
) = this_is_a_long_line(
|
||||
lion,
|
||||
hippo,
|
||||
lemur,
|
||||
bear,
|
||||
)
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
[doctest]: https://docs.python.org/3/library/doctest.html
|
||||
[fenced code blocks]: https://spec.commonmark.org/0.30/#fenced-code-blocks
|
||||
[literal blocks]: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks
|
||||
[`code-block` and `sourcecode` directives]: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block
|
||||
|
||||
## Format suppression
|
||||
|
||||
Like Black, Ruff supports `# fmt: on`, `# fmt: off`, and `# fmt: skip` pragma comments, which can
|
||||
be used to temporarily disable formatting for a given code block.
|
||||
|
||||
`# fmt: on` and `# fmt: off` comments are enforced at the statement level:
|
||||
|
||||
```python
|
||||
# fmt: off
|
||||
not_formatted=3
|
||||
also_not_formatted=4
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
As such, adding `# fmt: on` and `# fmt: off` comments within expressions will have no effect. In
|
||||
the following example, both list entries will be formatted, despite the `# fmt: off`:
|
||||
|
||||
```python
|
||||
[
|
||||
# fmt: off
|
||||
'1',
|
||||
# fmt: on
|
||||
'2',
|
||||
]
|
||||
```
|
||||
|
||||
Instead, apply the `# fmt: off` comment to the entire statement:
|
||||
|
||||
```python
|
||||
# fmt: off
|
||||
[
|
||||
'1',
|
||||
'2',
|
||||
]
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
Like Black, Ruff will _also_ recognize [YAPF](https://github.com/google/yapf)'s `# yapf: disable` and `# yapf: enable` pragma
|
||||
comments, which are treated equivalently to `# fmt: off` and `# fmt: on`, respectively.
|
||||
|
||||
`# fmt: skip` comments suppress formatting for a preceding statement, case header, decorator,
|
||||
function definition, or class definition:
|
||||
|
||||
```python
|
||||
if True:
|
||||
pass
|
||||
elif False: # fmt: skip
|
||||
pass
|
||||
|
||||
@Test
|
||||
@Test2 # fmt: skip
|
||||
def test(): ...
|
||||
|
||||
a = [1, 2, 3, 4, 5] # fmt: skip
|
||||
|
||||
def test(a, b, c, d, e, f) -> int: # fmt: skip
|
||||
pass
|
||||
```
|
||||
|
||||
As such, adding an `# fmt: skip` comment at the end of an expression will have no effect. In
|
||||
the following example, the list entry `'1'` will be formatted, despite the `# fmt: skip`:
|
||||
|
||||
```python
|
||||
a = call(
|
||||
[
|
||||
'1', # fmt: skip
|
||||
'2',
|
||||
],
|
||||
b
|
||||
)
|
||||
```
|
||||
|
||||
Instead, apply the `# fmt: skip` comment to the entire statement:
|
||||
|
||||
```python
|
||||
a = call(
|
||||
[
|
||||
'1',
|
||||
'2',
|
||||
],
|
||||
b
|
||||
) # fmt: skip
|
||||
```
|
||||
|
||||
## Conflicting lint rules
|
||||
|
||||
Ruff's formatter is designed to be used alongside the linter. However, the linter includes
|
||||
some rules that, when enabled, can cause conflicts with the formatter, leading to unexpected
|
||||
behavior. When configured appropriately, the goal of Ruff's formatter-linter compatibility is
|
||||
such that running the formatter should never introduce new lint errors.
|
||||
|
||||
When using Ruff as a formatter, we recommend avoiding the following lint rules:
|
||||
|
||||
* [`tab-indentation`](rules/tab-indentation.md) (`W191`)
|
||||
* [`indentation-with-invalid-multiple`](rules/indentation-with-invalid-multiple.md) (`E111`)
|
||||
* [`indentation-with-invalid-multiple-comment`](rules/indentation-with-invalid-multiple-comment.md) (`E114`)
|
||||
* [`over-indented`](rules/over-indented.md) (`E117`)
|
||||
* [`docstring-tab-indentation`](rules/docstring-tab-indentation.md) (`D206`)
|
||||
* [`triple-single-quotes`](rules/triple-single-quotes.md) (`D300`)
|
||||
* [`bad-quotes-inline-string`](rules/bad-quotes-inline-string.md) (`Q000`)
|
||||
* [`bad-quotes-multiline-string`](rules/bad-quotes-multiline-string.md) (`Q001`)
|
||||
* [`bad-quotes-docstring`](rules/bad-quotes-docstring.md) (`Q002`)
|
||||
* [`avoidable-escaped-quote`](rules/avoidable-escaped-quote.md) (`Q003`)
|
||||
* [`missing-trailing-comma`](rules/missing-trailing-comma.md) (`COM812`)
|
||||
* [`prohibited-trailing-comma`](rules/prohibited-trailing-comma.md) (`COM819`)
|
||||
* [`multi-line-implicit-string-concatenation`](rules/multi-line-implicit-string-concatenation.md) (`ISC002`) if used without `ISC001` and `flake8-implicit-str-concat.allow-multiline = false`
|
||||
|
||||
While the [`line-too-long`](rules/line-too-long.md) (`E501`) rule _can_ be used alongside the
|
||||
formatter, the formatter only makes a best-effort attempt to wrap lines at the configured
|
||||
[`line-length`](settings.md#line-length). As such, formatted code _may_ exceed the line length,
|
||||
leading to [`line-too-long`](rules/line-too-long.md) (`E501`) errors.
|
||||
|
||||
None of the above are included in Ruff's default configuration. However, if you've enabled
|
||||
any of these rules or their parent categories (like `Q`), we recommend disabling them via the
|
||||
linter's [`lint.ignore`](settings.md#lint_ignore) setting.
|
||||
|
||||
Similarly, we recommend avoiding the following isort settings, which are incompatible with the
|
||||
formatter's treatment of import statements when set to non-default values:
|
||||
|
||||
* [`force-single-line`](settings.md#lint_isort_force-single-line)
|
||||
* [`force-wrap-aliases`](settings.md#lint_isort_force-wrap-aliases)
|
||||
* [`lines-after-imports`](settings.md#lint_isort_lines-after-imports)
|
||||
* [`lines-between-types`](settings.md#lint_isort_lines-between-types)
|
||||
* [`split-on-trailing-comma`](settings.md#lint_isort_split-on-trailing-comma)
|
||||
|
||||
If you've configured any of these settings to take on non-default values, we recommend removing
|
||||
them from your Ruff configuration.
|
||||
|
||||
When an incompatible lint rule or setting is enabled, `ruff format` will emit a warning. If your
|
||||
`ruff format` is free of warnings, you're good to go!
|
||||
|
||||
## Exit codes
|
||||
|
||||
`ruff format` exits with the following status codes:
|
||||
|
||||
* `0` if Ruff terminates successfully, regardless of whether any files were formatted.
|
||||
* `2` if Ruff terminates abnormally due to invalid configuration, invalid CLI options, or an
|
||||
internal error.
|
||||
|
||||
Meanwhile, `ruff format --check` exits with the following status codes:
|
||||
|
||||
* `0` if Ruff terminates successfully, and no files would be formatted if `--check` were not
|
||||
specified.
|
||||
* `1` if Ruff terminates successfully, and one or more files would be formatted if `--check` were
|
||||
not specified.
|
||||
* `2` if Ruff terminates abnormally due to invalid configuration, invalid CLI options, or an
|
||||
internal error.
|
||||
|
||||
## Style Guide
|
||||
|
||||
The formatter is designed to be a drop-in replacement for [Black](https://github.com/psf/black).
|
||||
This section documents the areas where the Ruff formatter goes beyond Black in terms of code style.
|
||||
|
||||
### Intentional deviations
|
||||
|
||||
While the Ruff formatter aims to be a drop-in replacement for Black, it does differ from Black
|
||||
in a few known ways. Some of these differences emerge from conscious attempts to improve upon
|
||||
Black's code style, while others fall out of differences in the underlying implementations.
|
||||
|
||||
For a complete enumeration of these intentional deviations, see [_Known deviations_](formatter/black.md).
|
||||
|
||||
Unintentional deviations from Black are tracked in the [issue tracker](https://github.com/astral-sh/ruff/issues?q=is%3Aopen+is%3Aissue+label%3Aformatter).
|
||||
If you've identified a new deviation, please [file an issue](https://github.com/astral-sh/ruff/issues/new).
|
||||
|
||||
### Preview style
|
||||
|
||||
Similar to [Black](https://black.readthedocs.io/en/stable/the_black_code_style/future_style.html#preview-style), Ruff implements formatting changes
|
||||
under the [`preview`](https://docs.astral.sh/ruff/settings/#format_preview) flag, promoting them to stable through minor releases, in accordance with our [versioning policy](https://github.com/astral-sh/ruff/discussions/6998#discussioncomment-7016766).
|
||||
|
||||
### F-string formatting
|
||||
|
||||
_Stabilized in Ruff 0.9.0_
|
||||
|
||||
Unlike Black, Ruff formats the expression parts of f-strings which are the parts inside the curly
|
||||
braces `{...}`. This is a [known deviation](formatter/black.md#f-strings) from Black.
|
||||
|
||||
Ruff employs several heuristics to determine how an f-string should be formatted which are detailed
|
||||
below.
|
||||
|
||||
#### Quotes
|
||||
|
||||
Ruff will use the [configured quote style] for the f-string expression unless doing so would result in
|
||||
invalid syntax for the target Python version or requires more backslash escapes than the original
|
||||
expression. Specifically, Ruff will preserve the original quote style for the following cases:
|
||||
|
||||
When the target Python version is < 3.12 and a [self-documenting f-string] contains a string
|
||||
literal with the [configured quote style]:
|
||||
|
||||
```python
|
||||
# format.quote-style = "double"
|
||||
|
||||
f'{10 + len("hello")=}'
|
||||
# This f-string cannot be formatted as follows when targeting Python < 3.12
|
||||
f"{10 + len("hello")=}"
|
||||
```
|
||||
|
||||
When the target Python version is < 3.12 and an f-string contains any triple-quoted string, byte
|
||||
or f-string literal that contains the [configured quote style]:
|
||||
|
||||
```python
|
||||
# format.quote-style = "double"
|
||||
|
||||
f'{"""nested " """}'
|
||||
# This f-string cannot be formatted as follows when targeting Python < 3.12
|
||||
f"{'''nested " '''}"
|
||||
```
|
||||
|
||||
For all target Python versions, when a [self-documenting f-string] contains an expression between
|
||||
the curly braces (`{...}`) with a format specifier containing the [configured quote style]:
|
||||
|
||||
```python
|
||||
# format.quote-style = "double"
|
||||
|
||||
f'{1=:"foo}'
|
||||
# This f-string cannot be formatted as follows for all target Python versions
|
||||
f"{1=:"foo}"
|
||||
```
|
||||
|
||||
For nested f-strings, Ruff alternates quote styles, starting with the [configured quote style] for the
|
||||
outermost f-string. For example, consider the following f-string:
|
||||
|
||||
```python
|
||||
# format.quote-style = "double"
|
||||
|
||||
f"outer f-string {f"nested f-string {f"another nested f-string"} end"} end"
|
||||
```
|
||||
|
||||
Ruff formats it as:
|
||||
|
||||
```python
|
||||
f"outer f-string {f'nested f-string {f"another nested f-string"} end'} end"
|
||||
```
|
||||
|
||||
#### Line breaks
|
||||
|
||||
Starting with Python 3.12 ([PEP 701](https://peps.python.org/pep-0701/)), the expression parts of an f-string can
|
||||
span multiple lines. Ruff needs to decide when to introduce a line break in an f-string expression.
|
||||
This depends on the semantic content of the expression parts of an f-string - for example,
|
||||
introducing a line break in the middle of a natural-language sentence is undesirable. Since Ruff
|
||||
doesn't have enough information to make that decision, it adopts a heuristic similar to [Prettier](https://prettier.io/docs/en/next/rationale.html#template-literals):
|
||||
it will only split the expression parts of an f-string across multiple lines if there was already a line break
|
||||
within any of the expression parts.
|
||||
|
||||
For example, the following code:
|
||||
|
||||
```python
|
||||
f"this f-string has a multiline expression {
|
||||
['red', 'green', 'blue', 'yellow',]} and does not fit within the line length"
|
||||
```
|
||||
|
||||
... is formatted as:
|
||||
|
||||
```python
|
||||
# The list expression is split across multiple lines because of the trailing comma
|
||||
f"this f-string has a multiline expression {
|
||||
[
|
||||
'red',
|
||||
'green',
|
||||
'blue',
|
||||
'yellow',
|
||||
]
|
||||
} and does not fit within the line length"
|
||||
```
|
||||
|
||||
But, the following will not be split across multiple lines even though it exceeds the line length:
|
||||
|
||||
```python
|
||||
f"this f-string has a multiline expression {['red', 'green', 'blue', 'yellow']} and does not fit within the line length"
|
||||
```
|
||||
|
||||
If you want Ruff to split an f-string across multiple lines, ensure there's a linebreak somewhere within the
|
||||
`{...}` parts of an f-string.
|
||||
|
||||
[self-documenting f-string]: https://realpython.com/python-f-strings/#self-documenting-expressions-for-debugging
|
||||
[configured quote style]: settings.md/#format_quote-style
|
||||
|
||||
## Sorting imports
|
||||
|
||||
Currently, the Ruff formatter does not sort imports. In order to both sort imports and format,
|
||||
call the Ruff linter and then the formatter:
|
||||
|
||||
```shell
|
||||
ruff check --select I --fix
|
||||
ruff format
|
||||
```
|
||||
|
||||
A unified command for both linting and formatting is [planned](https://github.com/astral-sh/ruff/issues/8232).
|
||||
438
skills/python-code-quality/reference/ruff-linting-settings.md
Normal file
438
skills/python-code-quality/reference/ruff-linting-settings.md
Normal file
@@ -0,0 +1,438 @@
|
||||
# The Ruff Linter
|
||||
|
||||
The Ruff Linter is an extremely fast Python linter designed as a drop-in replacement for [Flake8](https://pypi.org/project/flake8/)
|
||||
(plus dozens of plugins), [isort](https://pypi.org/project/isort/), [pydocstyle](https://pypi.org/project/pydocstyle/),
|
||||
[pyupgrade](https://pypi.org/project/pyupgrade/), [autoflake](https://pypi.org/project/autoflake/),
|
||||
and more.
|
||||
|
||||
## `ruff check`
|
||||
|
||||
`ruff check` is the primary entrypoint to the Ruff linter. It accepts a list of files or
|
||||
directories, and lints all discovered Python files, optionally fixing any fixable errors.
|
||||
When linting a directory, Ruff searches for Python files recursively in that directory
|
||||
and all its subdirectories:
|
||||
|
||||
```console
|
||||
ruff check # Lint files in the current directory.
|
||||
ruff check --fix # Lint files in the current directory and fix any fixable errors.
|
||||
ruff check --watch # Lint files in the current directory and re-lint on change.
|
||||
ruff check path/to/code/ # Lint files in `path/to/code`.
|
||||
```
|
||||
|
||||
For the full list of supported options, run `ruff check --help`.
|
||||
|
||||
## Rule selection
|
||||
|
||||
The set of enabled rules is controlled via the [`lint.select`](settings.md#lint_select),
|
||||
[`lint.extend-select`](settings.md#lint_extend-select), and [`lint.ignore`](settings.md#lint_ignore) settings.
|
||||
|
||||
Ruff's linter mirrors Flake8's rule code system, in which each rule code consists of a one-to-three
|
||||
letter prefix, followed by three digits (e.g., `F401`). The prefix indicates that "source" of the rule
|
||||
(e.g., `F` for Pyflakes, `E` for pycodestyle, `ANN` for flake8-annotations).
|
||||
|
||||
Rule selectors like [`lint.select`](settings.md#lint_select) and [`lint.ignore`](settings.md#lint_ignore) accept either
|
||||
a full rule code (e.g., `F401`) or any valid prefix (e.g., `F`). For example, given the following
|
||||
configuration file:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F"]
|
||||
ignore = ["F401"]
|
||||
```
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```toml
|
||||
[lint]
|
||||
select = ["E", "F"]
|
||||
ignore = ["F401"]
|
||||
```
|
||||
|
||||
Ruff would enable all rules with the `E` (pycodestyle) or `F` (Pyflakes) prefix, with the exception
|
||||
of `F401`. For more on configuring Ruff via `pyproject.toml`, see [_Configuring Ruff_](configuration.md).
|
||||
|
||||
As a special-case, Ruff also supports the `ALL` code, which enables all rules. Note that some
|
||||
pydocstyle rules conflict (e.g., `D203` and `D211`) as they represent alternative docstring
|
||||
formats. Ruff will automatically disable any conflicting rules when `ALL` is enabled.
|
||||
|
||||
If you're wondering how to configure Ruff, here are some **recommended guidelines**:
|
||||
|
||||
- Prefer [`lint.select`](settings.md#lint_select) over [`lint.extend-select`](settings.md#lint_extend-select) to make your rule set explicit.
|
||||
- Use `ALL` with discretion. Enabling `ALL` will implicitly enable new rules whenever you upgrade.
|
||||
- Start with a small set of rules (`select = ["E", "F"]`) and add a category at-a-time. For example,
|
||||
you might consider expanding to `select = ["E", "F", "B"]` to enable the popular flake8-bugbear
|
||||
extension.
|
||||
|
||||
For example, a configuration that enables some of the most popular rules (without being too
|
||||
pedantic) might look like the following:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff.lint]
|
||||
```
|
||||
|
||||
select = [ select = [
|
||||
|
||||
# pycodestyle
|
||||
|
||||
"E",
|
||||
|
||||
# Pyflakes
|
||||
|
||||
"F",
|
||||
|
||||
# pyupgrade
|
||||
|
||||
"UP",
|
||||
|
||||
# flake8-bugbear
|
||||
|
||||
"B",
|
||||
|
||||
# flake8-simplify
|
||||
|
||||
"SIM",
|
||||
|
||||
# isort
|
||||
|
||||
"I",
|
||||
]
|
||||
|
||||
```text
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```
|
||||
|
||||
```toml
|
||||
[lint]
|
||||
select = [
|
||||
# pycodestyle
|
||||
"E",
|
||||
# Pyflakes
|
||||
"F",
|
||||
# pyupgrade
|
||||
"UP",
|
||||
# flake8-bugbear
|
||||
"B",
|
||||
# flake8-simplify
|
||||
"SIM",
|
||||
# isort
|
||||
"I",
|
||||
]
|
||||
```
|
||||
|
||||
To resolve the enabled rule set, Ruff may need to reconcile [`lint.select`](settings.md#lint_select) and
|
||||
[`lint.ignore`](settings.md#lint_ignore) from a variety of sources, including the current `pyproject.toml`,
|
||||
any inherited `pyproject.toml` files, and the CLI (e.g., [`--select`](settings.md#lint_select)).
|
||||
|
||||
In those scenarios, Ruff uses the "highest-priority" [`select`](settings.md#lint_select) as the basis for
|
||||
the rule set, and then applies [`extend-select`](settings.md#lint_extend-select) and
|
||||
[`ignore`](settings.md#lint_ignore) adjustments. CLI options are given higher priority than
|
||||
`pyproject.toml` options, and the current `pyproject.toml` file is given higher priority than any
|
||||
inherited `pyproject.toml` files.
|
||||
|
||||
For example, given the following configuration file:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F"]
|
||||
ignore = ["F401"]
|
||||
```
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```toml
|
||||
[lint]
|
||||
select = ["E", "F"]
|
||||
ignore = ["F401"]
|
||||
```
|
||||
|
||||
Running `ruff check --select F401` would result in Ruff enforcing `F401`, and no other rules.
|
||||
|
||||
Running `ruff check --extend-select B` would result in Ruff enforcing the `E`, `F`, and `B` rules,
|
||||
with the exception of `F401`.
|
||||
|
||||
## Fixes
|
||||
|
||||
Ruff supports automatic fixes for a variety of lint errors. For example, Ruff can remove unused
|
||||
imports, reformat docstrings, rewrite type annotations to use newer Python syntax, and more.
|
||||
|
||||
To enable fixes, pass the `--fix` flag to `ruff check`:
|
||||
|
||||
```console
|
||||
ruff check --fix
|
||||
```
|
||||
|
||||
By default, Ruff will fix all violations for which safe fixes are available; to determine
|
||||
whether a rule supports fixing, see [_Rules_](rules.md).
|
||||
|
||||
### Fix safety
|
||||
|
||||
Ruff labels fixes as "safe" and "unsafe". The meaning and intent of your code will be retained when
|
||||
applying safe fixes, but the meaning could change when applying unsafe fixes.
|
||||
|
||||
Specifically, an unsafe fix could lead to a change in runtime behavior, the removal of comments, or both,
|
||||
while safe fixes are intended to preserve runtime behavior and will only remove comments when deleting
|
||||
entire statements or expressions (e.g., removing unused imports).
|
||||
|
||||
For example, [`unnecessary-iterable-allocation-for-first-element`](rules/unnecessary-iterable-allocation-for-first-element.md)
|
||||
(`RUF015`) is a rule which checks for potentially unperformant use of `list(...)[0]`. The fix
|
||||
replaces this pattern with `next(iter(...))` which can result in a drastic speedup:
|
||||
|
||||
```console
|
||||
$ python -m timeit "head = list(range(99999999))[0]"
|
||||
1 loop, best of 5: 1.69 sec per loop
|
||||
```
|
||||
|
||||
```console
|
||||
$ python -m timeit "head = next(iter(range(99999999)))"
|
||||
5000000 loops, best of 5: 70.8 nsec per loop
|
||||
```
|
||||
|
||||
However, when the collection is empty, this raised exception changes from an `IndexError` to `StopIteration`:
|
||||
|
||||
```console
|
||||
$ python -c 'list(range(0))[0]'
|
||||
Traceback (most recent call last):
|
||||
File "<string>", line 1, in <module>
|
||||
IndexError: list index out of range
|
||||
```
|
||||
|
||||
```console
|
||||
$ python -c 'next(iter(range(0)))[0]'
|
||||
Traceback (most recent call last):
|
||||
File "<string>", line 1, in <module>
|
||||
StopIteration
|
||||
```
|
||||
|
||||
Since the change in exception type could break error handling upstream, this fix is categorized as unsafe.
|
||||
|
||||
Ruff only enables safe fixes by default. Unsafe fixes can be enabled by settings [`unsafe-fixes`](settings.md#unsafe-fixes) in your configuration file or passing the `--unsafe-fixes` flag to `ruff check`:
|
||||
|
||||
```console
|
||||
|
||||
## Show unsafe fixes
|
||||
|
||||
ruff check --unsafe-fixes
|
||||
|
||||
## Apply unsafe fixes
|
||||
|
||||
ruff check --fix --unsafe-fixes
|
||||
```
|
||||
|
||||
By default, Ruff will display a hint when unsafe fixes are available but not enabled. The suggestion can be silenced
|
||||
by setting the [`unsafe-fixes`](settings.md#unsafe-fixes) setting to `false` or using the `--no-unsafe-fixes` flag.
|
||||
|
||||
The safety of fixes can be adjusted per rule using the [`lint.extend-safe-fixes`](settings.md#lint_extend-safe-fixes) and [`lint.extend-unsafe-fixes`](settings.md#lint_extend-unsafe-fixes) settings.
|
||||
|
||||
For example, the following configuration would promote unsafe fixes for `F601` to safe fixes and demote safe fixes for `UP034` to unsafe fixes:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff.lint]
|
||||
extend-safe-fixes = ["F601"]
|
||||
extend-unsafe-fixes = ["UP034"]
|
||||
```
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```toml
|
||||
[lint]
|
||||
extend-safe-fixes = ["F601"]
|
||||
extend-unsafe-fixes = ["UP034"]
|
||||
```
|
||||
|
||||
You may use prefixes to select rules as well, e.g., `F` can be used to promote fixes for all rules in Pyflakes to safe.
|
||||
|
||||
!!! note
|
||||
All fixes will always be displayed by Ruff when using the `json` output format. The safety of each fix is available under the `applicability` field.
|
||||
|
||||
### Disabling fixes
|
||||
|
||||
To limit the set of rules that Ruff should fix, use the [`lint.fixable`](settings.md#lint_fixable)
|
||||
or [`lint.extend-fixable`](settings.md#lint_extend-fixable), and [`lint.unfixable`](settings.md#lint_unfixable) settings.
|
||||
|
||||
For example, the following configuration would enable fixes for all rules except
|
||||
[`unused-imports`](rules/unused-import.md) (`F401`):
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff.lint]
|
||||
fixable = ["ALL"]
|
||||
unfixable = ["F401"]
|
||||
```
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```toml
|
||||
[lint]
|
||||
fixable = ["ALL"]
|
||||
unfixable = ["F401"]
|
||||
```
|
||||
|
||||
Conversely, the following configuration would only enable fixes for `F401`:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ruff.lint]
|
||||
fixable = ["F401"]
|
||||
```
|
||||
|
||||
=== "ruff.toml"
|
||||
|
||||
```toml
|
||||
[lint]
|
||||
fixable = ["F401"]
|
||||
```
|
||||
|
||||
## Error suppression
|
||||
|
||||
Ruff supports several mechanisms for suppressing lint errors, be they false positives or
|
||||
permissible violations.
|
||||
|
||||
To omit a lint rule entirely, add it to the "ignore" list via the [`lint.ignore`](settings.md#lint_ignore)
|
||||
setting, either on the command-line or in your `pyproject.toml` or `ruff.toml` file.
|
||||
|
||||
To suppress a violation inline, Ruff uses a `noqa` system similar to [Flake8](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html).
|
||||
To ignore an individual violation, add `# noqa: {code}` to the end of the line, like so:
|
||||
|
||||
```python
|
||||
|
||||
## Ignore F841
|
||||
|
||||
x = 1 # noqa: F841
|
||||
|
||||
## Ignore E741 and F841
|
||||
|
||||
i = 1 # noqa: E741, F841
|
||||
|
||||
## Ignore _all_ violations
|
||||
|
||||
x = 1 # noqa
|
||||
```
|
||||
|
||||
For multi-line strings (like docstrings), the `noqa` directive should come at the end of the string
|
||||
(after the closing triple quote), and will apply to the entire string, like so:
|
||||
|
||||
```python
|
||||
"""Lorem ipsum dolor sit amet.
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
|
||||
""" # noqa: E501
|
||||
```
|
||||
|
||||
For import sorting, the `noqa` should come at the end of the first line in the import block, and
|
||||
will apply to all imports in the block, like so:
|
||||
|
||||
```python
|
||||
import os # noqa: I001
|
||||
import abc
|
||||
```
|
||||
|
||||
To ignore all violations across an entire file, add the line `# ruff: noqa` anywhere in the file,
|
||||
preferably towards the top, like so:
|
||||
|
||||
```python
|
||||
|
||||
## ruff: noqa
|
||||
```
|
||||
|
||||
To ignore a specific rule across an entire file, add the line `# ruff: noqa: {code}` anywhere in the
|
||||
file, preferably towards the top, like so:
|
||||
|
||||
```python
|
||||
|
||||
## ruff: noqa: F841
|
||||
```
|
||||
|
||||
Or see the [`lint.per-file-ignores`](settings.md#lint_per-file-ignores) setting, which enables the same
|
||||
functionality from within your `pyproject.toml` or `ruff.toml` file.
|
||||
|
||||
Global `noqa` comments must be on their own line to disambiguate from comments which ignore
|
||||
violations on a single line.
|
||||
|
||||
Note that Ruff will also respect Flake8's `# flake8: noqa` directive, and will treat it as
|
||||
equivalent to `# ruff: noqa`.
|
||||
|
||||
### Full suppression comment specification
|
||||
|
||||
The full specification is as follows:
|
||||
|
||||
- An inline blanket `noqa` comment is given by a case-insensitive match for
|
||||
`#noqa` with optional whitespace after the `#` symbol, followed by either: the
|
||||
end of the comment, the beginning of a new comment (`#`), or whitespace
|
||||
followed by any character other than `:`.
|
||||
- An inline rule suppression is given by first finding a case-insensitive match
|
||||
for `#noqa` with optional whitespace after the `#` symbol, optional whitespace
|
||||
after `noqa`, and followed by the symbol `:`. After this we are expected to
|
||||
have a list of rule codes which is given by sequences of uppercase ASCII
|
||||
characters followed by ASCII digits, separated by whitespace or commas. The
|
||||
list ends at the last valid code. We will attempt to interpret rules with a
|
||||
missing delimiter (e.g. `F401F841`), though a warning will be emitted in this
|
||||
case.
|
||||
- A file-level exemption comment is given by a case-sensitive match for `#ruff:`
|
||||
or `#flake8:`, with optional whitespace after `#` and before `:`, followed by
|
||||
optional whitespace and a case-insensitive match for `noqa`. After this, the
|
||||
specification is as in the inline case.
|
||||
|
||||
### Detecting unused suppression comments
|
||||
|
||||
Ruff implements a special rule, [`unused-noqa`](https://docs.astral.sh/ruff/rules/unused-noqa/),
|
||||
under the `RUF100` code, to enforce that your `noqa` directives are "valid", in that the violations
|
||||
they _say_ they ignore are actually being triggered on that line (and thus suppressed). To flag
|
||||
unused `noqa` directives, run: `ruff check /path/to/file.py --extend-select RUF100`.
|
||||
|
||||
Ruff can also _remove_ any unused `noqa` directives via its fix functionality. To remove any
|
||||
unused `noqa` directives, run: `ruff check /path/to/file.py --extend-select RUF100 --fix`.
|
||||
|
||||
### Inserting necessary suppression comments
|
||||
|
||||
Ruff can _automatically add_ `noqa` directives to all lines that contain violations, which is
|
||||
useful when migrating a new codebase to Ruff. To automatically add `noqa` directives to all
|
||||
relevant lines (with the appropriate rule codes), run: `ruff check /path/to/file.py --add-noqa`.
|
||||
|
||||
### Action comments
|
||||
|
||||
Ruff respects isort's [action comments](https://pycqa.github.io/isort/docs/configuration/action_comments.html)
|
||||
(`# isort: skip_file`, `# isort: on`, `# isort: off`, `# isort: skip`, and `# isort: split`), which
|
||||
enable selectively enabling and disabling import sorting for blocks of code and other inline
|
||||
configuration.
|
||||
|
||||
Ruff will also respect variants of these action comments with a `# ruff:` prefix
|
||||
(e.g., `# ruff: isort: skip_file`, `# ruff: isort: on`, and so on). These variants more clearly
|
||||
convey that the action comment is intended for Ruff, but are functionally equivalent to the
|
||||
isort variants.
|
||||
|
||||
Unlike isort, Ruff does not respect action comments within docstrings.
|
||||
|
||||
See the [isort documentation](https://pycqa.github.io/isort/docs/configuration/action_comments.html)
|
||||
for more.
|
||||
|
||||
## Exit codes
|
||||
|
||||
By default, `ruff check` exits with the following status codes:
|
||||
|
||||
- `0` if no violations were found, or if all present violations were fixed automatically.
|
||||
- `1` if violations were found.
|
||||
- `2` if Ruff terminates abnormally due to invalid configuration, invalid CLI options, or an
|
||||
internal error.
|
||||
|
||||
This convention mirrors that of tools like ESLint, Prettier, and RuboCop.
|
||||
|
||||
`ruff check` supports two command-line flags that alter its exit code behavior:
|
||||
|
||||
- `--exit-zero` will cause Ruff to exit with a status code of `0` even if violations were found.
|
||||
Note that Ruff will still exit with a status code of `2` if it terminates abnormally.
|
||||
- `--exit-non-zero-on-fix` will cause Ruff to exit with a status code of `1` if violations were
|
||||
found, _even if_ all such violations were fixed automatically. Note that the use of
|
||||
`--exit-non-zero-on-fix` can result in a non-zero exit code even if no violations remain after
|
||||
fixing.
|
||||
126
skills/python-code-quality/tools/python_formatter.py
Executable file
126
skills/python-code-quality/tools/python_formatter.py
Executable file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env -S uv run --script --quiet
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""
|
||||
Python formatter for Claude Code.
|
||||
Automatically formats and fixes Python files using ruff.
|
||||
|
||||
Usage:
|
||||
Hook mode (stdin): echo '{"tool_input":{"file_path":"test.py"}}' | python python_formatter.py
|
||||
CLI mode: python python_formatter.py test.py
|
||||
Blocking mode: python python_formatter.py --blocking test.py
|
||||
|
||||
Options:
|
||||
--blocking Exit with code 2 when changes are made (sends feedback to Claude)
|
||||
Default: Exit with code 0 (output only in transcript mode)
|
||||
|
||||
Features:
|
||||
- Dual mode: Works with Claude Code hooks (stdin) or command-line arguments
|
||||
- Auto-formats code with ruff format
|
||||
- Auto-fixes linting issues with ruff check --fix
|
||||
- Only processes .py files
|
||||
- Reports what was changed
|
||||
- Blocking mode option for immediate Claude feedback
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def format_python_file(file_path: str, blocking: bool = False) -> bool:
|
||||
"""Format and fix a Python file using ruff.
|
||||
|
||||
Args:
|
||||
file_path: Path to the Python file to format
|
||||
blocking: If True, output to stderr for blocking behavior
|
||||
|
||||
Returns:
|
||||
bool: True if changes were made, False otherwise
|
||||
"""
|
||||
changes_made = []
|
||||
|
||||
try:
|
||||
# Run ruff format
|
||||
format_result = subprocess.run(
|
||||
["ruff", "format", file_path], capture_output=True, text=True, timeout=30
|
||||
)
|
||||
|
||||
# Check if formatting made changes
|
||||
# ruff format returns 0 whether changes were made or not
|
||||
# but outputs "1 file reformatted" vs "1 file left unchanged"
|
||||
output = (format_result.stdout + format_result.stderr).lower()
|
||||
if "reformatted" in output:
|
||||
changes_made.append("formatted code style")
|
||||
|
||||
# Run ruff check --fix to auto-fix linting issues
|
||||
fix_result = subprocess.run(
|
||||
["ruff", "check", "--fix", file_path], capture_output=True, text=True, timeout=30
|
||||
)
|
||||
|
||||
# Check if fixes were applied
|
||||
if fix_result.stdout and "fixed" in fix_result.stdout.lower():
|
||||
changes_made.append("fixed linting issues")
|
||||
|
||||
# Report results
|
||||
if changes_made:
|
||||
message = f"✓ Python formatter: {', '.join(changes_made)} in {file_path}"
|
||||
if blocking:
|
||||
print(message, file=sys.stderr)
|
||||
else:
|
||||
print(message)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f"⚠ Ruff timed out for {file_path}", file=sys.stderr)
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
print("⚠ Ruff not found. Install with: uv tool install ruff", file=sys.stderr)
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error running ruff: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
# Main execution
|
||||
try:
|
||||
# Parse arguments
|
||||
blocking = False
|
||||
file_path = ""
|
||||
|
||||
# Check for --blocking flag and file path
|
||||
if len(sys.argv) > 1:
|
||||
args = sys.argv[1:]
|
||||
if "--blocking" in args:
|
||||
blocking = True
|
||||
args.remove("--blocking")
|
||||
if args:
|
||||
file_path = args[0]
|
||||
else:
|
||||
# Read from stdin (hook mode)
|
||||
input_data = json.load(sys.stdin)
|
||||
file_path = input_data.get("tool_input", {}).get("file_path", "")
|
||||
|
||||
if not file_path.endswith(".py"):
|
||||
sys.exit(0) # Not a Python file
|
||||
|
||||
if os.path.exists(file_path):
|
||||
changes_made = format_python_file(file_path, blocking=blocking)
|
||||
|
||||
# In blocking mode, exit with code 2 if changes were made
|
||||
if blocking and changes_made:
|
||||
sys.exit(2)
|
||||
else:
|
||||
print(f"⚠ File not found: {file_path}", file=sys.stderr)
|
||||
|
||||
# Always exit 0 to be non-blocking (unless blocking mode with changes)
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in Python formatter: {e}", file=sys.stderr)
|
||||
sys.exit(0) # Non-blocking even on errors
|
||||
72
skills/python-code-quality/tools/python_ruff_checker.py
Executable file
72
skills/python-code-quality/tools/python_ruff_checker.py
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env -S uv run --script --quiet
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""
|
||||
Ruff checker for Claude Code Python files.
|
||||
Automatically runs ruff check on Python files after edits.
|
||||
|
||||
Usage:
|
||||
Hook mode (stdin): echo '{"tool_input":{"file_path":"test.py"}}' | python python_ruff_checker.py
|
||||
CLI mode: python python_ruff_checker.py test.py
|
||||
|
||||
Features:
|
||||
- Dual mode: Works with Claude Code hooks (stdin) or command-line arguments
|
||||
- Only processes .py files
|
||||
- Provides feedback on code quality issues
|
||||
- Non-blocking (exits with code 0 even if ruff finds issues)
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def check_python_file(file_path: str) -> None:
|
||||
"""Run ruff check on a Python file."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ruff", "check", file_path], capture_output=True, text=True, timeout=30
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"✓ Ruff check passed: {file_path}")
|
||||
else:
|
||||
print(f"⚠ Ruff found issues in {file_path}:")
|
||||
if result.stdout:
|
||||
print(result.stdout)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f"⚠ Ruff check timed out for {file_path}", file=sys.stderr)
|
||||
except FileNotFoundError:
|
||||
print("⚠ Ruff not found. Install with: uv tool install ruff", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"Error running ruff: {e}", file=sys.stderr)
|
||||
|
||||
|
||||
# Main execution
|
||||
try:
|
||||
# Check if file path provided as command-line argument
|
||||
if len(sys.argv) > 1:
|
||||
file_path = sys.argv[1]
|
||||
else:
|
||||
# Read from stdin (hook mode)
|
||||
input_data = json.load(sys.stdin)
|
||||
file_path = input_data.get("tool_input", {}).get("file_path", "")
|
||||
|
||||
if not file_path.endswith(".py"):
|
||||
sys.exit(0) # Not a Python file
|
||||
|
||||
if os.path.exists(file_path):
|
||||
check_python_file(file_path)
|
||||
else:
|
||||
print(f"⚠ File not found: {file_path}", file=sys.stderr)
|
||||
|
||||
# Always exit 0 to be non-blocking
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in ruff checker: {e}", file=sys.stderr)
|
||||
sys.exit(0) # Non-blocking even on errors
|
||||
231
skills/python-json-parsing/SKILL.md
Normal file
231
skills/python-json-parsing/SKILL.md
Normal file
@@ -0,0 +1,231 @@
|
||||
---
|
||||
name: python-json-parsing
|
||||
description: >
|
||||
Python JSON parsing best practices covering performance optimization (orjson/msgspec),
|
||||
handling large files (streaming/JSONL), security (injection prevention),
|
||||
and advanced querying (JSONPath/JMESPath).
|
||||
Use when working with JSON data, parsing APIs, handling large JSON files,
|
||||
or optimizing JSON performance.
|
||||
---
|
||||
|
||||
# Python JSON Parsing Best Practices
|
||||
|
||||
Comprehensive guide to JSON parsing in Python with focus on performance, security, and scalability.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic JSON Parsing
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
# Parse JSON string
|
||||
data = json.loads('{"name": "Alice", "age": 30}')
|
||||
|
||||
# Parse JSON file
|
||||
with open("data.json", "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Write JSON file
|
||||
with open("output.json", "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
```
|
||||
|
||||
**Key Rule:** Always specify `encoding="utf-8"` when reading/writing files.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Working with JSON APIs or data interchange
|
||||
- Optimizing JSON performance in high-throughput applications
|
||||
- Handling large JSON files (> 100MB)
|
||||
- Securing applications against JSON injection
|
||||
- Extracting data from complex nested JSON structures
|
||||
|
||||
## Performance: Choose the Right Library
|
||||
|
||||
### Library Comparison (10,000 records benchmark)
|
||||
|
||||
| Library | Serialize (s) | Deserialize (s) | Best For |
|
||||
|---------|---------------|-----------------|----------|
|
||||
| **orjson** | 0.42 | 1.27 | FastAPI, web APIs (3.9x faster) |
|
||||
| **msgspec** | 0.49 | 0.93 | Maximum performance (1.7x faster deserialization) |
|
||||
| **json** (stdlib) | 1.62 | 1.62 | Universal compatibility |
|
||||
| **ujson** | 1.41 | 1.85 | Drop-in replacement (2x faster) |
|
||||
|
||||
**Recommendation:**
|
||||
|
||||
- Use **orjson** for FastAPI/web APIs (native support, fastest serialization)
|
||||
- Use **msgspec** for data pipelines (fastest overall, typed validation)
|
||||
- Use **json** when compatibility is critical
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# High-performance libraries
|
||||
pip install orjson msgspec ujson
|
||||
|
||||
# Advanced querying
|
||||
pip install jsonpath-ng jmespath
|
||||
|
||||
# Streaming large files
|
||||
pip install ijson
|
||||
|
||||
# Schema validation
|
||||
pip install jsonschema
|
||||
```
|
||||
|
||||
## Large Files: Streaming Strategies
|
||||
|
||||
For files > 100MB, avoid loading into memory.
|
||||
|
||||
### Strategy 1: JSONL (JSON Lines)
|
||||
|
||||
Convert large JSON arrays to line-delimited format:
|
||||
|
||||
```python
|
||||
# Stream process JSONL
|
||||
with open("large.jsonl", "r") as infile, open("output.jsonl", "w") as outfile:
|
||||
for line in infile:
|
||||
obj = json.loads(line)
|
||||
obj["processed"] = True
|
||||
outfile.write(json.dumps(obj) + "\n")
|
||||
```
|
||||
|
||||
### Strategy 2: Streaming with ijson
|
||||
|
||||
```python
|
||||
import ijson
|
||||
|
||||
# Process large JSON without loading into memory
|
||||
with open("huge.json", "rb") as f:
|
||||
for item in ijson.items(f, "products.item"):
|
||||
process(item) # Handle one item at a time
|
||||
```
|
||||
|
||||
See: `patterns/streaming-large-json.md`
|
||||
|
||||
## Security: Prevent JSON Injection
|
||||
|
||||
**Critical Rules:**
|
||||
|
||||
1. Always use `json.loads()`, never `eval()`
|
||||
2. Validate input with `jsonschema`
|
||||
3. Sanitize user input before serialization
|
||||
4. Escape special characters (`"` and `\`)
|
||||
|
||||
**Vulnerable Code:**
|
||||
|
||||
```python
|
||||
# NEVER DO THIS
|
||||
username = request.GET['username'] # User input: admin", "role": "admin
|
||||
json_string = f'{{"user":"{username}","role":"user"}}'
|
||||
# Result: privilege escalation
|
||||
```
|
||||
|
||||
**Secure Code:**
|
||||
|
||||
```python
|
||||
# Use json.dumps for serialization
|
||||
data = {"user": username, "role": "user"}
|
||||
json_string = json.dumps(data) # Properly escaped
|
||||
```
|
||||
|
||||
See: `anti-patterns/security-json-injection.md`, `anti-patterns/eval-usage.md`
|
||||
|
||||
## Advanced: JSONPath for Complex Queries
|
||||
|
||||
Extract data from nested JSON without complex loops:
|
||||
|
||||
```python
|
||||
import jsonpath_ng as jp
|
||||
|
||||
data = {
|
||||
"products": [
|
||||
{"name": "Apple", "price": 12.88},
|
||||
{"name": "Peach", "price": 27.25}
|
||||
]
|
||||
}
|
||||
|
||||
# Filter by price
|
||||
query = jp.parse("products[?price>20].name")
|
||||
results = [match.value for match in query.find(data)]
|
||||
# Output: ["Peach"]
|
||||
```
|
||||
|
||||
**Key Operators:**
|
||||
|
||||
- `$` - Root selector
|
||||
- `..` - Recursive descendant
|
||||
- `*` - Wildcard
|
||||
- `[?<predicate>]` - Filter (e.g., `[?price > 20]`)
|
||||
- `[start:end:step]` - Array slicing
|
||||
|
||||
See: `patterns/jsonpath-querying.md`
|
||||
|
||||
## Custom Objects: Serialization
|
||||
|
||||
Handle datetime, UUID, Decimal, and custom classes:
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
class CustomEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, datetime):
|
||||
return obj.isoformat()
|
||||
if isinstance(obj, set):
|
||||
return list(obj)
|
||||
return super().default(obj)
|
||||
|
||||
# Usage
|
||||
data = {"timestamp": datetime.now(), "tags": {"python", "json"}}
|
||||
json_str = json.dumps(data, cls=CustomEncoder)
|
||||
```
|
||||
|
||||
See: `patterns/custom-object-serialization.md`
|
||||
|
||||
## Performance Checklist
|
||||
|
||||
- [ ] Use orjson/msgspec for high-throughput applications
|
||||
- [ ] Specify UTF-8 encoding when reading/writing files
|
||||
- [ ] Use streaming (ijson/JSONL) for files > 100MB
|
||||
- [ ] Minify JSON for production (`separators=(',', ':')`)
|
||||
- [ ] Pretty-print for development (`indent=2`)
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] Never use `eval()` for JSON parsing
|
||||
- [ ] Validate input with `jsonschema`
|
||||
- [ ] Sanitize user input before serialization
|
||||
- [ ] Use `json.dumps()` to prevent injection
|
||||
- [ ] Escape special characters in user data
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
**Performance:**
|
||||
|
||||
- `reference/python-json-parsing-best-practices-2025.md` - Comprehensive research with benchmarks
|
||||
|
||||
**Patterns:**
|
||||
|
||||
- `patterns/streaming-large-json.md` - ijson and JSONL strategies
|
||||
- `patterns/custom-object-serialization.md` - Handle datetime, UUID, custom classes
|
||||
- `patterns/jsonpath-querying.md` - Advanced nested data extraction
|
||||
|
||||
**Security:**
|
||||
|
||||
- `anti-patterns/security-json-injection.md` - Prevent injection attacks
|
||||
- `anti-patterns/eval-usage.md` - Why never to use eval()
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `examples/high-performance-parsing.py` - orjson and msgspec code
|
||||
- `examples/large-file-streaming.py` - Streaming with ijson
|
||||
- `examples/secure-validation.py` - jsonschema validation
|
||||
|
||||
**Tools:**
|
||||
|
||||
- `tools/json-performance-benchmark.py` - Benchmark different libraries
|
||||
@@ -0,0 +1,419 @@
|
||||
# Python JSON Parsing Best Practices (2025)
|
||||
|
||||
**Research Date:** October 31, 2025
|
||||
**Query:** Best practices for parsing JSON in Python 2025
|
||||
|
||||
## Executive Summary
|
||||
|
||||
JSON parsing in Python has evolved significantly with performance-optimized libraries and enhanced security practices. This research identifies critical best practices for developers working with JSON data in 2025, covering library selection, performance optimization, security considerations, and handling large-scale datasets.
|
||||
|
||||
---
|
||||
|
||||
## 1. Core Library Selection & Performance
|
||||
|
||||
### Standard Library (`json` module)
|
||||
|
||||
The built-in `json` module remains the baseline for JSON operations in Python:
|
||||
|
||||
- **Serialization**: `json.dumps()` converts Python objects to JSON strings
|
||||
- **Deserialization**: `json.loads()` parses JSON strings to Python objects
|
||||
- **File Operations**: `json.dump()` and `json.load()` for direct file I/O
|
||||
- **Performance**: Adequate for most use cases but slower than alternatives
|
||||
|
||||
**Key Insight**: Always specify encoding when working with files - UTF-8 is the recommended standard per RFC requirements.
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
# Best practice: Always specify encoding
|
||||
with open("data.json", "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
```
|
||||
|
||||
**Source**: [Real Python - Working With JSON Data in Python](https://realpython.com/python-json)
|
||||
|
||||
### High-Performance Alternatives (2025 Benchmarks)
|
||||
|
||||
Based on comprehensive benchmarking of 10,000 records with 10 runs:
|
||||
|
||||
| Library | Serialization (s) | Deserialization (s) | Key Features |
|
||||
|---------|-------------------|---------------------|--------------|
|
||||
| **orjson** | 0.417962 | 1.272813 | Rust-based, fastest serialization, built-in FastAPI support |
|
||||
| **msgspec** | 0.489964 | 0.930834 | Ultra-fast, typed structs, supports YAML/TOML |
|
||||
| **json** (stdlib) | 1.616786 | 1.616203 | Universal compatibility, stable |
|
||||
| **ujson** | 1.413367 | 1.853332 | C-based, drop-in replacement |
|
||||
| **rapidjson** | 2.044958 | 1.717067 | C++ wrapper, flexible |
|
||||
|
||||
**Recommendation**:
|
||||
- Use **orjson** for web APIs (FastAPI native support, 3.9x faster serialization)
|
||||
- Use **msgspec** for maximum performance across all operations (1.7x faster deserialization)
|
||||
- Stick with **json** for compatibility-critical applications
|
||||
|
||||
**Source**: [DEV Community - Benchmarking Python JSON Libraries](https://dev.to/kanakos01/benchmarking-python-json-libraries-33bb)
|
||||
|
||||
---
|
||||
|
||||
## 2. Handling Large JSON Files
|
||||
|
||||
### Problem: Memory Constraints
|
||||
|
||||
Loading multi-million line JSON files with `json.load()` causes memory exhaustion.
|
||||
|
||||
### Solution Strategies
|
||||
|
||||
#### Strategy 1: JSON Lines (JSONL) Format
|
||||
|
||||
Convert large JSON arrays to line-delimited format for streaming processing:
|
||||
|
||||
```python
|
||||
# Streaming read + process + write
|
||||
with open("big.jsonl", "r") as infile, open("new.jsonl", "w") as outfile:
|
||||
for line in infile:
|
||||
obj = json.loads(line)
|
||||
obj["status"] = "processed"
|
||||
outfile.write(json.dumps(obj) + "\n")
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Easy appending of new records
|
||||
- Line-by-line updates without rewriting entire file
|
||||
- Native support in pandas, Spark, and `jq`
|
||||
|
||||
**Source**: [DEV Community - Handling Large JSON Files in Python](https://dev.to/lovestaco/handling-large-json-files-in-python-efficient-read-write-and-update-strategies-3jgg)
|
||||
|
||||
#### Strategy 2: Incremental Parsing with `ijson`
|
||||
|
||||
For true JSON arrays/objects, use streaming parsers:
|
||||
|
||||
```python
|
||||
import ijson
|
||||
|
||||
# Process large file without loading into memory
|
||||
with open("huge.json", "rb") as f:
|
||||
for item in ijson.items(f, "products.item"):
|
||||
process(item) # Handle one item at a time
|
||||
```
|
||||
|
||||
#### Strategy 3: Database Migration
|
||||
|
||||
For frequently queried/updated data, migrate from JSON to:
|
||||
- **SQLite**: Lightweight, file-based
|
||||
- **PostgreSQL/MongoDB**: Scalable solutions
|
||||
|
||||
**Critical Decision Matrix**:
|
||||
- JSON additions only → JSONL format
|
||||
- Batch updates → Stream read + rewrite
|
||||
- Frequent random updates → Database
|
||||
|
||||
**Source**: [DEV Community - Handling Large JSON Files](https://dev.to/lovestaco/handling-large-json-files-in-python-efficient-read-write-and-update-strategies-3jgg)
|
||||
|
||||
---
|
||||
|
||||
## 3. Advanced Parsing with JSONPath & JMESPath
|
||||
|
||||
### JSONPath: XPath for JSON
|
||||
|
||||
Use JSONPath for nested data extraction with complex queries:
|
||||
|
||||
```python
|
||||
import jsonpath_ng as jp
|
||||
|
||||
data = {
|
||||
"products": [
|
||||
{"name": "Apple", "price": 12.88},
|
||||
{"name": "Peach", "price": 27.25}
|
||||
]
|
||||
}
|
||||
|
||||
# Filter by price
|
||||
query = jp.parse("products[?price>20].name")
|
||||
results = [match.value for match in query.find(data)]
|
||||
# Output: ["Peach"]
|
||||
```
|
||||
|
||||
**Key Operators**:
|
||||
- `$` - Root selector
|
||||
- `..` - Recursive descendant
|
||||
- `*` - Wildcard
|
||||
- `[?<predicate>]` - Filter (e.g., `[?price > 20 & price < 100]`)
|
||||
- `[start:end:step]` - Array slicing
|
||||
|
||||
**Use Cases**:
|
||||
- Web scraping hidden JSON data in `<script>` tags
|
||||
- Extracting nested API response data
|
||||
- Complex filtering across multiple levels
|
||||
|
||||
**Source**: [ScrapFly - Introduction to Parsing JSON with Python JSONPath](https://scrapfly.io/blog/posts/parse-json-jsonpath-python)
|
||||
|
||||
### JMESPath Alternative
|
||||
|
||||
JMESPath offers easier dataset mutation and filtering for predictable structures, while JSONPath excels at extracting deeply nested data.
|
||||
|
||||
---
|
||||
|
||||
## 4. Security Best Practices
|
||||
|
||||
### Critical Vulnerabilities
|
||||
|
||||
#### JSON Injection Attacks
|
||||
|
||||
**Server-side injection** occurs when unsanitized user input is directly serialized:
|
||||
|
||||
```python
|
||||
# VULNERABLE CODE
|
||||
username = request.GET['username'] # User input: admin", "role": "administrator
|
||||
json_string = f'{{"user":"{username}","role":"user"}}'
|
||||
# Result: {"user":"admin", "role":"administrator", "role":"user"}
|
||||
# Parser takes last role → privilege escalation
|
||||
```
|
||||
|
||||
**Client-side injection** via `eval()`:
|
||||
|
||||
```python
|
||||
# NEVER DO THIS
|
||||
data = eval("(" + json_response + ")") # Code execution risk!
|
||||
|
||||
# CORRECT APPROACH
|
||||
data = json.loads(json_response) # Safe parsing
|
||||
```
|
||||
|
||||
**Source**: [Comparitech - JSON Injection Guide](https://www.comparitech.com/net-admin/json-injection-guide)
|
||||
|
||||
### Defense Strategies
|
||||
|
||||
1. **Input Sanitization**: Validate and escape all user input before serialization
|
||||
2. **Never Use `eval()`**: Always use `json.loads()` or `JSON.parse()`
|
||||
3. **Schema Validation**: Use `jsonschema` library to enforce data contracts
|
||||
4. **Content Security Policy (CSP)**: Prevents eval() usage by default
|
||||
5. **Escape Special Characters**: Properly escape `"` and `\` in user data
|
||||
|
||||
```python
|
||||
from jsonschema import validate, ValidationError
|
||||
|
||||
schema = {
|
||||
"type": "object",
|
||||
"required": ["id", "name", "email"],
|
||||
"properties": {
|
||||
"id": {"type": "integer", "minimum": 1},
|
||||
"email": {"type": "string", "format": "email"}
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
validate(instance=user_data, schema=schema)
|
||||
except ValidationError as e:
|
||||
print(f"Invalid data: {e.message}")
|
||||
```
|
||||
|
||||
**Source**: [Better Stack - Working With JSON Data in Python](https://betterstack.com/community/guides/scaling-nodejs/json-data-in-python)
|
||||
|
||||
---
|
||||
|
||||
## 5. Type Handling & Custom Objects
|
||||
|
||||
### Python ↔ JSON Type Mapping
|
||||
|
||||
| Python | → JSON | JSON → | Python |
|
||||
|--------|--------|--------|--------|
|
||||
| dict | object | object | dict |
|
||||
| list, tuple | array | array | list ⚠️ |
|
||||
| str | string | string | str |
|
||||
| int, float | number | number | int/float |
|
||||
| True/False | true/false | true/false | True/False |
|
||||
| None | null | null | None |
|
||||
|
||||
**⚠️ Gotcha**: Tuples serialize to arrays but deserialize back to lists (data type loss).
|
||||
|
||||
### Custom Object Serialization
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
class CustomEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, datetime):
|
||||
return obj.isoformat()
|
||||
if isinstance(obj, set):
|
||||
return list(obj)
|
||||
return super().default(obj)
|
||||
|
||||
# Usage
|
||||
data = {"timestamp": datetime.now(), "tags": {"python", "json"}}
|
||||
json_str = json.dumps(data, cls=CustomEncoder)
|
||||
```
|
||||
|
||||
**Advanced Alternative**: Use **Pydantic** or **msgspec** for typed validation and automatic serialization.
|
||||
|
||||
**Source**: [Better Stack Community Guide](https://betterstack.com/community/guides/scaling-nodejs/json-data-in-python)
|
||||
|
||||
---
|
||||
|
||||
## 6. Formatting & Debugging
|
||||
|
||||
### Pretty Printing
|
||||
|
||||
```python
|
||||
# Readable output with indentation
|
||||
json_str = json.dumps(data, indent=2, sort_keys=True)
|
||||
|
||||
# Command-line validation and formatting
|
||||
# Validate JSON file
|
||||
python -m json.tool config.json
|
||||
|
||||
# Pretty-print to new file
|
||||
python -m json.tool input.json output.json --indent 2
|
||||
```
|
||||
|
||||
### Minification for Production
|
||||
|
||||
```python
|
||||
# Remove all whitespace for minimal size
|
||||
minified = json.dumps(data, separators=(',', ':'))
|
||||
|
||||
# Command line
|
||||
python -m json.tool --compact input.json output.json
|
||||
```
|
||||
|
||||
**Performance Impact**: Pretty-printed JSON can be 2x larger (308 bytes → 645 bytes in benchmarks).
|
||||
|
||||
**Source**: [Real Python - Working With JSON](https://realpython.com/python-json)
|
||||
|
||||
---
|
||||
|
||||
## 7. Web Scraping: Handling Non-Standard JSON
|
||||
|
||||
### ChompJS for JavaScript Objects
|
||||
|
||||
Many websites embed data in JavaScript objects that aren't valid JSON:
|
||||
|
||||
```python
|
||||
import chompjs
|
||||
|
||||
# These are valid JS but invalid JSON:
|
||||
js_objects = [
|
||||
"{'a': 'b'}", # Single quotes
|
||||
"{a: 'b'}", # Unquoted keys
|
||||
'{"a": [1,2,3,]}', # Trailing comma
|
||||
'{"price": .99}' # Missing leading zero
|
||||
]
|
||||
|
||||
# ChompJS handles all of these
|
||||
for js in js_objects:
|
||||
python_dict = chompjs.parse_js_object(js)
|
||||
```
|
||||
|
||||
**Use Case**: Extracting hidden web data from `<script>` tags containing JavaScript initializers.
|
||||
|
||||
**Source**: [Zyte - JSON Parsing with Python](https://www.zyte.com/blog/json-parsing-with-python)
|
||||
|
||||
---
|
||||
|
||||
## 8. Production Optimization Checklist
|
||||
|
||||
### High-Performance Applications
|
||||
|
||||
Based on LinkedIn engineering insights for million-request APIs:
|
||||
|
||||
1. **Library Selection**:
|
||||
- FastAPI apps → `orjson` (native support, 4x faster)
|
||||
- Data pipelines → `msgspec` (fastest overall)
|
||||
- General use → `ujson` (2x faster than stdlib)
|
||||
|
||||
2. **Streaming for Scale**:
|
||||
- Use `ijson` for files > 100MB
|
||||
- Convert to JSONL for append-heavy workloads
|
||||
- Consider Protocol Buffers for ultra-high performance
|
||||
|
||||
3. **Buffer Optimization**:
|
||||
- Profile with `cProfile` to identify bottlenecks
|
||||
- Tune I/O buffer sizes for your workload
|
||||
- Use async I/O for concurrent request handling
|
||||
|
||||
4. **Monitoring**:
|
||||
- Track JSON processing time metrics
|
||||
- Set up alerts for parsing errors
|
||||
- Continuously benchmark against SLAs
|
||||
|
||||
**Source**: [LinkedIn - Optimizing JSON Parsing and Serialization](https://linkedin.com/pulse/optimizing-json-parsing-serialization-applications-amit-jindal-1g0tf)
|
||||
|
||||
---
|
||||
|
||||
## 9. Logging Best Practices
|
||||
|
||||
### Structured JSON Logging
|
||||
|
||||
```python
|
||||
import logging
|
||||
import json
|
||||
|
||||
# Configure JSON logging from the start
|
||||
class JsonFormatter(logging.Formatter):
|
||||
def format(self, record):
|
||||
log_data = {
|
||||
"timestamp": self.formatTime(record),
|
||||
"level": record.levelname,
|
||||
"message": record.getMessage(),
|
||||
"user_id": getattr(record, 'user_id', None),
|
||||
"session_id": getattr(record, 'session_id', None)
|
||||
}
|
||||
return json.dumps(log_data)
|
||||
|
||||
# Benefits:
|
||||
# - Easy parsing and searching
|
||||
# - Structured database storage
|
||||
# - Better correlation across services
|
||||
```
|
||||
|
||||
**Schema Design Tips**:
|
||||
- Use consistent key naming (snake_case recommended)
|
||||
- Flatten structures when possible (concatenate keys with separator)
|
||||
- Uniform data types per field
|
||||
- Parse stack traces into hierarchical attributes
|
||||
|
||||
**Source**: [Graylog - What To Know About Parsing JSON](https://graylog.org/post/what-to-know-parsing-json)
|
||||
|
||||
---
|
||||
|
||||
## 10. Key Takeaways for Developers
|
||||
|
||||
### Must-Do Practices
|
||||
|
||||
1. ✅ **Always use `json.loads()`, never `eval()`** for security
|
||||
2. ✅ **Specify UTF-8 encoding** when reading/writing files
|
||||
3. ✅ **Validate input** with `jsonschema` before processing
|
||||
4. ✅ **Choose performance library** based on workload (orjson/msgspec)
|
||||
5. ✅ **Use JSONL format** for large, append-heavy datasets
|
||||
6. ✅ **Implement streaming** for files > 100MB
|
||||
7. ✅ **Pretty-print for development**, minify for production
|
||||
8. ✅ **Add structured logging** from project start
|
||||
|
||||
### Common Pitfalls to Avoid
|
||||
|
||||
1. ❌ Loading entire large files into memory
|
||||
2. ❌ Using `eval()` for JSON parsing
|
||||
3. ❌ Skipping input validation on user data
|
||||
4. ❌ Ignoring type conversions (tuple → list)
|
||||
5. ❌ Not handling exceptions properly
|
||||
6. ❌ Over-logging during parsing (performance impact)
|
||||
7. ❌ Using sequential IDs (security risk - use UUID/GUID)
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
1. [Real Python - Working With JSON Data in Python](https://realpython.com/python-json) - Comprehensive guide to json module, Aug 2025
|
||||
2. [Better Stack Community - JSON Data in Python](https://betterstack.com/community/guides/scaling-nodejs/json-data-in-python) - Advanced techniques and validation, Apr 2025
|
||||
3. [DEV Community - Handling Large JSON Files](https://dev.to/lovestaco/handling-large-json-files-in-python-efficient-read-write-and-update-strategies-3jgg) - Strategies for massive datasets, Oct 2025
|
||||
4. [ScrapFly - JSONPath in Python](https://scrapfly.io/blog/posts/parse-json-jsonpath-python) - Advanced querying techniques, Sep 2025
|
||||
5. [DEV Community - Benchmarking JSON Libraries](https://dev.to/kanakos01/benchmarking-python-json-libraries-33bb) - Performance comparison, Jul 2025
|
||||
6. [LinkedIn - Optimizing JSON for High-Performance](https://linkedin.com/pulse/optimizing-json-parsing-serialization-applications-amit-jindal-1g0tf) - Enterprise optimization, Mar 2025
|
||||
7. [Graylog - Parsing JSON](https://graylog.org/post/what-to-know-parsing-json) - Logging best practices, Mar 2025
|
||||
8. [Comparitech - JSON Injection Guide](https://www.comparitech.com/net-admin/json-injection-guide) - Security vulnerabilities, Nov 2024
|
||||
9. [Zyte - JSON Parsing with Python](https://www.zyte.com/blog/json-parsing-with-python) - Practical guide, Dec 2024
|
||||
|
||||
---
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** October 31, 2025
|
||||
**Maintained By:** Lunar Claude Research Team
|
||||
545
skills/python-uv-scripts/SKILL.md
Normal file
545
skills/python-uv-scripts/SKILL.md
Normal file
@@ -0,0 +1,545 @@
|
||||
---
|
||||
name: python-uv-scripts
|
||||
description: >
|
||||
Python single-file script development using uv and PEP 723 inline metadata.
|
||||
Prevents invalid patterns like [tool.uv.metadata].
|
||||
Use when creating standalone Python utilities, converting scripts to uv format,
|
||||
managing script dependencies, implementing script testing,
|
||||
or establishing team standards for script development.
|
||||
---
|
||||
|
||||
# Python Single-File Scripts with uv
|
||||
|
||||
Expert guidance for creating production-ready, self-contained Python scripts using uv's inline dependency management
|
||||
(PEP 723).
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Create Your First uv Script
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0",
|
||||
# "rich>=13.0.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import httpx
|
||||
from rich import print
|
||||
|
||||
response = httpx.get("https://api.github.com")
|
||||
print(f"[green]Status: {response.status_code}[/green]")
|
||||
```
|
||||
|
||||
Make it executable:
|
||||
|
||||
```bash
|
||||
chmod +x script.py
|
||||
./script.py # uv automatically installs dependencies
|
||||
```
|
||||
|
||||
### Convert Existing Script
|
||||
|
||||
```bash
|
||||
# Add inline metadata to existing script
|
||||
./tools/convert_to_uv.py existing_script.py
|
||||
|
||||
# Validate PEP 723 metadata
|
||||
./tools/validate_script.py script.py
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### What is PEP 723?
|
||||
|
||||
**PEP 723** defines inline script metadata for Python files:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "package>=1.0.0",
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- ✅ Dependencies live with the code
|
||||
- ✅ No separate `requirements.txt`
|
||||
- ✅ Reproducible execution
|
||||
- ✅ Version constraints included
|
||||
- ✅ Self-documenting
|
||||
|
||||
### uv Script Execution Modes
|
||||
|
||||
**Mode 1: Inline Dependencies** (Recommended for utilities)
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# dependencies = ["httpx"]
|
||||
# ///
|
||||
```
|
||||
|
||||
**Mode 2: Project Mode** (For larger scripts)
|
||||
|
||||
```bash
|
||||
uv run script.py # Uses pyproject.toml
|
||||
```
|
||||
|
||||
### Mode 3: No Dependencies
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run
|
||||
# Standard library only
|
||||
```
|
||||
|
||||
## Critical Anti-Patterns: What NOT to Do
|
||||
|
||||
### ❌ NEVER Use [tool.uv.metadata]
|
||||
|
||||
**WRONG** - This will cause errors:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# [tool.uv.metadata] # ❌ THIS DOES NOT WORK
|
||||
# purpose = "testing"
|
||||
# ///
|
||||
```
|
||||
|
||||
**Error**:
|
||||
|
||||
```text
|
||||
error: TOML parse error at line 3, column 7
|
||||
unknown field `metadata`
|
||||
```
|
||||
|
||||
**Why**: `[tool.uv.metadata]` is not part of PEP 723 and is not supported by uv.
|
||||
|
||||
**CORRECT** - Use Python docstrings for metadata:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""
|
||||
Purpose: Testing automation
|
||||
Team: DevOps
|
||||
Author: team@example.com
|
||||
"""
|
||||
```
|
||||
|
||||
**Valid `tool.uv` fields** (if needed):
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# [tool.uv]
|
||||
# exclude-newer = "2025-01-01T00:00:00Z" # For reproducibility
|
||||
# ///
|
||||
```
|
||||
|
||||
## Real-World Examples from This Repository
|
||||
|
||||
### Example 1: Cluster Health Checker
|
||||
|
||||
See [examples/03-production-ready/check_cluster_health_enhanced.py](examples/03-production-ready/check_cluster_health_enhanced.py)
|
||||
|
||||
**Current version** (basic):
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
# Manual dependency installation required
|
||||
```
|
||||
|
||||
**Enhanced with uv** (production-ready):
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script --quiet
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "rich>=13.0.0",
|
||||
# "typer>=0.9.0",
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
Purpose: Cluster health monitoring
|
||||
Team: Infrastructure
|
||||
"""
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
```
|
||||
|
||||
### Example 2: CEPH Health Monitor
|
||||
|
||||
See [examples/03-production-ready/ceph_health.py](examples/03-production-ready/ceph_health.py)
|
||||
|
||||
Pattern: JSON API interaction with structured output
|
||||
|
||||
## Best Practices from This Repository
|
||||
|
||||
### 1. Security Patterns
|
||||
|
||||
See [reference/security-patterns.md](reference/security-patterns.md) for complete security guide including:
|
||||
|
||||
- Secrets management (environment variables, keyring, Infisical)
|
||||
- Input validation
|
||||
- Dependency security
|
||||
- File operations security
|
||||
- Command execution security
|
||||
|
||||
### 2. Version Pinning Strategy
|
||||
|
||||
Following this repository's approach (from `pyproject.toml`):
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0", # Minimum version for compatibility
|
||||
# "rich>=13.0.0", # Known good version
|
||||
# "ansible>=11.1.0", # Match project requirements
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
**Pinning levels:**
|
||||
|
||||
- `>=X.Y.Z` - Minimum version (most flexible)
|
||||
- `~=X.Y.Z` - Compatible release (patch updates only)
|
||||
- `==X.Y.Z` - Exact version (most strict)
|
||||
|
||||
See [reference/dependency-management.md](reference/dependency-management.md).
|
||||
|
||||
### 3. Team Standards
|
||||
|
||||
**File naming:**
|
||||
|
||||
```bash
|
||||
check_cluster_health.py # ✅ Descriptive, snake_case
|
||||
validate_template.py # ✅ Action-oriented
|
||||
cluster.py # ❌ Too generic
|
||||
```
|
||||
|
||||
**Shebang pattern:**
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script --quiet
|
||||
# --quiet suppresses uv's own output
|
||||
```
|
||||
|
||||
**Documentation template:**
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""
|
||||
Check Proxmox cluster health
|
||||
|
||||
Purpose: cluster-monitoring
|
||||
Team: infrastructure
|
||||
Author: devops@spaceships.work
|
||||
|
||||
Usage:
|
||||
python check_cluster_health.py [--node NODE] [--json]
|
||||
|
||||
Examples:
|
||||
python check_cluster_health.py --node foxtrot
|
||||
python check_cluster_health.py --json
|
||||
"""
|
||||
```
|
||||
|
||||
### 4. Error Handling Patterns
|
||||
|
||||
Following Ansible best practices from this repository:
|
||||
|
||||
```python
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
def run_command(cmd: str) -> str:
|
||||
"""Execute command with proper error handling"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd.split(),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return result.stdout
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error: Command failed: {cmd}", file=sys.stderr)
|
||||
print(f" {e.stderr}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Command not found: {cmd.split()[0]}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
See [patterns/error-handling.md](patterns/error-handling.md).
|
||||
|
||||
### 5. Testing Patterns
|
||||
|
||||
**Inline testing** (for simple scripts):
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
def validate_ip(ip: str) -> bool:
|
||||
"""Validate IP address format"""
|
||||
import re
|
||||
pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
|
||||
return bool(re.match(pattern, ip))
|
||||
|
||||
# Inline tests
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
# Run tests if --test flag provided
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--test":
|
||||
assert validate_ip("192.168.1.1") == True
|
||||
assert validate_ip("256.1.1.1") == False
|
||||
print("All tests passed!")
|
||||
sys.exit(0)
|
||||
|
||||
# Normal execution
|
||||
print(validate_ip("192.168.3.5"))
|
||||
```
|
||||
|
||||
See [workflows/testing-strategies.md](workflows/testing-strategies.md).
|
||||
|
||||
## When NOT to Use Single-File Scripts
|
||||
|
||||
See [anti-patterns/when-not-to-use.md](anti-patterns/when-not-to-use.md) for details.
|
||||
|
||||
**Use a proper project instead when:**
|
||||
|
||||
- ❌ Script exceeds 500 lines
|
||||
- ❌ Multiple modules/files needed
|
||||
- ❌ Complex configuration management
|
||||
- ❌ Requires packaging/distribution
|
||||
- ❌ Shared library code across multiple scripts
|
||||
- ❌ Web applications or long-running services
|
||||
|
||||
**Example - Too Complex for Single File:**
|
||||
|
||||
```python
|
||||
# This should be a uv project, not a script:
|
||||
# - 15+ dependencies
|
||||
# - Database models
|
||||
# - API routes
|
||||
# - Background workers
|
||||
# - Configuration management
|
||||
# - Multiple environments
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
See pattern guides for complete examples:
|
||||
|
||||
- [CLI Applications](patterns/cli-applications.md) - Typer, Click, argparse patterns
|
||||
- [API Clients](patterns/api-clients.md) - httpx, requests, authentication
|
||||
- [Data Processing](patterns/data-processing.md) - Polars, pandas, analysis
|
||||
- [System Automation](patterns/system-automation.md) - psutil, subprocess, system admin
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
name: Run Health Checks
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */6 * * *' # Every 6 hours
|
||||
|
||||
jobs:
|
||||
health-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Check cluster health
|
||||
run: |
|
||||
uv run --script tools/check_cluster_health.py --json
|
||||
env:
|
||||
PROXMOX_TOKEN: ${{ secrets.PROXMOX_TOKEN }}
|
||||
```
|
||||
|
||||
### GitLab CI
|
||||
|
||||
```yaml
|
||||
cluster-health:
|
||||
image: ghcr.io/astral-sh/uv:python3.11-bookworm-slim
|
||||
script:
|
||||
- uv run --script tools/check_cluster_health.py
|
||||
only:
|
||||
- schedules
|
||||
```
|
||||
|
||||
See [workflows/ci-cd-integration.md](workflows/ci-cd-integration.md).
|
||||
|
||||
## Tools Available
|
||||
|
||||
### Script Validation
|
||||
|
||||
```bash
|
||||
# Validate PEP 723 metadata
|
||||
./tools/validate_script.py script.py
|
||||
|
||||
# Output:
|
||||
# ✓ Valid PEP 723 metadata
|
||||
# ✓ Python version specified
|
||||
# ✓ Dependencies properly formatted
|
||||
```
|
||||
|
||||
### Script Conversion
|
||||
|
||||
```bash
|
||||
# Convert requirements.txt-based script to uv
|
||||
./tools/convert_to_uv.py old_script.py
|
||||
|
||||
# Creates:
|
||||
# - old_script_uv.py with inline dependencies
|
||||
# - Preserves original script
|
||||
```
|
||||
|
||||
## Progressive Disclosure
|
||||
|
||||
For deeper knowledge:
|
||||
|
||||
### Reference Documentation
|
||||
|
||||
- [PEP 723 Specification](reference/pep-723-spec.md) - Complete inline metadata spec
|
||||
- [Dependency Management](reference/dependency-management.md) - Version pinning strategies
|
||||
- [Security Patterns](reference/security-patterns.md) - Secrets, validation, input sanitization
|
||||
|
||||
### Pattern Guides
|
||||
|
||||
- [CLI Applications](patterns/cli-applications.md) - Typer, Click, argparse patterns
|
||||
- [API Clients](patterns/api-clients.md) - httpx, requests, authentication
|
||||
- [Data Processing](patterns/data-processing.md) - Polars, pandas, analysis
|
||||
- [System Automation](patterns/system-automation.md) - psutil, subprocess, system admin
|
||||
- [Error Handling](patterns/error-handling.md) - Exception handling, logging
|
||||
|
||||
> **Note:** See [Common Patterns](#common-patterns) section above for quick access to these guides.
|
||||
|
||||
### Working Examples
|
||||
|
||||
- [NetBox API Client](examples/04-api-clients/netbox_client.py) - Production-ready API client with Infisical, validation, error handling, and Rich output
|
||||
- [Cluster Health Checker](examples/03-production-ready/check_cluster_health_enhanced.py) - Production-ready monitoring script with Typer, Rich, and JSON output
|
||||
|
||||
### Anti-Patterns
|
||||
|
||||
- [When NOT to Use](anti-patterns/when-not-to-use.md) - Signs you need a proper project
|
||||
- [Common Mistakes](anti-patterns/common-mistakes.md) - Pitfalls and how to avoid them
|
||||
|
||||
### Workflows
|
||||
|
||||
- [Team Adoption](workflows/team-adoption.md) - Rolling out uv scripts across teams
|
||||
- [CI/CD Integration](workflows/ci-cd-integration.md) - GitHub Actions, GitLab CI
|
||||
- [Testing Strategies](workflows/testing-strategies.md) - Inline tests, pytest integration
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **Ansible Best Practices** - Many Ansible modules could be standalone uv scripts
|
||||
- **Proxmox Infrastructure** - Validation tools use this pattern
|
||||
- **NetBox + PowerDNS Integration** - API interaction scripts
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Shebang Options
|
||||
|
||||
```python
|
||||
# Standard script execution
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
# Quiet mode (suppress uv output)
|
||||
#!/usr/bin/env -S uv run --script --quiet
|
||||
|
||||
# With Python version
|
||||
#!/usr/bin/env -S uv run --script --python 3.11
|
||||
```
|
||||
|
||||
### Common Dependencies
|
||||
|
||||
```python
|
||||
# CLI applications
|
||||
"typer>=0.9.0" # Modern CLI framework
|
||||
"click>=8.0.0" # Alternative CLI framework
|
||||
"rich>=13.0.0" # Rich text and formatting
|
||||
|
||||
# API clients
|
||||
"httpx>=0.27.0" # Modern async HTTP client
|
||||
"requests>=2.31.0" # Traditional HTTP client
|
||||
|
||||
# Data processing
|
||||
"polars>=0.20.0" # Fast dataframe library
|
||||
"pandas>=2.0.0" # Traditional dataframe library
|
||||
|
||||
# Infrastructure
|
||||
"ansible>=11.1.0" # Automation (from this repo)
|
||||
"infisical-python>=2.3.3" # Secrets (from this repo)
|
||||
|
||||
# System automation
|
||||
"psutil>=5.9.0" # System monitoring
|
||||
```
|
||||
|
||||
### Metadata Template
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script --quiet
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# # Add dependencies here
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
One-line description
|
||||
|
||||
Purpose: describe-purpose
|
||||
Team: team-name
|
||||
Author: email@example.com
|
||||
|
||||
Usage:
|
||||
python script.py [OPTIONS]
|
||||
|
||||
Examples:
|
||||
python script.py --help
|
||||
"""
|
||||
```
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
1. **Always specify Python version** - `requires-python = ">=3.11"`
|
||||
2. **Pin dependencies appropriately** - Use `>=X.Y.Z` for utilities
|
||||
3. **Add metadata in docstrings** - Put team info, purpose, and author in module docstring
|
||||
4. **Include comprehensive docstrings** - Document purpose, usage, and examples
|
||||
5. **Handle errors gracefully** - Use try/except with clear messages
|
||||
6. **Validate inputs** - Check arguments before processing
|
||||
7. **Use quiet mode** - `--quiet` flag for production scripts
|
||||
8. **Keep it focused** - Single file, single purpose
|
||||
9. **Test inline** - Add `--test` flag for simple validation
|
||||
10. **Secure secrets** - Never hardcode, use env vars or keyring
|
||||
373
skills/python-uv-scripts/anti-patterns/common-mistakes.md
Normal file
373
skills/python-uv-scripts/anti-patterns/common-mistakes.md
Normal file
@@ -0,0 +1,373 @@
|
||||
# Anti-Patterns: Common Mistakes with uv Scripts
|
||||
|
||||
This document lists common mistakes when creating uv scripts and how to avoid them.
|
||||
|
||||
## Critical Mistakes
|
||||
|
||||
### ❌ Using [tool.uv.metadata]
|
||||
|
||||
**WRONG**:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# [tool.uv.metadata] # ❌ THIS DOES NOT WORK
|
||||
# purpose = "testing"
|
||||
# team = "devops"
|
||||
# ///
|
||||
```
|
||||
|
||||
**Error**:
|
||||
|
||||
```text
|
||||
error: TOML parse error at line 4, column 7
|
||||
|
|
||||
4 | [tool.uv.metadata]
|
||||
| ^^
|
||||
unknown field `metadata`
|
||||
```
|
||||
|
||||
**Why**: `[tool.uv.metadata]` is not part of PEP 723 and is not supported by uv.
|
||||
|
||||
**CORRECT**: Use docstrings for metadata:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""
|
||||
Purpose: Testing automation
|
||||
Team: DevOps
|
||||
Author: team@example.com
|
||||
"""
|
||||
```
|
||||
|
||||
**NOTE**: `[tool.uv]` (without `.metadata`) IS valid for fields like `exclude-newer`. See reference/pep-723-spec.md.
|
||||
|
||||
### ❌ Adding Custom TOML Fields
|
||||
|
||||
**WRONG**:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# author = "me" # ❌ INVALID
|
||||
# version = "1.0.0" # ❌ INVALID
|
||||
# description = "test" # ❌ INVALID
|
||||
# dependencies = []
|
||||
# ///
|
||||
```
|
||||
|
||||
**CORRECT**: Only use `requires-python` and `dependencies` at the top level:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""
|
||||
Author: me
|
||||
Version: 1.0.0
|
||||
Description: test
|
||||
"""
|
||||
```
|
||||
|
||||
### ❌ Hardcoded Secrets
|
||||
|
||||
**WRONG**:
|
||||
|
||||
```python
|
||||
API_KEY = "sk-1234567890abcdef" # ❌ NEVER DO THIS
|
||||
PASSWORD = "super_secret_password" # ❌ NEVER DO THIS
|
||||
```
|
||||
|
||||
**CORRECT**: Use environment variables:
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
|
||||
API_KEY = os.getenv("API_KEY")
|
||||
if not API_KEY:
|
||||
print("Error: API_KEY environment variable not set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
**BETTER**: Use Infisical (following repo pattern):
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "infisical-python>=2.3.3",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
from infisical import InfisicalClient
|
||||
|
||||
client = InfisicalClient()
|
||||
api_key = client.get_secret("API_KEY", path="/production")
|
||||
```
|
||||
|
||||
### ❌ Missing Error Handling
|
||||
|
||||
**WRONG**:
|
||||
|
||||
```python
|
||||
import httpx
|
||||
|
||||
response = httpx.get("https://api.example.com") # ❌ No error handling
|
||||
data = response.json()
|
||||
```
|
||||
|
||||
**CORRECT**:
|
||||
|
||||
```python
|
||||
import httpx
|
||||
import sys
|
||||
|
||||
try:
|
||||
response = httpx.get("https://api.example.com", timeout=10.0)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except httpx.HTTPStatusError as e:
|
||||
print(f"HTTP error: {e.response.status_code}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except httpx.RequestError as e:
|
||||
print(f"Request failed: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
## When NOT to Use Single-File Scripts
|
||||
|
||||
### ❌ Complex Applications
|
||||
|
||||
Don't use single-file scripts when:
|
||||
|
||||
- **Script exceeds 500 lines** → Use a proper uv project
|
||||
- **Multiple modules needed** → Use a proper uv project
|
||||
- **Shared code across scripts** → Use a proper uv project with shared library
|
||||
- **Web applications** → Use a proper uv project (Flask/FastAPI/Django)
|
||||
- **Long-running services** → Use a proper uv project
|
||||
- **Complex configuration** → Use a proper uv project with config files
|
||||
|
||||
**Example - Too Complex**:
|
||||
|
||||
```python
|
||||
# This should be a uv project, not a script:
|
||||
# - 15+ dependencies
|
||||
# - Database models
|
||||
# - API routes
|
||||
# - Background workers
|
||||
# - Multiple configuration files
|
||||
# - 1000+ lines of code
|
||||
```
|
||||
|
||||
### ❌ Heavy Dependencies
|
||||
|
||||
**WRONG**: Using heavy ML/data libraries in scripts:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "tensorflow>=2.15.0", # ❌ Too heavy for script
|
||||
# "torch>=2.1.0", # ❌ Too heavy for script
|
||||
# "transformers>=4.35.0", # ❌ Too heavy for script
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
**Why**: These create very large environments. Use a proper project instead.
|
||||
|
||||
**OK**: Lightweight data processing:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "polars>=0.20.0", # ✓ Reasonable for scripts
|
||||
# "httpx>=0.27.0", # ✓ Reasonable for scripts
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
## Common Syntax Mistakes
|
||||
|
||||
### ❌ Missing # on TOML Lines
|
||||
|
||||
**WRONG**:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
requires-python = ">=3.11" # ❌ Missing # at start
|
||||
dependencies = [] # ❌ Missing # at start
|
||||
# ///
|
||||
```
|
||||
|
||||
**CORRECT**:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11" # ✓ Each line starts with #
|
||||
# dependencies = [] # ✓ Each line starts with #
|
||||
# ///
|
||||
```
|
||||
|
||||
### ❌ Wrong Marker Format
|
||||
|
||||
**WRONG**:
|
||||
|
||||
```python
|
||||
# /// scripts # ❌ Wrong: "scripts" not "script"
|
||||
# requires-python = ">=3.11"
|
||||
# ///
|
||||
|
||||
# // script # ❌ Wrong: "//" not "///"
|
||||
# requires-python = ">=3.11"
|
||||
# //
|
||||
|
||||
# /// script # ❌ Wrong: No closing marker
|
||||
# requires-python = ">=3.11"
|
||||
```
|
||||
|
||||
**CORRECT**:
|
||||
|
||||
```python
|
||||
# /// script # ✓ Exactly "/// script"
|
||||
# requires-python = ">=3.11"
|
||||
# /// # ✓ Exactly "///"
|
||||
```
|
||||
|
||||
## Dependency Management Mistakes
|
||||
|
||||
### ❌ No Version Constraints
|
||||
|
||||
**WRONG**:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "httpx", # ❌ No version specified
|
||||
# "requests", # ❌ Could break unexpectedly
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
**CORRECT**:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0", # ✓ Minimum version specified
|
||||
# "requests>=2.31.0", # ✓ Prevents breaking changes
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
### ❌ Overly Strict Pinning
|
||||
|
||||
**WRONG** (for utility scripts):
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "httpx==0.27.0", # ❌ Too strict, prevents updates
|
||||
# "rich==13.7.0", # ❌ Blocks security fixes
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
**CORRECT**:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0", # ✓ Allows updates
|
||||
# "rich>=13.0.0", # ✓ Allows security fixes
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
**NOTE**: Exact pinning (`==`) is appropriate for deployment scripts where reproducibility is critical.
|
||||
|
||||
## Additional Pitfalls
|
||||
|
||||
### ❌ Missing Shebang
|
||||
|
||||
**WRONG**:
|
||||
|
||||
```python
|
||||
# No shebang - script won't be directly executable
|
||||
# /// script
|
||||
# dependencies = ["requests"]
|
||||
# ///
|
||||
```
|
||||
|
||||
**CORRECT**:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# dependencies = ["requests"]
|
||||
# ///
|
||||
```
|
||||
|
||||
### ❌ Broad Exception Handling
|
||||
|
||||
**WRONG**:
|
||||
|
||||
```python
|
||||
try:
|
||||
do_something()
|
||||
except Exception: # Too broad!
|
||||
pass
|
||||
```
|
||||
|
||||
**CORRECT**:
|
||||
|
||||
```python
|
||||
try:
|
||||
do_something()
|
||||
except (FileNotFoundError, PermissionError) as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
### ❌ Platform-Specific Code Without Guards
|
||||
|
||||
**WRONG**:
|
||||
|
||||
```python
|
||||
import pwd # Unix-only, crashes on Windows
|
||||
user = pwd.getpwuid(os.getuid())
|
||||
```
|
||||
|
||||
**CORRECT**:
|
||||
|
||||
```python
|
||||
import sys
|
||||
if sys.platform != "win32":
|
||||
import pwd
|
||||
user = pwd.getpwuid(os.getuid())
|
||||
else:
|
||||
user = os.environ.get("USERNAME")
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
**Never use**:
|
||||
|
||||
- `[tool.uv.metadata]` (invalid field)
|
||||
- Custom fields in PEP 723 metadata (only `requires-python` and `dependencies`)
|
||||
- Hardcoded secrets
|
||||
- Scripts for complex applications (>500 lines, web apps, services)
|
||||
|
||||
**Always use**:
|
||||
|
||||
- Valid PEP 723 format only
|
||||
- `[tool.uv]` (without `.metadata`) for valid fields like `exclude-newer`
|
||||
- Environment variables or Infisical for secrets
|
||||
- Error handling for external calls
|
||||
- Version constraints on dependencies
|
||||
- Proper projects when scripts become too complex
|
||||
363
skills/python-uv-scripts/anti-patterns/when-not-to-use.md
Normal file
363
skills/python-uv-scripts/anti-patterns/when-not-to-use.md
Normal file
@@ -0,0 +1,363 @@
|
||||
# When NOT to Use Single-File Scripts
|
||||
|
||||
This document helps determine when to use a single-file uv script vs. when to use a proper uv project.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
```text
|
||||
Is this a Python program?
|
||||
├─ No → Use appropriate language/tool
|
||||
└─ Yes
|
||||
├─ Is it a one-time task or simple automation?
|
||||
│ ├─ Yes → Consider single-file script
|
||||
│ └─ No → Use proper uv project
|
||||
└─ Does it meet ANY of these criteria?
|
||||
├─ >500 lines of code → Use proper uv project
|
||||
├─ Multiple Python files needed → Use proper uv project
|
||||
├─ Web application or API → Use proper uv project
|
||||
├─ Long-running service → Use proper uv project
|
||||
├─ Complex configuration → Use proper uv project
|
||||
├─ Shared library code → Use proper uv project
|
||||
├─ Heavy ML/data dependencies → Use proper uv project
|
||||
└─ None of the above → Single-file script is appropriate
|
||||
```
|
||||
|
||||
## Use Proper uv Project When
|
||||
|
||||
### 1. Code Complexity
|
||||
|
||||
**Use project if**:
|
||||
|
||||
- Script exceeds 500 lines
|
||||
- Logic is spread across multiple functions/classes
|
||||
- Code would benefit from splitting into modules
|
||||
- Complex data models or class hierarchies
|
||||
|
||||
**Example - Too Complex for Script**:
|
||||
|
||||
```python
|
||||
# This needs a proper project structure:
|
||||
# - 800 lines of code
|
||||
# - Multiple classes (User, Database, API, Config)
|
||||
# - Would be clearer as separate modules
|
||||
# - Needs tests
|
||||
```
|
||||
|
||||
### 2. Multiple Files Needed
|
||||
|
||||
**Use project if**:
|
||||
|
||||
- Shared utilities across multiple scripts
|
||||
- Common data models used by different tools
|
||||
- Reusable library code
|
||||
- Multiple entry points
|
||||
|
||||
**Example**:
|
||||
|
||||
```text
|
||||
# This needs a project:
|
||||
my-tool/
|
||||
├── src/
|
||||
│ ├── __init__.py
|
||||
│ ├── database.py # Shared by multiple tools
|
||||
│ ├── models.py # Common data structures
|
||||
│ └── utils.py # Utility functions
|
||||
├── scripts/
|
||||
│ ├── import_data.py # Uses shared code
|
||||
│ └── export_data.py # Uses shared code
|
||||
└── pyproject.toml
|
||||
```
|
||||
|
||||
### 3. Web Applications
|
||||
|
||||
**Use project for**:
|
||||
|
||||
- Flask/FastAPI/Django applications
|
||||
- REST APIs
|
||||
- Web services
|
||||
- Applications with routes/controllers
|
||||
|
||||
**Example - Needs Project**:
|
||||
|
||||
```python
|
||||
# Don't use single-file script for web apps:
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# 50+ routes
|
||||
# Database models
|
||||
# Authentication
|
||||
# Background tasks
|
||||
# Configuration management
|
||||
|
||||
# This should be a proper project structure
|
||||
```
|
||||
|
||||
### 4. Long-Running Services
|
||||
|
||||
**Use project for**:
|
||||
|
||||
- Daemons
|
||||
- Background workers
|
||||
- Queue consumers
|
||||
- Services that run continuously
|
||||
|
||||
**Example**:
|
||||
|
||||
```python
|
||||
# Don't use script for services:
|
||||
# - Runs 24/7
|
||||
# - Monitors message queue
|
||||
# - Complex retry logic
|
||||
# - Logging configuration
|
||||
# - Health checks
|
||||
# - Graceful shutdown
|
||||
|
||||
# Needs proper project structure with:
|
||||
# - Proper logging setup
|
||||
# - Configuration management
|
||||
# - Process management (systemd/supervisor)
|
||||
```
|
||||
|
||||
### 5. Complex Configuration
|
||||
|
||||
**Use project if**:
|
||||
|
||||
- Multiple environment configs (dev/staging/prod)
|
||||
- YAML/JSON configuration files
|
||||
- Feature flags
|
||||
- Database connection pools
|
||||
|
||||
**Example**:
|
||||
|
||||
```text
|
||||
# This needs a project:
|
||||
config/
|
||||
├── dev.yaml
|
||||
├── staging.yaml
|
||||
└── production.yaml
|
||||
|
||||
# Single-file scripts should use simple env vars instead
|
||||
```
|
||||
|
||||
### 6. Heavy Dependencies
|
||||
|
||||
**Use project for**:
|
||||
|
||||
- Machine learning frameworks (TensorFlow, PyTorch)
|
||||
- Large data processing (PySpark)
|
||||
- Complex scientific computing
|
||||
- GUI frameworks
|
||||
|
||||
**Example - Too Heavy**:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "tensorflow>=2.15.0", # ❌ ~500MB download
|
||||
# "torch>=2.1.0", # ❌ ~800MB download
|
||||
# "transformers>=4.35.0", # ❌ Complex dependency tree
|
||||
# ]
|
||||
# ///
|
||||
|
||||
# Use proper project with managed virtual environment
|
||||
```
|
||||
|
||||
### 7. Testing Requirements
|
||||
|
||||
**Use project if**:
|
||||
|
||||
- Comprehensive test suite needed
|
||||
- Multiple test files
|
||||
- Fixtures and mocking
|
||||
- CI/CD integration
|
||||
|
||||
**Example**:
|
||||
|
||||
```text
|
||||
# This needs a project:
|
||||
tests/
|
||||
├── unit/
|
||||
│ ├── test_models.py
|
||||
│ ├── test_utils.py
|
||||
│ └── test_api.py
|
||||
├── integration/
|
||||
│ └── test_database.py
|
||||
└── conftest.py
|
||||
|
||||
# Can't reasonably organize this with single-file script
|
||||
```
|
||||
|
||||
### 8. Team Collaboration
|
||||
|
||||
**Use project if**:
|
||||
|
||||
- Multiple developers working on code
|
||||
- Code review processes
|
||||
- Versioning and releases
|
||||
- Documentation requirements
|
||||
|
||||
## Single-File Scripts ARE Appropriate For
|
||||
|
||||
### ✅ Good Use Cases
|
||||
|
||||
**One-time tasks**:
|
||||
|
||||
```python
|
||||
# Convert CSV format
|
||||
# Migrate data between systems
|
||||
# Clean up old files
|
||||
# Generate reports
|
||||
```
|
||||
|
||||
**Simple automation**:
|
||||
|
||||
```python
|
||||
# Check server health
|
||||
# Send notifications
|
||||
# Backup files
|
||||
# Parse logs
|
||||
```
|
||||
|
||||
**CLI utilities**:
|
||||
|
||||
```python
|
||||
# Format files
|
||||
# Validate data
|
||||
# Query APIs
|
||||
# Process input
|
||||
```
|
||||
|
||||
**Prototyping**:
|
||||
|
||||
```python
|
||||
# Test API endpoints
|
||||
# Experiment with libraries
|
||||
# Quick data analysis
|
||||
# Proof of concept
|
||||
```
|
||||
|
||||
### ✅ Characteristics of Good Single-File Scripts
|
||||
|
||||
- **<500 lines of code**
|
||||
- **Self-contained logic**
|
||||
- **Simple, clear purpose**
|
||||
- **Minimal dependencies (1-5 packages)**
|
||||
- **Standalone execution**
|
||||
- **Quick to understand**
|
||||
|
||||
## Examples of Good Single-File Scripts
|
||||
|
||||
### Example 1: Health Check
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "psutil>=5.9.0",
|
||||
# "rich>=13.0.0",
|
||||
# ]
|
||||
# ///
|
||||
"""Check system health and display metrics."""
|
||||
|
||||
import psutil
|
||||
from rich import print
|
||||
|
||||
def main():
|
||||
cpu = psutil.cpu_percent()
|
||||
mem = psutil.virtual_memory().percent
|
||||
disk = psutil.disk_usage('/').percent
|
||||
|
||||
print(f"CPU: {cpu}% | Memory: {mem}% | Disk: {disk}%")
|
||||
|
||||
if cpu > 80 or mem > 80 or disk > 80:
|
||||
print("[red]⚠ High resource usage![/red]")
|
||||
exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
### Example 2: API Query
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0",
|
||||
# ]
|
||||
# ///
|
||||
"""Query API and display results."""
|
||||
|
||||
import httpx
|
||||
import sys
|
||||
|
||||
def main():
|
||||
response = httpx.get("https://api.github.com/users/octocat")
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
print(f"Name: {data['name']}")
|
||||
print(f"Public repos: {data['public_repos']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
## Migration Path
|
||||
|
||||
**When a script outgrows single-file format**:
|
||||
|
||||
1. Create proper uv project:
|
||||
|
||||
```bash
|
||||
uv init my-tool
|
||||
cd my-tool
|
||||
```
|
||||
|
||||
2. Move script logic to `src/`:
|
||||
|
||||
```bash
|
||||
mv script.py src/my_tool/main.py
|
||||
```
|
||||
|
||||
3. Add dependencies to `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[project]
|
||||
dependencies = [
|
||||
"httpx>=0.27.0",
|
||||
"rich>=13.0.0",
|
||||
]
|
||||
```
|
||||
|
||||
4. Create entry point in `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[project.scripts]
|
||||
my-tool = "my_tool.main:main"
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
**Use single-file scripts for**:
|
||||
|
||||
- Simple automation (<500 lines)
|
||||
- One-off tasks
|
||||
- CLI utilities
|
||||
- Prototypes
|
||||
- Standalone tools
|
||||
|
||||
**Use proper uv projects for**:
|
||||
|
||||
- Complex applications (>500 lines)
|
||||
- Multiple files/modules
|
||||
- Web applications
|
||||
- Long-running services
|
||||
- Heavy dependencies
|
||||
- Team collaboration
|
||||
- Comprehensive testing
|
||||
|
||||
**When in doubt**: Start with a script. If it grows too complex, migrate to a project.
|
||||
72
skills/python-uv-scripts/assets/templates/api-client.py
Executable file
72
skills/python-uv-scripts/assets/templates/api-client.py
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0",
|
||||
# "rich>=13.0.0",
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
API client template using httpx.
|
||||
|
||||
Demonstrates HTTP requests, error handling, and structured output.
|
||||
Includes environment variable usage for API credentials.
|
||||
|
||||
Usage:
|
||||
export API_URL="https://api.example.com"
|
||||
export API_TOKEN="your-token"
|
||||
python api-client.py
|
||||
"""
|
||||
|
||||
import httpx
|
||||
import os
|
||||
import sys
|
||||
from rich import print
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def get_env_var(name: str) -> str:
|
||||
"""Get required environment variable or exit with error."""
|
||||
value = os.getenv(name)
|
||||
if not value:
|
||||
console.print(f"[red]Error: {name} environment variable not set[/red]")
|
||||
sys.exit(1)
|
||||
return value
|
||||
|
||||
|
||||
def fetch_data(api_url: str, token: str):
|
||||
"""Fetch data from API with error handling."""
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.get(api_url, headers=headers, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
console.print(f"[red]HTTP error {e.response.status_code}[/red]")
|
||||
console.print(f"Response: {e.response.text}")
|
||||
sys.exit(1)
|
||||
except httpx.RequestError as e:
|
||||
console.print(f"[red]Request failed: {e}[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
api_url = get_env_var("API_URL")
|
||||
api_token = get_env_var("API_TOKEN")
|
||||
|
||||
console.print(f"[cyan]Fetching data from {api_url}...[/cyan]")
|
||||
|
||||
data = fetch_data(api_url, api_token)
|
||||
|
||||
# Process and display data
|
||||
print(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
24
skills/python-uv-scripts/assets/templates/basic-script.py
Executable file
24
skills/python-uv-scripts/assets/templates/basic-script.py
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""
|
||||
Basic Python script template using uv.
|
||||
|
||||
This is a minimal working example showing the correct PEP 723 format.
|
||||
No external dependencies - uses only Python standard library.
|
||||
|
||||
Usage:
|
||||
python basic-script.py
|
||||
"""
|
||||
|
||||
def main():
|
||||
"""Main entry point for the script."""
|
||||
print("Hello from uv!")
|
||||
|
||||
# Your code here
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
62
skills/python-uv-scripts/assets/templates/cli-app.py
Executable file
62
skills/python-uv-scripts/assets/templates/cli-app.py
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "typer>=0.9.0",
|
||||
# "rich>=13.0.0",
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
CLI application template using Typer and Rich.
|
||||
|
||||
Demonstrates command-line argument parsing, subcommands, and formatted output.
|
||||
|
||||
Usage:
|
||||
python cli-app.py --help
|
||||
python cli-app.py greet "World"
|
||||
python cli-app.py process input.txt --output output.txt
|
||||
"""
|
||||
|
||||
import typer
|
||||
from rich import print
|
||||
from rich.console import Console
|
||||
from pathlib import Path
|
||||
|
||||
app = typer.Typer()
|
||||
console = Console()
|
||||
|
||||
|
||||
@app.command()
|
||||
def greet(name: str):
|
||||
"""Greet someone by name."""
|
||||
print(f"[green]Hello, {name}![/green]")
|
||||
|
||||
|
||||
@app.command()
|
||||
def process(
|
||||
input_file: Path = typer.Argument(..., help="Input file to process"),
|
||||
output: Path = typer.Option(None, "--output", "-o", help="Output file path"),
|
||||
):
|
||||
"""Process a file and optionally write results."""
|
||||
if not input_file.exists():
|
||||
console.print(f"[red]Error: {input_file} not found[/red]")
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
# Process file
|
||||
with open(input_file) as f:
|
||||
content = f.read()
|
||||
|
||||
console.print(f"[cyan]Processing {input_file}...[/cyan]")
|
||||
|
||||
# Your processing logic here
|
||||
result = content.upper() # Example transformation
|
||||
|
||||
if output:
|
||||
output.write_text(result)
|
||||
console.print(f"[green]✓ Written to {output}[/green]")
|
||||
else:
|
||||
print(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
84
skills/python-uv-scripts/assets/templates/data-processor.py
Executable file
84
skills/python-uv-scripts/assets/templates/data-processor.py
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "polars>=0.20.0",
|
||||
# "rich>=13.0.0",
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
Data processing template using Polars.
|
||||
|
||||
Demonstrates reading, transforming, and analyzing data with
|
||||
modern data processing libraries.
|
||||
|
||||
Usage:
|
||||
python data-processor.py input.csv
|
||||
python data-processor.py data/*.csv --output results/
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import polars as pl
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def process_csv(file_path: Path) -> pl.DataFrame:
|
||||
"""Read and process a CSV file."""
|
||||
try:
|
||||
df = pl.read_csv(file_path)
|
||||
console.print(f"[cyan]Loaded {len(df)} rows from {file_path.name}[/cyan]")
|
||||
|
||||
# Example transformations
|
||||
# df = df.filter(pl.col("status") == "active")
|
||||
# df = df.with_columns(pl.col("amount").cast(pl.Float64))
|
||||
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error processing {file_path}: {e}[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def display_summary(df: pl.DataFrame):
|
||||
"""Display data summary using Rich tables."""
|
||||
table = Table(title="Data Summary")
|
||||
table.add_column("Metric", style="cyan")
|
||||
table.add_column("Value", style="green")
|
||||
|
||||
table.add_row("Total Rows", str(len(df)))
|
||||
table.add_row("Columns", str(len(df.columns)))
|
||||
|
||||
# Example statistics
|
||||
# if "amount" in df.columns:
|
||||
# table.add_row("Total Amount", f"${df['amount'].sum():,.2f}")
|
||||
|
||||
console.print(table)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
if len(sys.argv) < 2:
|
||||
console.print("[red]Usage: python data-processor.py <input.csv>[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
input_path = Path(sys.argv[1])
|
||||
|
||||
if not input_path.exists():
|
||||
console.print(f"[red]Error: {input_path} not found[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
df = process_csv(input_path)
|
||||
display_summary(df)
|
||||
|
||||
# Optional: Save results
|
||||
# output_path = Path("output.csv")
|
||||
# df.write_csv(output_path)
|
||||
# console.print(f"[green]✓ Saved to {output_path}[/green]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,257 @@
|
||||
#!/usr/bin/env -S uv run --script --quiet
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "rich>=13.0.0",
|
||||
# "typer>=0.9.0",
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
Proxmox Cluster Health Checker - Production Ready Example
|
||||
|
||||
Purpose: cluster-monitoring
|
||||
Team: infrastructure
|
||||
Author: devops@spaceships.work
|
||||
|
||||
This is an enhanced version of the basic cluster health checker,
|
||||
demonstrating all best practices for production uv scripts.
|
||||
|
||||
Features:
|
||||
- Rich CLI with Typer
|
||||
- Structured output with Rich
|
||||
- Proper error handling
|
||||
- Input validation
|
||||
- Security best practices
|
||||
- Comprehensive logging
|
||||
- Exit codes for automation
|
||||
|
||||
Usage:
|
||||
# Interactive mode with rich output
|
||||
./check_cluster_health_enhanced.py --node foxtrot
|
||||
|
||||
# JSON output for automation
|
||||
./check_cluster_health_enhanced.py --node foxtrot --json
|
||||
|
||||
# Quiet mode (only errors)
|
||||
./check_cluster_health_enhanced.py --node foxtrot --quiet
|
||||
|
||||
Examples:
|
||||
# Check specific cluster node
|
||||
./check_cluster_health_enhanced.py --node golf
|
||||
|
||||
# CI/CD integration
|
||||
./check_cluster_health_enhanced.py --json | jq '.is_healthy'
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass, asdict
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich import print as rprint
|
||||
|
||||
app = typer.Typer(help="Check Proxmox cluster health")
|
||||
console = Console()
|
||||
|
||||
|
||||
@dataclass
|
||||
class NodeStatus:
|
||||
"""Cluster node status"""
|
||||
name: str
|
||||
online: bool
|
||||
node_id: int
|
||||
ip: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClusterHealth:
|
||||
"""Overall cluster health status"""
|
||||
cluster_name: str
|
||||
quorate: bool
|
||||
node_count: int
|
||||
expected_votes: int
|
||||
total_votes: int
|
||||
nodes: list[NodeStatus]
|
||||
warnings: list[str]
|
||||
errors: list[str]
|
||||
|
||||
@property
|
||||
def is_healthy(self) -> bool:
|
||||
"""Check if cluster is healthy"""
|
||||
return self.quorate and len(self.errors) == 0
|
||||
|
||||
|
||||
def validate_hostname(hostname: str) -> bool:
|
||||
"""Validate hostname format"""
|
||||
pattern = r'^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$'
|
||||
return bool(re.match(pattern, hostname)) and len(hostname) <= 253
|
||||
|
||||
|
||||
def run_ssh_command(node: str, command: str) -> str:
|
||||
"""Execute command on remote node via SSH"""
|
||||
if not validate_hostname(node):
|
||||
console.print(f"[red]Error: Invalid hostname: {node}[/red]", file=sys.stderr)
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ssh", f"root@{node}", command],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
timeout=30
|
||||
)
|
||||
return result.stdout
|
||||
except subprocess.CalledProcessError as e:
|
||||
console.print(f"[red]Command failed: {command}[/red]", file=sys.stderr)
|
||||
console.print(f" {e.stderr}", file=sys.stderr)
|
||||
raise typer.Exit(code=1)
|
||||
except subprocess.TimeoutExpired:
|
||||
console.print(f"[red]Command timed out: {command}[/red]", file=sys.stderr)
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
|
||||
def check_cluster_status(node: str) -> ClusterHealth:
|
||||
"""Check cluster status and return health object"""
|
||||
health = ClusterHealth(
|
||||
cluster_name="",
|
||||
quorate=False,
|
||||
node_count=0,
|
||||
expected_votes=0,
|
||||
total_votes=0,
|
||||
nodes=[],
|
||||
warnings=[],
|
||||
errors=[]
|
||||
)
|
||||
|
||||
# Get cluster status
|
||||
output = run_ssh_command(node, "pvecm status")
|
||||
|
||||
# Parse cluster name
|
||||
if match := re.search(r'Cluster name:\s+(\S+)', output):
|
||||
health.cluster_name = match.group(1)
|
||||
|
||||
# Parse quorum
|
||||
if match := re.search(r'Quorate:\s+(\w+)', output):
|
||||
health.quorate = match.group(1).lower() == 'yes'
|
||||
|
||||
if not health.quorate:
|
||||
health.errors.append("Cluster does not have quorum!")
|
||||
|
||||
# Parse votes
|
||||
if match := re.search(r'Nodes:\s+(\d+)', output):
|
||||
health.node_count = int(match.group(1))
|
||||
|
||||
if match := re.search(r'Expected votes:\s+(\d+)', output):
|
||||
health.expected_votes = int(match.group(1))
|
||||
|
||||
if match := re.search(r'Total votes:\s+(\d+)', output):
|
||||
health.total_votes = int(match.group(1))
|
||||
|
||||
# Get node list
|
||||
output = run_ssh_command(node, "pvecm nodes")
|
||||
for line in output.strip().split('\n')[1:]: # Skip header
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) >= 3:
|
||||
try:
|
||||
health.nodes.append(NodeStatus(
|
||||
name=parts[2] if len(parts) >= 3 else "unknown",
|
||||
online=True,
|
||||
node_id=int(parts[0]),
|
||||
ip=parts[3] if len(parts) >= 4 else "unknown"
|
||||
))
|
||||
except (ValueError, IndexError):
|
||||
health.warnings.append(f"Failed to parse node line: {line}")
|
||||
|
||||
return health
|
||||
|
||||
|
||||
@app.command()
|
||||
def main(
|
||||
node: str = typer.Option("foxtrot", help="Cluster node to check"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
||||
quiet: bool = typer.Option(False, help="Only show errors"),
|
||||
):
|
||||
"""
|
||||
Check Proxmox cluster health
|
||||
|
||||
Connects to a cluster node via SSH and checks:
|
||||
- Quorum status
|
||||
- Node membership
|
||||
- Vote distribution
|
||||
"""
|
||||
try:
|
||||
health = check_cluster_status(node)
|
||||
|
||||
if json_output:
|
||||
# JSON output for automation
|
||||
print(json.dumps(asdict(health), indent=2))
|
||||
sys.exit(0 if health.is_healthy else 1)
|
||||
|
||||
if not quiet:
|
||||
# Rich table output
|
||||
console.print(f"\n[bold]Cluster Health: {health.cluster_name}[/bold]")
|
||||
console.print("=" * 60)
|
||||
|
||||
# Status
|
||||
quorum_icon = "✓" if health.quorate else "✗"
|
||||
quorum_color = "green" if health.quorate else "red"
|
||||
console.print(f"Quorum: [{quorum_color}]{quorum_icon} {'YES' if health.quorate else 'NO'}[/{quorum_color}]")
|
||||
console.print(f"Nodes: {health.node_count} ({health.total_votes}/{health.expected_votes} votes)")
|
||||
|
||||
# Nodes table
|
||||
table = Table(title="\nCluster Nodes")
|
||||
table.add_column("Node", style="cyan")
|
||||
table.add_column("Node ID", style="magenta")
|
||||
table.add_column("IP Address", style="yellow")
|
||||
table.add_column("Status", style="green")
|
||||
|
||||
for node_status in health.nodes:
|
||||
status_icon = "✓" if node_status.online else "✗"
|
||||
table.add_row(
|
||||
node_status.name,
|
||||
str(node_status.node_id),
|
||||
node_status.ip,
|
||||
status_icon
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
# Warnings
|
||||
if health.warnings:
|
||||
console.print("\n[yellow]Warnings:[/yellow]")
|
||||
for warning in health.warnings:
|
||||
console.print(f" ⚠ {warning}")
|
||||
|
||||
# Errors
|
||||
if health.errors:
|
||||
console.print("\n[red]Errors:[/red]")
|
||||
for error in health.errors:
|
||||
console.print(f" ✗ {error}")
|
||||
|
||||
console.print("\n" + "=" * 60)
|
||||
|
||||
# Final status
|
||||
if health.is_healthy:
|
||||
if not quiet:
|
||||
console.print("[green]Status: ✓ HEALTHY[/green]\n")
|
||||
sys.exit(0)
|
||||
else:
|
||||
console.print("[red]Status: ✗ UNHEALTHY[/red]\n", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]Unexpected error: {e}[/red]", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
358
skills/python-uv-scripts/examples/04-api-clients/netbox_client.py
Executable file
358
skills/python-uv-scripts/examples/04-api-clients/netbox_client.py
Executable file
@@ -0,0 +1,358 @@
|
||||
#!/usr/bin/env -S uv run --script --quiet
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "pynetbox>=7.0.0",
|
||||
# "infisical-python>=2.3.3",
|
||||
# "rich>=13.0.0",
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
NetBox API Client Example
|
||||
|
||||
Purpose: api-client-example
|
||||
Team: infrastructure
|
||||
Author: devops@spaceships.work
|
||||
|
||||
Demonstrates best practices for API client scripts using uv:
|
||||
- PEP 723 inline dependencies
|
||||
- Infisical for secrets management
|
||||
- Error handling and validation
|
||||
- Rich output formatting
|
||||
- Type hints and documentation
|
||||
|
||||
This is a production-ready example showing all patterns together.
|
||||
|
||||
Usage:
|
||||
# List Matrix cluster VMs
|
||||
./netbox_client.py
|
||||
|
||||
# Query specific VM
|
||||
./netbox_client.py --vm docker-01
|
||||
|
||||
Example output:
|
||||
Matrix Cluster Virtual Machines
|
||||
┌────┬───────────┬──────┬─────────┬──────────────┬──────────────────────────────────┐
|
||||
│ ID │ Name │vCPUs │Memory MB│ Status │ Primary IP │
|
||||
├────┼───────────┼──────┼─────────┼──────────────┼──────────────────────────────────┤
|
||||
│ 1 │ docker-01 │ 4 │ 8192 │ active │ 192.168.3.10/24 │
|
||||
│ 2 │ k8s-01 │ 8 │ 16384 │ active │ 192.168.3.20/24 │
|
||||
└────┴───────────┴──────┴─────────┴──────────────┴──────────────────────────────────┘
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pynetbox
|
||||
from infisical import InfisicalClient
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.panel import Panel
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class NetBoxConfig:
|
||||
"""NetBox connection configuration."""
|
||||
url: str = "https://netbox.spaceships.work"
|
||||
project_id: str = "7b832220-24c0-45bc-a5f1-ce9794a31259"
|
||||
environment: str = "prod"
|
||||
path: str = "/matrix"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Authentication
|
||||
# ============================================================================
|
||||
|
||||
def get_netbox_client(config: NetBoxConfig) -> Optional[pynetbox.api]:
|
||||
"""
|
||||
Get authenticated NetBox API client.
|
||||
|
||||
Uses Infisical to securely retrieve API token (Virgo-Core security pattern).
|
||||
|
||||
Args:
|
||||
config: NetBox configuration
|
||||
|
||||
Returns:
|
||||
Authenticated pynetbox client or None on error
|
||||
|
||||
Raises:
|
||||
Exception: If token cannot be retrieved or connection fails
|
||||
"""
|
||||
try:
|
||||
# Get token from Infisical (never hardcoded)
|
||||
client = InfisicalClient()
|
||||
token = client.get_secret(
|
||||
secret_name="NETBOX_API_TOKEN",
|
||||
project_id=config.project_id,
|
||||
environment=config.environment,
|
||||
path=config.path
|
||||
).secret_value
|
||||
|
||||
if not token:
|
||||
raise ValueError("NETBOX_API_TOKEN is empty")
|
||||
|
||||
# Connect to NetBox
|
||||
nb = pynetbox.api(config.url, token=token)
|
||||
|
||||
# Test connection
|
||||
_ = nb.status()
|
||||
|
||||
return nb
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]Failed to connect to NetBox: {e}[/red]")
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Data Validation
|
||||
# ============================================================================
|
||||
|
||||
def validate_vm_name(name: str) -> bool:
|
||||
"""
|
||||
Validate VM name format.
|
||||
|
||||
Pattern: lowercase letters, numbers, hyphens only
|
||||
Example: docker-01, k8s-01-master
|
||||
|
||||
Args:
|
||||
name: VM name to validate
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
pattern = r'^[a-z0-9-]+$'
|
||||
return bool(re.match(pattern, name))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# NetBox Queries
|
||||
# ============================================================================
|
||||
|
||||
def get_cluster_vms(nb: pynetbox.api, cluster_name: str = "Matrix") -> list:
|
||||
"""
|
||||
Get all VMs in a cluster.
|
||||
|
||||
Args:
|
||||
nb: NetBox API client
|
||||
cluster_name: Cluster name to query
|
||||
|
||||
Returns:
|
||||
List of VM objects
|
||||
"""
|
||||
try:
|
||||
vms = nb.virtualization.virtual_machines.filter(cluster=cluster_name.lower())
|
||||
return list(vms)
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error querying VMs: {e}[/red]")
|
||||
return []
|
||||
|
||||
|
||||
def get_vm_details(nb: pynetbox.api, vm_name: str) -> Optional[dict]:
|
||||
"""
|
||||
Get detailed information about a VM.
|
||||
|
||||
Args:
|
||||
nb: NetBox API client
|
||||
vm_name: VM name
|
||||
|
||||
Returns:
|
||||
VM details dict or None if not found
|
||||
"""
|
||||
try:
|
||||
# Validate name
|
||||
if not validate_vm_name(vm_name):
|
||||
console.print(f"[red]Invalid VM name format: {vm_name}[/red]")
|
||||
return None
|
||||
|
||||
vm = nb.virtualization.virtual_machines.get(name=vm_name)
|
||||
|
||||
if not vm:
|
||||
console.print(f"[yellow]VM '{vm_name}' not found[/yellow]")
|
||||
return None
|
||||
|
||||
# Get interfaces
|
||||
interfaces = nb.virtualization.interfaces.filter(virtual_machine_id=vm.id)
|
||||
|
||||
# Get IPs
|
||||
ips = []
|
||||
for iface in interfaces:
|
||||
iface_ips = nb.ipam.ip_addresses.filter(vminterface_id=iface.id)
|
||||
ips.extend(iface_ips)
|
||||
|
||||
return {
|
||||
"vm": vm,
|
||||
"interfaces": list(interfaces),
|
||||
"ips": ips
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error getting VM details: {e}[/red]")
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Output Formatting
|
||||
# ============================================================================
|
||||
|
||||
def display_vms_table(vms: list) -> None:
|
||||
"""
|
||||
Display VMs in a formatted table.
|
||||
|
||||
Args:
|
||||
vms: List of VM objects
|
||||
"""
|
||||
if not vms:
|
||||
console.print("[yellow]No VMs found[/yellow]")
|
||||
return
|
||||
|
||||
table = Table(title="Matrix Cluster Virtual Machines")
|
||||
table.add_column("ID", style="cyan", justify="right")
|
||||
table.add_column("Name", style="green")
|
||||
table.add_column("vCPUs", justify="right")
|
||||
table.add_column("Memory (MB)", justify="right")
|
||||
table.add_column("Status")
|
||||
table.add_column("Primary IP", style="yellow")
|
||||
|
||||
for vm in vms:
|
||||
table.add_row(
|
||||
str(vm.id),
|
||||
vm.name,
|
||||
str(vm.vcpus) if vm.vcpus else "N/A",
|
||||
str(vm.memory) if vm.memory else "N/A",
|
||||
vm.status.value if hasattr(vm.status, 'value') else str(vm.status),
|
||||
str(vm.primary_ip4.address) if vm.primary_ip4 else "N/A"
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
console.print(f"\n[green]Total VMs: {len(vms)}[/green]")
|
||||
|
||||
|
||||
def display_vm_details(details: dict) -> None:
|
||||
"""
|
||||
Display detailed VM information.
|
||||
|
||||
Args:
|
||||
details: VM details dict from get_vm_details()
|
||||
"""
|
||||
vm = details["vm"]
|
||||
interfaces = details["interfaces"]
|
||||
ips = details["ips"]
|
||||
|
||||
# VM info panel
|
||||
info = (
|
||||
f"[green]ID:[/green] {vm.id}\n"
|
||||
f"[green]Name:[/green] {vm.name}\n"
|
||||
f"[green]Cluster:[/green] {vm.cluster.name if vm.cluster else 'N/A'}\n"
|
||||
f"[green]Status:[/green] {vm.status}\n"
|
||||
f"[green]vCPUs:[/green] {vm.vcpus or 'N/A'}\n"
|
||||
f"[green]Memory:[/green] {vm.memory or 'N/A'} MB\n"
|
||||
f"[green]Disk:[/green] {vm.disk or 'N/A'} GB\n"
|
||||
f"[green]Primary IP:[/green] {vm.primary_ip4.address if vm.primary_ip4 else 'N/A'}"
|
||||
)
|
||||
|
||||
console.print(Panel(info, title=f"VM: {vm.name}", border_style="cyan"))
|
||||
|
||||
# Interfaces table
|
||||
if interfaces:
|
||||
iface_table = Table(title="Network Interfaces")
|
||||
iface_table.add_column("Name", style="cyan")
|
||||
iface_table.add_column("Type")
|
||||
iface_table.add_column("Enabled")
|
||||
iface_table.add_column("MTU")
|
||||
|
||||
for iface in interfaces:
|
||||
iface_table.add_row(
|
||||
iface.name,
|
||||
iface.type.value if hasattr(iface.type, 'value') else str(iface.type),
|
||||
"✓" if iface.enabled else "✗",
|
||||
str(iface.mtu) if iface.mtu else "default"
|
||||
)
|
||||
|
||||
console.print("\n", iface_table)
|
||||
|
||||
# IPs table
|
||||
if ips:
|
||||
ip_table = Table(title="IP Addresses")
|
||||
ip_table.add_column("Address", style="yellow")
|
||||
ip_table.add_column("DNS Name", style="green")
|
||||
ip_table.add_column("Status")
|
||||
|
||||
for ip in ips:
|
||||
ip_table.add_row(
|
||||
str(ip.address),
|
||||
ip.dns_name or "",
|
||||
ip.status.value if hasattr(ip.status, 'value') else str(ip.status)
|
||||
)
|
||||
|
||||
console.print("\n", ip_table)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
def main(vm_name: Optional[str] = None) -> int:
|
||||
"""
|
||||
Main entry point.
|
||||
|
||||
Args:
|
||||
vm_name: Optional specific VM to query
|
||||
|
||||
Returns:
|
||||
Exit code (0 = success, 1 = error)
|
||||
"""
|
||||
config = NetBoxConfig()
|
||||
|
||||
# Get NetBox client
|
||||
nb = get_netbox_client(config)
|
||||
if not nb:
|
||||
return 1
|
||||
|
||||
try:
|
||||
if vm_name:
|
||||
# Query specific VM
|
||||
details = get_vm_details(nb, vm_name)
|
||||
if details:
|
||||
display_vm_details(details)
|
||||
return 0
|
||||
return 1
|
||||
else:
|
||||
# List all VMs in Matrix cluster
|
||||
vms = get_cluster_vms(nb, cluster_name="Matrix")
|
||||
display_vms_table(vms)
|
||||
return 0
|
||||
|
||||
except KeyboardInterrupt:
|
||||
console.print("\n[yellow]Interrupted by user[/yellow]")
|
||||
return 1
|
||||
except Exception as e:
|
||||
console.print(f"[red]Unexpected error: {e}[/red]")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="NetBox API client example",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__
|
||||
)
|
||||
parser.add_argument(
|
||||
"--vm",
|
||||
help="Specific VM to query (default: list all)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
sys.exit(main(vm_name=args.vm))
|
||||
209
skills/python-uv-scripts/patterns/api-clients.md
Normal file
209
skills/python-uv-scripts/patterns/api-clients.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# API Client Patterns
|
||||
|
||||
Patterns for building API clients with uv scripts.
|
||||
|
||||
## Basic GET Request
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import httpx
|
||||
|
||||
response = httpx.get("https://api.github.com/users/octocat")
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
print(f"Name: {data['name']}")
|
||||
```
|
||||
|
||||
## Authenticated Requests
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import httpx
|
||||
import os
|
||||
import sys
|
||||
|
||||
api_token = os.getenv("API_TOKEN")
|
||||
if not api_token:
|
||||
print("Error: API_TOKEN not set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
headers = {"Authorization": f"Bearer {api_token}"}
|
||||
|
||||
response = httpx.get(
|
||||
"https://api.example.com/data",
|
||||
headers=headers,
|
||||
timeout=10.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
## POST with JSON
|
||||
|
||||
```python
|
||||
import httpx
|
||||
|
||||
data = {
|
||||
"name": "example",
|
||||
"status": "active"
|
||||
}
|
||||
|
||||
response = httpx.post(
|
||||
"https://api.example.com/resources",
|
||||
json=data,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
print(f"Created: {result['id']}")
|
||||
```
|
||||
|
||||
## Query Parameters
|
||||
|
||||
```python
|
||||
import httpx
|
||||
|
||||
params = {
|
||||
"q": "python",
|
||||
"sort": "stars",
|
||||
"order": "desc"
|
||||
}
|
||||
|
||||
response = httpx.get(
|
||||
"https://api.github.com/search/repositories",
|
||||
params=params
|
||||
)
|
||||
response.raise_for_status()
|
||||
repos = response.json()
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```python
|
||||
import httpx
|
||||
import sys
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=10.0) as client:
|
||||
response = client.get("https://api.example.com/data")
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
status = e.response.status_code
|
||||
if status == 401:
|
||||
print("Error: Unauthorized - check API key", file=sys.stderr)
|
||||
elif status == 404:
|
||||
print("Error: Resource not found", file=sys.stderr)
|
||||
elif status >= 500:
|
||||
print(f"Error: Server error ({status})", file=sys.stderr)
|
||||
else:
|
||||
print(f"Error: HTTP {status}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
except httpx.RequestError as e:
|
||||
print(f"Error: Request failed - {type(e).__name__}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
## Retry Logic
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0",
|
||||
# "tenacity>=8.2.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import httpx
|
||||
from tenacity import retry, stop_after_attempt, wait_exponential
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=1, min=2, max=10)
|
||||
)
|
||||
def fetch_data(url: str):
|
||||
"""Fetch data with automatic retry."""
|
||||
response = httpx.get(url, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
data = fetch_data("https://api.example.com/data")
|
||||
```
|
||||
|
||||
## Pagination
|
||||
|
||||
```python
|
||||
import httpx
|
||||
|
||||
def fetch_all_pages(base_url: str, headers: dict):
|
||||
"""Fetch all pages from paginated API."""
|
||||
all_results = []
|
||||
next_url = base_url
|
||||
|
||||
with httpx.Client(headers=headers, timeout=10.0) as client:
|
||||
while next_url:
|
||||
response = client.get(next_url)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
all_results.extend(data["results"])
|
||||
|
||||
# Get next page URL
|
||||
next_url = data.get("next")
|
||||
|
||||
return all_results
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
```python
|
||||
import httpx
|
||||
import time
|
||||
|
||||
def fetch_with_rate_limit(urls: list[str], requests_per_second: int = 2):
|
||||
"""Fetch URLs respecting rate limit."""
|
||||
delay = 1.0 / requests_per_second
|
||||
results = []
|
||||
|
||||
for url in urls:
|
||||
response = httpx.get(url)
|
||||
response.raise_for_status()
|
||||
results.append(response.json())
|
||||
|
||||
time.sleep(delay)
|
||||
|
||||
return results
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
For a complete API client template, see: `assets/templates/api-client.py`
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Always set timeouts (default: 10 seconds)
|
||||
- Use `with httpx.Client()` for multiple requests
|
||||
- Handle specific HTTP status codes (401, 404, 500)
|
||||
- Don't log sensitive data (tokens, responses)
|
||||
- Use environment variables for credentials
|
||||
- Implement retry logic for transient failures
|
||||
- Respect rate limits
|
||||
129
skills/python-uv-scripts/patterns/cli-applications.md
Normal file
129
skills/python-uv-scripts/patterns/cli-applications.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# CLI Application Patterns
|
||||
|
||||
Patterns for building command-line applications with uv scripts.
|
||||
|
||||
## Basic CLI with Typer
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "typer>=0.9.0",
|
||||
# "rich>=13.0.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import typer
|
||||
from rich import print
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
@app.command()
|
||||
def main(
|
||||
name: str = typer.Argument(..., help="Your name"),
|
||||
greeting: str = typer.Option("Hello", "--greeting", "-g"),
|
||||
):
|
||||
"""Greet someone."""
|
||||
print(f"[green]{greeting}, {name}![/green]")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
```
|
||||
|
||||
## Multiple Subcommands
|
||||
|
||||
```python
|
||||
import typer
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
@app.command()
|
||||
def create(name: str):
|
||||
"""Create a new resource."""
|
||||
print(f"Creating: {name}")
|
||||
|
||||
@app.command()
|
||||
def delete(name: str):
|
||||
"""Delete a resource."""
|
||||
print(f"Deleting: {name}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```bash
|
||||
./script.py create foo
|
||||
./script.py delete bar
|
||||
```
|
||||
|
||||
## File Input/Output
|
||||
|
||||
```python
|
||||
import typer
|
||||
from pathlib import Path
|
||||
|
||||
def process_file(
|
||||
input_file: Path = typer.Argument(..., exists=True),
|
||||
output: Path = typer.Option(None, "--output", "-o"),
|
||||
):
|
||||
"""Process a file."""
|
||||
content = input_file.read_text()
|
||||
|
||||
# Process
|
||||
result = content.upper()
|
||||
|
||||
if output:
|
||||
output.write_text(result)
|
||||
print(f"Written to: {output}")
|
||||
else:
|
||||
print(result)
|
||||
```
|
||||
|
||||
## Progress Bars
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "rich>=13.0.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
from rich.progress import track
|
||||
import time
|
||||
|
||||
for item in track(range(100), description="Processing..."):
|
||||
time.sleep(0.01) # Simulate work
|
||||
```
|
||||
|
||||
## Formatted Tables
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "rich>=13.0.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
|
||||
table = Table(title="Results")
|
||||
table.add_column("Name", style="cyan")
|
||||
table.add_column("Status", style="green")
|
||||
|
||||
table.add_row("Task 1", "✓ Complete")
|
||||
table.add_row("Task 2", "⏳ Pending")
|
||||
|
||||
console.print(table)
|
||||
```
|
||||
|
||||
For complete template, see: `assets/templates/cli-app.py`
|
||||
54
skills/python-uv-scripts/patterns/data-processing.md
Normal file
54
skills/python-uv-scripts/patterns/data-processing.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Data Processing Patterns
|
||||
|
||||
> **Status**: 🚧 Placeholder - Content in development
|
||||
|
||||
## Overview
|
||||
|
||||
Patterns for data analysis, ETL, and processing using Polars, pandas, and other data libraries in UV single-file
|
||||
scripts.
|
||||
|
||||
## Topics to Cover
|
||||
|
||||
- [ ] Polars patterns (recommended for performance)
|
||||
- [ ] Pandas alternatives
|
||||
- [ ] CSV/Excel processing
|
||||
- [ ] JSON data manipulation
|
||||
- [ ] Data validation and cleaning
|
||||
- [ ] Aggregation and transformation
|
||||
- [ ] Memory-efficient processing
|
||||
|
||||
## Quick Example
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = ["polars>=0.20.0"]
|
||||
# ///
|
||||
import polars as pl
|
||||
|
||||
def analyze_csv(file_path: str):
|
||||
df = pl.read_csv(file_path)
|
||||
|
||||
# Basic analysis
|
||||
summary = df.describe()
|
||||
print(summary)
|
||||
|
||||
# Filter and aggregate
|
||||
result = (
|
||||
df.filter(pl.col("value") > 100)
|
||||
.groupby("category")
|
||||
.agg(pl.col("value").mean())
|
||||
)
|
||||
print(result)
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
This file will be expanded to include:
|
||||
|
||||
- Complete Polars patterns
|
||||
- Performance optimization techniques
|
||||
- Large file processing strategies
|
||||
- Data validation patterns
|
||||
- Export formats and options
|
||||
198
skills/python-uv-scripts/patterns/error-handling.md
Normal file
198
skills/python-uv-scripts/patterns/error-handling.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# Error Handling Patterns
|
||||
|
||||
Best practices for error handling in uv scripts.
|
||||
|
||||
## Exit Codes
|
||||
|
||||
Always use appropriate exit codes:
|
||||
|
||||
- `0` - Success
|
||||
- `1` - General error
|
||||
- `2` - Invalid usage
|
||||
|
||||
```python
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: script.py <input>", file=sys.stderr)
|
||||
sys.exit(2) # Invalid usage
|
||||
|
||||
try:
|
||||
result = process()
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1) # General error
|
||||
|
||||
# Success
|
||||
sys.exit(0)
|
||||
```
|
||||
|
||||
## Try-Except Pattern
|
||||
|
||||
```python
|
||||
import sys
|
||||
|
||||
def main():
|
||||
try:
|
||||
# Operations that might fail
|
||||
data = fetch_data()
|
||||
result = process(data)
|
||||
save(result)
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print(f"File not found: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
except ValueError as e:
|
||||
print(f"Invalid data: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
## HTTP Error Handling
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import httpx
|
||||
import sys
|
||||
|
||||
try:
|
||||
response = httpx.get("https://api.example.com", timeout=10.0)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
print(f"HTTP {e.response.status_code} error", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
except httpx.RequestError as e:
|
||||
print(f"Request failed: {type(e).__name__}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
except httpx.TimeoutException:
|
||||
print("Request timed out", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
## File Operation Errors
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
def read_config(path: Path):
|
||||
"""Read configuration file with error handling."""
|
||||
try:
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Config not found: {path}")
|
||||
|
||||
if not path.is_file():
|
||||
raise ValueError(f"Not a file: {path}")
|
||||
|
||||
return path.read_text()
|
||||
|
||||
except PermissionError:
|
||||
print(f"Permission denied: {path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading config: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
## Subprocess Errors
|
||||
|
||||
```python
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["command", "arg"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
print(result.stdout)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Command failed with exit code {e.returncode}", file=sys.stderr)
|
||||
print(f"Error output: {e.stderr}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
except FileNotFoundError:
|
||||
print("Command not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
## Validation Errors
|
||||
|
||||
```python
|
||||
import sys
|
||||
|
||||
def validate_input(value: str) -> int:
|
||||
"""Validate and convert input."""
|
||||
if not value:
|
||||
print("Error: Empty value", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
try:
|
||||
number = int(value)
|
||||
except ValueError:
|
||||
print(f"Error: Not a number: {value}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
if number < 0:
|
||||
print("Error: Must be non-negative", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
return number
|
||||
```
|
||||
|
||||
## Graceful Cleanup
|
||||
|
||||
```python
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
def main():
|
||||
temp_file = Path("/tmp/data.tmp")
|
||||
|
||||
try:
|
||||
# Create temporary file
|
||||
temp_file.write_text("data")
|
||||
|
||||
# Process
|
||||
result = process(temp_file)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
finally:
|
||||
# Always cleanup
|
||||
if temp_file.exists():
|
||||
temp_file.unlink()
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
- Always write errors to `stderr`
|
||||
- Use specific exception types
|
||||
- Provide helpful error messages
|
||||
- Use appropriate exit codes
|
||||
- Clean up resources in `finally` blocks
|
||||
- Don't expose secrets in error messages
|
||||
32
skills/python-uv-scripts/patterns/security-patterns.md
Normal file
32
skills/python-uv-scripts/patterns/security-patterns.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Security Patterns for UV Single-File Scripts
|
||||
|
||||
> **Status**: 🚧 Placeholder - Content in development
|
||||
|
||||
## Overview
|
||||
|
||||
Security patterns and best practices for handling secrets, authentication, and input validation in UV single-file
|
||||
Python scripts.
|
||||
|
||||
## Topics to Cover
|
||||
|
||||
- [ ] Infisical secrets management integration
|
||||
- [ ] Keyring for credential storage
|
||||
- [ ] Environment variable security
|
||||
- [ ] Input validation and sanitization
|
||||
- [ ] API token handling
|
||||
- [ ] SSH key management
|
||||
- [ ] Avoiding hardcoded credentials
|
||||
|
||||
## Quick Reference
|
||||
|
||||
For now, see the main security section in [../reference/security-patterns.md](../reference/security-patterns.md).
|
||||
|
||||
## TODO
|
||||
|
||||
This file will be expanded to include:
|
||||
|
||||
- Complete Infisical integration patterns
|
||||
- Best practices for secret rotation
|
||||
- Multi-environment secret management
|
||||
- Security checklist for scripts
|
||||
- Common security vulnerabilities and fixes
|
||||
61
skills/python-uv-scripts/patterns/system-automation.md
Normal file
61
skills/python-uv-scripts/patterns/system-automation.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# System Automation Patterns
|
||||
|
||||
> **Status**: 🚧 Placeholder - Content in development
|
||||
|
||||
## Overview
|
||||
|
||||
Patterns for system administration, monitoring, and automation using psutil, subprocess, and system libraries in UV
|
||||
single-file scripts.
|
||||
|
||||
## Topics to Cover
|
||||
|
||||
- [ ] psutil for system monitoring
|
||||
- [ ] subprocess for command execution
|
||||
- [ ] File system operations
|
||||
- [ ] Process management
|
||||
- [ ] SSH remote execution
|
||||
- [ ] Cron/scheduled task integration
|
||||
- [ ] Log file analysis
|
||||
|
||||
## Quick Example
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = ["psutil>=5.9.0", "rich>=13.0.0"]
|
||||
# ///
|
||||
import psutil
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
|
||||
def show_disk_usage():
|
||||
table = Table(title="Disk Usage")
|
||||
table.add_column("Device", style="cyan")
|
||||
table.add_column("Mount", style="magenta")
|
||||
table.add_column("Used", style="yellow")
|
||||
table.add_column("Free", style="green")
|
||||
|
||||
for partition in psutil.disk_partitions():
|
||||
usage = psutil.disk_usage(partition.mountpoint)
|
||||
table.add_row(
|
||||
partition.device,
|
||||
partition.mountpoint,
|
||||
f"{usage.percent}%",
|
||||
f"{usage.free / (1024**3):.2f} GB"
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
This file will be expanded to include:
|
||||
|
||||
- Complete psutil monitoring patterns
|
||||
- Safe subprocess execution
|
||||
- SSH automation patterns
|
||||
- System health checks
|
||||
- Automated maintenance tasks
|
||||
293
skills/python-uv-scripts/reference/bash-to-python.md
Normal file
293
skills/python-uv-scripts/reference/bash-to-python.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# Converting Bash Scripts to Python uv Scripts
|
||||
|
||||
This document guides the conversion of bash scripts to Python uv scripts.
|
||||
|
||||
## When Conversion Makes Sense
|
||||
|
||||
### ✅ Good Candidates for Conversion
|
||||
|
||||
**Convert when**:
|
||||
|
||||
- Script needs better error handling
|
||||
- Cross-platform compatibility required
|
||||
- Complex data processing needed
|
||||
- API interactions involved
|
||||
- Script will grow in complexity
|
||||
|
||||
**Examples**:
|
||||
|
||||
```bash
|
||||
# Good candidate - API interaction
|
||||
curl -X POST https://api.example.com/data \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"status": "active"}'
|
||||
|
||||
# Good candidate - Data processing
|
||||
cat data.json | jq '.users[] | select(.active == true)'
|
||||
|
||||
# Good candidate - Complex logic
|
||||
for file in $(find /data -name "*.log"); do
|
||||
count=$(grep -c "ERROR" "$file")
|
||||
if [ $count -gt 100 ]; then
|
||||
# Complex processing
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### ❌ Keep as Bash When
|
||||
|
||||
**Don't convert when**:
|
||||
|
||||
- Simple file operations (cp, mv, mkdir)
|
||||
- Shell-specific features heavily used (job control, pipes)
|
||||
- System administration tasks
|
||||
- Script is <10 lines and works fine
|
||||
- Team primarily knows bash
|
||||
|
||||
**Examples**:
|
||||
|
||||
```bash
|
||||
# Keep as bash - Simple operations
|
||||
#!/bin/bash
|
||||
mkdir -p /var/log/app
|
||||
cp config.yaml /etc/app/
|
||||
|
||||
# Keep as bash - Shell-specific
|
||||
#!/bin/bash
|
||||
find /data -name "*.tmp" -mtime +7 -delete
|
||||
|
||||
# Keep as bash - System admin
|
||||
#!/bin/bash
|
||||
systemctl restart nginx
|
||||
journalctl -u nginx -f
|
||||
```
|
||||
|
||||
## Common Conversions
|
||||
|
||||
### File Operations
|
||||
|
||||
**Bash**:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
if [ -f "/etc/config.yaml" ]; then
|
||||
cp /etc/config.yaml /backup/
|
||||
fi
|
||||
```
|
||||
|
||||
**Python**:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
config = Path("/etc/config.yaml")
|
||||
if config.exists():
|
||||
shutil.copy(config, "/backup/")
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
**Bash**:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
API_URL=${API_URL:-"https://api.example.com"}
|
||||
if [ -z "$API_TOKEN" ]; then
|
||||
echo "Error: API_TOKEN not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
**Python**:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
API_URL = os.getenv("API_URL", "https://api.example.com")
|
||||
API_TOKEN = os.getenv("API_TOKEN")
|
||||
|
||||
if not API_TOKEN:
|
||||
print("Error: API_TOKEN not set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
### Running Commands
|
||||
|
||||
**Bash**:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
output=$(systemctl status nginx)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: nginx not running"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
**Python**:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["systemctl", "status", "nginx"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
print(result.stdout)
|
||||
except subprocess.CalledProcessError:
|
||||
print("Error: nginx not running", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
### HTTP Requests
|
||||
|
||||
**Bash**:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
response=$(curl -s -X GET https://api.github.com/users/octocat)
|
||||
name=$(echo "$response" | jq -r '.name')
|
||||
echo "Name: $name"
|
||||
```
|
||||
|
||||
**Python**:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import httpx
|
||||
|
||||
response = httpx.get("https://api.github.com/users/octocat")
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
print(f"Name: {data['name']}")
|
||||
```
|
||||
|
||||
### JSON Processing
|
||||
|
||||
**Bash**:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
jq '.users[] | select(.active == true) | .name' data.json
|
||||
```
|
||||
|
||||
**Python**:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
data = json.loads(Path("data.json").read_text())
|
||||
active_names = [
|
||||
user["name"]
|
||||
for user in data["users"]
|
||||
if user.get("active")
|
||||
]
|
||||
print("\n".join(active_names))
|
||||
```
|
||||
|
||||
## Decision Framework
|
||||
|
||||
```text
|
||||
Should I convert this bash script?
|
||||
|
||||
├─ Is it >50 lines? → Consider conversion
|
||||
├─ Does it process JSON/YAML? → Strong candidate
|
||||
├─ Does it make API calls? → Strong candidate
|
||||
├─ Does it have complex logic? → Consider conversion
|
||||
├─ Does it need better error handling? → Consider conversion
|
||||
├─ Is it mostly shell commands? → Keep as bash
|
||||
└─ Is it <10 lines and works? → Keep as bash
|
||||
```
|
||||
|
||||
## Hybrid Approach
|
||||
|
||||
Sometimes the best solution is calling bash from Python:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""
|
||||
Hybrid: Use Python for logic, bash for system commands.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
|
||||
def backup_with_rsync(source: str, dest: str):
|
||||
"""Use rsync (better than reimplementing in Python)."""
|
||||
subprocess.run(
|
||||
["rsync", "-av", "--delete", source, dest],
|
||||
check=True
|
||||
)
|
||||
|
||||
# Python logic here
|
||||
# ...
|
||||
|
||||
# Leverage bash tools where appropriate
|
||||
backup_with_rsync("/data/", "/backup/")
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
**Convert to Python when**:
|
||||
|
||||
- Complex logic or data processing
|
||||
- API interactions
|
||||
- Cross-platform needs
|
||||
- Better error handling required
|
||||
- Will grow in complexity
|
||||
|
||||
**Keep as Bash when**:
|
||||
|
||||
- Simple file operations
|
||||
- System administration
|
||||
- Heavily uses shell features
|
||||
- Works well and won't change
|
||||
- Team expertise is bash
|
||||
|
||||
**Consider Hybrid**:
|
||||
|
||||
- Complex Python logic + system commands
|
||||
- Leverage both Python libraries and shell tools
|
||||
63
skills/python-uv-scripts/reference/dependency-management.md
Normal file
63
skills/python-uv-scripts/reference/dependency-management.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Dependency Management Reference
|
||||
|
||||
> **Status**: 🚧 Placeholder - Content in development
|
||||
|
||||
## Overview
|
||||
|
||||
Comprehensive guide to managing dependencies in UV single-file scripts using PEP 723 inline metadata.
|
||||
|
||||
## Topics to Cover
|
||||
|
||||
- [ ] Version pinning strategies
|
||||
- [ ] Semantic versioning best practices
|
||||
- [ ] Dependency conflict resolution
|
||||
- [ ] Optional dependencies
|
||||
- [ ] Development vs production dependencies
|
||||
- [ ] Security updates and vulnerability scanning
|
||||
- [ ] Lock file equivalents for scripts
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Version Pinning Strategies
|
||||
|
||||
**Exact pinning** (most restrictive):
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = ["requests==2.31.0"]
|
||||
# ///
|
||||
```
|
||||
|
||||
**Compatible release** (recommended):
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = ["requests~=2.31.0"] # >=2.31.0, <2.32.0
|
||||
# ///
|
||||
```
|
||||
|
||||
**Minimum version**:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = ["requests>=2.31.0"]
|
||||
# ///
|
||||
```
|
||||
|
||||
### Following Repository Standards
|
||||
|
||||
From this repository's `pyproject.toml`, we use:
|
||||
|
||||
- `>=` for minimum versions with flexibility
|
||||
- Specific version ranges for critical dependencies
|
||||
- Regular dependency audits with Renovate
|
||||
|
||||
## TODO
|
||||
|
||||
This file will be expanded to include:
|
||||
|
||||
- Complete version specifier syntax
|
||||
- Dependency resolution strategies
|
||||
- Security scanning integration
|
||||
- Update strategies and automation
|
||||
- Conflict resolution techniques
|
||||
391
skills/python-uv-scripts/reference/pep-723-spec.md
Normal file
391
skills/python-uv-scripts/reference/pep-723-spec.md
Normal file
@@ -0,0 +1,391 @@
|
||||
# PEP 723: Inline Script Metadata Specification
|
||||
|
||||
Complete reference for PEP 723 inline script metadata format used by uv.
|
||||
|
||||
## Overview
|
||||
|
||||
PEP 723 defines a standardized way to embed dependency and configuration metadata directly within Python script files. This eliminates the need for separate `requirements.txt` files and enables self-contained, reproducible scripts.
|
||||
|
||||
## Basic Format
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "package-name>=1.0.0",
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
**Key Requirements:**
|
||||
|
||||
- Must appear as comments (`#`)
|
||||
- Must use `# /// script` as opening marker
|
||||
- Must use `# ///` as closing marker
|
||||
- Must be valid TOML syntax
|
||||
- Recommended placement: After shebang, before module docstring
|
||||
|
||||
## Complete Specification
|
||||
|
||||
### Required Fields
|
||||
|
||||
#### requires-python
|
||||
|
||||
Specifies minimum Python version:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# ///
|
||||
```
|
||||
|
||||
**Formats:**
|
||||
|
||||
```python
|
||||
requires-python = ">=3.11" # Minimum version
|
||||
requires-python = ">=3.11,<3.13" # Version range
|
||||
requires-python = "==3.12" # Exact version
|
||||
```
|
||||
|
||||
#### dependencies
|
||||
|
||||
Lists required packages:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0",
|
||||
# "rich>=13.0.0",
|
||||
# "typer~=0.9.0",
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
**Version Specifiers:**
|
||||
|
||||
```python
|
||||
"package" # Any version
|
||||
"package>=1.0" # Minimum version
|
||||
"package>=1.0,<2.0" # Version range
|
||||
"package~=1.2.3" # Compatible release (1.2.x)
|
||||
"package==1.2.3" # Exact version
|
||||
```
|
||||
|
||||
### Optional Fields
|
||||
|
||||
#### [tool.uv] Section
|
||||
|
||||
uv-specific configuration:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
#
|
||||
# [tool.uv]
|
||||
# exclude-newer = "2024-10-01T00:00:00Z"
|
||||
# index-url = "https://pypi.org/simple"
|
||||
# ///
|
||||
```
|
||||
|
||||
**Available options:**
|
||||
|
||||
- `exclude-newer`: Only use packages published before this date
|
||||
- `index-url`: Alternative PyPI index
|
||||
- `extra-index-url`: Additional package indexes
|
||||
- `find-links`: Additional package sources
|
||||
- `no-index`: Ignore PyPI entirely
|
||||
|
||||
#### [tool.uv.sources]
|
||||
|
||||
Custom package sources:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = ["my-package"]
|
||||
#
|
||||
# [tool.uv.sources]
|
||||
# my-package = { git = "https://github.com/user/repo", tag = "v1.0" }
|
||||
# ///
|
||||
```
|
||||
|
||||
#### Valid [tool.uv] Fields
|
||||
|
||||
Additional uv-specific configuration (optional):
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
#
|
||||
# [tool.uv]
|
||||
# exclude-newer = "2025-01-01T00:00:00Z" # Reproducibility constraint
|
||||
# ///
|
||||
```
|
||||
|
||||
**Note**: For custom metadata like purpose, team, author, use Python docstrings instead:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""
|
||||
Purpose: cluster-monitoring
|
||||
Team: infrastructure
|
||||
Author: devops@example.com
|
||||
Created: 2024-10-20
|
||||
"""
|
||||
```
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Minimal Script
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""Simple script with no dependencies"""
|
||||
|
||||
print("Hello, world!")
|
||||
```
|
||||
|
||||
### Production Script
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script --quiet
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0",
|
||||
# "rich>=13.0.0",
|
||||
# "typer>=0.9.0",
|
||||
# ]
|
||||
#
|
||||
# [tool.uv]
|
||||
# exclude-newer = "2025-01-01T00:00:00Z"
|
||||
# ///
|
||||
"""
|
||||
API client for Proxmox cluster monitoring
|
||||
|
||||
Purpose: api-client
|
||||
Team: infrastructure
|
||||
Author: devops@spaceships.work
|
||||
|
||||
Usage:
|
||||
check_cluster.py [--node NODE] [--json]
|
||||
"""
|
||||
|
||||
import typer
|
||||
import httpx
|
||||
from rich import print
|
||||
```
|
||||
|
||||
### With Git Dependencies
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "my-internal-lib",
|
||||
# ]
|
||||
#
|
||||
# [tool.uv.sources]
|
||||
# my-internal-lib = { git = "https://github.com/org/lib.git", tag = "v1.2.3" }
|
||||
# ///
|
||||
```
|
||||
|
||||
### With Local Dependencies
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "my-local-package",
|
||||
# ]
|
||||
#
|
||||
# [tool.uv.sources]
|
||||
# my-local-package = { path = "../my-package", editable = true }
|
||||
# ///
|
||||
```
|
||||
|
||||
## Placement Guidelines
|
||||
|
||||
### Correct Placement
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""Module docstring comes after metadata"""
|
||||
|
||||
import sys
|
||||
|
||||
def main():
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
### Multiple Metadata Blocks (Invalid)
|
||||
|
||||
```python
|
||||
# ❌ INVALID - Only one metadata block allowed
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# ///
|
||||
|
||||
# /// script
|
||||
# dependencies = ["httpx"]
|
||||
# ///
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
### Valid Metadata
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0", # Comments allowed in arrays
|
||||
# "rich",
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
### Invalid Metadata
|
||||
|
||||
```python
|
||||
# ❌ Missing closing marker
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
|
||||
# ❌ Invalid TOML syntax
|
||||
# /// script
|
||||
# dependencies = httpx # Missing quotes
|
||||
# ///
|
||||
|
||||
# ❌ Not in comments
|
||||
/// script
|
||||
requires-python = ">=3.11"
|
||||
///
|
||||
```
|
||||
|
||||
## Creating Metadata
|
||||
|
||||
### Manual Creation
|
||||
|
||||
Add metadata block manually:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import httpx
|
||||
```
|
||||
|
||||
### Using uv init
|
||||
|
||||
Generate script with metadata:
|
||||
|
||||
```bash
|
||||
uv init --script my_script.py --python 3.11
|
||||
```
|
||||
|
||||
### Using uv add
|
||||
|
||||
Add dependencies to existing script:
|
||||
|
||||
```bash
|
||||
uv add --script my_script.py httpx rich typer
|
||||
```
|
||||
|
||||
## Validation Tools
|
||||
|
||||
### Check Metadata Validity
|
||||
|
||||
```bash
|
||||
# Parse metadata
|
||||
uv run --script my_script.py --dry-run
|
||||
|
||||
# Validate with custom tool
|
||||
python tools/validate_script.py my_script.py
|
||||
```
|
||||
|
||||
### Extract Metadata
|
||||
|
||||
```python
|
||||
import re
|
||||
import tomllib
|
||||
|
||||
def extract_metadata(script_path: str) -> dict:
|
||||
"""Extract PEP 723 metadata from script"""
|
||||
with open(script_path) as f:
|
||||
content = f.read()
|
||||
|
||||
# Find metadata block
|
||||
pattern = r'# /// script\n((?:# .*\n)+)# ///'
|
||||
match = re.search(pattern, content)
|
||||
|
||||
if not match:
|
||||
return {}
|
||||
|
||||
# Parse TOML (remove leading # from each line)
|
||||
toml_lines = match.group(1).split('\n')
|
||||
toml_content = '\n'.join(line[2:] for line in toml_lines if line.startswith('# '))
|
||||
|
||||
return tomllib.loads(toml_content)
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
||||
### PEP 723 Support
|
||||
|
||||
- ✅ uv (native support)
|
||||
- ✅ pip (via `pip-run`)
|
||||
- ✅ pipx (v1.4.0+)
|
||||
- ⚠️ Other tools (check documentation)
|
||||
|
||||
### Fallback for Non-Supporting Tools
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = ["httpx"]
|
||||
# ///
|
||||
"""
|
||||
Fallback installation for non-PEP-723 tools:
|
||||
pip install httpx
|
||||
python script.py
|
||||
"""
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always include requires-python** - Prevents compatibility issues
|
||||
2. **Pin major versions** - `>=X.Y.Z` for stability
|
||||
3. **Add metadata section** - Document purpose and ownership
|
||||
4. **Keep dependencies minimal** - Only what's necessary
|
||||
5. **Document fallbacks** - Help non-uv users
|
||||
6. **Validate syntax** - Use validation tools
|
||||
7. **Version consistently** - Match project conventions
|
||||
|
||||
## References
|
||||
|
||||
- [PEP 723 Specification](https://peps.python.org/pep-0723/)
|
||||
- [uv Documentation](https://docs.astral.sh/uv/)
|
||||
- [TOML Specification](https://toml.io/)
|
||||
- [Python Version Specifiers](https://peps.python.org/pep-0440/)
|
||||
523
skills/python-uv-scripts/reference/security-patterns.md
Normal file
523
skills/python-uv-scripts/reference/security-patterns.md
Normal file
@@ -0,0 +1,523 @@
|
||||
# Security Patterns for uv Scripts
|
||||
|
||||
Security best practices for Python single-file scripts following Virgo-Core patterns.
|
||||
|
||||
## Never Hardcode Secrets
|
||||
|
||||
### ❌ Anti-Pattern: Hardcoded Secrets
|
||||
|
||||
```python
|
||||
# NEVER DO THIS
|
||||
API_KEY = "sk_live_1234567890abcdef"
|
||||
DATABASE_URL = "postgresql://user:password@localhost/db"
|
||||
PROXMOX_PASSWORD = "admin123"
|
||||
```
|
||||
|
||||
**Risks:**
|
||||
|
||||
- Secrets exposed in version control
|
||||
- No audit trail
|
||||
- Difficult to rotate
|
||||
- Same credentials across environments
|
||||
|
||||
### ✅ Pattern 1: Environment Variables
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
def get_secret(name: str, required: bool = True) -> str:
|
||||
"""Get secret from environment"""
|
||||
value = os.getenv(name)
|
||||
|
||||
if required and not value:
|
||||
print(f"Error: {name} environment variable not set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return value
|
||||
|
||||
# Usage
|
||||
PROXMOX_PASSWORD = get_secret("PROXMOX_PASSWORD")
|
||||
API_URL = get_secret("PROXMOX_API_URL")
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
export PROXMOX_PASSWORD="$(cat ~/.secrets/proxmox_pass)"
|
||||
./script.py
|
||||
```
|
||||
|
||||
### ✅ Pattern 2: Keyring Integration
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "keyring>=24.0.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import keyring
|
||||
import sys
|
||||
|
||||
def get_password(service: str, username: str) -> str:
|
||||
"""Get password from system keyring"""
|
||||
password = keyring.get_password(service, username)
|
||||
|
||||
if not password:
|
||||
print(f"Error: No password found for {username}@{service}", file=sys.stderr)
|
||||
print(f"Set with: keyring set {service} {username}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return password
|
||||
|
||||
# Usage
|
||||
proxmox_password = get_password("proxmox", "terraform@pam")
|
||||
```
|
||||
|
||||
**Setup:**
|
||||
|
||||
```bash
|
||||
# Store password in system keyring
|
||||
keyring set proxmox terraform@pam
|
||||
# Prompts for password, stores securely
|
||||
|
||||
# Run script (no password in environment)
|
||||
./script.py
|
||||
```
|
||||
|
||||
### ✅ Pattern 3: Infisical Integration (Repository Standard)
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "infisical-python>=2.3.3",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
from infisical import InfisicalClient
|
||||
import os
|
||||
import sys
|
||||
|
||||
def get_infisical_secret(
|
||||
secret_name: str,
|
||||
project_id: str,
|
||||
environment: str = "prod",
|
||||
path: str = "/"
|
||||
) -> str:
|
||||
"""Get secret from Infisical vault"""
|
||||
try:
|
||||
client = InfisicalClient()
|
||||
secret = client.get_secret(
|
||||
secret_name=secret_name,
|
||||
project_id=project_id,
|
||||
environment=environment,
|
||||
path=path
|
||||
)
|
||||
return secret.secret_value
|
||||
except Exception as e:
|
||||
print(f"Error retrieving secret {secret_name}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Usage (following Virgo-Core pattern)
|
||||
PROXMOX_PASSWORD = get_infisical_secret(
|
||||
secret_name="PROXMOX_PASSWORD",
|
||||
project_id="7b832220-24c0-45bc-a5f1-ce9794a31259",
|
||||
environment="prod",
|
||||
path="/matrix"
|
||||
)
|
||||
```
|
||||
|
||||
**Setup:**
|
||||
|
||||
```bash
|
||||
# Authenticate with Infisical
|
||||
infisical login
|
||||
|
||||
# Run script (secrets fetched automatically)
|
||||
./script.py
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
|
||||
### ❌ Anti-Pattern: No Validation
|
||||
|
||||
```python
|
||||
import subprocess
|
||||
|
||||
# DANGEROUS - Command injection risk
|
||||
def ping_host(hostname: str):
|
||||
subprocess.run(f"ping -c 3 {hostname}", shell=True)
|
||||
|
||||
# User provides: "localhost; rm -rf /"
|
||||
ping_host(sys.argv[1]) # 💥 System destroyed
|
||||
```
|
||||
|
||||
### ✅ Pattern: Validate All Inputs
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
import re
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
def validate_hostname(hostname: str) -> bool:
|
||||
"""Validate hostname format"""
|
||||
# Only allow alphanumeric, dots, hyphens
|
||||
pattern = r'^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$'
|
||||
if not re.match(pattern, hostname):
|
||||
return False
|
||||
|
||||
# Max length check
|
||||
if len(hostname) > 253:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def ping_host(hostname: str):
|
||||
"""Safely ping a host"""
|
||||
if not validate_hostname(hostname):
|
||||
print(f"Error: Invalid hostname: {hostname}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Use list form (no shell injection)
|
||||
result = subprocess.run(
|
||||
["ping", "-c", "3", hostname],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
return result.returncode == 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: ping_host.py <hostname>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
ping_host(sys.argv[1])
|
||||
```
|
||||
|
||||
### Input Validation Patterns
|
||||
|
||||
```python
|
||||
import re
|
||||
from ipaddress import IPv4Address, AddressValueError
|
||||
|
||||
def validate_ip(ip: str) -> bool:
|
||||
"""Validate IPv4 address"""
|
||||
try:
|
||||
IPv4Address(ip)
|
||||
return True
|
||||
except AddressValueError:
|
||||
return False
|
||||
|
||||
def validate_port(port: int) -> bool:
|
||||
"""Validate TCP/UDP port number"""
|
||||
return 1 <= port <= 65535
|
||||
|
||||
def validate_vmid(vmid: int) -> bool:
|
||||
"""Validate Proxmox VMID (100-999999999)"""
|
||||
return 100 <= vmid <= 999999999
|
||||
|
||||
def validate_path(path: str) -> bool:
|
||||
"""Validate file path (no directory traversal)"""
|
||||
# Reject paths with ../
|
||||
if ".." in path:
|
||||
return False
|
||||
|
||||
# Reject absolute paths
|
||||
if path.startswith("/"):
|
||||
return False
|
||||
|
||||
return True
|
||||
```
|
||||
|
||||
## Dependency Security
|
||||
|
||||
### Pin Dependencies
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0", # ✅ Minimum version pinned
|
||||
# "rich>=13.0.0", # ✅ Known good version
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
**Why pin?**
|
||||
|
||||
- Prevents automatic upgrades with vulnerabilities
|
||||
- Ensures reproducible execution
|
||||
- Allows controlled dependency updates
|
||||
|
||||
### Exclude Recent Packages
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = ["httpx>=0.27.0"]
|
||||
#
|
||||
# [tool.uv]
|
||||
# exclude-newer = "2024-10-01T00:00:00Z" # Only packages before this date
|
||||
# ///
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
|
||||
- Prevent supply chain attacks from compromised packages
|
||||
- Freeze dependencies at known-good state
|
||||
- Reproducible builds in CI/CD
|
||||
|
||||
### Check for Vulnerabilities
|
||||
|
||||
```bash
|
||||
# Use safety or pip-audit
|
||||
uv pip install safety
|
||||
safety check --json
|
||||
|
||||
# Or use built-in tools
|
||||
uv pip list --format json | jq '.[] | select(.name == "httpx")'
|
||||
```
|
||||
|
||||
## File Operations Security
|
||||
|
||||
### ❌ Anti-Pattern: Unsafe File Access
|
||||
|
||||
```python
|
||||
# DANGEROUS - Path traversal vulnerability
|
||||
def read_log(filename: str):
|
||||
with open(f"/var/log/{filename}") as f:
|
||||
return f.read()
|
||||
|
||||
# User provides: "../../../etc/passwd"
|
||||
read_log(sys.argv[1]) # 💥 Reads /etc/passwd
|
||||
```
|
||||
|
||||
### ✅ Pattern: Safe File Operations
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
def safe_read_log(filename: str, log_dir: str = "/var/log") -> str:
|
||||
"""Safely read log file"""
|
||||
# Resolve to absolute paths
|
||||
log_dir_path = Path(log_dir).resolve()
|
||||
file_path = (log_dir_path / filename).resolve()
|
||||
|
||||
# Ensure file is within log directory
|
||||
try:
|
||||
file_path.relative_to(log_dir_path)
|
||||
except ValueError:
|
||||
print(f"Error: Path traversal detected: {filename}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Check file exists and is readable
|
||||
if not file_path.exists():
|
||||
print(f"Error: File not found: {filename}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not file_path.is_file():
|
||||
print(f"Error: Not a file: {filename}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Read with size limit
|
||||
MAX_SIZE = 10 * 1024 * 1024 # 10MB
|
||||
if file_path.stat().st_size > MAX_SIZE:
|
||||
print(f"Error: File too large: {filename}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open(file_path) as f:
|
||||
return f.read()
|
||||
```
|
||||
|
||||
## Command Execution Security
|
||||
|
||||
### ❌ Anti-Pattern: Shell Injection
|
||||
|
||||
```python
|
||||
# DANGEROUS
|
||||
import subprocess
|
||||
|
||||
def list_directory(path: str):
|
||||
subprocess.run(f"ls -la {path}", shell=True)
|
||||
|
||||
# User provides: "; rm -rf /"
|
||||
list_directory(sys.argv[1]) # 💥 Disaster
|
||||
```
|
||||
|
||||
### ✅ Pattern: Safe Command Execution
|
||||
|
||||
```python
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
def list_directory(path: str):
|
||||
"""Safely list directory contents"""
|
||||
# Validate path first
|
||||
if not validate_path(path):
|
||||
print(f"Error: Invalid path: {path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Use list form (no shell)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ls", "-la", path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
timeout=5 # Prevent hanging
|
||||
)
|
||||
print(result.stdout)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error: {e.stderr}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except subprocess.TimeoutExpired:
|
||||
print("Error: Command timed out", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
## Logging and Audit
|
||||
|
||||
### Secure Logging
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "structlog>=24.0.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import structlog
|
||||
import sys
|
||||
|
||||
# Configure structured logging
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.processors.add_log_level,
|
||||
structlog.processors.TimeStamper(fmt="iso"),
|
||||
structlog.processors.JSONRenderer(),
|
||||
],
|
||||
logger_factory=structlog.PrintLoggerFactory(file=sys.stderr),
|
||||
)
|
||||
|
||||
log = structlog.get_logger()
|
||||
|
||||
def process_data(user_id: int, action: str):
|
||||
"""Process user action with audit logging"""
|
||||
log.info(
|
||||
"user_action",
|
||||
user_id=user_id,
|
||||
action=action,
|
||||
# Don't log sensitive data!
|
||||
)
|
||||
|
||||
# ... processing logic ...
|
||||
|
||||
log.info(
|
||||
"action_completed",
|
||||
user_id=user_id,
|
||||
action=action,
|
||||
status="success"
|
||||
)
|
||||
```
|
||||
|
||||
### Never Log Secrets
|
||||
|
||||
```python
|
||||
# ❌ BAD - Logs password
|
||||
log.info(f"Connecting with password: {password}")
|
||||
|
||||
# ✅ GOOD - No sensitive data
|
||||
log.info("Connecting to API", endpoint=api_url)
|
||||
|
||||
# ✅ GOOD - Masked credentials
|
||||
log.info("Authentication successful", user=username)
|
||||
```
|
||||
|
||||
## Network Security
|
||||
|
||||
### HTTPS Only
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# dependencies = ["httpx>=0.27.0"]
|
||||
# ///
|
||||
|
||||
import httpx
|
||||
import sys
|
||||
|
||||
def fetch_api(url: str) -> dict:
|
||||
"""Fetch data from API (HTTPS only)"""
|
||||
# Validate HTTPS
|
||||
if not url.startswith("https://"):
|
||||
print("Error: Only HTTPS URLs allowed", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with httpx.Client(verify=True) as client: # Verify SSL certs
|
||||
response = client.get(url, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
```
|
||||
|
||||
### Certificate Verification
|
||||
|
||||
```python
|
||||
import httpx
|
||||
import certifi
|
||||
|
||||
# Use trusted CA bundle
|
||||
client = httpx.Client(verify=certifi.where())
|
||||
|
||||
# For internal CAs, specify cert path
|
||||
client = httpx.Client(verify="/path/to/internal-ca.pem")
|
||||
|
||||
# ONLY disable for development (never in production)
|
||||
if os.getenv("DEVELOPMENT") == "true":
|
||||
client = httpx.Client(verify=False)
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
Before deploying a script:
|
||||
|
||||
- [ ] No hardcoded secrets
|
||||
- [ ] Secrets from environment/keyring/Infisical
|
||||
- [ ] All inputs validated
|
||||
- [ ] No shell=True in subprocess
|
||||
- [ ] File paths checked for traversal
|
||||
- [ ] Dependencies pinned
|
||||
- [ ] No sensitive data in logs
|
||||
- [ ] HTTPS for network requests
|
||||
- [ ] Certificate verification enabled
|
||||
- [ ] Timeouts on external calls
|
||||
- [ ] Error messages don't leak information
|
||||
- [ ] Principle of least privilege applied
|
||||
|
||||
## References
|
||||
|
||||
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||
- [Infisical Python SDK](https://infisical.com/docs/sdks/languages/python)
|
||||
- [Python Security Best Practices](https://python.readthedocs.io/en/latest/library/security_warnings.html)
|
||||
- Virgo-Core Infisical integration: [../../ansible/tasks/infisical-secret-lookup.yml](../../ansible/tasks/infisical-secret-lookup.yml)
|
||||
495
skills/python-uv-scripts/tools/convert_to_uv.py
Executable file
495
skills/python-uv-scripts/tools/convert_to_uv.py
Executable file
@@ -0,0 +1,495 @@
|
||||
#!/usr/bin/env -S uv run --script --quiet
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0",
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
Convert Python scripts to uv single-file format (PEP 723)
|
||||
|
||||
Purpose: script-conversion-automation
|
||||
Team: devops
|
||||
Author: devops@spaceships.work
|
||||
|
||||
Converts existing Python scripts to use inline dependency management.
|
||||
Reads dependencies from requirements.txt or detects from imports.
|
||||
|
||||
Usage:
|
||||
convert_to_uv.py <script.py>
|
||||
convert_to_uv.py <script.py> --requirements requirements.txt
|
||||
convert_to_uv.py <script.py> --detect-imports
|
||||
convert_to_uv.py <script.py> --dry-run
|
||||
|
||||
Examples:
|
||||
# Convert script with requirements.txt in same directory
|
||||
convert_to_uv.py my_script.py
|
||||
|
||||
# Convert script with specific requirements file
|
||||
convert_to_uv.py my_script.py --requirements ../requirements.txt
|
||||
|
||||
# Detect dependencies from imports (basic detection)
|
||||
convert_to_uv.py my_script.py --detect-imports
|
||||
|
||||
# Preview conversion without creating file
|
||||
convert_to_uv.py my_script.py --dry-run
|
||||
|
||||
# Specify output filename
|
||||
convert_to_uv.py my_script.py --output my_script_new.py
|
||||
"""
|
||||
|
||||
import ast
|
||||
import re
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
# Common import name -> PyPI package name mappings
|
||||
IMPORT_TO_PACKAGE = {
|
||||
'cv2': 'opencv-python',
|
||||
'PIL': 'Pillow',
|
||||
'yaml': 'PyYAML',
|
||||
'bs4': 'beautifulsoup4',
|
||||
'sklearn': 'scikit-learn',
|
||||
'dotenv': 'python-dotenv',
|
||||
'claude_agent_sdk': 'claude-agent-sdk',
|
||||
}
|
||||
|
||||
|
||||
def get_pypi_latest_version(package_name: str) -> str | None:
|
||||
"""
|
||||
Query PyPI API for latest version of package.
|
||||
|
||||
Returns version string like "1.2.3" or None if not found.
|
||||
"""
|
||||
try:
|
||||
import httpx
|
||||
|
||||
url = f"https://pypi.org/pypi/{package_name}/json"
|
||||
with httpx.Client(timeout=5.0) as client:
|
||||
response = client.get(url)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return data['info']['version']
|
||||
except Exception:
|
||||
# Network error, package not found, etc. - fail silently
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def find_version_in_project(package_name: str, script_path: Path) -> str | None:
|
||||
"""
|
||||
Look for version constraint in project's pyproject.toml.
|
||||
|
||||
Searches up directory tree from script location.
|
||||
"""
|
||||
current = script_path.parent
|
||||
|
||||
# Search up to 3 levels for pyproject.toml
|
||||
for _ in range(3):
|
||||
pyproject_path = current / "pyproject.toml"
|
||||
if pyproject_path.exists():
|
||||
try:
|
||||
content = pyproject_path.read_text(encoding='utf-8')
|
||||
data = tomllib.loads(content)
|
||||
|
||||
# Check [project.dependencies]
|
||||
deps = data.get('project', {}).get('dependencies', [])
|
||||
for dep in deps:
|
||||
if isinstance(dep, str) and dep.startswith(package_name):
|
||||
# Extract version constraint
|
||||
# e.g., "package>=1.0.0" -> ">=1.0.0"
|
||||
version_part = dep[len(package_name):].strip()
|
||||
if version_part:
|
||||
return version_part
|
||||
|
||||
# Check [tool.uv.sources] or other sections if needed
|
||||
# (could expand this to check dev-dependencies, etc.)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Move up one directory
|
||||
parent = current.parent
|
||||
if parent == current: # Reached filesystem root
|
||||
break
|
||||
current = parent
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def normalize_package_name(import_name: str, use_pypi: bool = True) -> str:
|
||||
"""
|
||||
Normalize import name to PyPI package name.
|
||||
|
||||
Strategy:
|
||||
1. Check known mappings first (fast)
|
||||
2. Try hyphen normalization (common pattern: my_package → my-package)
|
||||
3. Validate with PyPI if enabled
|
||||
4. Fall back to original
|
||||
|
||||
Returns: Normalized package name
|
||||
"""
|
||||
# Known mapping takes precedence
|
||||
if import_name in IMPORT_TO_PACKAGE:
|
||||
return IMPORT_TO_PACKAGE[import_name]
|
||||
|
||||
# Try hyphen normalization (most common pattern)
|
||||
if '_' in import_name:
|
||||
hyphenated = import_name.replace('_', '-')
|
||||
|
||||
# Validate with PyPI if enabled
|
||||
if use_pypi and get_pypi_latest_version(hyphenated):
|
||||
return hyphenated
|
||||
|
||||
# Without PyPI, trust the normalization (common convention)
|
||||
if not use_pypi:
|
||||
return hyphenated
|
||||
|
||||
# Fall back to original
|
||||
return import_name
|
||||
|
||||
|
||||
def resolve_package_version(
|
||||
import_name: str,
|
||||
script_path: Path,
|
||||
use_pypi: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
Resolve package name and version constraint.
|
||||
|
||||
Returns: "package>=X.Y.Z" format
|
||||
|
||||
Strategy:
|
||||
1. Normalize import name to package name (handles underscore → hyphen)
|
||||
2. Check project's pyproject.toml for version
|
||||
3. Query PyPI for latest version
|
||||
4. Fall back to unversioned package name
|
||||
"""
|
||||
# Normalize import name to package name
|
||||
package_name = normalize_package_name(import_name, use_pypi)
|
||||
|
||||
# Try to find version in project
|
||||
version = find_version_in_project(package_name, script_path)
|
||||
if version:
|
||||
return f"{package_name}{version}"
|
||||
|
||||
# Try PyPI if enabled
|
||||
if use_pypi:
|
||||
latest = get_pypi_latest_version(package_name)
|
||||
if latest:
|
||||
# Use minimum version constraint with latest
|
||||
return f"{package_name}>={latest}"
|
||||
|
||||
# Fall back to unversioned (let uv resolve)
|
||||
return package_name
|
||||
|
||||
|
||||
def has_pep723_metadata(content: str) -> bool:
|
||||
"""Check if script already has PEP 723 metadata"""
|
||||
pattern = r'# /// script\r?\n((?:#.*(?:\r?\n|(?=\r?\n?#\s*///)))+)(?:\r?\n)?#\s*///'
|
||||
return bool(re.search(pattern, content, re.MULTILINE))
|
||||
|
||||
|
||||
def read_requirements_file(req_path: Path) -> list[str]:
|
||||
"""Read dependencies from requirements.txt"""
|
||||
if not req_path.exists():
|
||||
return []
|
||||
|
||||
try:
|
||||
content = req_path.read_text(encoding='utf-8')
|
||||
deps = []
|
||||
for line in content.splitlines():
|
||||
line = line.strip()
|
||||
# Skip empty lines and comments
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
# Skip -e and --editable
|
||||
if line.startswith('-e') or line.startswith('--editable'):
|
||||
continue
|
||||
# Skip -r and --requirement
|
||||
if line.startswith('-r') or line.startswith('--requirement'):
|
||||
continue
|
||||
deps.append(line)
|
||||
return deps
|
||||
except (UnicodeDecodeError, OSError) as e:
|
||||
print(f"Warning: Could not read {req_path}: {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
|
||||
def detect_imports(content: str) -> list[str]:
|
||||
"""
|
||||
Detect third-party imports from script (basic detection).
|
||||
|
||||
Returns import names, not package names (caller should map these).
|
||||
"""
|
||||
imports = set()
|
||||
|
||||
try:
|
||||
tree = ast.parse(content)
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
# Get base module name
|
||||
base = alias.name.split('.')[0]
|
||||
imports.add(base)
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.module:
|
||||
base = node.module.split('.')[0]
|
||||
imports.add(base)
|
||||
except SyntaxError as e:
|
||||
print(f"Warning: Could not parse script for imports: {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
# Filter out standard library modules (basic filter)
|
||||
stdlib_modules = {
|
||||
'abc', 'argparse', 'ast', 'asyncio', 'base64', 'collections', 'contextlib',
|
||||
'copy', 'csv', 'dataclasses', 'datetime', 'decimal', 'email', 'enum', 'functools',
|
||||
'glob', 'hashlib', 'http', 'inspect', 'io', 'itertools', 'json', 'logging',
|
||||
'math', 'multiprocessing', 'operator', 'os', 'pathlib', 'pickle', 'platform',
|
||||
'pprint', 'queue', 're', 'secrets', 'shutil', 'socket', 'sqlite3', 'ssl',
|
||||
'string', 'subprocess', 'sys', 'tempfile', 'threading', 'time', 'tomllib',
|
||||
'traceback', 'typing', 'unittest', 'urllib', 'uuid', 'warnings', 'weakref',
|
||||
'xml', 'zipfile', 'zoneinfo'
|
||||
}
|
||||
|
||||
third_party = [imp for imp in imports if imp not in stdlib_modules]
|
||||
return sorted(third_party)
|
||||
|
||||
|
||||
def generate_header(
|
||||
dependencies: list[str],
|
||||
python_version: str = ">=3.11",
|
||||
quiet: bool = False
|
||||
) -> str:
|
||||
"""Generate PEP 723 header with shebang"""
|
||||
shebang = "#!/usr/bin/env -S uv run --script"
|
||||
if quiet:
|
||||
shebang += " --quiet"
|
||||
|
||||
# Format dependencies for TOML array
|
||||
if dependencies:
|
||||
deps_str = ",\n# ".join(f'"{dep}"' for dep in dependencies)
|
||||
deps_section = f"# dependencies = [\n# {deps_str},\n# ]"
|
||||
else:
|
||||
deps_section = "# dependencies = []"
|
||||
|
||||
header = f"""{shebang}
|
||||
# /// script
|
||||
# requires-python = "{python_version}"
|
||||
{deps_section}
|
||||
# ///
|
||||
"""
|
||||
return header
|
||||
|
||||
|
||||
def convert_script(
|
||||
script_path: Path,
|
||||
output_path: Path | None = None,
|
||||
requirements_path: Path | None = None,
|
||||
detect_imports_flag: bool = False,
|
||||
dry_run: bool = False,
|
||||
python_version: str = ">=3.11",
|
||||
quiet_mode: bool = False,
|
||||
use_pypi: bool = True
|
||||
) -> bool:
|
||||
"""
|
||||
Convert script to uv format.
|
||||
|
||||
Returns True if successful, False otherwise.
|
||||
"""
|
||||
# Read original script
|
||||
try:
|
||||
content = script_path.read_text(encoding='utf-8')
|
||||
except (FileNotFoundError, PermissionError, OSError, UnicodeDecodeError) as e:
|
||||
print(f"Error: Cannot read {script_path}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Check if already has metadata
|
||||
if has_pep723_metadata(content):
|
||||
print(f"Error: {script_path} already has PEP 723 metadata", file=sys.stderr)
|
||||
print(" Use validate_script.py to check the existing metadata", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Determine dependencies
|
||||
dependencies = []
|
||||
|
||||
if requirements_path:
|
||||
# Use specified requirements file
|
||||
dependencies = read_requirements_file(requirements_path)
|
||||
if dependencies:
|
||||
print(f"Found {len(dependencies)} dependencies in {requirements_path}")
|
||||
else:
|
||||
# Look for requirements.txt in same directory
|
||||
default_req = script_path.parent / "requirements.txt"
|
||||
if default_req.exists():
|
||||
dependencies = read_requirements_file(default_req)
|
||||
if dependencies:
|
||||
print(f"Found {len(dependencies)} dependencies in {default_req}")
|
||||
|
||||
# Optionally detect imports
|
||||
if detect_imports_flag:
|
||||
detected = detect_imports(content)
|
||||
if detected:
|
||||
print(f"Detected imports: {', '.join(detected)}")
|
||||
print("Resolving versions...")
|
||||
|
||||
# If no dependencies from requirements, use detected
|
||||
if not dependencies:
|
||||
# Use smart version resolution
|
||||
resolved = []
|
||||
for imp in detected:
|
||||
# Normalize package name first
|
||||
normalized_pkg = normalize_package_name(imp, use_pypi)
|
||||
|
||||
# Then resolve version
|
||||
dep = resolve_package_version(imp, script_path, use_pypi=use_pypi)
|
||||
resolved.append(dep)
|
||||
|
||||
# Show what was resolved
|
||||
if imp in IMPORT_TO_PACKAGE:
|
||||
print(f" - Mapped '{imp}' → '{IMPORT_TO_PACKAGE[imp]}' (known mapping)")
|
||||
elif imp != normalized_pkg:
|
||||
print(f" - Normalized '{imp}' → '{normalized_pkg}' (auto-detected)")
|
||||
|
||||
if '>=' in dep:
|
||||
version = dep.split('>=')[1]
|
||||
source = "from project" if find_version_in_project(dep.split('>=')[0], script_path) else "from PyPI"
|
||||
print(f" - Resolved version: {version} {source}")
|
||||
else:
|
||||
print(f" - Using package: {dep} (uv will resolve version)")
|
||||
|
||||
dependencies = resolved
|
||||
|
||||
# Generate header
|
||||
header = generate_header(dependencies, python_version, quiet_mode)
|
||||
|
||||
# Remove old shebang if present
|
||||
lines = content.split('\n')
|
||||
if lines and lines[0].startswith('#!'):
|
||||
# Skip old shebang
|
||||
content_without_shebang = '\n'.join(lines[1:])
|
||||
else:
|
||||
content_without_shebang = content
|
||||
|
||||
# Combine header and content
|
||||
new_content = header + content_without_shebang
|
||||
|
||||
# Determine output path
|
||||
if output_path is None:
|
||||
# Default: add _uv before extension
|
||||
stem = script_path.stem
|
||||
suffix = script_path.suffix
|
||||
output_path = script_path.parent / f"{stem}_uv{suffix}"
|
||||
|
||||
# Dry run - just print
|
||||
if dry_run:
|
||||
print("\n" + "=" * 60)
|
||||
print("DRY RUN - Preview of converted script:")
|
||||
print("=" * 60)
|
||||
print(new_content[:500]) # Show first 500 chars
|
||||
if len(new_content) > 500:
|
||||
print(f"\n... ({len(new_content) - 500} more characters)")
|
||||
print("=" * 60)
|
||||
print(f"Would create: {output_path}")
|
||||
return True
|
||||
|
||||
# Write output
|
||||
try:
|
||||
output_path.write_text(new_content, encoding='utf-8')
|
||||
print(f"✓ Created: {output_path}")
|
||||
|
||||
# Make executable
|
||||
import stat
|
||||
current_permissions = output_path.stat().st_mode
|
||||
output_path.chmod(current_permissions | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
||||
print("✓ Made executable")
|
||||
|
||||
# Print next steps
|
||||
print("\nNext steps:")
|
||||
print(f" 1. Review dependencies in {output_path}")
|
||||
print(" 2. Add version constraints if needed")
|
||||
print(f" 3. Test: {output_path}")
|
||||
print(f" 4. Validate: validate_script.py {output_path}")
|
||||
|
||||
return True
|
||||
except (PermissionError, OSError) as e:
|
||||
print(f"Error: Cannot write {output_path}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Convert Python scripts to uv single-file format",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__
|
||||
)
|
||||
parser.add_argument('script', help='Python script to convert')
|
||||
parser.add_argument(
|
||||
'--requirements', '-r',
|
||||
help='Path to requirements.txt (default: look in same directory)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--detect-imports', '-d',
|
||||
action='store_true',
|
||||
help='Detect dependencies from imports (basic detection)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output', '-o',
|
||||
help='Output filename (default: <script>_uv.py)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--python-version', '-p',
|
||||
default='>=3.11',
|
||||
help='Python version constraint (default: >=3.11)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--quiet', '-q',
|
||||
action='store_true',
|
||||
help='Add --quiet flag to shebang'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run', '-n',
|
||||
action='store_true',
|
||||
help='Preview conversion without creating file'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-pypi',
|
||||
action='store_true',
|
||||
help='Skip PyPI queries for version resolution (faster, offline)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
script_path = Path(args.script)
|
||||
|
||||
if not script_path.exists():
|
||||
print(f"Error: File not found: {script_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Parse requirements path if provided
|
||||
req_path = Path(args.requirements) if args.requirements else None
|
||||
|
||||
# Parse output path if provided
|
||||
out_path = Path(args.output) if args.output else None
|
||||
|
||||
# Convert
|
||||
success = convert_script(
|
||||
script_path=script_path,
|
||||
output_path=out_path,
|
||||
requirements_path=req_path,
|
||||
detect_imports_flag=args.detect_imports,
|
||||
dry_run=args.dry_run,
|
||||
python_version=args.python_version,
|
||||
quiet_mode=args.quiet,
|
||||
use_pypi=not args.no_pypi
|
||||
)
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
356
skills/python-uv-scripts/tools/validate_script.py
Executable file
356
skills/python-uv-scripts/tools/validate_script.py
Executable file
@@ -0,0 +1,356 @@
|
||||
#!/usr/bin/env -S uv run --script --quiet
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""
|
||||
Validate PEP 723 inline script metadata
|
||||
|
||||
Checks Python scripts for:
|
||||
- Valid PEP 723 metadata block
|
||||
- Required fields (requires-python, dependencies)
|
||||
- TOML syntax validity
|
||||
- Shebang presence and format
|
||||
- Security issues
|
||||
|
||||
Usage:
|
||||
validate_script.py <script.py>
|
||||
validate_script.py --strict <script.py>
|
||||
validate_script.py --force <script>
|
||||
|
||||
Examples:
|
||||
# Basic validation
|
||||
validate_script.py my_script.py
|
||||
|
||||
# Strict mode (all best practices)
|
||||
validate_script.py --strict my_script.py
|
||||
|
||||
# Validate executable Python script without .py extension
|
||||
validate_script.py my_script
|
||||
|
||||
# Force validation, skip extension check
|
||||
validate_script.py --force my_script
|
||||
|
||||
# Validate all scripts in directory
|
||||
find . -name '*.py' -exec python validate_script.py {} \\;
|
||||
"""
|
||||
|
||||
import ast
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tomllib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationResult:
|
||||
"""Validation result"""
|
||||
valid: bool
|
||||
has_metadata: bool
|
||||
has_shebang: bool
|
||||
has_docstring: bool
|
||||
warnings: list[str]
|
||||
errors: list[str]
|
||||
|
||||
|
||||
def extract_metadata_block(content: str) -> str | None:
|
||||
"""Extract PEP 723 metadata block"""
|
||||
# Match metadata block with CRLF tolerance and flexible whitespace
|
||||
# Uses lookahead to allow last metadata line without trailing newline
|
||||
pattern = r'# /// script\r?\n((?:#.*(?:\r?\n|(?=\r?\n?#\s*///)))+)(?:\r?\n)?#\s*///'
|
||||
match = re.search(pattern, content, re.MULTILINE)
|
||||
|
||||
if not match:
|
||||
return None
|
||||
|
||||
# Extract TOML content (remove leading # and optional whitespace from each line)
|
||||
lines = match.group(1).splitlines()
|
||||
toml_lines = []
|
||||
for line in lines:
|
||||
if line.startswith('#'):
|
||||
# Strip '#' followed by optional space or tab
|
||||
stripped = re.sub(r'^#[ \t]?', '', line)
|
||||
toml_lines.append(stripped)
|
||||
else:
|
||||
# Preserve non-comment lines (shouldn't occur with our regex but be safe)
|
||||
toml_lines.append(line)
|
||||
return '\n'.join(toml_lines)
|
||||
|
||||
|
||||
def validate_toml_syntax(toml_content: str) -> list[str]:
|
||||
"""Validate TOML syntax using structured parsing"""
|
||||
errors = []
|
||||
|
||||
# Parse TOML content
|
||||
try:
|
||||
data = tomllib.loads(toml_content)
|
||||
except tomllib.TOMLDecodeError as e:
|
||||
errors.append(f"Invalid TOML syntax: {e}")
|
||||
return errors
|
||||
|
||||
# Validate required fields
|
||||
if 'requires-python' not in data:
|
||||
errors.append("Missing 'requires-python' field")
|
||||
elif not isinstance(data['requires-python'], str):
|
||||
errors.append("'requires-python' must be a string")
|
||||
|
||||
if 'dependencies' not in data:
|
||||
errors.append("Missing 'dependencies' field")
|
||||
else:
|
||||
dependencies = data['dependencies']
|
||||
# Dependencies should be a list/array
|
||||
if not isinstance(dependencies, list):
|
||||
errors.append("'dependencies' must be an array/list")
|
||||
else:
|
||||
# Validate each dependency item
|
||||
for idx, dep in enumerate(dependencies):
|
||||
if not isinstance(dep, str):
|
||||
errors.append(
|
||||
f"Dependency at index {idx} must be a string, got {type(dep).__name__}")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def check_shebang(content: str) -> tuple[bool, list[str]]:
|
||||
"""Check shebang line"""
|
||||
warnings = []
|
||||
lines = content.split('\n')
|
||||
|
||||
if not lines:
|
||||
return False, ["Empty file"]
|
||||
|
||||
first_line = lines[0]
|
||||
|
||||
if not first_line.startswith('#!'):
|
||||
return False, []
|
||||
|
||||
# Check for recommended shebangs
|
||||
recommended = [
|
||||
'#!/usr/bin/env -S uv run --script',
|
||||
'#!/usr/bin/env -S uv run --script --quiet',
|
||||
]
|
||||
|
||||
if first_line not in recommended:
|
||||
warnings.append(f"Shebang not recommended. Use: {recommended[0]}")
|
||||
|
||||
return True, warnings
|
||||
|
||||
|
||||
def check_security_issues(content: str) -> list[str]:
|
||||
"""Check for common security issues"""
|
||||
warnings = []
|
||||
|
||||
# Check for hardcoded secrets
|
||||
secret_patterns = [
|
||||
(r'password\s*=\s*["\']', "Possible hardcoded password"),
|
||||
(r'api[_-]?key\s*=\s*["\']', "Possible hardcoded API key"),
|
||||
(r'secret\s*=\s*["\']', "Possible hardcoded secret"),
|
||||
(r'token\s*=\s*["\']', "Possible hardcoded token"),
|
||||
]
|
||||
|
||||
for pattern, message in secret_patterns:
|
||||
if re.search(pattern, content, re.IGNORECASE):
|
||||
warnings.append(f"Security: {message}")
|
||||
|
||||
# Check for shell=True
|
||||
if re.search(r'shell\s*=\s*True', content):
|
||||
warnings.append(
|
||||
"Security: subprocess.run with shell=True (command injection risk)")
|
||||
|
||||
# Check for eval/exec
|
||||
if re.search(r'\b(eval|exec)\s*\(', content):
|
||||
warnings.append(
|
||||
"Security: Use of eval() or exec() (code injection risk)")
|
||||
|
||||
return warnings
|
||||
|
||||
|
||||
def is_valid_python_file(script_path: Path) -> tuple[bool, str]:
|
||||
"""
|
||||
Check if a non-.py file is a valid Python script.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, reason) where reason describes why it's valid or invalid
|
||||
"""
|
||||
try:
|
||||
content = script_path.read_text(encoding='utf-8')
|
||||
except (FileNotFoundError, PermissionError, OSError, UnicodeDecodeError) as e:
|
||||
return False, f"Cannot read file: {e}"
|
||||
|
||||
# Check if file is executable with Python shebang
|
||||
is_executable = os.access(script_path, os.X_OK)
|
||||
lines = content.split('\n')
|
||||
has_python_shebang = False
|
||||
|
||||
if lines and lines[0].startswith('#!'):
|
||||
shebang = lines[0].lower()
|
||||
has_python_shebang = 'python' in shebang
|
||||
|
||||
if is_executable and has_python_shebang:
|
||||
return True, "executable with Python shebang"
|
||||
|
||||
# Try to parse as Python to confirm it's valid Python code
|
||||
try:
|
||||
ast.parse(content)
|
||||
return True, "valid Python syntax"
|
||||
except SyntaxError as e:
|
||||
return False, f"not valid Python: {e}"
|
||||
|
||||
|
||||
def validate_script(script_path: Path, strict: bool = False) -> ValidationResult:
|
||||
"""Validate Python script"""
|
||||
result = ValidationResult(
|
||||
valid=True,
|
||||
has_metadata=False,
|
||||
has_shebang=False,
|
||||
has_docstring=False,
|
||||
warnings=[],
|
||||
errors=[]
|
||||
)
|
||||
|
||||
# Read file
|
||||
try:
|
||||
content = script_path.read_text(encoding='utf-8')
|
||||
except (FileNotFoundError, PermissionError, OSError, UnicodeDecodeError) as e:
|
||||
result.valid = False
|
||||
result.errors.append(f"Failed to read file: {e}")
|
||||
return result
|
||||
|
||||
# Check shebang
|
||||
has_shebang, shebang_warnings = check_shebang(content)
|
||||
result.has_shebang = has_shebang
|
||||
result.warnings.extend(shebang_warnings)
|
||||
|
||||
if strict and not has_shebang:
|
||||
result.errors.append("Missing shebang (required in strict mode)")
|
||||
result.valid = False
|
||||
|
||||
# Check for metadata block
|
||||
metadata = extract_metadata_block(content)
|
||||
result.has_metadata = metadata is not None
|
||||
|
||||
if not metadata:
|
||||
result.errors.append("No PEP 723 metadata block found")
|
||||
result.valid = False
|
||||
return result
|
||||
|
||||
# Validate TOML syntax
|
||||
toml_errors = validate_toml_syntax(metadata)
|
||||
result.errors.extend(toml_errors)
|
||||
|
||||
if toml_errors:
|
||||
result.valid = False
|
||||
|
||||
# Check for module docstring using AST parsing
|
||||
try:
|
||||
module_node = ast.parse(content)
|
||||
module_docstring = ast.get_docstring(module_node)
|
||||
result.has_docstring = module_docstring is not None
|
||||
except SyntaxError as e:
|
||||
result.has_docstring = False
|
||||
result.warnings.append(
|
||||
f"Could not parse file for docstring check: {e}")
|
||||
|
||||
if strict and not result.has_docstring:
|
||||
result.warnings.append(
|
||||
"Missing module docstring (recommended in strict mode)")
|
||||
|
||||
# Security checks (always warnings, never errors - these are heuristic checks)
|
||||
security_warnings = check_security_issues(content)
|
||||
result.warnings.extend(security_warnings)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate PEP 723 script metadata",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__
|
||||
)
|
||||
parser.add_argument('script', help='Python script to validate')
|
||||
parser.add_argument('--strict', action='store_true',
|
||||
help='Enable strict validation')
|
||||
parser.add_argument('--force', action='store_true',
|
||||
help='Skip Python file extension check')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
script_path = Path(args.script)
|
||||
|
||||
if not script_path.exists():
|
||||
print(f"Error: File not found: {script_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Check if file is a Python file
|
||||
if script_path.suffix != '.py':
|
||||
if args.force:
|
||||
print(
|
||||
f"Warning: File lacks .py extension, but --force was specified", file=sys.stderr)
|
||||
else:
|
||||
# Check if it's a valid Python file by other means
|
||||
is_valid, reason = is_valid_python_file(script_path)
|
||||
if not is_valid:
|
||||
print(
|
||||
f"Error: Not a Python file: {script_path}", file=sys.stderr)
|
||||
print(f" Reason: {reason}", file=sys.stderr)
|
||||
print(f" Hint: File must either:", file=sys.stderr)
|
||||
print(f" - Have a .py extension, OR", file=sys.stderr)
|
||||
print(f" - Be executable with a Python shebang, OR",
|
||||
file=sys.stderr)
|
||||
print(f" - Contain valid Python syntax", file=sys.stderr)
|
||||
print(f" Use --force to skip this check", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(
|
||||
f"Info: File accepted as Python ({reason})", file=sys.stderr)
|
||||
|
||||
# Validate
|
||||
result = validate_script(script_path, strict=args.strict)
|
||||
|
||||
# Print results
|
||||
print(f"Validating: {script_path}")
|
||||
print("=" * 60)
|
||||
|
||||
if result.has_shebang:
|
||||
print("✓ Has shebang")
|
||||
else:
|
||||
print("✗ Missing shebang")
|
||||
|
||||
if result.has_metadata:
|
||||
print("✓ Has PEP 723 metadata")
|
||||
else:
|
||||
print("✗ Missing PEP 723 metadata")
|
||||
|
||||
if result.has_docstring:
|
||||
print("✓ Has docstring")
|
||||
else:
|
||||
print("○ No docstring")
|
||||
|
||||
if result.warnings:
|
||||
print("\nWarnings:")
|
||||
for warning in result.warnings:
|
||||
print(f" ⚠ {warning}")
|
||||
|
||||
if result.errors:
|
||||
print("\nErrors:")
|
||||
for error in result.errors:
|
||||
print(f" ✗ {error}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
if result.valid:
|
||||
print("Status: ✓ VALID")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("Status: ✗ INVALID")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
67
skills/python-uv-scripts/workflows/ci-cd-integration.md
Normal file
67
skills/python-uv-scripts/workflows/ci-cd-integration.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# CI/CD Integration for UV Scripts
|
||||
|
||||
> **Status**: 🚧 Placeholder - Content in development
|
||||
|
||||
## Overview
|
||||
|
||||
Integrating UV single-file scripts into CI/CD pipelines with GitHub Actions, GitLab CI, and other platforms.
|
||||
|
||||
## Topics to Cover
|
||||
|
||||
- [ ] GitHub Actions workflows
|
||||
- [ ] GitLab CI configuration
|
||||
- [ ] Pre-commit hooks integration
|
||||
- [ ] Automated testing
|
||||
- [ ] Security scanning
|
||||
- [ ] Deployment strategies
|
||||
- [ ] Version management
|
||||
|
||||
## Quick Example
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
name: Test UV Scripts
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Run script tests
|
||||
run: |
|
||||
uv run scripts/check_health.py --validate
|
||||
uv run scripts/analyze_data.py --dry-run
|
||||
```
|
||||
|
||||
### Pre-commit Hook
|
||||
|
||||
```yaml
|
||||
# .pre-commit-config.yaml
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: validate-uv-scripts
|
||||
name: Validate UV Scripts
|
||||
entry: uv run scripts/validate_all.py
|
||||
language: system
|
||||
pass_filenames: false
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
This file will be expanded to include:
|
||||
|
||||
- Complete GitHub Actions examples
|
||||
- GitLab CI patterns
|
||||
- Pre-commit hook configurations
|
||||
- Automated deployment workflows
|
||||
- Security scanning integration
|
||||
58
skills/python-uv-scripts/workflows/team-adoption.md
Normal file
58
skills/python-uv-scripts/workflows/team-adoption.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Team Adoption Guide
|
||||
|
||||
> **Status**: 🚧 Placeholder - Content in development
|
||||
|
||||
## Overview
|
||||
|
||||
Guide for rolling out UV single-file scripts across development teams, establishing standards, and ensuring adoption.
|
||||
|
||||
## Topics to Cover
|
||||
|
||||
- [ ] Introduction and training materials
|
||||
- [ ] Team standards and conventions
|
||||
- [ ] Code review guidelines
|
||||
- [ ] Migration from existing scripts
|
||||
- [ ] Tooling setup across platforms
|
||||
- [ ] Documentation requirements
|
||||
- [ ] Success metrics and tracking
|
||||
|
||||
## Adoption Roadmap
|
||||
|
||||
### Phase 1: Pilot (Week 1-2)
|
||||
|
||||
- Select 2-3 team members for initial trial
|
||||
- Convert 3-5 existing scripts
|
||||
- Gather feedback and iterate
|
||||
|
||||
### Phase 2: Expansion (Week 3-4)
|
||||
|
||||
- Team-wide training session
|
||||
- Establish coding standards
|
||||
- Set up CI/CD integration
|
||||
- Create internal documentation
|
||||
|
||||
### Phase 3: Full Adoption (Week 5+)
|
||||
|
||||
- All new scripts use UV format
|
||||
- Migration plan for legacy scripts
|
||||
- Regular reviews and improvements
|
||||
|
||||
## Team Standards Checklist
|
||||
|
||||
- [ ] Required shebang format: `#!/usr/bin/env -S uv run`
|
||||
- [ ] Minimum Python version: `>=3.11`
|
||||
- [ ] Required metadata fields
|
||||
- [ ] Naming conventions
|
||||
- [ ] Documentation requirements
|
||||
- [ ] Testing requirements
|
||||
- [ ] Security review process
|
||||
|
||||
## TODO
|
||||
|
||||
This file will be expanded to include:
|
||||
|
||||
- Detailed training materials
|
||||
- Complete adoption roadmap
|
||||
- Team communication templates
|
||||
- Migration guides
|
||||
- Success stories and case studies
|
||||
72
skills/python-uv-scripts/workflows/testing-strategies.md
Normal file
72
skills/python-uv-scripts/workflows/testing-strategies.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Testing Strategies for UV Scripts
|
||||
|
||||
> **Status**: 🚧 Placeholder - Content in development
|
||||
|
||||
## Overview
|
||||
|
||||
Strategies for testing UV single-file scripts, from inline tests to pytest integration.
|
||||
|
||||
## Topics to Cover
|
||||
|
||||
- [ ] Inline test functions
|
||||
- [ ] pytest integration
|
||||
- [ ] Mock and fixtures for testing
|
||||
- [ ] Testing CLI applications
|
||||
- [ ] API client testing with mocks
|
||||
- [ ] Coverage reporting
|
||||
- [ ] Test automation in CI/CD
|
||||
|
||||
## Quick Example
|
||||
|
||||
### Inline Testing
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# ///
|
||||
def add(a: int, b: int) -> int:
|
||||
return a + b
|
||||
|
||||
def test_add():
|
||||
assert add(2, 3) == 5
|
||||
assert add(-1, 1) == 0
|
||||
print("✓ All tests passed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run tests
|
||||
test_add()
|
||||
|
||||
# Or run main application
|
||||
result = add(10, 20)
|
||||
print(f"Result: {result}")
|
||||
```
|
||||
|
||||
### Pytest Integration
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = ["pytest>=7.0.0"]
|
||||
# ///
|
||||
"""
|
||||
Run with: pytest this_script.py
|
||||
"""
|
||||
def multiply(a: int, b: int) -> int:
|
||||
return a * b
|
||||
|
||||
def test_multiply():
|
||||
assert multiply(2, 3) == 6
|
||||
assert multiply(-1, 5) == -5
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
This file will be expanded to include:
|
||||
|
||||
- Complete testing patterns
|
||||
- Mocking external dependencies
|
||||
- Test organization strategies
|
||||
- CI/CD integration examples
|
||||
- Coverage tools and reporting
|
||||
Reference in New Issue
Block a user