chore: import upstream snapshot with attribution
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:58 +08:00
commit bb5c75ce05
8824 changed files with 1946442 additions and 0 deletions
@@ -0,0 +1,111 @@
# Python Linter
Run Python code linting and formatting tools.
## Purpose
This command helps you maintain code quality using Python's best linting and formatting tools.
## Usage
```
/lint
```
## What this command does
1. **Runs multiple linters** (flake8, pylint, black, isort)
2. **Provides detailed feedback** on code quality issues
3. **Auto-fixes formatting** where possible
4. **Checks type hints** if mypy is configured
## Example Commands
### Black (code formatting)
```bash
# Format all Python files
black .
# Check formatting without changing files
black --check .
# Format specific file
black src/main.py
```
### flake8 (style guide enforcement)
```bash
# Check all Python files
flake8 .
# Check specific directory
flake8 src/
# Check with specific rules
flake8 --max-line-length=88 .
```
### isort (import sorting)
```bash
# Sort imports in all files
isort .
# Check import sorting
isort --check-only .
# Sort imports in specific file
isort src/main.py
```
### pylint (comprehensive linting)
```bash
# Run pylint on all files
pylint src/
# Run with specific score threshold
pylint --fail-under=8.0 src/
# Generate detailed report
pylint --output-format=html src/ > pylint_report.html
```
### mypy (type checking)
```bash
# Check types in all files
mypy .
# Check specific module
mypy src/models.py
# Check with strict mode
mypy --strict src/
```
## Configuration Files
Most projects benefit from configuration files:
### .flake8
```ini
[flake8]
max-line-length = 88
exclude = .git,__pycache__,venv
ignore = E203,W503
```
### pyproject.toml
```toml
[tool.black]
line-length = 88
[tool.isort]
profile = "black"
```
## Best Practices
- Run linters before committing code
- Use consistent formatting across the project
- Fix linting issues promptly
- Configure linters to match your team's style
- Use type hints for better code documentation
@@ -0,0 +1,73 @@
# Test Runner
Run Python tests with pytest, unittest, or other testing frameworks.
## Purpose
This command helps you run Python tests effectively with proper configuration and reporting.
## Usage
```
/test
```
## What this command does
1. **Detects test framework** (pytest, unittest, nose2)
2. **Runs appropriate tests** with proper configuration
3. **Provides coverage reporting** if available
4. **Shows clear test results** with failure details
## Example Commands
### pytest (recommended)
```bash
# Run all tests
pytest
# Run with coverage
pytest --cov=src --cov-report=html
# Run specific test file
pytest tests/test_models.py
# Run with verbose output
pytest -v
# Run tests matching pattern
pytest -k "test_user"
```
### unittest
```bash
# Run all tests
python -m unittest discover
# Run specific test file
python -m unittest tests.test_models
# Run with verbose output
python -m unittest -v
```
### Django tests
```bash
# Run all Django tests
python manage.py test
# Run specific app tests
python manage.py test myapp
# Run with coverage
coverage run --source='.' manage.py test
coverage report
```
## Best Practices
- Write tests for all critical functionality
- Use descriptive test names
- Keep tests isolated and independent
- Mock external dependencies
- Aim for high test coverage (80%+)
@@ -0,0 +1,153 @@
{
"permissions": {
"allow": [
"Bash",
"Edit",
"MultiEdit",
"Write",
"Bash(python:*)",
"Bash(pip:*)",
"Bash(pytest:*)",
"Bash(black:*)",
"Bash(isort:*)",
"Bash(flake8:*)",
"Bash(mypy:*)",
"Bash(django-admin:*)",
"Bash(flask:*)",
"Bash(uvicorn:*)",
"Bash(gunicorn:*)",
"Bash(git:*)"
],
"deny": [
"Bash(curl:*)",
"Bash(wget:*)",
"Bash(rm -rf:*)"
],
"defaultMode": "allowEdits"
},
"env": {
"BASH_DEFAULT_TIMEOUT_MS": "60000",
"BASH_MAX_OUTPUT_LENGTH": "20000",
"CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR": "1",
"PYTHONPATH": "."
},
"includeCoAuthoredBy": true,
"cleanupPeriodDays": 30,
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '\"\\(.tool_input.command) - \\(.tool_input.description // \"No description\")\"' >> ~/.claude/bash-command-log.txt"
}
]
},
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(echo $STDIN_JSON | jq -r '.tool_input.file_path // \"\"); CONTENT=$(echo $STDIN_JSON | jq -r '.tool_input.content // \"\"); if [[ \"$FILE\" =~ \\.py$ ]] && echo \"$CONTENT\" | grep -q 'print('; then echo 'Warning: print() statements should be replaced with logging' >&2; exit 2; fi"
}
]
},
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(echo $STDIN_JSON | jq -r '.tool_input.file_path // \"\"'); if [[ \"$FILE\" == \"requirements.txt\" ]] || [[ \"$FILE\" == \"pyproject.toml\" ]] || [[ \"$FILE\" == \"setup.py\" ]]; then echo 'Checking for vulnerable dependencies...'; if command -v safety >/dev/null 2>&1; then safety check; elif command -v pip-audit >/dev/null 2>&1; then pip-audit; else echo 'No security audit tool found. Install safety or pip-audit'; fi; fi",
"timeout": 60
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(echo $STDIN_JSON | jq -r '.tool_input.file_path // \"\"'); if [[ \"$FILE\" =~ \\.py$ ]]; then black \"$FILE\" 2>/dev/null || echo 'Black formatting skipped (not installed)'; fi",
"timeout": 30
}
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(echo $STDIN_JSON | jq -r '.tool_input.file_path // \"\"'); if [[ \"$FILE\" =~ \\.py$ ]]; then isort \"$FILE\" 2>/dev/null || echo 'isort skipped (not installed)'; fi",
"timeout": 30
}
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(echo $STDIN_JSON | jq -r '.tool_input.file_path // \"\"'); if [[ \"$FILE\" =~ \\.py$ ]]; then RESULT=$(flake8 \"$FILE\" 2>&1); if [ $? -ne 0 ] && command -v flake8 >/dev/null 2>&1; then echo \"Flake8 linting issues found: $RESULT\" >&2; exit 2; fi; fi",
"timeout": 30
}
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(echo $STDIN_JSON | jq -r '.tool_input.file_path // \"\"'); if [[ \"$FILE\" =~ \\.py$ ]]; then RESULT=$(mypy \"$FILE\" 2>&1); if [ $? -ne 0 ] && command -v mypy >/dev/null 2>&1; then echo \"MyPy type checking issues found: $RESULT\" >&2; exit 2; fi; fi",
"timeout": 30
}
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(echo $STDIN_JSON | jq -r '.tool_input.file_path // \"\"'); if [[ \"$FILE\" =~ \\.py$ && \"$FILE\" != *\"test_\"* && \"$FILE\" != *\"_test.py\" ]]; then DIR=$(dirname \"$FILE\"); BASENAME=$(basename \"$FILE\" .py); for TEST_FILE in \"$DIR/test_$BASENAME.py\" \"$DIR/${BASENAME}_test.py\" \"tests/test_$BASENAME.py\"; do if [ -f \"$TEST_FILE\" ]; then echo \"Running tests for $TEST_FILE...\"; if command -v pytest >/dev/null 2>&1; then pytest \"$TEST_FILE\" -v; elif python -m pytest \"$TEST_FILE\" 2>/dev/null; then python -m pytest \"$TEST_FILE\" -v; else python -m unittest \"$TEST_FILE\" 2>/dev/null || echo 'No test runner found'; fi; break; fi; done; fi",
"timeout": 60
}
]
}
],
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "echo \"Claude Code notification: $(date)\" >> ~/.claude/notifications.log"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "if [[ -f requirements.txt || -f pyproject.toml || -f setup.py ]] && [[ $(git status --porcelain | grep '\\.py$') ]]; then echo 'Running linter on changed Python files...'; if command -v flake8 >/dev/null 2>&1; then flake8 $(git diff --name-only --diff-filter=ACMR | grep '\\.py$'); elif command -v pylint >/dev/null 2>&1; then pylint $(git diff --name-only --diff-filter=ACMR | grep '\\.py$'); else echo 'No Python linter found (flake8/pylint)'; fi; fi",
"timeout": 60
}
]
},
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "if [[ -f requirements.txt || -f pyproject.toml || -f setup.py ]] && [[ $(git status --porcelain | grep '\\.py$') ]]; then echo 'Running type checking on changed files...'; if command -v mypy >/dev/null 2>&1; then mypy $(git diff --name-only --diff-filter=ACMR | grep '\\.py$') || echo 'Type checking completed with issues'; else echo 'MyPy not found for type checking'; fi; fi",
"timeout": 60
}
]
}
]
}
}
+78
View File
@@ -0,0 +1,78 @@
{
"mcpServers": {
"python-sdk": {
"name": "Python SDK",
"description": "Official Python SDK with FastMCP for rapid MCP development",
"command": "python",
"args": ["-m", "python_sdk.server"],
"env": {}
},
"docker": {
"name": "Docker MCP",
"description": "Isolated code execution via Docker containers",
"command": "python",
"args": ["-m", "mcp_server_docker"],
"env": {}
},
"jupyter": {
"name": "Jupyter MCP",
"description": "MCP integration for interactive Jupyter notebooks",
"command": "python",
"args": ["-m", "server_jupyter"],
"env": {}
},
"postgresql": {
"name": "PostgreSQL MCP",
"description": "Natural language queries to PostgreSQL databases",
"command": "python",
"args": ["-m", "server_postgres"],
"env": {
"DATABASE_URL": "postgresql://user:pass@host/db"
}
},
"opik": {
"name": "Opik MCP",
"description": "Observability for LLM apps with tracing and metrics",
"command": "python",
"args": ["-m", "opik_mcp"],
"env": {}
},
"memory-bank": {
"name": "Memory Bank MCP",
"description": "Centralized memory system for AI agents",
"command": "server-memory",
"args": [],
"env": {}
},
"sequential-thinking": {
"name": "Sequential Thinking MCP",
"description": "Helps LLMs decompose complex tasks into logical steps",
"command": "code-reasoning",
"args": [],
"env": {}
},
"brave-search": {
"name": "Brave Search MCP",
"description": "Privacy-focused web search tool",
"command": "server-brave-search",
"args": [],
"env": {}
},
"google-maps": {
"name": "Google Maps MCP",
"description": "Integrates Google Maps for geolocation and directions",
"command": "server-google-maps",
"args": [],
"env": {
"GOOGLE_MAPS_API_KEY": "..."
}
},
"deep-graph": {
"name": "Deep Graph MCP (Code Graph)",
"description": "Transforms source code into semantic graphs via DeepGraph",
"command": "mcp-code-graph",
"args": [],
"env": {}
}
}
}
+276
View File
@@ -0,0 +1,276 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a Python project optimized for modern Python development. The project uses industry-standard tools and follows best practices for scalable application development.
## Development Commands
### Environment Management
- `python -m venv venv` - Create virtual environment
- `source venv/bin/activate` (Linux/Mac) or `venv\Scripts\activate` (Windows) - Activate virtual environment
- `deactivate` - Deactivate virtual environment
- `pip install -r requirements.txt` - Install dependencies
- `pip install -r requirements-dev.txt` - Install development dependencies
### Package Management
- `pip install <package>` - Install a package
- `pip install -e .` - Install project in development mode
- `pip freeze > requirements.txt` - Generate requirements file
- `pip-tools compile requirements.in` - Compile requirements with pip-tools
### Testing Commands
- `pytest` - Run all tests
- `pytest -v` - Run tests with verbose output
- `pytest --cov` - Run tests with coverage report
- `pytest --cov-report=html` - Generate HTML coverage report
- `pytest -x` - Stop on first failure
- `pytest -k "test_name"` - Run specific test by name
- `python -m unittest` - Run tests with unittest
### Code Quality Commands
- `black .` - Format code with Black
- `black --check .` - Check code formatting without changes
- `isort .` - Sort imports
- `isort --check-only .` - Check import sorting
- `flake8` - Run linting with Flake8
- `pylint src/` - Run linting with Pylint
- `mypy src/` - Run type checking with MyPy
### Development Tools
- `python -m pip install --upgrade pip` - Upgrade pip
- `python -c "import sys; print(sys.version)"` - Check Python version
- `python -m site` - Show Python site information
- `python -m pdb script.py` - Debug with pdb
## Technology Stack
### Core Technologies
- **Python** - Primary programming language (3.8+)
- **pip** - Package management
- **venv** - Virtual environment management
### Common Frameworks
- **Django** - High-level web framework
- **Flask** - Micro web framework
- **FastAPI** - Modern API framework with automatic documentation
- **SQLAlchemy** - SQL toolkit and ORM
- **Pydantic** - Data validation using Python type hints
### Data Science & ML
- **NumPy** - Numerical computing
- **Pandas** - Data manipulation and analysis
- **Matplotlib/Seaborn** - Data visualization
- **Scikit-learn** - Machine learning library
- **TensorFlow/PyTorch** - Deep learning frameworks
### Testing Frameworks
- **pytest** - Testing framework
- **unittest** - Built-in testing framework
- **pytest-cov** - Coverage plugin for pytest
- **factory-boy** - Test fixtures
- **responses** - Mock HTTP requests
### Code Quality Tools
- **Black** - Code formatter
- **isort** - Import sorter
- **flake8** - Style guide enforcement
- **pylint** - Code analysis
- **mypy** - Static type checker
- **pre-commit** - Git hooks framework
## Project Structure Guidelines
### File Organization
```
src/
├── package_name/
│ ├── __init__.py
│ ├── main.py # Application entry point
│ ├── models/ # Data models
│ ├── views/ # Web views (Django/Flask)
│ ├── api/ # API endpoints
│ ├── services/ # Business logic
│ ├── utils/ # Utility functions
│ └── config/ # Configuration files
tests/
├── __init__.py
├── conftest.py # pytest configuration
├── test_models.py
├── test_views.py
└── test_utils.py
requirements/
├── base.txt # Base requirements
├── dev.txt # Development requirements
└── prod.txt # Production requirements
```
### Naming Conventions
- **Files/Modules**: Use snake_case (`user_profile.py`)
- **Classes**: Use PascalCase (`UserProfile`)
- **Functions/Variables**: Use snake_case (`get_user_data`)
- **Constants**: Use UPPER_SNAKE_CASE (`API_BASE_URL`)
- **Private methods**: Prefix with underscore (`_private_method`)
## Python Guidelines
### Type Hints
- Use type hints for function parameters and return values
- Import types from `typing` module when needed
- Use `Optional` for nullable values
- Use `Union` for multiple possible types
- Document complex types with comments
### Code Style
- Follow PEP 8 style guide
- Use meaningful variable and function names
- Keep functions focused and single-purpose
- Use docstrings for modules, classes, and functions
- Limit line length to 88 characters (Black default)
### Best Practices
- Use list comprehensions for simple transformations
- Prefer `pathlib` over `os.path` for file operations
- Use context managers (`with` statements) for resource management
- Handle exceptions appropriately with try/except blocks
- Use `logging` module instead of print statements
## Testing Standards
### Test Structure
- Organize tests to mirror source code structure
- Use descriptive test names that explain the behavior
- Follow AAA pattern (Arrange, Act, Assert)
- Use fixtures for common test data
- Group related tests in classes
### Coverage Goals
- Aim for 90%+ test coverage
- Write unit tests for business logic
- Use integration tests for external dependencies
- Mock external services in tests
- Test error conditions and edge cases
### pytest Configuration
```python
# pytest.ini or pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "--cov=src --cov-report=term-missing"
```
## Virtual Environment Setup
### Creation and Activation
```bash
# Create virtual environment
python -m venv venv
# Activate (Linux/Mac)
source venv/bin/activate
# Activate (Windows)
venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
pip install -r requirements-dev.txt
```
### Requirements Management
- Use `requirements.txt` for production dependencies
- Use `requirements-dev.txt` for development dependencies
- Consider using `pip-tools` for dependency resolution
- Pin versions for reproducible builds
## Django-Specific Guidelines
### Project Structure
```
project_name/
├── manage.py
├── project_name/
│ ├── __init__.py
│ ├── settings/
│ ├── urls.py
│ └── wsgi.py
├── apps/
│ ├── users/
│ ├── products/
│ └── orders/
└── requirements/
```
### Common Commands
- `python manage.py runserver` - Start development server
- `python manage.py migrate` - Apply database migrations
- `python manage.py makemigrations` - Create new migrations
- `python manage.py createsuperuser` - Create admin user
- `python manage.py collectstatic` - Collect static files
- `python manage.py test` - Run Django tests
## FastAPI-Specific Guidelines
### Project Structure
```
src/
├── main.py # FastAPI application
├── api/
│ ├── __init__.py
│ ├── dependencies.py # Dependency injection
│ └── v1/
│ ├── __init__.py
│ └── endpoints/
├── core/
│ ├── __init__.py
│ ├── config.py # Settings
│ └── security.py # Authentication
├── models/
├── schemas/ # Pydantic models
└── services/
```
### Common Commands
- `uvicorn main:app --reload` - Start development server
- `uvicorn main:app --host 0.0.0.0 --port 8000` - Start production server
## Security Guidelines
### Dependencies
- Regularly update dependencies with `pip list --outdated`
- Use `safety` package to check for known vulnerabilities
- Pin dependency versions in requirements files
- Use virtual environments to isolate dependencies
### Code Security
- Validate input data with Pydantic or similar
- Use environment variables for sensitive configuration
- Implement proper authentication and authorization
- Sanitize data before database operations
- Use HTTPS for production deployments
## Development Workflow
### Before Starting
1. Check Python version compatibility
2. Create and activate virtual environment
3. Install dependencies from requirements files
4. Run type checking with `mypy`
### During Development
1. Use type hints for better code documentation
2. Run tests frequently to catch issues early
3. Use meaningful commit messages
4. Format code with Black before committing
### Before Committing
1. Run full test suite: `pytest`
2. Check code formatting: `black --check .`
3. Sort imports: `isort --check-only .`
4. Run linting: `flake8`
5. Run type checking: `mypy src/`
@@ -0,0 +1,264 @@
# Django Admin Configuration
Configure Django admin interface with custom admin classes and functionality.
## Purpose
This command helps you create comprehensive Django admin configurations with advanced features and customizations.
## Usage
```
/admin
```
## What this command does
1. **Registers models** with custom admin classes
2. **Creates advanced admin interfaces** with filtering, search, and actions
3. **Adds inline editing** for related models
4. **Customizes list displays** and forms
5. **Implements admin actions** for bulk operations
## Example Output
```python
# admin.py
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.utils.safestring import mark_safe
from .models import Post, Category, Tag, Comment
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
"""Admin configuration for Category model."""
list_display = ['name', 'slug', 'post_count', 'created_at']
list_filter = ['created_at']
search_fields = ['name', 'description']
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ['created_at', 'updated_at']
def post_count(self, obj):
"""Display number of posts in this category."""
count = obj.posts.count()
url = reverse('admin:blog_post_changelist') + f'?category__id__exact={obj.id}'
return format_html('<a href="{}">{} posts</a>', url, count)
post_count.short_description = 'Posts'
class CommentInline(admin.TabularInline):
"""Inline admin for comments."""
model = Comment
extra = 0
readonly_fields = ['created_at', 'author']
fields = ['author', 'content', 'is_approved', 'created_at']
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
"""Advanced admin configuration for Post model."""
list_display = [
'title',
'author',
'category',
'status',
'view_count',
'created_at',
'post_preview'
]
list_filter = [
'status',
'category',
'created_at',
'updated_at',
('author', admin.RelatedOnlyFieldListFilter)
]
search_fields = ['title', 'content', 'author__username']
prepopulated_fields = {'slug': ('title',)}
readonly_fields = ['created_at', 'updated_at', 'view_count', 'post_preview']
# Custom form layout
fieldsets = (
('Content', {
'fields': ('title', 'slug', 'content', 'status')
}),
('Metadata', {
'fields': ('author', 'category', 'tags'),
'classes': ('collapse',)
}),
('SEO', {
'fields': ('meta_description', 'meta_keywords'),
'classes': ('collapse',)
}),
('Timestamps', {
'fields': ('created_at', 'updated_at', 'view_count'),
'classes': ('collapse',)
}),
)
# Many-to-many field display
filter_horizontal = ['tags']
# Inline models
inlines = [CommentInline]
# Custom list display methods
def post_preview(self, obj):
"""Show thumbnail preview of post."""
if obj.featured_image:
return format_html(
'<img src="{}" width="50" height="50" style="border-radius: 5px;" />',
obj.featured_image.url
)
return "No image"
post_preview.short_description = 'Preview'
# Custom admin actions
actions = ['make_published', 'make_draft', 'duplicate_posts']
def make_published(self, request, queryset):
"""Bulk action to publish selected posts."""
updated = queryset.update(status='published')
self.message_user(
request,
f'{updated} posts were successfully marked as published.'
)
make_published.short_description = "Mark selected posts as published"
def make_draft(self, request, queryset):
"""Bulk action to set selected posts as draft."""
updated = queryset.update(status='draft')
self.message_user(
request,
f'{updated} posts were successfully marked as draft.'
)
make_draft.short_description = "Mark selected posts as draft"
def duplicate_posts(self, request, queryset):
"""Bulk action to duplicate selected posts."""
count = 0
for post in queryset:
post.pk = None # Create new instance
post.title = f"Copy of {post.title}"
post.slug = f"copy-of-{post.slug}"
post.status = 'draft'
post.save()
count += 1
self.message_user(
request,
f'{count} posts were successfully duplicated.'
)
duplicate_posts.short_description = "Duplicate selected posts"
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
"""Admin configuration for Tag model."""
list_display = ['name', 'slug', 'post_count', 'color_preview']
search_fields = ['name']
prepopulated_fields = {'slug': ('name',)}
def post_count(self, obj):
"""Display number of posts with this tag."""
return obj.posts.count()
post_count.short_description = 'Posts'
def color_preview(self, obj):
"""Show color preview if tag has color field."""
if hasattr(obj, 'color') and obj.color:
return format_html(
'<span style="background-color: {}; padding: 2px 8px; border-radius: 3px; color: white;">{}</span>',
obj.color,
obj.name
)
return obj.name
color_preview.short_description = 'Preview'
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
"""Admin configuration for Comment model."""
list_display = ['author', 'post', 'content_preview', 'is_approved', 'created_at']
list_filter = ['is_approved', 'created_at', 'post__category']
search_fields = ['content', 'author__username', 'post__title']
readonly_fields = ['created_at', 'updated_at']
actions = ['approve_comments', 'disapprove_comments']
def content_preview(self, obj):
"""Show truncated content preview."""
return obj.content[:50] + "..." if len(obj.content) > 50 else obj.content
content_preview.short_description = 'Content'
def approve_comments(self, request, queryset):
"""Bulk approve comments."""
updated = queryset.update(is_approved=True)
self.message_user(request, f'{updated} comments were approved.')
approve_comments.short_description = "Approve selected comments"
def disapprove_comments(self, request, queryset):
"""Bulk disapprove comments."""
updated = queryset.update(is_approved=False)
self.message_user(request, f'{updated} comments were disapproved.')
disapprove_comments.short_description = "Disapprove selected comments"
# Custom admin site configuration
admin.site.site_header = "Blog Administration"
admin.site.site_title = "Blog Admin Portal"
admin.site.index_title = "Welcome to Blog Administration"
```
## Advanced Features
### Custom List Display
- **Thumbnail previews** for images
- **Related object counts** with links
- **Status indicators** with colors
- **Custom formatting** for data display
### Filtering and Search
- **Advanced filters** including date ranges
- **Related field filtering** for foreign keys
- **Search across multiple fields** including relations
- **Custom filter classes** for complex queries
### Inline Editing
- **TabularInline** for compact editing
- **StackedInline** for detailed forms
- **Custom inline forms** with additional functionality
- **Readonly fields** in inlines
### Bulk Actions
- **Status changes** for multiple objects
- **Data export** functionality
- **Batch operations** for efficiency
- **Custom business logic** in actions
### Form Customization
- **Fieldsets** for organized layouts
- **Collapsed sections** for advanced options
- **Custom widgets** for better UX
- **Validation** and error handling
## Security Considerations
- **Permission checks** in custom methods
- **Input sanitization** in admin actions
- **CSRF protection** (automatic)
- **User authentication** (built-in)
## Performance Optimization
```python
# Optimize queries with select_related/prefetch_related
class PostAdmin(admin.ModelAdmin):
def get_queryset(self, request):
queryset = super().get_queryset(request)
return queryset.select_related('author', 'category').prefetch_related('tags')
```
## Custom Admin Templates
```python
# Override admin templates
class PostAdmin(admin.ModelAdmin):
change_form_template = 'admin/blog/post/change_form.html'
change_list_template = 'admin/blog/post/change_list.html'
```
@@ -0,0 +1,124 @@
# Django Model Generator
Create Django models with proper structure and relationships.
## Purpose
This command helps you quickly create Django models with fields, relationships, and best practices.
## Usage
```
/django-model
```
## What this command does
1. **Creates model classes** with proper field definitions
2. **Adds relationships** (ForeignKey, ManyToMany, OneToOne)
3. **Includes meta options** and model methods
4. **Generates migrations** automatically
5. **Follows Django conventions** and best practices
## Example Output
```python
# models.py
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name_plural = "categories"
ordering = ['name']
def __str__(self):
return self.name
class Post(models.Model):
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
('archived', 'Archived'),
]
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
tags = models.ManyToManyField('Tag', blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
verbose_name = 'blog post'
verbose_name_plural = 'blog posts'
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post_detail', kwargs={'slug': self.slug})
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
```
## Field Types Supported
- **CharField** - Text fields with max length
- **TextField** - Large text fields
- **IntegerField** - Integer numbers
- **FloatField** - Floating point numbers
- **BooleanField** - True/False values
- **DateField** - Date only
- **DateTimeField** - Date and time
- **EmailField** - Email addresses
- **URLField** - URLs
- **ImageField** - Image uploads
- **FileField** - File uploads
- **JSONField** - JSON data (PostgreSQL)
## Relationships
- **ForeignKey** - One-to-many relationships
- **ManyToManyField** - Many-to-many relationships
- **OneToOneField** - One-to-one relationships
## Best Practices Included
- Proper field choices and defaults
- Appropriate related_name attributes
- __str__ methods for admin interface
- Meta class with ordering and verbose names
- get_absolute_url methods where appropriate
- Proper use of null and blank parameters
- Field validation and constraints
## After Creating Models
```bash
# Create and apply migrations
python manage.py makemigrations
python manage.py migrate
# Register in admin (optional)
# Add to admin.py:
from django.contrib import admin
from .models import Post, Category, Tag
admin.site.register([Post, Category, Tag])
```
@@ -0,0 +1,222 @@
# Django Views Generator
Create Django views with proper structure and best practices.
## Purpose
This command helps you quickly create Django views (Function-Based Views and Class-Based Views) following Django conventions.
## Usage
```
/views
```
## What this command does
1. **Creates view functions/classes** with proper structure
2. **Handles HTTP methods** (GET, POST, PUT, DELETE)
3. **Includes form handling** and validation
4. **Adds authentication/authorization** checks
5. **Follows Django best practices** and security guidelines
## Example Output
```python
# views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.http import JsonResponse
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from .models import Post, Category
from .forms import PostForm
# Function-Based Views
def post_list(request):
"""Display list of posts with pagination and filtering."""
posts = Post.objects.filter(status='published').select_related('author', 'category')
# Search functionality
search_query = request.GET.get('search')
if search_query:
posts = posts.filter(title__icontains=search_query)
# Category filtering
category_id = request.GET.get('category')
if category_id:
posts = posts.filter(category_id=category_id)
context = {
'posts': posts,
'categories': Category.objects.all(),
'search_query': search_query,
}
return render(request, 'blog/post_list.html', context)
def post_detail(request, slug):
"""Display individual post details."""
post = get_object_or_404(Post, slug=slug, status='published')
context = {
'post': post,
'related_posts': Post.objects.filter(
category=post.category,
status='published'
).exclude(id=post.id)[:3]
}
return render(request, 'blog/post_detail.html', context)
@login_required
def post_create(request):
"""Create new post."""
if request.method == 'POST':
form = PostForm(request.POST, request.FILES)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
form.save_m2m() # Save many-to-many relationships
messages.success(request, 'Post created successfully!')
return redirect('post_detail', slug=post.slug)
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form})
@login_required
def post_edit(request, slug):
"""Edit existing post."""
post = get_object_or_404(Post, slug=slug, author=request.user)
if request.method == 'POST':
form = PostForm(request.POST, request.FILES, instance=post)
if form.is_valid():
form.save()
messages.success(request, 'Post updated successfully!')
return redirect('post_detail', slug=post.slug)
else:
form = PostForm(instance=post)
return render(request, 'blog/post_form.html', {
'form': form,
'post': post
})
# Class-Based Views
class PostListView(ListView):
"""List view for posts with pagination."""
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
def get_queryset(self):
return Post.objects.filter(status='published').select_related('author', 'category')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.all()
return context
class PostDetailView(DetailView):
"""Detail view for individual posts."""
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def get_queryset(self):
return Post.objects.filter(status='published')
class PostCreateView(LoginRequiredMixin, CreateView):
"""Create view for new posts."""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class PostUpdateView(LoginRequiredMixin, UpdateView):
"""Update view for existing posts."""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def get_queryset(self):
return Post.objects.filter(author=self.request.user)
class PostDeleteView(LoginRequiredMixin, DeleteView):
"""Delete view for posts."""
model = Post
template_name = 'blog/post_confirm_delete.html'
success_url = reverse_lazy('post_list')
def get_queryset(self):
return Post.objects.filter(author=self.request.user)
# API Views
def api_post_list(request):
"""API endpoint for post list."""
posts = Post.objects.filter(status='published').values(
'id', 'title', 'slug', 'created_at', 'author__username'
)
return JsonResponse(list(posts), safe=False)
```
## View Types Supported
- **Function-Based Views (FBV)** - Simple, explicit control
- **Class-Based Views (CBV)** - Reusable, inheritance-based
- **Generic Views** - ListView, DetailView, CreateView, etc.
- **API Views** - JSON responses for AJAX/API calls
## Features Included
- **Authentication checks** with decorators/mixins
- **Permission handling** for user authorization
- **Form processing** with validation
- **Error handling** and user feedback
- **SEO-friendly URLs** with slugs
- **Database optimization** with select_related/prefetch_related
- **Pagination** for large datasets
- **Search and filtering** functionality
## Security Best Practices
- CSRF protection (automatic with forms)
- User authentication and authorization
- SQL injection prevention (ORM)
- XSS protection with template escaping
- Proper error handling
## URL Configuration
```python
# urls.py
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
# Function-based views
path('', views.post_list, name='post_list'),
path('post/<slug:slug>/', views.post_detail, name='post_detail'),
path('create/', views.post_create, name='post_create'),
path('edit/<slug:slug>/', views.post_edit, name='post_edit'),
# Class-based views
path('posts/', views.PostListView.as_view(), name='post_list_cbv'),
path('posts/<slug:slug>/', views.PostDetailView.as_view(), name='post_detail_cbv'),
path('posts/create/', views.PostCreateView.as_view(), name='post_create_cbv'),
path('posts/<slug:slug>/edit/', views.PostUpdateView.as_view(), name='post_edit_cbv'),
path('posts/<slug:slug>/delete/', views.PostDeleteView.as_view(), name='post_delete_cbv'),
# API endpoints
path('api/posts/', views.api_post_list, name='api_post_list'),
]
```
@@ -0,0 +1,313 @@
# Django Project Configuration
This file provides specific guidance for Django web application development using Claude Code.
## Project Overview
This is a Django web application project optimized for scalable web development with the Django framework. The project follows Django best practices and conventions.
## Django-Specific Development Commands
### Project Management
- `django-admin startproject myproject` - Create new Django project
- `python manage.py startapp myapp` - Create new Django app
- `python manage.py runserver` - Start development server
- `python manage.py runserver 0.0.0.0:8000` - Start server accessible from network
### Database Management
- `python manage.py makemigrations` - Create database migrations
- `python manage.py migrate` - Apply database migrations
- `python manage.py showmigrations` - Show migration status
- `python manage.py sqlmigrate app_name migration_name` - Show SQL for migration
- `python manage.py dbshell` - Open database shell
### User Management
- `python manage.py createsuperuser` - Create admin superuser
- `python manage.py changepassword username` - Change user password
- `python manage.py shell` - Open Django shell
### Static Files & Media
- `python manage.py collectstatic` - Collect static files for production
- `python manage.py findstatic filename` - Find static file location
### Testing & Quality
- `python manage.py test` - Run Django tests
- `python manage.py test app_name` - Run tests for specific app
- `python manage.py test --keepdb` - Run tests keeping test database
- `coverage run --source='.' manage.py test` - Run tests with coverage
### Development Tools
- `python manage.py check` - Check for Django issues
- `python manage.py validate` - Validate models
- `python manage.py inspectdb` - Generate models from existing database
- `python manage.py dumpdata app_name` - Export data
- `python manage.py loaddata fixture.json` - Import data
## Django Project Structure
```
myproject/
├── manage.py # Django management script
├── myproject/ # Project configuration
│ ├── __init__.py
│ ├── settings/ # Settings modules
│ │ ├── __init__.py
│ │ ├── base.py # Base settings
│ │ ├── development.py # Development settings
│ │ ├── production.py # Production settings
│ │ └── testing.py # Testing settings
│ ├── urls.py # URL configuration
│ ├── wsgi.py # WSGI configuration
│ └── asgi.py # ASGI configuration
├── apps/ # Django applications
│ ├── users/ # User management app
│ ├── blog/ # Blog app example
│ └── api/ # API app
├── static/ # Static files
├── media/ # User uploaded files
├── templates/ # Django templates
├── requirements/ # Requirements files
│ ├── base.txt
│ ├── development.txt
│ └── production.txt
└── tests/ # Test files
```
## Django Settings Configuration
### Base Settings (settings/base.py)
```python
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Security
SECRET_KEY = os.environ.get('SECRET_KEY')
DEBUG = False
ALLOWED_HOSTS = []
# Application definition
DJANGO_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
THIRD_PARTY_APPS = [
'rest_framework',
'django_extensions',
]
LOCAL_APPS = [
'apps.users',
'apps.blog',
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME'),
'USER': os.environ.get('DB_USER'),
'PASSWORD': os.environ.get('DB_PASSWORD'),
'HOST': os.environ.get('DB_HOST', 'localhost'),
'PORT': os.environ.get('DB_PORT', '5432'),
}
}
# Internationalization
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [BASE_DIR / 'static']
# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
```
## Django Best Practices
### Models
- Use descriptive model names (singular)
- Add `__str__` methods for better admin interface
- Use `related_name` for foreign keys
- Implement `get_absolute_url` method
- Add proper Meta class with ordering
### Views
- Use class-based views for complex logic
- Implement proper error handling
- Add pagination for list views
- Use `select_related` and `prefetch_related` for optimization
- Implement proper permission checks
### URLs
- Use app namespaces
- Use descriptive URL names
- Group related URLs in separate files
- Use slug fields for SEO-friendly URLs
### Templates
- Extend base templates
- Use template inheritance effectively
- Create reusable template tags
- Implement proper CSRF protection
- Use Django's built-in template filters
### Forms
- Use Django forms for validation
- Implement custom form validation
- Use ModelForms when appropriate
- Add proper error handling
- Implement CSRF protection
## Security Considerations
### Django Security Settings
```python
# Security settings for production
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_HSTS_SECONDS = 31536000
SECURE_REDIRECT_EXEMPT = []
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
```
### User Authentication
- Use Django's built-in authentication
- Implement proper password policies
- Add two-factor authentication if needed
- Use Django's permission system
- Implement proper session management
## Testing Strategy
### Test Organization
```python
# tests/test_models.py
from django.test import TestCase
from django.contrib.auth import get_user_model
from apps.blog.models import Post
User = get_user_model()
class PostModelTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
def test_post_creation(self):
post = Post.objects.create(
title='Test Post',
content='Test content',
author=self.user
)
self.assertEqual(post.title, 'Test Post')
self.assertEqual(str(post), 'Test Post')
```
### Test Types
- **Unit tests** for models and utilities
- **Integration tests** for views and forms
- **Functional tests** for user workflows
- **API tests** for REST endpoints
## Deployment Considerations
### Production Settings
- Use environment variables for sensitive data
- Configure proper logging
- Set up static file serving
- Configure database connection pooling
- Implement proper caching strategy
### Docker Configuration
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "myproject.wsgi:application"]
```
## Performance Optimization
### Database Optimization
- Use `select_related()` for foreign keys
- Use `prefetch_related()` for many-to-many
- Add database indexes for frequent queries
- Implement database connection pooling
- Use database query optimization tools
### Caching Strategy
- Implement Redis/Memcached for session storage
- Use template fragment caching
- Implement view-level caching
- Add database query caching
- Use CDN for static files
## Common Django Patterns
### Custom User Model
```python
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
email = models.EmailField(unique=True)
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username', 'first_name', 'last_name']
```
### Custom Managers
```python
class PublishedManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(status='published')
class Post(models.Model):
title = models.CharField(max_length=200)
status = models.CharField(max_length=20, default='draft')
objects = models.Manager() # Default manager
published = PublishedManager() # Custom manager
```
## Django Extensions & Tools
### Useful Third-Party Packages
- **Django REST Framework** - API development
- **Celery** - Asynchronous task processing
- **Django Debug Toolbar** - Development debugging
- **Django Extensions** - Additional management commands
- **Pillow** - Image processing
- **psycopg2-binary** - PostgreSQL adapter
@@ -0,0 +1,513 @@
# FastAPI Endpoints Generator
Create comprehensive FastAPI endpoints with proper structure, validation, and documentation.
## Purpose
This command helps you quickly create FastAPI endpoints with Pydantic models, dependency injection, and automatic API documentation.
## Usage
```
/api-endpoints
```
## What this command does
1. **Creates API endpoints** with proper HTTP methods
2. **Adds Pydantic models** for request/response validation
3. **Implements dependency injection** for database and auth
4. **Includes error handling** and status codes
5. **Generates automatic documentation** with OpenAPI
## Example Output
```python
# main.py
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
import uvicorn
from app.database import get_db, engine
from app.models import models
from app.routers import auth, users, posts, comments
from app.core.config import settings
# Create database tables
models.Base.metadata.create_all(bind=engine)
# Initialize FastAPI app
app = FastAPI(
title="Blog API",
description="A comprehensive blog API built with FastAPI",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc"
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_HOSTS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include routers
app.include_router(auth.router, prefix="/auth", tags=["Authentication"])
app.include_router(users.router, prefix="/users", tags=["Users"])
app.include_router(posts.router, prefix="/posts", tags=["Posts"])
app.include_router(comments.router, prefix="/comments", tags=["Comments"])
@app.get("/", tags=["Root"])
async def root():
"""API root endpoint."""
return {
"message": "Welcome to Blog API",
"version": "1.0.0",
"docs": "/docs",
"redoc": "/redoc"
}
@app.get("/health", tags=["Health"])
async def health_check():
"""Health check endpoint."""
return {"status": "healthy", "timestamp": "2024-01-01T00:00:00Z"}
if __name__ == "__main__":
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
reload=True
)
```
```python
# app/routers/posts.py
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from app.database import get_db
from app.schemas import post_schemas
from app.services import post_service
from app.core.dependencies import get_current_user, get_current_active_user
from app.models.user import User
router = APIRouter()
@router.get("/", response_model=List[post_schemas.PostResponse])
async def get_posts(
skip: int = Query(0, ge=0, description="Number of posts to skip"),
limit: int = Query(10, ge=1, le=100, description="Number of posts to return"),
search: Optional[str] = Query(None, description="Search in title and content"),
category: Optional[str] = Query(None, description="Filter by category"),
published: Optional[bool] = Query(True, description="Filter by published status"),
db: Session = Depends(get_db)
):
"""
Get all posts with pagination and filtering.
- **skip**: Number of posts to skip (for pagination)
- **limit**: Maximum number of posts to return (1-100)
- **search**: Search term for title and content
- **category**: Filter posts by category
- **published**: Filter by published status
"""
posts = post_service.get_posts(
db=db,
skip=skip,
limit=limit,
search=search,
category=category,
published=published
)
return posts
@router.get("/{post_id}", response_model=post_schemas.PostResponse)
async def get_post(
post_id: int,
db: Session = Depends(get_db)
):
"""
Get a specific post by ID.
- **post_id**: Unique identifier for the post
"""
post = post_service.get_post(db=db, post_id=post_id)
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
return post
@router.post("/", response_model=post_schemas.PostResponse, status_code=status.HTTP_201_CREATED)
async def create_post(
post: post_schemas.PostCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""
Create a new post.
- **title**: Post title (required)
- **content**: Post content (required)
- **category**: Post category (optional)
- **published**: Publication status (default: false)
"""
return post_service.create_post(
db=db,
post=post,
user_id=current_user.id
)
@router.put("/{post_id}", response_model=post_schemas.PostResponse)
async def update_post(
post_id: int,
post_update: post_schemas.PostUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""
Update an existing post.
- **post_id**: Unique identifier for the post
- **title**: Updated post title (optional)
- **content**: Updated post content (optional)
- **category**: Updated post category (optional)
- **published**: Updated publication status (optional)
"""
post = post_service.get_post(db=db, post_id=post_id)
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
if post.author_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to update this post"
)
return post_service.update_post(
db=db,
post_id=post_id,
post_update=post_update
)
@router.delete("/{post_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_post(
post_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""
Delete a post.
- **post_id**: Unique identifier for the post to delete
"""
post = post_service.get_post(db=db, post_id=post_id)
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
if post.author_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this post"
)
post_service.delete_post(db=db, post_id=post_id)
@router.post("/{post_id}/like", response_model=post_schemas.PostResponse)
async def like_post(
post_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""
Like/unlike a post.
- **post_id**: Unique identifier for the post to like
"""
post = post_service.get_post(db=db, post_id=post_id)
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
return post_service.toggle_like(
db=db,
post_id=post_id,
user_id=current_user.id
)
@router.get("/{post_id}/comments", response_model=List[post_schemas.CommentResponse])
async def get_post_comments(
post_id: int,
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
db: Session = Depends(get_db)
):
"""
Get all comments for a specific post.
- **post_id**: Unique identifier for the post
- **skip**: Number of comments to skip
- **limit**: Maximum number of comments to return
"""
post = post_service.get_post(db=db, post_id=post_id)
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
return post_service.get_post_comments(
db=db,
post_id=post_id,
skip=skip,
limit=limit
)
```
```python
# app/schemas/post_schemas.py
from typing import Optional, List
from datetime import datetime
from pydantic import BaseModel, Field, validator
class PostBase(BaseModel):
"""Base post schema."""
title: str = Field(..., min_length=1, max_length=200, description="Post title")
content: str = Field(..., min_length=1, description="Post content")
category: Optional[str] = Field(None, max_length=50, description="Post category")
published: bool = Field(False, description="Publication status")
class PostCreate(PostBase):
"""Schema for creating a post."""
@validator('title')
def validate_title(cls, v):
if not v.strip():
raise ValueError('Title cannot be empty')
return v.strip()
@validator('content')
def validate_content(cls, v):
if len(v.strip()) < 10:
raise ValueError('Content must be at least 10 characters long')
return v.strip()
class PostUpdate(BaseModel):
"""Schema for updating a post."""
title: Optional[str] = Field(None, min_length=1, max_length=200)
content: Optional[str] = Field(None, min_length=1)
category: Optional[str] = Field(None, max_length=50)
published: Optional[bool] = None
@validator('title')
def validate_title(cls, v):
if v is not None and not v.strip():
raise ValueError('Title cannot be empty')
return v.strip() if v else v
@validator('content')
def validate_content(cls, v):
if v is not None and len(v.strip()) < 10:
raise ValueError('Content must be at least 10 characters long')
return v.strip() if v else v
class PostResponse(PostBase):
"""Schema for post responses."""
id: int
author_id: int
created_at: datetime
updated_at: datetime
like_count: int = 0
comment_count: int = 0
class Config:
from_attributes = True
class CommentBase(BaseModel):
"""Base comment schema."""
content: str = Field(..., min_length=1, max_length=1000, description="Comment content")
class CommentCreate(CommentBase):
"""Schema for creating a comment."""
post_id: int = Field(..., description="ID of the post to comment on")
class CommentResponse(CommentBase):
"""Schema for comment responses."""
id: int
post_id: int
author_id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
```
```python
# app/services/post_service.py
from typing import List, Optional
from sqlalchemy.orm import Session
from sqlalchemy import or_
from app.models.post import Post, Comment, PostLike
from app.schemas.post_schemas import PostCreate, PostUpdate
def get_posts(
db: Session,
skip: int = 0,
limit: int = 10,
search: Optional[str] = None,
category: Optional[str] = None,
published: Optional[bool] = True
) -> List[Post]:
"""Get posts with filtering and pagination."""
query = db.query(Post)
if published is not None:
query = query.filter(Post.published == published)
if category:
query = query.filter(Post.category == category)
if search:
query = query.filter(
or_(
Post.title.contains(search),
Post.content.contains(search)
)
)
return query.offset(skip).limit(limit).all()
def get_post(db: Session, post_id: int) -> Optional[Post]:
"""Get a single post by ID."""
return db.query(Post).filter(Post.id == post_id).first()
def create_post(db: Session, post: PostCreate, user_id: int) -> Post:
"""Create a new post."""
db_post = Post(**post.dict(), author_id=user_id)
db.add(db_post)
db.commit()
db.refresh(db_post)
return db_post
def update_post(
db: Session,
post_id: int,
post_update: PostUpdate
) -> Optional[Post]:
"""Update an existing post."""
db_post = db.query(Post).filter(Post.id == post_id).first()
if not db_post:
return None
update_data = post_update.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(db_post, field, value)
db.commit()
db.refresh(db_post)
return db_post
def delete_post(db: Session, post_id: int) -> bool:
"""Delete a post."""
db_post = db.query(Post).filter(Post.id == post_id).first()
if not db_post:
return False
db.delete(db_post)
db.commit()
return True
def toggle_like(db: Session, post_id: int, user_id: int) -> Post:
"""Toggle like status for a post."""
existing_like = db.query(PostLike).filter(
PostLike.post_id == post_id,
PostLike.user_id == user_id
).first()
if existing_like:
db.delete(existing_like)
else:
new_like = PostLike(post_id=post_id, user_id=user_id)
db.add(new_like)
db.commit()
return get_post(db, post_id)
```
## Features Included
### API Documentation
- **Automatic OpenAPI** schema generation
- **Interactive docs** at `/docs`
- **ReDoc documentation** at `/redoc`
- **Request/Response examples** in schemas
### Validation & Serialization
- **Pydantic models** for data validation
- **Custom validators** for business rules
- **Type hints** for better IDE support
- **Automatic data conversion** and validation
### Error Handling
- **HTTP status codes** for different scenarios
- **Detailed error messages** with context
- **Input validation errors** with field-specific messages
- **Custom exception handlers** for consistent responses
### Security
- **JWT authentication** with dependencies
- **Role-based access control** for endpoints
- **CORS middleware** for cross-origin requests
- **Input sanitization** through Pydantic
### Performance
- **Database query optimization** with SQLAlchemy
- **Pagination support** for large datasets
- **Async/await support** for concurrent requests
- **Connection pooling** for database efficiency
## Testing Example
```python
# tests/test_posts.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_get_posts():
"""Test getting posts."""
response = client.get("/posts/")
assert response.status_code == 200
assert isinstance(response.json(), list)
def test_create_post():
"""Test creating a new post."""
post_data = {
"title": "Test Post",
"content": "This is a test post content.",
"published": True
}
response = client.post("/posts/", json=post_data)
assert response.status_code == 201
assert response.json()["title"] == "Test Post"
```
@@ -0,0 +1,775 @@
# FastAPI Authentication & Authorization
Complete authentication system with JWT tokens, OAuth2, and role-based access control.
## Usage
```bash
# Install auth dependencies
pip install python-jose[cryptography] passlib[bcrypt] python-multipart
# Generate secret key
openssl rand -hex 32
```
## JWT Configuration
```python
# app/core/security.py
from datetime import datetime, timedelta
from typing import Optional, Union, Any
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.core.config import settings
# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# JWT settings
SECRET_KEY = settings.SECRET_KEY
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
REFRESH_TOKEN_EXPIRE_DAYS = 7
def create_access_token(
subject: Union[str, Any],
expires_delta: Optional[timedelta] = None
) -> str:
"""Create JWT access token."""
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode = {"exp": expire, "sub": str(subject), "type": "access"}
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def create_refresh_token(subject: Union[str, Any]) -> str:
"""Create JWT refresh token."""
expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
to_encode = {"exp": expire, "sub": str(subject), "type": "refresh"}
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify password against hash."""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""Generate password hash."""
return pwd_context.hash(password)
def decode_token(token: str) -> Optional[dict]:
"""Decode and verify JWT token."""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
return None
```
## Authentication Dependencies
```python
# app/api/dependencies/auth.py
from typing import Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.security import decode_token
from app.db.database import get_db
from app.models.user import User
from app.repositories.user import UserRepository
# OAuth2 scheme
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="/api/v1/auth/login",
scheme_name="JWT"
)
# Bearer token scheme
security = HTTPBearer()
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db)
) -> User:
"""Get current authenticated user."""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
payload = decode_token(token)
if payload is None:
raise credentials_exception
user_id: str = payload.get("sub")
token_type: str = payload.get("type")
if user_id is None or token_type != "access":
raise credentials_exception
user_repo = UserRepository(User, db)
user = await user_repo.get(int(user_id))
if user is None:
raise credentials_exception
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
return user
async def get_current_active_user(
current_user: User = Depends(get_current_user)
) -> User:
"""Get current active user."""
if not current_user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
return current_user
async def get_current_superuser(
current_user: User = Depends(get_current_user)
) -> User:
"""Get current superuser."""
if not current_user.is_superuser:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
return current_user
def require_permissions(*permissions: str):
"""Decorator for permission-based access control."""
async def permission_checker(
current_user: User = Depends(get_current_active_user)
) -> User:
# Check if user has required permissions
user_permissions = set(current_user.permissions or [])
required_permissions = set(permissions)
if not required_permissions.issubset(user_permissions) and not current_user.is_superuser:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions"
)
return current_user
return permission_checker
def require_roles(*roles: str):
"""Decorator for role-based access control."""
async def role_checker(
current_user: User = Depends(get_current_active_user)
) -> User:
user_roles = set(role.name for role in current_user.roles or [])
required_roles = set(roles)
if not required_roles.issubset(user_roles) and not current_user.is_superuser:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient role permissions"
)
return current_user
return role_checker
```
## Authentication Schemas
```python
# app/schemas/auth.py
from pydantic import BaseModel, EmailStr
from typing import Optional
class Token(BaseModel):
"""Token response schema."""
access_token: str
refresh_token: str
token_type: str = "bearer"
expires_in: int
class TokenPayload(BaseModel):
"""Token payload schema."""
sub: Optional[int] = None
exp: Optional[int] = None
type: Optional[str] = None
class UserLogin(BaseModel):
"""User login schema."""
username: str
password: str
class UserRegister(BaseModel):
"""User registration schema."""
username: str
email: EmailStr
password: str
first_name: str
last_name: str
class PasswordReset(BaseModel):
"""Password reset schema."""
email: EmailStr
class PasswordResetConfirm(BaseModel):
"""Password reset confirmation schema."""
token: str
new_password: str
class ChangePassword(BaseModel):
"""Change password schema."""
current_password: str
new_password: str
class RefreshToken(BaseModel):
"""Refresh token schema."""
refresh_token: str
```
## Authentication Endpoints
```python
# app/api/v1/auth.py
from datetime import timedelta
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.database import get_db
from app.models.user import User
from app.repositories.user import UserRepository
from app.schemas.auth import (
Token, UserLogin, UserRegister, PasswordReset,
PasswordResetConfirm, ChangePassword, RefreshToken
)
from app.schemas.user import UserCreate, UserResponse
from app.core.security import (
create_access_token, create_refresh_token, verify_password,
decode_token, ACCESS_TOKEN_EXPIRE_MINUTES
)
from app.api.dependencies.auth import get_current_user, get_current_active_user
from app.services.email import send_password_reset_email
from app.services.auth import AuthService
router = APIRouter()
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def register(
user_data: UserRegister,
db: AsyncSession = Depends(get_db)
) -> Any:
"""Register new user."""
user_repo = UserRepository(User, db)
auth_service = AuthService(user_repo)
# Check if user already exists
if await user_repo.get_by_email(user_data.email):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
if await user_repo.get_by_username(user_data.username):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already taken"
)
# Create user
user = await auth_service.create_user(user_data.dict())
return user
@router.post("/login", response_model=Token)
async def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(get_db)
) -> Any:
"""OAuth2 compatible token login."""
user_repo = UserRepository(User, db)
auth_service = AuthService(user_repo)
user = await auth_service.authenticate_user(
form_data.username,
form_data.password
)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
# Create tokens
access_token = create_access_token(subject=user.id)
refresh_token = create_refresh_token(subject=user.id)
return {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "bearer",
"expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60
}
@router.post("/refresh", response_model=Token)
async def refresh_token(
refresh_data: RefreshToken,
db: AsyncSession = Depends(get_db)
) -> Any:
"""Refresh access token."""
payload = decode_token(refresh_data.refresh_token)
if payload is None or payload.get("type") != "refresh":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token"
)
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token"
)
user_repo = UserRepository(User, db)
user = await user_repo.get(int(user_id))
if user is None or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token"
)
# Create new tokens
access_token = create_access_token(subject=user.id)
new_refresh_token = create_refresh_token(subject=user.id)
return {
"access_token": access_token,
"refresh_token": new_refresh_token,
"token_type": "bearer",
"expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60
}
@router.get("/me", response_model=UserResponse)
async def read_users_me(
current_user: User = Depends(get_current_active_user)
) -> Any:
"""Get current user."""
return current_user
@router.post("/change-password", status_code=status.HTTP_200_OK)
async def change_password(
password_data: ChangePassword,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
) -> Any:
"""Change user password."""
if not verify_password(password_data.current_password, current_user.hashed_password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Incorrect current password"
)
user_repo = UserRepository(User, db)
auth_service = AuthService(user_repo)
await auth_service.change_password(current_user.id, password_data.new_password)
return {"message": "Password changed successfully"}
@router.post("/password-reset", status_code=status.HTTP_200_OK)
async def password_reset(
reset_data: PasswordReset,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db)
) -> Any:
"""Request password reset."""
user_repo = UserRepository(User, db)
user = await user_repo.get_by_email(reset_data.email)
if user:
# Generate reset token
reset_token = create_access_token(
subject=user.id,
expires_delta=timedelta(hours=1) # 1 hour expiry
)
# Send email with reset token
background_tasks.add_task(
send_password_reset_email,
email=user.email,
username=user.username,
token=reset_token
)
# Always return success to prevent email enumeration
return {"message": "Password reset email sent if account exists"}
@router.post("/password-reset-confirm", status_code=status.HTTP_200_OK)
async def password_reset_confirm(
reset_data: PasswordResetConfirm,
db: AsyncSession = Depends(get_db)
) -> Any:
"""Confirm password reset."""
payload = decode_token(reset_data.token)
if payload is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid or expired reset token"
)
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid reset token"
)
user_repo = UserRepository(User, db)
auth_service = AuthService(user_repo)
user = await user_repo.get(int(user_id))
if user is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid reset token"
)
await auth_service.change_password(user.id, reset_data.new_password)
return {"message": "Password reset successful"}
@router.post("/logout", status_code=status.HTTP_200_OK)
async def logout(
current_user: User = Depends(get_current_user)
) -> Any:
"""Logout user (invalidate token on client side)."""
# In a more sophisticated setup, you might want to blacklist the token
return {"message": "Successfully logged out"}
```
## Authentication Service
```python
# app/services/auth.py
from typing import Optional
from app.models.user import User
from app.repositories.user import UserRepository
from app.core.security import verify_password, get_password_hash
class AuthService:
"""Authentication service."""
def __init__(self, user_repository: UserRepository):
self.user_repo = user_repository
async def authenticate_user(
self,
username_or_email: str,
password: str
) -> Optional[User]:
"""Authenticate user by username/email and password."""
# Try to get user by username first, then by email
user = await self.user_repo.get_by_username(username_or_email)
if not user:
user = await self.user_repo.get_by_email(username_or_email)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user
async def create_user(self, user_data: dict) -> User:
"""Create new user."""
# Hash password
password = user_data.pop('password')
hashed_password = get_password_hash(password)
# Create user data
user_create_data = {
**user_data,
'hashed_password': hashed_password,
'is_active': True,
'is_superuser': False
}
return await self.user_repo.create(user_create_data)
async def change_password(self, user_id: int, new_password: str) -> bool:
"""Change user password."""
hashed_password = get_password_hash(new_password)
result = await self.user_repo.update(user_id, {
'hashed_password': hashed_password
})
return result is not None
async def activate_user(self, user_id: int) -> bool:
"""Activate user account."""
result = await self.user_repo.update(user_id, {'is_active': True})
return result is not None
async def deactivate_user(self, user_id: int) -> bool:
"""Deactivate user account."""
result = await self.user_repo.update(user_id, {'is_active': False})
return result is not None
```
## Role-Based Access Control
```python
# app/models/rbac.py
from sqlalchemy import Column, String, Text, ForeignKey, Table
from sqlalchemy.orm import relationship
from app.models.base import BaseModel
# Association tables for many-to-many relationships
user_roles = Table(
'user_roles',
Base.metadata,
Column('user_id', Integer, ForeignKey('users.id')),
Column('role_id', Integer, ForeignKey('roles.id'))
)
role_permissions = Table(
'role_permissions',
Base.metadata,
Column('role_id', Integer, ForeignKey('roles.id')),
Column('permission_id', Integer, ForeignKey('permissions.id'))
)
class Role(BaseModel):
"""Role model for RBAC."""
__tablename__ = "roles"
name = Column(String(50), unique=True, nullable=False, index=True)
description = Column(Text)
# Relationships
users = relationship("User", secondary=user_roles, back_populates="roles")
permissions = relationship("Permission", secondary=role_permissions, back_populates="roles")
class Permission(BaseModel):
"""Permission model for RBAC."""
__tablename__ = "permissions"
name = Column(String(100), unique=True, nullable=False, index=True)
description = Column(Text)
resource = Column(String(50), nullable=False) # e.g., 'users', 'posts'
action = Column(String(50), nullable=False) # e.g., 'create', 'read', 'update', 'delete'
# Relationships
roles = relationship("Role", secondary=role_permissions, back_populates="permissions")
# Update User model to include roles
class User(BaseModel):
# ... existing fields ...
# Relationships
roles = relationship("Role", secondary=user_roles, back_populates="users")
@property
def permissions(self) -> list[str]:
"""Get all permissions for user."""
perms = set()
for role in self.roles:
for permission in role.permissions:
perms.add(f"{permission.resource}:{permission.action}")
return list(perms)
```
## OAuth2 Integration
```python
# app/api/v1/oauth.py
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security.utils import get_authorization_scheme_param
from starlette.requests import Request
from authlib.integrations.starlette_client import OAuth
from app.core.config import settings
router = APIRouter()
# OAuth configuration
oauth = OAuth()
# Google OAuth
google = oauth.register(
name='google',
client_id=settings.GOOGLE_CLIENT_ID,
client_secret=settings.GOOGLE_CLIENT_SECRET,
server_metadata_url='https://accounts.google.com/.well-known/openid_configuration',
client_kwargs={
'scope': 'openid email profile'
}
)
# GitHub OAuth
github = oauth.register(
name='github',
client_id=settings.GITHUB_CLIENT_ID,
client_secret=settings.GITHUB_CLIENT_SECRET,
access_token_url='https://github.com/login/oauth/access_token',
access_token_params=None,
authorize_url='https://github.com/login/oauth/authorize',
authorize_params=None,
api_base_url='https://api.github.com/',
client_kwargs={'scope': 'user:email'},
)
@router.get('/google')
async def google_login(request: Request):
"""Initiate Google OAuth login."""
redirect_uri = request.url_for('google_callback')
return await google.authorize_redirect(request, redirect_uri)
@router.get('/google/callback')
async def google_callback(request: Request):
"""Handle Google OAuth callback."""
token = await google.authorize_access_token(request)
user_info = token.get('userinfo')
if user_info:
# Create or get user
# Generate JWT token
# Return token
pass
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="OAuth authentication failed"
)
```
## API Key Authentication
```python
# app/models/api_key.py
from sqlalchemy import Column, String, Boolean, ForeignKey, DateTime
from sqlalchemy.orm import relationship
from app.models.base import BaseModel
import secrets
class APIKey(BaseModel):
"""API Key model."""
__tablename__ = "api_keys"
name = Column(String(100), nullable=False)
key_hash = Column(String(255), unique=True, nullable=False, index=True)
prefix = Column(String(10), nullable=False, index=True)
is_active = Column(Boolean, default=True, nullable=False)
expires_at = Column(DateTime)
last_used_at = Column(DateTime)
# Foreign key
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Relationships
user = relationship("User", back_populates="api_keys")
@classmethod
def generate_key(cls) -> tuple[str, str]:
"""Generate API key and return (key, hash)."""
key = secrets.token_urlsafe(32)
prefix = key[:8]
key_hash = get_password_hash(key)
return key, prefix, key_hash
def verify_key(self, key: str) -> bool:
"""Verify API key."""
return verify_password(key, self.key_hash)
# Add to User model
class User(BaseModel):
# ... existing fields ...
# Relationships
api_keys = relationship("APIKey", back_populates="user", cascade="all, delete-orphan")
```
## Testing Authentication
```python
# tests/test_auth.py
import pytest
from httpx import AsyncClient
from app.core.security import create_access_token
@pytest.mark.asyncio
async def test_register_user(client: AsyncClient):
"""Test user registration."""
user_data = {
"username": "testuser",
"email": "test@example.com",
"password": "testpass123",
"first_name": "Test",
"last_name": "User"
}
response = await client.post("/api/v1/auth/register", json=user_data)
assert response.status_code == 201
data = response.json()
assert data["username"] == user_data["username"]
assert data["email"] == user_data["email"]
assert "hashed_password" not in data
@pytest.mark.asyncio
async def test_login_user(client: AsyncClient, test_user):
"""Test user login."""
login_data = {
"username": test_user.username,
"password": "testpass123"
}
response = await client.post(
"/api/v1/auth/login",
data=login_data,
headers={"Content-Type": "application/x-www-form-urlencoded"}
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert "refresh_token" in data
assert data["token_type"] == "bearer"
@pytest.mark.asyncio
async def test_get_current_user(client: AsyncClient, test_user):
"""Test get current user endpoint."""
token = create_access_token(subject=test_user.id)
headers = {"Authorization": f"Bearer {token}"}
response = await client.get("/api/v1/auth/me", headers=headers)
assert response.status_code == 200
data = response.json()
assert data["username"] == test_user.username
assert data["email"] == test_user.email
```
@@ -0,0 +1,657 @@
# FastAPI Database Integration
Complete database setup with SQLAlchemy, Alembic, and async support for FastAPI.
## Usage
```bash
# Initialize Alembic
alembic init alembic
# Create migration
alembic revision --autogenerate -m "Initial migration"
# Apply migrations
alembic upgrade head
# Downgrade migration
alembic downgrade -1
```
## Database Configuration
```python
# app/core/config.py
from pydantic import BaseSettings, PostgresDsn, validator
from typing import Optional, Dict, Any
import os
class Settings(BaseSettings):
"""Application settings."""
# Database
POSTGRES_SERVER: str = "localhost"
POSTGRES_USER: str = "postgres"
POSTGRES_PASSWORD: str = "password"
POSTGRES_DB: str = "fastapi_app"
POSTGRES_PORT: str = "5432"
DATABASE_URL: Optional[PostgresDsn] = None
@validator("DATABASE_URL", pre=True)
def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any:
if isinstance(v, str):
return v
return PostgresDsn.build(
scheme="postgresql+asyncpg",
user=values.get("POSTGRES_USER"),
password=values.get("POSTGRES_PASSWORD"),
host=values.get("POSTGRES_SERVER"),
port=values.get("POSTGRES_PORT"),
path=f"/{values.get('POSTGRES_DB') or ''}",
)
# Redis
REDIS_URL: str = "redis://localhost:6379/0"
# Database settings
DATABASE_POOL_SIZE: int = 10
DATABASE_MAX_OVERFLOW: int = 20
DATABASE_POOL_RECYCLE: int = 3600
class Config:
env_file = ".env"
case_sensitive = True
settings = Settings()
```
## Database Setup
```python
# app/db/database.py
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
# Create async engine
engine = create_async_engine(
str(settings.DATABASE_URL),
pool_size=settings.DATABASE_POOL_SIZE,
max_overflow=settings.DATABASE_MAX_OVERFLOW,
pool_recycle=settings.DATABASE_POOL_RECYCLE,
pool_pre_ping=True,
echo=False # Set to True for SQL debugging
)
# Create async session factory
AsyncSessionLocal = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
# Base class for models
Base = declarative_base()
async def get_db() -> AsyncSession:
"""Dependency to get database session."""
async with AsyncSessionLocal() as session:
try:
yield session
finally:
await session.close()
async def create_tables():
"""Create database tables."""
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async def drop_tables():
"""Drop database tables."""
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
```
## Base Model
```python
# app/models/base.py
from sqlalchemy import Column, Integer, DateTime, func
from sqlalchemy.ext.declarative import declared_attr
from app.db.database import Base
from datetime import datetime
from typing import Any
class TimestampMixin:
"""Mixin for timestamp fields."""
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at = Column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False
)
class BaseModel(Base, TimestampMixin):
"""Base model with common functionality."""
__abstract__ = True
id = Column(Integer, primary_key=True, index=True)
@declared_attr
def __tablename__(cls) -> str:
return cls.__name__.lower()
def dict(self, exclude: set = None) -> dict[str, Any]:
"""Convert model to dictionary."""
exclude = exclude or set()
return {
column.name: getattr(self, column.name)
for column in self.__table__.columns
if column.name not in exclude
}
def __repr__(self) -> str:
return f"<{self.__class__.__name__}(id={self.id})>"
```
## Example Models
```python
# app/models/user.py
from sqlalchemy import Column, String, Boolean, Text, Index
from sqlalchemy.orm import relationship
from app.models.base import BaseModel
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class User(BaseModel):
"""User model."""
__tablename__ = "users"
username = Column(String(50), unique=True, index=True, nullable=False)
email = Column(String(100), unique=True, index=True, nullable=False)
hashed_password = Column(String(255), nullable=False)
first_name = Column(String(50), nullable=False)
last_name = Column(String(50), nullable=False)
is_active = Column(Boolean, default=True, nullable=False)
is_superuser = Column(Boolean, default=False, nullable=False)
bio = Column(Text)
# Relationships
posts = relationship("Post", back_populates="author", cascade="all, delete-orphan")
# Indexes
__table_args__ = (
Index('idx_user_email_active', email, is_active),
Index('idx_user_username_active', username, is_active),
)
def verify_password(self, password: str) -> bool:
"""Verify password against hash."""
return pwd_context.verify(password, self.hashed_password)
@staticmethod
def get_password_hash(password: str) -> str:
"""Generate password hash."""
return pwd_context.hash(password)
def set_password(self, password: str) -> None:
"""Set user password."""
self.hashed_password = self.get_password_hash(password)
@property
def full_name(self) -> str:
"""Get user's full name."""
return f"{self.first_name} {self.last_name}"
def dict(self, exclude: set = None) -> dict:
"""Convert to dict excluding sensitive data."""
exclude = exclude or set()
exclude.add('hashed_password')
return super().dict(exclude=exclude)
# app/models/post.py
from sqlalchemy import Column, String, Text, ForeignKey, Boolean, Index
from sqlalchemy.orm import relationship
from app.models.base import BaseModel
class Post(BaseModel):
"""Blog post model."""
__tablename__ = "posts"
title = Column(String(200), nullable=False, index=True)
content = Column(Text, nullable=False)
slug = Column(String(200), unique=True, nullable=False, index=True)
is_published = Column(Boolean, default=False, nullable=False)
# Foreign keys
author_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Relationships
author = relationship("User", back_populates="posts")
# Indexes
__table_args__ = (
Index('idx_post_published_created', is_published, 'created_at'),
Index('idx_post_author_published', author_id, is_published),
)
```
## Repository Pattern
```python
# app/repositories/base.py
from typing import Generic, TypeVar, Type, Optional, List, Dict, Any
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update, delete, func
from sqlalchemy.orm import selectinload
from app.models.base import BaseModel
ModelType = TypeVar("ModelType", bound=BaseModel)
class BaseRepository(Generic[ModelType]):
"""Base repository with common CRUD operations."""
def __init__(self, model: Type[ModelType], db: AsyncSession):
self.model = model
self.db = db
async def get(self, id: int) -> Optional[ModelType]:
"""Get model by ID."""
result = await self.db.execute(
select(self.model).where(self.model.id == id)
)
return result.scalar_one_or_none()
async def get_multi(
self,
skip: int = 0,
limit: int = 100,
filters: Dict[str, Any] = None
) -> List[ModelType]:
"""Get multiple models with pagination."""
query = select(self.model)
if filters:
for field, value in filters.items():
if hasattr(self.model, field):
query = query.where(getattr(self.model, field) == value)
query = query.offset(skip).limit(limit)
result = await self.db.execute(query)
return result.scalars().all()
async def create(self, obj_in: Dict[str, Any]) -> ModelType:
"""Create new model."""
db_obj = self.model(**obj_in)
self.db.add(db_obj)
await self.db.commit()
await self.db.refresh(db_obj)
return db_obj
async def update(
self,
id: int,
obj_in: Dict[str, Any]
) -> Optional[ModelType]:
"""Update model by ID."""
await self.db.execute(
update(self.model)
.where(self.model.id == id)
.values(**obj_in)
)
await self.db.commit()
return await self.get(id)
async def delete(self, id: int) -> bool:
"""Delete model by ID."""
result = await self.db.execute(
delete(self.model).where(self.model.id == id)
)
await self.db.commit()
return result.rowcount > 0
async def count(self, filters: Dict[str, Any] = None) -> int:
"""Count models with optional filters."""
query = select(func.count(self.model.id))
if filters:
for field, value in filters.items():
if hasattr(self.model, field):
query = query.where(getattr(self.model, field) == value)
result = await self.db.execute(query)
return result.scalar()
# app/repositories/user.py
from typing import Optional
from sqlalchemy import select
from app.models.user import User
from app.repositories.base import BaseRepository
class UserRepository(BaseRepository[User]):
"""User repository with custom methods."""
async def get_by_email(self, email: str) -> Optional[User]:
"""Get user by email."""
result = await self.db.execute(
select(User).where(User.email == email)
)
return result.scalar_one_or_none()
async def get_by_username(self, username: str) -> Optional[User]:
"""Get user by username."""
result = await self.db.execute(
select(User).where(User.username == username)
)
return result.scalar_one_or_none()
async def get_active_users(self, skip: int = 0, limit: int = 100):
"""Get active users."""
return await self.get_multi(
skip=skip,
limit=limit,
filters={'is_active': True}
)
```
## Alembic Configuration
```python
# alembic/env.py
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from sqlalchemy.ext.asyncio import AsyncEngine
from alembic import context
import asyncio
# Import your models
from app.models.base import Base
from app.models.user import User
from app.models.post import Post
from app.core.config import settings
# Alembic Config object
config = context.config
# Override database URL
config.set_main_option("sqlalchemy.url", str(settings.DATABASE_URL))
# Interpret the config file for logging
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# Add your model's MetaData object here
target_metadata = Base.metadata
def do_run_migrations(connection):
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
compare_server_default=True,
)
with context.begin_transaction():
context.run_migrations()
async def run_migrations_online():
"""Run migrations in 'online' mode."""
configuration = config.get_section(config.config_ini_section)
configuration["sqlalchemy.url"] = str(settings.DATABASE_URL)
connectable = AsyncEngine(
engine_from_config(
configuration,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
if context.is_offline_mode():
run_migrations_offline()
else:
asyncio.run(run_migrations_online())
```
## Database Utilities
```python
# app/db/utils.py
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from app.db.database import engine, AsyncSessionLocal
from app.core.config import settings
import asyncio
async def check_database_connection() -> bool:
"""Check if database is accessible."""
try:
async with AsyncSessionLocal() as session:
await session.execute(text("SELECT 1"))
return True
except Exception:
return False
async def create_database_if_not_exists():
"""Create database if it doesn't exist."""
# This is PostgreSQL specific
import asyncpg
from urllib.parse import urlparse
url = urlparse(str(settings.DATABASE_URL))
try:
# Connect to postgres database to create our database
conn = await asyncpg.connect(
host=url.hostname,
port=url.port,
user=url.username,
password=url.password,
database='postgres'
)
# Check if database exists
exists = await conn.fetchval(
"SELECT 1 FROM pg_database WHERE datname = $1",
url.path[1:] # Remove leading slash
)
if not exists:
await conn.execute(f'CREATE DATABASE "{url.path[1:]}"')
print(f"Database {url.path[1:]} created.")
await conn.close()
except Exception as e:
print(f"Error creating database: {e}")
async def execute_raw_sql(sql: str, params: dict = None) -> list:
"""Execute raw SQL query."""
async with AsyncSessionLocal() as session:
result = await session.execute(text(sql), params or {})
return result.fetchall()
async def get_table_info(table_name: str) -> dict:
"""Get information about a table."""
sql = """
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_name = :table_name
ORDER BY ordinal_position;
"""
result = await execute_raw_sql(sql, {'table_name': table_name})
return [
{
'column_name': row[0],
'data_type': row[1],
'is_nullable': row[2],
'column_default': row[3]
}
for row in result
]
```
## Database Initialization
```python
# app/db/init_db.py
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.database import get_db, create_tables
from app.models.user import User
from app.repositories.user import UserRepository
from app.core.config import settings
import asyncio
async def init_db() -> None:
"""Initialize database with tables and default data."""
# Create tables
await create_tables()
print("Database tables created.")
# Create default superuser
async with AsyncSessionLocal() as session:
user_repo = UserRepository(User, session)
# Check if superuser exists
existing_user = await user_repo.get_by_email("admin@example.com")
if not existing_user:
superuser_data = {
"username": "admin",
"email": "admin@example.com",
"first_name": "Admin",
"last_name": "User",
"is_superuser": True,
"is_active": True
}
superuser = User(**superuser_data)
superuser.set_password("admin123")
session.add(superuser)
await session.commit()
print("Superuser created.")
else:
print("Superuser already exists.")
if __name__ == "__main__":
asyncio.run(init_db())
```
## Testing Database
```python
# tests/conftest.py
import pytest
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.db.database import Base, get_db
from app.main import app
import pytest_asyncio
# Test database URL
TEST_DATABASE_URL = "sqlite+aiosqlite:///./test.db"
@pytest.fixture(scope="session")
def event_loop():
"""Create event loop for async tests."""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest_asyncio.fixture(scope="session")
async def test_engine():
"""Create test database engine."""
engine = create_async_engine(TEST_DATABASE_URL, echo=False)
# Create tables
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
# Drop tables
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await engine.dispose()
@pytest_asyncio.fixture
async def test_session(test_engine):
"""Create test database session."""
TestSessionLocal = sessionmaker(
test_engine, class_=AsyncSession, expire_on_commit=False
)
async with TestSessionLocal() as session:
yield session
@pytest.fixture
def override_get_db(test_session):
"""Override database dependency."""
async def _override_get_db():
yield test_session
app.dependency_overrides[get_db] = _override_get_db
yield
app.dependency_overrides = {}
```
## Database Health Check
```python
# app/api/health.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from app.db.database import get_db
import time
router = APIRouter()
@router.get("/health")
async def health_check(db: AsyncSession = Depends(get_db)):
"""Health check endpoint."""
checks = {
"status": "healthy",
"timestamp": time.time(),
"database": await check_database_health(db),
}
# Determine overall status
if checks["database"]["status"] != "ok":
checks["status"] = "unhealthy"
raise HTTPException(status_code=503, detail=checks)
return checks
async def check_database_health(db: AsyncSession) -> dict:
"""Check database connection."""
try:
start_time = time.time()
await db.execute(text("SELECT 1"))
response_time = (time.time() - start_time) * 1000 # milliseconds
return {
"status": "ok",
"response_time_ms": round(response_time, 2)
}
except Exception as e:
return {
"status": "error",
"error": str(e)
}
```
@@ -0,0 +1,160 @@
# FastAPI Deployment
Basic production deployment setup for FastAPI applications.
## Usage
```bash
# Run with Uvicorn
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
# Docker deployment
docker build -t fastapi-app .
docker run -p 8000:8000 fastapi-app
```
## Production Configuration
```python
# app/core/config.py
from pydantic import BaseSettings
import os
class Settings(BaseSettings):
"""Production settings."""
# App
PROJECT_NAME: str = "FastAPI App"
VERSION: str = "1.0.0"
SECRET_KEY: str = os.getenv("SECRET_KEY", "dev-key")
# Server
HOST: str = "0.0.0.0"
PORT: int = 8000
WORKERS: int = 4
# Database
DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./app.db")
# Redis
REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379")
class Config:
env_file = ".env"
settings = Settings()
```
## Docker Setup
```dockerfile
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy app
COPY . .
# Create non-root user
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
# Health check
HEALTHCHECK CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
## Docker Compose
```yaml
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/fastapi_db
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
db:
image: postgres:15
environment:
POSTGRES_DB: fastapi_db
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
```
## Environment Variables
```bash
# .env
SECRET_KEY=your-secret-key
DATABASE_URL=postgresql://user:pass@localhost/fastapi_db
REDIS_URL=redis://localhost:6379
```
## Health Check
```python
# app/api/health.py
from fastapi import APIRouter
from app.core.config import settings
router = APIRouter()
@router.get("/health")
async def health_check():
return {
"status": "healthy",
"version": settings.VERSION
}
```
## Deployment Script
```bash
#!/bin/bash
# deploy.sh
set -e
echo "Deploying FastAPI app..."
# Build and deploy
docker-compose build
docker-compose up -d
# Health check
echo "Checking health..."
if curl -f http://localhost:8000/health; then
echo "✅ Deployment successful!"
else
echo "❌ Deployment failed!"
exit 1
fi
```
@@ -0,0 +1,927 @@
# FastAPI Testing Framework
Comprehensive testing setup for FastAPI applications with pytest and async support.
## Usage
```bash
# Run all tests
pytest
# Run with coverage
pytest --cov=app --cov-report=html
# Run specific test file
pytest tests/test_api.py
# Run with verbose output
pytest -v -s
```
## Test Configuration
```python
# pytest.ini
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--cov=app
--cov-report=term-missing
--cov-report=html:htmlcov
--asyncio-mode=auto
--strict-markers
--disable-warnings
markers =
unit: Unit tests
integration: Integration tests
e2e: End-to-end tests
slow: Slow running tests
auth: Authentication tests
api: API tests
asyncio_mode = auto
```
## Test Dependencies
```python
# requirements/test.txt
pytest>=7.0.0
pytest-asyncio>=0.21.0
pytest-cov>=4.0.0
httpx>=0.24.0
factory-boy>=3.2.0
faker>=18.0.0
respx>=0.20.0
pytest-mock>=3.10.0
```
## Test Fixtures
```python
# tests/conftest.py
import pytest
import asyncio
from typing import AsyncGenerator, Generator
from fastapi.testclient import TestClient
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.db.database import get_db, Base
from app.models.user import User
from app.core.security import get_password_hash
from tests.factories import UserFactory
# Test database URL
TEST_DATABASE_URL = "sqlite+aiosqlite:///./test.db"
@pytest.fixture(scope="session")
def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
"""Create event loop for the test session."""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture(scope="session")
async def test_engine():
"""Create test database engine."""
engine = create_async_engine(
TEST_DATABASE_URL,
echo=False,
future=True
)
# Create tables
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
# Drop tables and dispose engine
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await engine.dispose()
@pytest.fixture
async def db_session(test_engine) -> AsyncGenerator[AsyncSession, None]:
"""Create database session for testing."""
TestSessionLocal = sessionmaker(
test_engine,
class_=AsyncSession,
expire_on_commit=False
)
async with TestSessionLocal() as session:
yield session
@pytest.fixture
def override_get_db(db_session: AsyncSession) -> Generator:
"""Override database dependency."""
async def _override_get_db():
yield db_session
app.dependency_overrides[get_db] = _override_get_db
yield
app.dependency_overrides = {}
@pytest.fixture
def client(override_get_db) -> Generator[TestClient, None, None]:
"""Create test client."""
with TestClient(app) as test_client:
yield test_client
@pytest.fixture
async def async_client(override_get_db) -> AsyncGenerator[AsyncClient, None]:
"""Create async test client."""
async with AsyncClient(app=app, base_url="http://test") as ac:
yield ac
@pytest.fixture
async def test_user(db_session: AsyncSession) -> User:
"""Create test user."""
user_data = {
"username": "testuser",
"email": "test@example.com",
"hashed_password": get_password_hash("testpass123"),
"first_name": "Test",
"last_name": "User",
"is_active": True,
"is_superuser": False
}
user = User(**user_data)
db_session.add(user)
await db_session.commit()
await db_session.refresh(user)
return user
@pytest.fixture
async def superuser(db_session: AsyncSession) -> User:
"""Create superuser."""
user_data = {
"username": "admin",
"email": "admin@example.com",
"hashed_password": get_password_hash("adminpass123"),
"first_name": "Admin",
"last_name": "User",
"is_active": True,
"is_superuser": True
}
user = User(**user_data)
db_session.add(user)
await db_session.commit()
await db_session.refresh(user)
return user
@pytest.fixture
def user_token(test_user: User) -> str:
"""Create authentication token for test user."""
from app.core.security import create_access_token
return create_access_token(subject=test_user.id)
@pytest.fixture
def superuser_token(superuser: User) -> str:
"""Create authentication token for superuser."""
from app.core.security import create_access_token
return create_access_token(subject=superuser.id)
@pytest.fixture
def auth_headers(user_token: str) -> dict[str, str]:
"""Create authorization headers."""
return {"Authorization": f"Bearer {user_token}"}
@pytest.fixture
def superuser_headers(superuser_token: str) -> dict[str, str]:
"""Create superuser authorization headers."""
return {"Authorization": f"Bearer {superuser_token}"}
```
## Test Factories
```python
# tests/factories.py
import factory
from factory import Faker, SubFactory
from app.models.user import User
from app.models.post import Post
from app.core.security import get_password_hash
class UserFactory(factory.Factory):
"""Factory for User model."""
class Meta:
model = User
username = Faker('user_name')
email = Faker('email')
first_name = Faker('first_name')
last_name = Faker('last_name')
hashed_password = factory.LazyAttribute(lambda obj: get_password_hash('testpass123'))
is_active = True
is_superuser = False
bio = Faker('text', max_nb_chars=200)
class SuperUserFactory(UserFactory):
"""Factory for superuser."""
username = 'admin'
email = 'admin@example.com'
first_name = 'Admin'
last_name = 'User'
is_superuser = True
class PostFactory(factory.Factory):
"""Factory for Post model."""
class Meta:
model = Post
title = Faker('sentence', nb_words=4)
content = Faker('text', max_nb_chars=1000)
slug = Faker('slug')
is_published = True
author = SubFactory(UserFactory)
class InactiveUserFactory(UserFactory):
"""Factory for inactive user."""
is_active = False
```
## API Testing
```python
# tests/test_api/test_users.py
import pytest
from httpx import AsyncClient
from app.models.user import User
from tests.factories import UserFactory
class TestUserAPI:
"""Test User API endpoints."""
@pytest.mark.asyncio
async def test_create_user(
self,
async_client: AsyncClient,
superuser_headers: dict
):
"""Test POST /api/v1/users/."""
user_data = {
"username": "newuser",
"email": "new@example.com",
"password": "newpass123",
"first_name": "New",
"last_name": "User"
}
response = await async_client.post(
"/api/v1/users/",
json=user_data,
headers=superuser_headers
)
assert response.status_code == 201
data = response.json()
assert data["username"] == user_data["username"]
assert data["email"] == user_data["email"]
assert "password" not in data
assert "hashed_password" not in data
@pytest.mark.asyncio
async def test_get_users(
self,
async_client: AsyncClient,
test_user: User,
auth_headers: dict
):
"""Test GET /api/v1/users/."""
response = await async_client.get(
"/api/v1/users/",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "items" in data
assert "total" in data
assert "page" in data
assert "size" in data
assert len(data["items"]) >= 1
@pytest.mark.asyncio
async def test_get_user(
self,
async_client: AsyncClient,
test_user: User,
auth_headers: dict
):
"""Test GET /api/v1/users/{id}."""
response = await async_client.get(
f"/api/v1/users/{test_user.id}",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["id"] == test_user.id
assert data["username"] == test_user.username
assert data["email"] == test_user.email
@pytest.mark.asyncio
async def test_update_user(
self,
async_client: AsyncClient,
test_user: User,
auth_headers: dict
):
"""Test PUT /api/v1/users/{id}."""
update_data = {
"first_name": "Updated",
"last_name": "Name",
"bio": "Updated bio"
}
response = await async_client.put(
f"/api/v1/users/{test_user.id}",
json=update_data,
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["first_name"] == update_data["first_name"]
assert data["last_name"] == update_data["last_name"]
assert data["bio"] == update_data["bio"]
@pytest.mark.asyncio
async def test_delete_user(
self,
async_client: AsyncClient,
test_user: User,
superuser_headers: dict
):
"""Test DELETE /api/v1/users/{id}."""
response = await async_client.delete(
f"/api/v1/users/{test_user.id}",
headers=superuser_headers
)
assert response.status_code == 204
# Verify user is deleted
get_response = await async_client.get(
f"/api/v1/users/{test_user.id}",
headers=superuser_headers
)
assert get_response.status_code == 404
@pytest.mark.asyncio
async def test_unauthorized_access(self, async_client: AsyncClient):
"""Test unauthorized access to protected endpoints."""
response = await async_client.get("/api/v1/users/")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_forbidden_access(
self,
async_client: AsyncClient,
auth_headers: dict
):
"""Test forbidden access to admin endpoints."""
user_data = {
"username": "unauthorized",
"email": "unauthorized@example.com",
"password": "pass123"
}
response = await async_client.post(
"/api/v1/users/",
json=user_data,
headers=auth_headers # Regular user, not superuser
)
assert response.status_code == 403
```
## Authentication Testing
```python
# tests/test_api/test_auth.py
import pytest
from httpx import AsyncClient
from app.models.user import User
from app.core.security import create_access_token, decode_token
class TestAuthAPI:
"""Test authentication API endpoints."""
@pytest.mark.asyncio
async def test_register(
self,
async_client: AsyncClient
):
"""Test user registration."""
user_data = {
"username": "newuser",
"email": "new@example.com",
"password": "newpass123",
"first_name": "New",
"last_name": "User"
}
response = await async_client.post(
"/api/v1/auth/register",
json=user_data
)
assert response.status_code == 201
data = response.json()
assert data["username"] == user_data["username"]
assert data["email"] == user_data["email"]
assert "password" not in data
@pytest.mark.asyncio
async def test_register_duplicate_email(
self,
async_client: AsyncClient,
test_user: User
):
"""Test registration with duplicate email."""
user_data = {
"username": "different",
"email": test_user.email, # Duplicate email
"password": "pass123",
"first_name": "Test",
"last_name": "User"
}
response = await async_client.post(
"/api/v1/auth/register",
json=user_data
)
assert response.status_code == 400
assert "Email already registered" in response.json()["detail"]
@pytest.mark.asyncio
async def test_login(
self,
async_client: AsyncClient,
test_user: User
):
"""Test user login."""
login_data = {
"username": test_user.username,
"password": "testpass123"
}
response = await async_client.post(
"/api/v1/auth/login",
data=login_data,
headers={"Content-Type": "application/x-www-form-urlencoded"}
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert "refresh_token" in data
assert data["token_type"] == "bearer"
assert "expires_in" in data
# Verify token is valid
payload = decode_token(data["access_token"])
assert payload is not None
assert payload["sub"] == str(test_user.id)
@pytest.mark.asyncio
async def test_login_invalid_credentials(
self,
async_client: AsyncClient,
test_user: User
):
"""Test login with invalid credentials."""
login_data = {
"username": test_user.username,
"password": "wrongpassword"
}
response = await async_client.post(
"/api/v1/auth/login",
data=login_data,
headers={"Content-Type": "application/x-www-form-urlencoded"}
)
assert response.status_code == 401
assert "Incorrect username or password" in response.json()["detail"]
@pytest.mark.asyncio
async def test_get_current_user(
self,
async_client: AsyncClient,
test_user: User,
auth_headers: dict
):
"""Test get current user endpoint."""
response = await async_client.get(
"/api/v1/auth/me",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["id"] == test_user.id
assert data["username"] == test_user.username
assert data["email"] == test_user.email
@pytest.mark.asyncio
async def test_refresh_token(
self,
async_client: AsyncClient,
test_user: User
):
"""Test token refresh."""
from app.core.security import create_refresh_token
refresh_token = create_refresh_token(subject=test_user.id)
response = await async_client.post(
"/api/v1/auth/refresh",
json={"refresh_token": refresh_token}
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert "refresh_token" in data
assert data["token_type"] == "bearer"
@pytest.mark.asyncio
async def test_change_password(
self,
async_client: AsyncClient,
test_user: User,
auth_headers: dict
):
"""Test password change."""
password_data = {
"current_password": "testpass123",
"new_password": "newpass123"
}
response = await async_client.post(
"/api/v1/auth/change-password",
json=password_data,
headers=auth_headers
)
assert response.status_code == 200
assert "Password changed successfully" in response.json()["message"]
```
## Model Testing
```python
# tests/test_models/test_user.py
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.user import User
from app.core.security import verify_password, get_password_hash
class TestUserModel:
"""Test User model."""
@pytest.mark.asyncio
async def test_create_user(self, db_session: AsyncSession):
"""Test user creation."""
user_data = {
"username": "testuser",
"email": "test@example.com",
"hashed_password": get_password_hash("password123"),
"first_name": "Test",
"last_name": "User"
}
user = User(**user_data)
db_session.add(user)
await db_session.commit()
await db_session.refresh(user)
assert user.id is not None
assert user.username == "testuser"
assert user.email == "test@example.com"
assert user.full_name == "Test User"
assert user.is_active is True
assert user.is_superuser is False
assert user.created_at is not None
assert user.updated_at is not None
def test_password_verification(self):
"""Test password verification."""
password = "testpassword123"
hashed = get_password_hash(password)
user = User(
username="test",
email="test@example.com",
hashed_password=hashed,
first_name="Test",
last_name="User"
)
assert user.verify_password(password)
assert not user.verify_password("wrongpassword")
def test_user_dict_excludes_password(self):
"""Test that dict() method excludes password."""
user = User(
username="test",
email="test@example.com",
hashed_password="hashed_password",
first_name="Test",
last_name="User"
)
user_dict = user.dict()
assert "hashed_password" not in user_dict
assert "username" in user_dict
assert "email" in user_dict
def test_user_repr(self):
"""Test user string representation."""
user = User(
id=1,
username="test",
email="test@example.com",
hashed_password="hash",
first_name="Test",
last_name="User"
)
assert repr(user) == "<User(id=1)>"
```
## Repository Testing
```python
# tests/test_repositories/test_user.py
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.user import User
from app.repositories.user import UserRepository
from app.core.security import get_password_hash
class TestUserRepository:
"""Test UserRepository."""
@pytest.mark.asyncio
async def test_create_user(self, db_session: AsyncSession):
"""Test user creation through repository."""
repo = UserRepository(User, db_session)
user_data = {
"username": "repouser",
"email": "repo@example.com",
"hashed_password": get_password_hash("password123"),
"first_name": "Repo",
"last_name": "User"
}
user = await repo.create(user_data)
assert user.id is not None
assert user.username == "repouser"
assert user.email == "repo@example.com"
@pytest.mark.asyncio
async def test_get_by_email(self, db_session: AsyncSession, test_user: User):
"""Test get user by email."""
repo = UserRepository(User, db_session)
found_user = await repo.get_by_email(test_user.email)
assert found_user is not None
assert found_user.id == test_user.id
assert found_user.email == test_user.email
@pytest.mark.asyncio
async def test_get_by_username(self, db_session: AsyncSession, test_user: User):
"""Test get user by username."""
repo = UserRepository(User, db_session)
found_user = await repo.get_by_username(test_user.username)
assert found_user is not None
assert found_user.id == test_user.id
assert found_user.username == test_user.username
@pytest.mark.asyncio
async def test_get_multi_with_pagination(
self,
db_session: AsyncSession,
test_user: User
):
"""Test get multiple users with pagination."""
repo = UserRepository(User, db_session)
# Create additional users
for i in range(5):
user_data = {
"username": f"user{i}",
"email": f"user{i}@example.com",
"hashed_password": get_password_hash("password123"),
"first_name": f"User{i}",
"last_name": "Test"
}
await repo.create(user_data)
# Test pagination
users = await repo.get_multi(skip=0, limit=3)
assert len(users) == 3
users_page_2 = await repo.get_multi(skip=3, limit=3)
assert len(users_page_2) >= 1 # At least test_user
@pytest.mark.asyncio
async def test_update_user(self, db_session: AsyncSession, test_user: User):
"""Test user update."""
repo = UserRepository(User, db_session)
updated_user = await repo.update(test_user.id, {
"first_name": "Updated",
"bio": "Updated bio"
})
assert updated_user is not None
assert updated_user.first_name == "Updated"
assert updated_user.bio == "Updated bio"
@pytest.mark.asyncio
async def test_delete_user(self, db_session: AsyncSession, test_user: User):
"""Test user deletion."""
repo = UserRepository(User, db_session)
result = await repo.delete(test_user.id)
assert result is True
# Verify user is deleted
deleted_user = await repo.get(test_user.id)
assert deleted_user is None
```
## Performance Testing
```python
# tests/test_performance.py
import pytest
import time
import asyncio
from httpx import AsyncClient
from app.models.user import User
from tests.factories import UserFactory
@pytest.mark.slow
class TestPerformance:
"""Test application performance."""
@pytest.mark.asyncio
async def test_concurrent_requests(
self,
async_client: AsyncClient,
auth_headers: dict
):
"""Test concurrent API requests."""
async def make_request():
response = await async_client.get(
"/api/v1/users/",
headers=auth_headers
)
return response.status_code
# Make 10 concurrent requests
start_time = time.time()
tasks = [make_request() for _ in range(10)]
results = await asyncio.gather(*tasks)
end_time = time.time()
# All requests should succeed
assert all(status == 200 for status in results)
# Should complete within reasonable time
assert (end_time - start_time) < 5.0
@pytest.mark.asyncio
async def test_large_dataset_pagination(
self,
async_client: AsyncClient,
db_session,
auth_headers: dict
):
"""Test pagination with large dataset."""
# Create 100 users
users = []
for i in range(100):
user = UserFactory.build()
users.append(user)
db_session.add_all(users)
await db_session.commit()
# Test pagination performance
start_time = time.time()
response = await async_client.get(
"/api/v1/users/?skip=0&limit=50",
headers=auth_headers
)
end_time = time.time()
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 50
# Should complete quickly
assert (end_time - start_time) < 1.0
```
## Mocking External Services
```python
# tests/test_external.py
import pytest
import respx
import httpx
from app.services.email import EmailService
class TestExternalServices:
"""Test external service integrations."""
@pytest.mark.asyncio
@respx.mock
async def test_email_service(
self,
async_client: AsyncClient
):
"""Test email service with mocked external API."""
# Mock email service API
respx.post("https://api.emailservice.com/send").mock(
return_value=httpx.Response(
200,
json={"message": "Email sent successfully"}
)
)
email_service = EmailService()
result = await email_service.send_email(
to="test@example.com",
subject="Test",
body="Test email"
)
assert result["success"] is True
```
## Test Utilities
```python
# tests/utils.py
from typing import Dict, Any
from httpx import Response
import json
def assert_response_status(response: Response, expected_status: int = 200):
"""Assert response status code."""
assert response.status_code == expected_status, f"Expected {expected_status}, got {response.status_code}. Response: {response.text}"
def assert_response_json(response: Response, expected_keys: list[str] = None):
"""Assert response is valid JSON with expected keys."""
assert response.headers.get("content-type") == "application/json"
data = response.json()
if expected_keys:
for key in expected_keys:
assert key in data, f"Missing key '{key}' in response"
return data
def create_auth_headers(token: str) -> Dict[str, str]:
"""Create authorization headers with token."""
return {"Authorization": f"Bearer {token}"}
async def create_test_users(db_session, count: int = 5) -> list:
"""Create multiple test users."""
from tests.factories import UserFactory
users = []
for i in range(count):
user = UserFactory.build()
users.append(user)
db_session.add_all(users)
await db_session.commit()
return users
```
@@ -0,0 +1,229 @@
# FastAPI Project Configuration
This file provides specific guidance for FastAPI web application development using Claude Code.
## Project Overview
This is a FastAPI application project optimized for modern API development with automatic documentation, type hints, and async support.
## FastAPI-Specific Development Commands
### Project Management
- `uvicorn app.main:app --reload` - Start development server with auto-reload
- `uvicorn app.main:app --host 0.0.0.0 --port 8000` - Start server on all interfaces
- `uvicorn app.main:app --workers 4` - Start with multiple workers
### Database Management
- `alembic init alembic` - Initialize Alembic migrations
- `alembic revision --autogenerate -m "message"` - Create migration
- `alembic upgrade head` - Apply migrations
- `alembic downgrade -1` - Rollback one migration
### Development Tools
- `python -m pytest` - Run tests
- `python -m pytest --cov=app` - Run tests with coverage
- `mypy app/` - Type checking
- `black app/` - Code formatting
## FastAPI Project Structure
```
myproject/
├── app/ # Application package
│ ├── __init__.py
│ ├── main.py # FastAPI application
│ ├── core/ # Core configuration
│ │ ├── __init__.py
│ │ ├── config.py # Settings
│ │ └── security.py # Authentication
│ ├── api/ # API routes
│ │ ├── __init__.py
│ │ ├── deps.py # Dependencies
│ │ └── v1/
│ │ ├── __init__.py
│ │ ├── api.py # API router
│ │ └── endpoints/
│ ├── models/ # Database models
│ │ ├── __init__.py
│ │ ├── base.py
│ │ └── user.py
│ ├── schemas/ # Pydantic schemas
│ │ ├── __init__.py
│ │ └── user.py
│ ├── repositories/ # Data access layer
│ │ ├── __init__.py
│ │ └── user.py
│ ├── services/ # Business logic
│ │ ├── __init__.py
│ │ └── auth.py
│ └── db/ # Database configuration
│ ├── __init__.py
│ └── database.py
├── alembic/ # Database migrations
├── tests/ # Test files
├── requirements.txt # Dependencies
└── docker-compose.yml # Docker configuration
```
## FastAPI Application Setup
```python
# app/main.py
from fastapi import FastAPI
from app.core.config import settings
from app.api.v1.api import api_router
app = FastAPI(
title=settings.PROJECT_NAME,
version=settings.VERSION,
openapi_url=f"/api/v1/openapi.json"
)
# Include routers
app.include_router(api_router, prefix="/api/v1")
@app.get("/")
async def root():
return {"message": "Welcome to FastAPI"}
```
## Configuration Management
```python
# app/core/config.py
from pydantic import BaseSettings
class Settings(BaseSettings):
PROJECT_NAME: str = "FastAPI App"
VERSION: str = "1.0.0"
SECRET_KEY: str
DATABASE_URL: str
class Config:
env_file = ".env"
settings = Settings()
```
## FastAPI Best Practices
### API Design
- Use Pydantic models for request/response validation
- Implement proper HTTP status codes
- Add comprehensive API documentation
- Use dependency injection for common functionality
- Implement proper error handling
### Database Integration
- Use SQLAlchemy with async support
- Implement repository pattern for data access
- Use Alembic for database migrations
- Add proper database connection pooling
- Implement database health checks
### Authentication & Security
- Use JWT tokens for authentication
- Implement OAuth2 with scopes
- Add rate limiting for API endpoints
- Use HTTPS in production
- Implement proper CORS configuration
### Performance Optimization
- Use async/await for I/O operations
- Implement response caching
- Add database query optimization
- Use connection pooling
- Monitor application performance
## Testing Strategy
### Test Organization
```python
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
@pytest.fixture
def client():
return TestClient(app)
```
### Test Types
- **Unit tests** for business logic
- **Integration tests** for API endpoints
- **Database tests** with test fixtures
- **Authentication tests** for security
## Deployment Considerations
### Production Setup
- Use Uvicorn with multiple workers
- Implement proper logging and monitoring
- Set up reverse proxy (Nginx)
- Use environment variables for configuration
- Implement health checks
### Docker Configuration
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0"]
```
### Environment Variables
```bash
SECRET_KEY=your-secret-key
DATABASE_URL=postgresql://user:pass@host/db
REDIS_URL=redis://localhost:6379
```
## Common FastAPI Patterns
### Dependency Injection
```python
from fastapi import Depends
from app.db.database import get_db
@app.get("/users/")
async def get_users(db: Session = Depends(get_db)):
return users
```
### Background Tasks
```python
from fastapi import BackgroundTasks
@app.post("/send-email/")
async def send_email(background_tasks: BackgroundTasks):
background_tasks.add_task(send_email_task)
return {"message": "Email sent"}
```
### Middleware
```python
@app.middleware("http")
async def add_process_time_header(request, call_next):
response = await call_next(request)
return response
```
## Development Workflow
### Getting Started
1. Clone repository
2. Create virtual environment: `python -m venv venv`
3. Install dependencies: `pip install -r requirements.txt`
4. Set environment variables
5. Run migrations: `alembic upgrade head`
6. Start server: `uvicorn app.main:app --reload`
### Code Quality
- **Black** - Code formatting
- **isort** - Import sorting
- **mypy** - Type checking
- **pytest** - Testing framework
- **flake8** - Linting
@@ -0,0 +1,384 @@
# Flask Application Factory Pattern
Create a scalable Flask application using the factory pattern with blueprints and configuration management.
## Purpose
This command helps you set up a Flask application using the application factory pattern, which is the recommended approach for larger Flask applications.
## Usage
```
/app-factory
```
## What this command does
1. **Creates application factory** with proper structure
2. **Sets up configuration management** for different environments
3. **Implements blueprints** for modular design
4. **Configures extensions** (database, auth, etc.)
5. **Adds error handling** and logging
## Example Output
```python
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_mail import Mail
from flask_wtf.csrf import CSRFProtect
from flask_cors import CORS
import logging
from logging.handlers import RotatingFileHandler
import os
# Initialize extensions
db = SQLAlchemy()
migrate = Migrate()
login = LoginManager()
mail = Mail()
csrf = CSRFProtect()
cors = CORS()
def create_app(config_class=None):
"""Application factory function."""
app = Flask(__name__)
# Load configuration
if config_class is None:
config_class = os.environ.get('FLASK_CONFIG', 'development')
if isinstance(config_class, str):
from app.config import config
app.config.from_object(config[config_class])
else:
app.config.from_object(config_class)
# Initialize extensions
db.init_app(app)
migrate.init_app(app, db)
login.init_app(app)
mail.init_app(app)
csrf.init_app(app)
cors.init_app(app)
# Configure login manager
login.login_view = 'auth.login'
login.login_message = 'Please log in to access this page.'
login.login_message_category = 'info'
# Register blueprints
from app.main import bp as main_bp
app.register_blueprint(main_bp)
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
from app.api import bp as api_bp
app.register_blueprint(api_bp, url_prefix='/api')
from app.admin import bp as admin_bp
app.register_blueprint(admin_bp, url_prefix='/admin')
# Error handlers
from app.errors import bp as errors_bp
app.register_blueprint(errors_bp)
# Configure logging
if not app.debug and not app.testing:
if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = RotatingFileHandler(
'logs/app.log',
maxBytes=10240,
backupCount=10
)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Flask application startup')
return app
# Import models (avoid circular imports)
from app import models
```
```python
# app/config.py
import os
from dotenv import load_dotenv
basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))
class Config:
"""Base configuration class."""
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
# Mail configuration
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
ADMIN_EMAIL = os.environ.get('ADMIN_EMAIL')
# Pagination
POSTS_PER_PAGE = 10
USERS_PER_PAGE = 50
# Upload configuration
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
UPLOAD_FOLDER = os.path.join(basedir, 'uploads')
@staticmethod
def init_app(app):
pass
class DevelopmentConfig(Config):
"""Development configuration."""
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'app-dev.db')
class TestingConfig(Config):
"""Testing configuration."""
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or 'sqlite://'
WTF_CSRF_ENABLED = False
class ProductionConfig(Config):
"""Production configuration."""
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'app.db')
@classmethod
def init_app(cls, app):
Config.init_app(app)
# Log to stderr
import logging
from logging import StreamHandler
file_handler = StreamHandler()
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
```
```python
# app/main/__init__.py
from flask import Blueprint
bp = Blueprint('main', __name__)
from app.main import routes
```
```python
# app/main/routes.py
from flask import render_template, request, current_app
from app.main import bp
from app.models import Post
@bp.route('/')
@bp.route('/index')
def index():
"""Home page."""
page = request.args.get('page', 1, type=int)
posts = Post.query.filter_by(published=True).order_by(
Post.created_at.desc()
).paginate(
page=page,
per_page=current_app.config['POSTS_PER_PAGE'],
error_out=False
)
return render_template('index.html', posts=posts)
@bp.route('/about')
def about():
"""About page."""
return render_template('about.html')
```
```python
# app/auth/__init__.py
from flask import Blueprint
bp = Blueprint('auth', __name__)
from app.auth import routes
```
```python
# app/auth/routes.py
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.urls import url_parse
from app import db
from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm
from app.models import User
@bp.route('/login', methods=['GET', 'POST'])
def login():
"""User login."""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password', 'error')
return redirect(url_for('auth.login'))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('main.index')
return redirect(next_page)
return render_template('auth/login.html', form=form)
@bp.route('/logout')
@login_required
def logout():
"""User logout."""
logout_user()
flash('You have been logged out.', 'info')
return redirect(url_for('main.index'))
@bp.route('/register', methods=['GET', 'POST'])
def register():
"""User registration."""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Registration successful!', 'success')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
```
```python
# app/models.py
from app import db, login
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from datetime import datetime
class User(UserMixin, db.Model):
"""User model."""
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
created_at = db.Column(db.DateTime, index=True, default=datetime.utcnow)
posts = db.relationship('Post', backref='author', lazy='dynamic')
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<User {self.username}>'
@login.user_loader
def load_user(id):
return User.query.get(int(id))
class Post(db.Model):
"""Post model."""
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text, nullable=False)
published = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, index=True, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
def __repr__(self):
return f'<Post {self.title}>'
```
```python
# app.py (main application entry point)
from app import create_app, db
from app.models import User, Post
app = create_app()
@app.shell_context_processor
def make_shell_context():
"""Add models to shell context."""
return {'db': db, 'User': User, 'Post': Post}
if __name__ == '__main__':
app.run(debug=True)
```
## Project Structure
```
project/
├── app/
│ ├── __init__.py # Application factory
│ ├── config.py # Configuration classes
│ ├── models.py # Database models
│ ├── main/
│ │ ├── __init__.py # Main blueprint
│ │ └── routes.py # Main routes
│ ├── auth/
│ │ ├── __init__.py # Auth blueprint
│ │ ├── routes.py # Auth routes
│ │ └── forms.py # Auth forms
│ ├── api/
│ │ ├── __init__.py # API blueprint
│ │ └── routes.py # API routes
│ └── templates/ # Jinja2 templates
├── migrations/ # Database migrations
├── logs/ # Application logs
├── app.py # Application entry point
├── requirements.txt # Dependencies
└── .env # Environment variables
```
## Benefits of Application Factory
- **Testing**: Easy to create app instances with different configs
- **Scalability**: Modular design with blueprints
- **Configuration**: Environment-specific settings
- **Extensions**: Proper initialization order
- **Maintainability**: Clear separation of concerns
@@ -0,0 +1,243 @@
# Flask Blueprint Generator
Create organized Flask blueprints for modular application structure.
## Usage
```bash
# Create a new blueprint
flask create-blueprint users
flask create-blueprint api/v1
```
## Blueprint Structure
Generates a complete blueprint with:
- Routes and view functions
- Error handlers
- Template folder structure
- Static file organization
## Example Blueprint
```python
# app/blueprints/users/__init__.py
from flask import Blueprint
users_bp = Blueprint(
'users',
__name__,
url_prefix='/users',
template_folder='templates',
static_folder='static'
)
from . import routes, models
# app/blueprints/users/routes.py
from flask import render_template, request, redirect, url_for, flash
from . import users_bp
from .models import User
from .forms import UserForm
@users_bp.route('/')
def index():
"""List all users."""
users = User.query.all()
return render_template('users/index.html', users=users)
@users_bp.route('/create', methods=['GET', 'POST'])
def create():
"""Create a new user."""
form = UserForm()
if form.validate_on_submit():
user = User(
username=form.username.data,
email=form.email.data
)
user.save()
flash('User created successfully!', 'success')
return redirect(url_for('users.index'))
return render_template('users/create.html', form=form)
@users_bp.route('/<int:user_id>')
def detail(user_id):
"""Show user details."""
user = User.query.get_or_404(user_id)
return render_template('users/detail.html', user=user)
@users_bp.route('/<int:user_id>/edit', methods=['GET', 'POST'])
def edit(user_id):
"""Edit an existing user."""
user = User.query.get_or_404(user_id)
form = UserForm(obj=user)
if form.validate_on_submit():
user.username = form.username.data
user.email = form.email.data
user.save()
flash('User updated successfully!', 'success')
return redirect(url_for('users.detail', user_id=user.id))
return render_template('users/edit.html', form=form, user=user)
@users_bp.route('/<int:user_id>/delete', methods=['POST'])
def delete(user_id):
"""Delete a user."""
user = User.query.get_or_404(user_id)
user.delete()
flash('User deleted successfully!', 'success')
return redirect(url_for('users.index'))
# Error handlers
@users_bp.errorhandler(404)
def not_found(error):
return render_template('users/404.html'), 404
@users_bp.errorhandler(500)
def internal_error(error):
return render_template('users/500.html'), 500
```
## Blueprint Models
```python
# app/blueprints/users/models.py
from app.extensions import db
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
class User(db.Model):
"""User model."""
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
is_active = db.Column(db.Boolean, default=True)
def __repr__(self):
return f'<User {self.username}>'
def set_password(self, password):
"""Set password hash."""
self.password_hash = generate_password_hash(password)
def check_password(self, password):
"""Check password hash."""
return check_password_hash(self.password_hash, password)
def save(self):
"""Save user to database."""
db.session.add(self)
db.session.commit()
def delete(self):
"""Delete user from database."""
db.session.delete(self)
db.session.commit()
def to_dict(self):
"""Convert to dictionary."""
return {
'id': self.id,
'username': self.username,
'email': self.email,
'created_at': self.created_at.isoformat(),
'is_active': self.is_active
}
```
## Blueprint Forms
```python
# app/blueprints/users/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, EmailField, PasswordField, BooleanField
from wtforms.validators import DataRequired, Email, Length, EqualTo
from .models import User
class UserForm(FlaskForm):
"""User creation/edit form."""
username = StringField(
'Username',
validators=[
DataRequired(),
Length(min=3, max=80)
]
)
email = EmailField(
'Email',
validators=[
DataRequired(),
Email(),
Length(max=120)
]
)
password = PasswordField(
'Password',
validators=[
DataRequired(),
Length(min=8)
]
)
confirm_password = PasswordField(
'Confirm Password',
validators=[
DataRequired(),
EqualTo('password', message='Passwords must match')
]
)
is_active = BooleanField('Active')
def validate_username(self, field):
"""Validate username uniqueness."""
if User.query.filter_by(username=field.data).first():
raise ValidationError('Username already exists.')
def validate_email(self, field):
"""Validate email uniqueness."""
if User.query.filter_by(email=field.data).first():
raise ValidationError('Email already registered.')
```
## Registration in Main App
```python
# app/__init__.py
from flask import Flask
from app.blueprints.users import users_bp
def create_app():
app = Flask(__name__)
# Register blueprints
app.register_blueprint(users_bp)
return app
```
## Template Structure
```
templates/
├── base.html
└── users/
├── index.html
├── create.html
├── detail.html
├── edit.html
├── 404.html
└── 500.html
```
## Best Practices
- Use blueprints to organize related functionality
- Keep models, forms, and routes in separate files
- Implement proper error handling
- Use URL prefixes for namespacing
- Follow RESTful routing conventions
- Include comprehensive docstrings
- Add form validation and CSRF protection
@@ -0,0 +1,410 @@
# Flask Database Management
Complete database setup and management for Flask applications using SQLAlchemy.
## Usage
```bash
# Initialize database
flask db init
# Create migration
flask db migrate -m "Initial migration"
# Apply migrations
flask db upgrade
# Downgrade migration
flask db downgrade
```
## Database Configuration
```python
# config.py
import os
from urllib.parse import quote_plus
class Config:
"""Base configuration."""
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_RECORD_QUERIES = True
class DevelopmentConfig(Config):
"""Development configuration."""
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///app.db'
class ProductionConfig(Config):
"""Production configuration."""
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
f"postgresql://{os.environ.get('DB_USER')}:{quote_plus(os.environ.get('DB_PASSWORD'))}@" \
f"{os.environ.get('DB_HOST')}:{os.environ.get('DB_PORT', '5432')}/{os.environ.get('DB_NAME')}"
class TestingConfig(Config):
"""Testing configuration."""
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
WTF_CSRF_ENABLED = False
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig,
'default': DevelopmentConfig
}
```
## Database Extensions
```python
# app/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_wtf.csrf import CSRFProtect
from flask_caching import Cache
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
# Initialize extensions
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
csrf = CSRFProtect()
cache = Cache()
limiter = Limiter(key_func=get_remote_address)
def init_extensions(app):
"""Initialize Flask extensions."""
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
csrf.init_app(app)
cache.init_app(app)
limiter.init_app(app)
# Configure login manager
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Please log in to access this page.'
login_manager.login_message_category = 'info'
```
## Base Model
```python
# app/models/base.py
from app.extensions import db
from datetime import datetime
from sqlalchemy.ext.declarative import declared_attr
class TimestampMixin:
"""Add timestamp fields to model."""
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class BaseModel(db.Model, TimestampMixin):
"""Base model with common functionality."""
__abstract__ = True
id = db.Column(db.Integer, primary_key=True)
@declared_attr
def __tablename__(cls):
return cls.__name__.lower()
def save(self, commit=True):
"""Save model to database."""
db.session.add(self)
if commit:
db.session.commit()
return self
def delete(self, commit=True):
"""Delete model from database."""
db.session.delete(self)
if commit:
db.session.commit()
def update(self, **kwargs):
"""Update model attributes."""
for key, value in kwargs.items():
if hasattr(self, key):
setattr(self, key, value)
return self.save()
def to_dict(self, exclude=None):
"""Convert model to dictionary."""
exclude = exclude or []
return {
column.name: getattr(self, column.name)
for column in self.__table__.columns
if column.name not in exclude
}
@classmethod
def get_or_404(cls, id):
"""Get model by ID or raise 404."""
return cls.query.get_or_404(id)
@classmethod
def create(cls, **kwargs):
"""Create new model instance."""
instance = cls(**kwargs)
return instance.save()
```
## Example Models
```python
# app/models/user.py
from app.extensions import db
from app.models.base import BaseModel
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
class User(UserMixin, BaseModel):
"""User model."""
__tablename__ = 'users'
username = db.Column(db.String(80), unique=True, nullable=False, index=True)
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(255), nullable=False)
first_name = db.Column(db.String(50), nullable=False)
last_name = db.Column(db.String(50), nullable=False)
is_active = db.Column(db.Boolean, default=True, nullable=False)
is_admin = db.Column(db.Boolean, default=False, nullable=False)
last_login = db.Column(db.DateTime)
# Relationships
posts = db.relationship('Post', backref='author', lazy='dynamic', cascade='all, delete-orphan')
def __repr__(self):
return f'<User {self.username}>'
def set_password(self, password):
"""Set password hash."""
self.password_hash = generate_password_hash(password)
def check_password(self, password):
"""Check password hash."""
return check_password_hash(self.password_hash, password)
@property
def full_name(self):
"""Get user's full name."""
return f"{self.first_name} {self.last_name}"
def to_dict(self, exclude=None):
"""Convert to dictionary excluding sensitive data."""
exclude = exclude or ['password_hash']
return super().to_dict(exclude=exclude)
# app/models/post.py
class Post(BaseModel):
"""Blog post model."""
__tablename__ = 'posts'
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
slug = db.Column(db.String(200), unique=True, nullable=False, index=True)
status = db.Column(db.String(20), default='draft', nullable=False)
published_at = db.Column(db.DateTime)
# Foreign keys
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'))
# Relationships
category = db.relationship('Category', backref='posts')
tags = db.relationship('Tag', secondary='post_tags', backref='posts')
def __repr__(self):
return f'<Post {self.title}>'
@property
def is_published(self):
"""Check if post is published."""
return self.status == 'published' and self.published_at is not None
```
## Database CLI Commands
```python
# app/cli.py
import click
from flask import current_app
from flask.cli import with_appcontext
from app.extensions import db
from app.models import User, Post, Category
@click.command()
@with_appcontext
def init_db():
"""Initialize database."""
db.create_all()
click.echo('Database initialized.')
@click.command()
@with_appcontext
def seed_db():
"""Seed database with sample data."""
# Create admin user
admin = User(
username='admin',
email='admin@example.com',
first_name='Admin',
last_name='User',
is_admin=True
)
admin.set_password('admin123')
admin.save()
# Create sample category
category = Category(
name='Technology',
description='Tech-related posts'
)
category.save()
# Create sample post
post = Post(
title='Welcome to Flask',
content='This is a sample blog post.',
slug='welcome-to-flask',
status='published',
user_id=admin.id,
category_id=category.id
)
post.save()
click.echo('Database seeded with sample data.')
@click.command()
@with_appcontext
def reset_db():
"""Reset database."""
if click.confirm('Are you sure you want to reset the database?'):
db.drop_all()
db.create_all()
click.echo('Database reset.')
def init_commands(app):
"""Register CLI commands."""
app.cli.add_command(init_db)
app.cli.add_command(seed_db)
app.cli.add_command(reset_db)
```
## Connection Pooling
```python
# app/database.py
from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool
def configure_database(app):
"""Configure database with connection pooling."""
if app.config.get('SQLALCHEMY_DATABASE_URI', '').startswith('postgresql'):
# PostgreSQL configuration
engine = create_engine(
app.config['SQLALCHEMY_DATABASE_URI'],
poolclass=QueuePool,
pool_size=10,
max_overflow=20,
pool_recycle=3600,
pool_pre_ping=True
)
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
'pool_size': 10,
'max_overflow': 20,
'pool_recycle': 3600,
'pool_pre_ping': True
}
```
## Database Utilities
```python
# app/utils/database.py
from app.extensions import db
from sqlalchemy import text
from flask import current_app
def execute_sql(sql, params=None):
"""Execute raw SQL query."""
with db.engine.connect() as conn:
result = conn.execute(text(sql), params or {})
return result.fetchall()
def backup_database():
"""Create database backup."""
# Implementation depends on database type
pass
def check_database_health():
"""Check database connection health."""
try:
db.session.execute(text('SELECT 1'))
return True
except Exception as e:
current_app.logger.error(f'Database health check failed: {e}')
return False
def get_table_info(table_name):
"""Get table information."""
inspector = db.inspect(db.engine)
return {
'columns': inspector.get_columns(table_name),
'indexes': inspector.get_indexes(table_name),
'foreign_keys': inspector.get_foreign_keys(table_name)
}
```
## Testing Database
```python
# tests/conftest.py
import pytest
from app import create_app
from app.extensions import db
from app.models import User
@pytest.fixture(scope='session')
def app():
"""Create test app."""
app = create_app('testing')
with app.app_context():
db.create_all()
yield app
db.drop_all()
@pytest.fixture
def client(app):
"""Create test client."""
return app.test_client()
@pytest.fixture
def db_session(app):
"""Create database session for testing."""
with app.app_context():
db.session.begin()
yield db.session
db.session.rollback()
@pytest.fixture
def user(db_session):
"""Create test user."""
user = User(
username='testuser',
email='test@example.com',
first_name='Test',
last_name='User'
)
user.set_password('testpass')
user.save()
return user
```
@@ -0,0 +1,620 @@
# Flask Deployment Configuration
Complete production deployment setup for Flask applications.
## Usage
```bash
# Build Docker image
docker build -t myapp .
# Run with Docker Compose
docker-compose up -d
# Deploy to cloud
gunicorn --bind 0.0.0.0:8000 app:app
```
## Production Configuration
```python
# config.py
import os
from urllib.parse import quote_plus
class ProductionConfig:
"""Production configuration."""
# Security
SECRET_KEY = os.environ.get('SECRET_KEY')
DEBUG = False
TESTING = False
# Database
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
f"postgresql://{os.environ.get('DB_USER')}:{quote_plus(os.environ.get('DB_PASSWORD'))}@" \
f"{os.environ.get('DB_HOST')}:{os.environ.get('DB_PORT', '5432')}/{os.environ.get('DB_NAME')}"
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ENGINE_OPTIONS = {
'pool_size': 10,
'max_overflow': 20,
'pool_recycle': 3600,
'pool_pre_ping': True
}
# Security Headers
SECURITY_HEADERS = {
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
}
# Session
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
PERMANENT_SESSION_LIFETIME = 3600 # 1 hour
# Cache
CACHE_TYPE = 'redis'
CACHE_REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')
CACHE_DEFAULT_TIMEOUT = 300
# Rate Limiting
RATELIMIT_STORAGE_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/1')
RATELIMIT_DEFAULT = '100/hour'
# Logging
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO')
LOG_FILE = os.environ.get('LOG_FILE', '/var/log/app/app.log')
# File Upload
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER', '/var/uploads')
# Email
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT', 587))
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER')
```
## WSGI Configuration
```python
# wsgi.py
import os
from app import create_app
# Get environment
config_name = os.environ.get('FLASK_ENV', 'production')
app = create_app(config_name)
if __name__ == "__main__":
app.run()
```
## Gunicorn Configuration
```python
# gunicorn.conf.py
import multiprocessing
import os
# Server socket
bind = f"0.0.0.0:{os.environ.get('PORT', 8000)}"
backlog = 2048
# Worker processes
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = 'sync'
worker_connections = 1000
timeout = 30
keepalive = 60
max_requests = 1000
max_requests_jitter = 100
# Security
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190
# Logging
accesslog = '-'
errorlog = '-'
loglevel = os.environ.get('LOG_LEVEL', 'info').lower()
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
# Process naming
proc_name = 'flask_app'
# Server mechanics
daemon = False
pidfile = '/tmp/gunicorn.pid'
user = os.environ.get('USER', 'www-data')
group = os.environ.get('GROUP', 'www-data')
tmp_upload_dir = None
# SSL
keyfile = os.environ.get('SSL_KEYFILE')
certfile = os.environ.get('SSL_CERTFILE')
```
## Docker Configuration
```dockerfile
# Dockerfile
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Install system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create app user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Set work directory
WORKDIR /app
# Install Python dependencies
COPY requirements/production.txt ./requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create necessary directories
RUN mkdir -p /var/log/app /var/uploads && \
chown -R appuser:appuser /app /var/log/app /var/uploads
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Run application
CMD ["gunicorn", "--config", "gunicorn.conf.py", "wsgi:app"]
```
## Docker Compose
```yaml
# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
environment:
- FLASK_ENV=production
- DATABASE_URL=postgresql://postgres:password@db:5432/myapp
- REDIS_URL=redis://redis:6379/0
- SECRET_KEY=${SECRET_KEY}
depends_on:
- db
- redis
volumes:
- uploads:/var/uploads
- logs:/var/log/app
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:15
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 30s
timeout: 10s
retries: 3
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
- uploads:/var/uploads:ro
depends_on:
- web
restart: unless-stopped
volumes:
postgres_data:
redis_data:
uploads:
logs:
```
## Nginx Configuration
```nginx
# nginx.conf
events {
worker_connections 1024;
}
http {
upstream app {
server web:8000;
}
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
# File upload size
client_max_body_size 16M;
# Static files
location /static/ {
alias /var/uploads/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /uploads/ {
alias /var/uploads/;
expires 1h;
}
# API rate limiting
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Login rate limiting
location /auth/login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Main application
location / {
proxy_pass http://app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
}
```
## Environment Variables
```bash
# .env.production
# Application
FLASK_ENV=production
SECRET_KEY=your-super-secret-key-here
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
DB_USER=user
DB_PASSWORD=password
# Redis
REDIS_URL=redis://localhost:6379/0
# Email
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
MAIL_USE_TLS=true
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=your-app-password
MAIL_DEFAULT_SENDER=your-email@gmail.com
# Logging
LOG_LEVEL=INFO
LOG_FILE=/var/log/app/app.log
# File Upload
UPLOAD_FOLDER=/var/uploads
# SSL (if using)
SSL_KEYFILE=/path/to/private.key
SSL_CERTFILE=/path/to/certificate.crt
```
## Health Check Endpoint
```python
# app/health.py
from flask import Blueprint, jsonify
from app.extensions import db
from sqlalchemy import text
import redis
import os
health_bp = Blueprint('health', __name__)
@health_bp.route('/health')
def health_check():
"""Application health check."""
checks = {
'status': 'healthy',
'database': check_database(),
'redis': check_redis(),
'disk_space': check_disk_space()
}
# Determine overall status
if all(check['status'] == 'ok' for check in checks.values() if isinstance(check, dict)):
status_code = 200
else:
status_code = 503
checks['status'] = 'unhealthy'
return jsonify(checks), status_code
def check_database():
"""Check database connectivity."""
try:
db.session.execute(text('SELECT 1'))
return {'status': 'ok', 'message': 'Database connection successful'}
except Exception as e:
return {'status': 'error', 'message': str(e)}
def check_redis():
"""Check Redis connectivity."""
try:
redis_url = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')
r = redis.from_url(redis_url)
r.ping()
return {'status': 'ok', 'message': 'Redis connection successful'}
except Exception as e:
return {'status': 'error', 'message': str(e)}
def check_disk_space():
"""Check available disk space."""
try:
import shutil
total, used, free = shutil.disk_usage('/')
free_percent = (free / total) * 100
if free_percent > 10:
status = 'ok'
elif free_percent > 5:
status = 'warning'
else:
status = 'critical'
return {
'status': status,
'free_space_percent': round(free_percent, 2),
'free_space_gb': round(free / (1024**3), 2)
}
except Exception as e:
return {'status': 'error', 'message': str(e)}
```
## Monitoring and Logging
```python
# app/logging.py
import logging
import logging.handlers
import os
from flask import request, g
import time
def setup_logging(app):
"""Setup application logging."""
if not app.debug and not app.testing:
# File logging
if app.config.get('LOG_FILE'):
file_handler = logging.handlers.RotatingFileHandler(
app.config['LOG_FILE'],
maxBytes=10240000, # 10MB
backupCount=10
)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s '
'[in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(getattr(logging, app.config.get('LOG_LEVEL', 'INFO')))
app.logger.addHandler(file_handler)
# Console logging
if not app.logger.handlers:
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s'
))
stream_handler.setLevel(logging.INFO)
app.logger.addHandler(stream_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Application startup')
# Request timing middleware
@app.before_request
def before_request():
g.start_time = time.time()
@app.after_request
def after_request(response):
if hasattr(g, 'start_time'):
duration = time.time() - g.start_time
app.logger.info(
f'{request.method} {request.path} - '
f'{response.status_code} - {duration:.3f}s'
)
return response
```
## Database Backup Script
```bash
#!/bin/bash
# backup.sh
set -e
# Configuration
BACKUP_DIR="/var/backups/db"
DATABASE_URL="$DATABASE_URL"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/backup_$DATE.sql"
RETENTION_DAYS=7
# Create backup directory
mkdir -p "$BACKUP_DIR"
# Create backup
echo "Creating database backup..."
pg_dump "$DATABASE_URL" > "$BACKUP_FILE"
# Compress backup
gzip "$BACKUP_FILE"
# Remove old backups
echo "Cleaning up old backups..."
find "$BACKUP_DIR" -name "backup_*.sql.gz" -mtime +$RETENTION_DAYS -delete
echo "Backup completed: $BACKUP_FILE.gz"
```
## Deployment Scripts
```bash
#!/bin/bash
# deploy.sh
set -e
echo "Starting deployment..."
# Pull latest code
git pull origin main
# Build new Docker image
docker-compose build web
# Run database migrations
docker-compose run --rm web flask db upgrade
# Update services
docker-compose up -d
# Health check
echo "Waiting for application to be ready..."
sleep 10
if curl -f http://localhost:8000/health; then
echo "Deployment successful!"
else
echo "Deployment failed - health check failed"
exit 1
fi
```
## Monitoring with Prometheus
```python
# app/metrics.py
from prometheus_flask_exporter import PrometheusMetrics
from flask import request
import time
def setup_metrics(app):
"""Setup Prometheus metrics."""
metrics = PrometheusMetrics(app)
# Custom metrics
metrics.info('app_info', 'Application info', version='1.0.0')
# Database connection pool metrics
@metrics.gauge('db_pool_size', 'Database connection pool size')
def db_pool_size():
from app.extensions import db
return db.engine.pool.size()
return metrics
```
@@ -0,0 +1,217 @@
# Flask Route Generator
Create Flask routes with proper structure and error handling.
## Purpose
This command helps you quickly create Flask routes with validation, error handling, and best practices.
## Usage
```
/flask-route
```
## What this command does
1. **Creates route functions** with proper decorators
2. **Adds request validation** and error handling
3. **Includes JSON responses** and status codes
4. **Implements authentication** if needed
5. **Follows Flask conventions** and best practices
## Example Output
```python
# routes.py or app.py
from flask import Flask, request, jsonify, abort
from flask_sqlalchemy import SQLAlchemy
from werkzeug.exceptions import BadRequest
app = Flask(__name__)
@app.route('/users', methods=['GET'])
def get_users():
"""Get all users with optional pagination."""
try:
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
users = User.query.paginate(
page=page,
per_page=per_page,
error_out=False
)
return jsonify({
'users': [user.to_dict() for user in users.items],
'total': users.total,
'pages': users.pages,
'current_page': page
}), 200
except Exception as e:
return jsonify({'error': 'Failed to fetch users'}), 500
@app.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
"""Get a specific user by ID."""
try:
user = User.query.get_or_404(user_id)
return jsonify(user.to_dict()), 200
except Exception as e:
return jsonify({'error': 'User not found'}), 404
@app.route('/users', methods=['POST'])
def create_user():
"""Create a new user."""
try:
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
# Validate required fields
required_fields = ['name', 'email']
for field in required_fields:
if field not in data:
return jsonify({'error': f'{field} is required'}), 400
# Check if email already exists
if User.query.filter_by(email=data['email']).first():
return jsonify({'error': 'Email already exists'}), 409
# Create new user
user = User(
name=data['name'],
email=data['email'],
phone=data.get('phone'),
address=data.get('address')
)
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict()), 201
except BadRequest:
return jsonify({'error': 'Invalid JSON data'}), 400
except Exception as e:
db.session.rollback()
return jsonify({'error': 'Failed to create user'}), 500
@app.route('/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
"""Update an existing user."""
try:
user = User.query.get_or_404(user_id)
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
# Update fields
if 'name' in data:
user.name = data['name']
if 'email' in data:
# Check if new email already exists
existing_user = User.query.filter_by(email=data['email']).first()
if existing_user and existing_user.id != user_id:
return jsonify({'error': 'Email already exists'}), 409
user.email = data['email']
if 'phone' in data:
user.phone = data['phone']
if 'address' in data:
user.address = data['address']
db.session.commit()
return jsonify(user.to_dict()), 200
except BadRequest:
return jsonify({'error': 'Invalid JSON data'}), 400
except Exception as e:
db.session.rollback()
return jsonify({'error': 'Failed to update user'}), 500
@app.route('/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
"""Delete a user."""
try:
user = User.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return jsonify({'message': 'User deleted successfully'}), 200
except Exception as e:
db.session.rollback()
return jsonify({'error': 'Failed to delete user'}), 500
# Error handlers
@app.errorhandler(404)
def not_found(error):
return jsonify({'error': 'Resource not found'}), 404
@app.errorhandler(400)
def bad_request(error):
return jsonify({'error': 'Bad request'}), 400
@app.errorhandler(500)
def internal_error(error):
return jsonify({'error': 'Internal server error'}), 500
```
## Route Patterns Supported
### Basic Routes
```python
@app.route('/')
@app.route('/users')
@app.route('/users/<int:user_id>')
```
### HTTP Methods
```python
@app.route('/users', methods=['GET', 'POST'])
@app.route('/users/<int:id>', methods=['GET', 'PUT', 'DELETE'])
```
### URL Parameters
```python
@app.route('/users/<int:user_id>')
@app.route('/posts/<string:slug>')
@app.route('/files/<path:filename>')
```
## Best Practices Included
- **Input validation** for all user data
- **Proper HTTP status codes** (200, 201, 400, 404, 500)
- **JSON responses** with consistent structure
- **Error handling** with try/catch blocks
- **Database rollback** on errors
- **RESTful conventions** for URL design
- **Documentation strings** for each route
- **Request data validation** before processing
## Common Response Patterns
```python
# Success with data
return jsonify({'data': result}), 200
# Created resource
return jsonify({'data': new_resource, 'id': new_id}), 201
# Validation error
return jsonify({'error': 'Field is required'}), 400
# Not found
return jsonify({'error': 'Resource not found'}), 404
# Server error
return jsonify({'error': 'Internal server error'}), 500
```
@@ -0,0 +1,559 @@
# Flask Testing Suite
Comprehensive testing setup for Flask applications with pytest.
## Usage
```bash
# Run all tests
pytest
# Run with coverage
pytest --cov=app --cov-report=html
# Run specific test file
pytest tests/test_models.py
# Run with verbose output
pytest -v
```
## Test Configuration
```python
# pytest.ini
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--cov=app
--cov-report=term-missing
--cov-report=html:htmlcov
--strict-markers
--disable-warnings
markers =
unit: Unit tests
integration: Integration tests
slow: Slow running tests
auth: Authentication tests
```
## Test Fixtures
```python
# tests/conftest.py
import pytest
import tempfile
import os
from app import create_app
from app.extensions import db
from app.models import User, Post, Category
from flask_login import login_user
@pytest.fixture(scope='session')
def app():
"""Create test application."""
# Create temporary database
db_fd, db_path = tempfile.mkstemp()
app = create_app({
'TESTING': True,
'SQLALCHEMY_DATABASE_URI': f'sqlite:///{db_path}',
'WTF_CSRF_ENABLED': False,
'SECRET_KEY': 'test-secret-key'
})
with app.app_context():
db.create_all()
yield app
# Cleanup
os.close(db_fd)
os.unlink(db_path)
@pytest.fixture
def client(app):
"""Create test client."""
return app.test_client()
@pytest.fixture
def runner(app):
"""Create test CLI runner."""
return app.test_cli_runner()
@pytest.fixture
def db_session(app):
"""Create database session for testing."""
with app.app_context():
connection = db.engine.connect()
transaction = connection.begin()
# Configure session to use the connection
db.session.configure(bind=connection)
yield db.session
# Rollback transaction
transaction.rollback()
connection.close()
db.session.remove()
@pytest.fixture
def user(db_session):
"""Create test user."""
user = User(
username='testuser',
email='test@example.com',
first_name='Test',
last_name='User'
)
user.set_password('testpass123')
user.save()
return user
@pytest.fixture
def admin_user(db_session):
"""Create admin user."""
admin = User(
username='admin',
email='admin@example.com',
first_name='Admin',
last_name='User',
is_admin=True
)
admin.set_password('adminpass123')
admin.save()
return admin
@pytest.fixture
def category(db_session):
"""Create test category."""
category = Category(
name='Test Category',
description='A test category'
)
category.save()
return category
@pytest.fixture
def post(db_session, user, category):
"""Create test post."""
post = Post(
title='Test Post',
content='This is a test post content.',
slug='test-post',
status='published',
user_id=user.id,
category_id=category.id
)
post.save()
return post
@pytest.fixture
def auth_headers(user):
"""Create authentication headers."""
# For API testing
token = user.generate_auth_token()
return {'Authorization': f'Bearer {token}'}
```
## Model Testing
```python
# tests/test_models.py
import pytest
from datetime import datetime
from app.models import User, Post, Category
from werkzeug.security import check_password_hash
class TestUser:
"""Test User model."""
def test_user_creation(self, db_session):
"""Test user creation."""
user = User(
username='newuser',
email='new@example.com',
first_name='New',
last_name='User'
)
user.set_password('password123')
user.save()
assert user.id is not None
assert user.username == 'newuser'
assert user.email == 'new@example.com'
assert user.full_name == 'New User'
assert user.is_active is True
assert user.is_admin is False
assert user.created_at is not None
def test_password_hashing(self, user):
"""Test password hashing."""
user.set_password('newpassword')
assert user.password_hash != 'newpassword'
assert check_password_hash(user.password_hash, 'newpassword')
assert user.check_password('newpassword')
assert not user.check_password('wrongpassword')
def test_user_repr(self, user):
"""Test user string representation."""
assert repr(user) == '<User testuser>'
def test_user_to_dict(self, user):
"""Test user dictionary conversion."""
user_dict = user.to_dict()
assert 'username' in user_dict
assert 'email' in user_dict
assert 'password_hash' not in user_dict # Should be excluded
def test_user_relationships(self, user, post):
"""Test user relationships."""
assert post in user.posts
assert user.posts.count() == 1
class TestPost:
"""Test Post model."""
def test_post_creation(self, db_session, user, category):
"""Test post creation."""
post = Post(
title='New Post',
content='New post content',
slug='new-post',
status='draft',
user_id=user.id,
category_id=category.id
)
post.save()
assert post.id is not None
assert post.title == 'New Post'
assert post.author == user
assert post.category == category
assert not post.is_published
def test_published_status(self, post):
"""Test post published status."""
assert post.is_published # Published with published_at
post.status = 'draft'
assert not post.is_published
```
## View Testing
```python
# tests/test_views.py
import pytest
from flask import url_for
from app.models import User
class TestMainViews:
"""Test main application views."""
def test_home_page(self, client):
"""Test home page."""
response = client.get('/')
assert response.status_code == 200
assert b'Welcome' in response.data
def test_about_page(self, client):
"""Test about page."""
response = client.get('/about')
assert response.status_code == 200
class TestUserViews:
"""Test user-related views."""
def test_user_list(self, client, user):
"""Test user list page."""
response = client.get('/users/')
assert response.status_code == 200
assert user.username.encode() in response.data
def test_user_detail(self, client, user):
"""Test user detail page."""
response = client.get(f'/users/{user.id}')
assert response.status_code == 200
assert user.username.encode() in response.data
def test_user_create_get(self, client):
"""Test user creation form."""
response = client.get('/users/create')
assert response.status_code == 200
assert b'Create User' in response.data
def test_user_create_post(self, client, db_session):
"""Test user creation submission."""
data = {
'username': 'newuser',
'email': 'new@example.com',
'first_name': 'New',
'last_name': 'User',
'password': 'password123',
'confirm_password': 'password123'
}
response = client.post('/users/create', data=data, follow_redirects=True)
assert response.status_code == 200
# Check user was created
user = User.query.filter_by(username='newuser').first()
assert user is not None
assert user.email == 'new@example.com'
def test_user_edit(self, client, user):
"""Test user editing."""
data = {
'username': user.username,
'email': 'updated@example.com',
'first_name': 'Updated',
'last_name': 'User'
}
response = client.post(f'/users/{user.id}/edit', data=data, follow_redirects=True)
assert response.status_code == 200
# Refresh user from database
db_session.refresh(user)
assert user.email == 'updated@example.com'
assert user.first_name == 'Updated'
```
## Authentication Testing
```python
# tests/test_auth.py
import pytest
from flask import url_for
from app.models import User
class TestAuthentication:
"""Test authentication functionality."""
def test_login_page(self, client):
"""Test login page access."""
response = client.get('/auth/login')
assert response.status_code == 200
assert b'Login' in response.data
def test_valid_login(self, client, user):
"""Test valid user login."""
data = {
'username': user.username,
'password': 'testpass123'
}
response = client.post('/auth/login', data=data, follow_redirects=True)
assert response.status_code == 200
assert b'Welcome' in response.data
def test_invalid_login(self, client, user):
"""Test invalid login credentials."""
data = {
'username': user.username,
'password': 'wrongpassword'
}
response = client.post('/auth/login', data=data)
assert response.status_code == 200
assert b'Invalid' in response.data
def test_logout(self, client, user):
"""Test user logout."""
# Login first
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.get('/auth/logout', follow_redirects=True)
assert response.status_code == 200
def test_register_page(self, client):
"""Test registration page."""
response = client.get('/auth/register')
assert response.status_code == 200
assert b'Register' in response.data
def test_valid_registration(self, client, db_session):
"""Test valid user registration."""
data = {
'username': 'newuser',
'email': 'new@example.com',
'first_name': 'New',
'last_name': 'User',
'password': 'password123',
'confirm_password': 'password123'
}
response = client.post('/auth/register', data=data, follow_redirects=True)
assert response.status_code == 200
# Check user was created
user = User.query.filter_by(username='newuser').first()
assert user is not None
```
## API Testing
```python
# tests/test_api.py
import pytest
import json
from flask import url_for
class TestUserAPI:
"""Test User API endpoints."""
def test_get_users(self, client, user):
"""Test GET /api/users."""
response = client.get('/api/users')
assert response.status_code == 200
data = json.loads(response.data)
assert 'users' in data
assert len(data['users']) >= 1
def test_get_user(self, client, user):
"""Test GET /api/users/<id>."""
response = client.get(f'/api/users/{user.id}')
assert response.status_code == 200
data = json.loads(response.data)
assert data['username'] == user.username
assert data['email'] == user.email
def test_create_user(self, client, db_session):
"""Test POST /api/users."""
user_data = {
'username': 'apiuser',
'email': 'api@example.com',
'first_name': 'API',
'last_name': 'User',
'password': 'password123'
}
response = client.post(
'/api/users',
data=json.dumps(user_data),
content_type='application/json'
)
assert response.status_code == 201
data = json.loads(response.data)
assert data['username'] == 'apiuser'
def test_update_user(self, client, user, auth_headers):
"""Test PUT /api/users/<id>."""
update_data = {
'email': 'updated@example.com',
'first_name': 'Updated'
}
response = client.put(
f'/api/users/{user.id}',
data=json.dumps(update_data),
content_type='application/json',
headers=auth_headers
)
assert response.status_code == 200
data = json.loads(response.data)
assert data['email'] == 'updated@example.com'
def test_delete_user(self, client, user, auth_headers):
"""Test DELETE /api/users/<id>."""
response = client.delete(
f'/api/users/{user.id}',
headers=auth_headers
)
assert response.status_code == 204
```
## Test Utilities
```python
# tests/utils.py
import json
from flask import url_for
def login_user(client, username, password):
"""Helper to login user in tests."""
return client.post('/auth/login', data={
'username': username,
'password': password
}, follow_redirects=True)
def logout_user(client):
"""Helper to logout user in tests."""
return client.get('/auth/logout', follow_redirects=True)
def assert_json_response(response, expected_status=200):
"""Assert JSON response format and status."""
assert response.status_code == expected_status
assert response.content_type == 'application/json'
return json.loads(response.data)
def create_test_data(db_session):
"""Create common test data."""
from app.models import User, Category, Post
# Create test users
users = []
for i in range(3):
user = User(
username=f'user{i}',
email=f'user{i}@example.com',
first_name=f'User{i}',
last_name='Test'
)
user.set_password('password123')
user.save()
users.append(user)
return {'users': users}
```
## Performance Testing
```python
# tests/test_performance.py
import pytest
import time
from app.models import User
@pytest.mark.slow
class TestPerformance:
"""Test application performance."""
def test_user_query_performance(self, db_session):
"""Test user query performance."""
# Create multiple users
users = []
for i in range(100):
user = User(
username=f'perfuser{i}',
email=f'perf{i}@example.com',
first_name=f'Perf{i}',
last_name='User'
)
users.append(user)
db_session.bulk_save_objects(users)
db_session.commit()
# Test query performance
start_time = time.time()
result = User.query.all()
end_time = time.time()
assert len(result) >= 100
assert (end_time - start_time) < 0.1 # Should complete in under 100ms
def test_endpoint_response_time(self, client):
"""Test endpoint response time."""
start_time = time.time()
response = client.get('/')
end_time = time.time()
assert response.status_code == 200
assert (end_time - start_time) < 0.5 # Should respond in under 500ms
```
@@ -0,0 +1,391 @@
# Flask Project Configuration
This file provides specific guidance for Flask web application development using Claude Code.
## Project Overview
This is a Flask web application project optimized for scalable web development with the Flask micro-framework. The project follows Flask best practices and modern Python development patterns.
## Flask-Specific Development Commands
### Project Management
- `flask run` - Start development server
- `flask run --host=0.0.0.0 --port=5000` - Start server accessible from network
- `flask shell` - Open Flask shell with application context
- `python -m flask --help` - Show available Flask commands
### Database Management
- `flask db init` - Initialize database migrations
- `flask db migrate -m "message"` - Create database migration
- `flask db upgrade` - Apply database migrations
- `flask db downgrade` - Rollback database migration
- `flask db current` - Show current migration
- `flask db history` - Show migration history
### Development Tools
- `flask routes` - Show all registered routes
- `flask --version` - Show Flask version
- `export FLASK_ENV=development` - Set development environment
- `export FLASK_DEBUG=1` - Enable debug mode
### Custom Commands
- `flask init-db` - Initialize database with tables
- `flask seed-db` - Seed database with sample data
- `flask reset-db` - Reset database (development only)
## Flask Project Structure
```
myproject/
├── app/ # Application package
│ ├── __init__.py # Application factory
│ ├── extensions.py # Flask extensions
│ ├── config.py # Configuration settings
│ ├── models/ # Database models
│ │ ├── __init__.py
│ │ ├── base.py # Base model class
│ │ ├── user.py # User model
│ │ └── post.py # Post model
│ ├── blueprints/ # Application blueprints
│ │ ├── __init__.py
│ │ ├── main/ # Main blueprint
│ │ ├── auth/ # Authentication blueprint
│ │ ├── api/ # API blueprint
│ │ └── admin/ # Admin blueprint
│ ├── templates/ # Jinja2 templates
│ │ ├── base.html
│ │ ├── index.html
│ │ └── auth/
│ ├── static/ # Static files
│ │ ├── css/
│ │ ├── js/
│ │ └── images/
│ ├── forms/ # WTForms
│ │ ├── __init__.py
│ │ ├── auth.py
│ │ └── user.py
│ ├── utils/ # Utility functions
│ └── cli.py # Custom CLI commands
├── migrations/ # Database migrations
├── tests/ # Test files
│ ├── conftest.py
│ ├── test_models.py
│ ├── test_views.py
│ └── test_api.py
├── requirements/ # Requirements files
│ ├── base.txt
│ ├── development.txt
│ └── production.txt
├── wsgi.py # WSGI entry point
├── gunicorn.conf.py # Gunicorn configuration
└── docker-compose.yml # Docker Compose configuration
```
## Flask Application Factory
```python
# app/__init__.py
from flask import Flask
from app.extensions import db, migrate, login_manager, csrf, cache
from app.config import config
def create_app(config_name='default'):
app = Flask(__name__)
app.config.from_object(config[config_name])
# Initialize extensions
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
csrf.init_app(app)
cache.init_app(app)
# Register blueprints
from app.blueprints.main import main_bp
from app.blueprints.auth import auth_bp
from app.blueprints.api import api_bp
app.register_blueprint(main_bp)
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(api_bp, url_prefix='/api/v1')
# Register CLI commands
from app.cli import init_commands
init_commands(app)
return app
```
## Configuration Management
```python
# app/config.py
import os
from datetime import timedelta
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_RECORD_QUERIES = True
# Session configuration
PERMANENT_SESSION_LIFETIME = timedelta(hours=1)
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
# File upload
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
UPLOAD_FOLDER = os.path.join(os.getcwd(), 'uploads')
# Cache
CACHE_TYPE = 'simple'
CACHE_DEFAULT_TIMEOUT = 300
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
'sqlite:///dev.db'
SESSION_COOKIE_SECURE = False
class ProductionConfig(Config):
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
# Security headers
SECURITY_HEADERS = {
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block'
}
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
WTF_CSRF_ENABLED = False
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig,
'default': DevelopmentConfig
}
```
## Flask Best Practices
### Application Structure
- Use application factory pattern for configuration flexibility
- Organize code into blueprints for modularity
- Separate models, views, and forms into different modules
- Use extensions.py to initialize Flask extensions
- Implement proper error handling and logging
### Database Models
- Use SQLAlchemy ORM for database operations
- Implement base model with common functionality
- Add proper relationships between models
- Use database migrations for schema changes
- Implement model validation and constraints
### Blueprint Organization
- Group related functionality into blueprints
- Use URL prefixes for namespacing
- Implement blueprint-specific templates
- Add proper error handlers for each blueprint
- Use blueprint factories for complex blueprints
### Template Management
- Use template inheritance for consistent layout
- Create reusable template macros
- Implement proper CSRF protection in forms
- Use Flask-WTF for form handling and validation
- Organize templates by blueprint
### Security Considerations
- Always validate and sanitize user input
- Use Flask-Login for user session management
- Implement proper authentication and authorization
- Use CSRF protection for all forms
- Set secure session cookie configuration
- Implement rate limiting for API endpoints
## Flask Extensions
### Essential Extensions
```python
# app/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_wtf.csrf import CSRFProtect
from flask_caching import Cache
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
csrf = CSRFProtect()
cache = Cache()
limiter = Limiter(key_func=get_remote_address)
```
### Recommended Extensions
- **Flask-SQLAlchemy** - Database ORM
- **Flask-Migrate** - Database migrations
- **Flask-Login** - User session management
- **Flask-WTF** - Form handling and CSRF protection
- **Flask-Caching** - Caching support
- **Flask-Limiter** - Rate limiting
- **Flask-Mail** - Email support
- **Flask-Admin** - Admin interface
## Testing Strategy
### Test Organization
```python
# tests/conftest.py
import pytest
from app import create_app
from app.extensions import db
@pytest.fixture(scope='session')
def app():
app = create_app('testing')
with app.app_context():
db.create_all()
yield app
db.drop_all()
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture
def runner(app):
return app.test_cli_runner()
```
### Test Types
- **Unit tests** for models and utilities
- **Integration tests** for views and API endpoints
- **Functional tests** for user workflows
- **Performance tests** for critical paths
### Testing Best Practices
- Use fixtures for common test data
- Test both success and error conditions
- Mock external dependencies
- Use factory_boy for test data generation
- Implement database transaction rollback in tests
## Performance Optimization
### Database Optimization
- Use connection pooling for production
- Implement query optimization with indexes
- Use lazy loading for relationships
- Cache frequently accessed data
- Monitor database query performance
### Caching Strategy
- Implement Redis for session storage
- Use view-level caching for static content
- Cache database query results
- Implement cache invalidation strategies
- Use CDN for static files
### Application Optimization
- Use Gunicorn with multiple workers
- Implement proper logging and monitoring
- Optimize static file serving
- Use async tasks for long-running operations
- Implement proper error handling
## Deployment Considerations
### Production Setup
- Use environment variables for configuration
- Implement proper logging and monitoring
- Set up database connection pooling
- Configure reverse proxy (Nginx)
- Use HTTPS with proper SSL certificates
### Docker Configuration
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements/production.txt requirements.txt
RUN pip install -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["gunicorn", "--config", "gunicorn.conf.py", "wsgi:app"]
```
### Environment Variables
```bash
FLASK_ENV=production
SECRET_KEY=your-secret-key
DATABASE_URL=postgresql://user:pass@host:port/db
REDIS_URL=redis://host:port/db
```
## Common Flask Patterns
### Custom Decorators
```python
from functools import wraps
from flask import abort
from flask_login import current_user
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_admin:
abort(403)
return f(*args, **kwargs)
return decorated_function
```
### Request Context Processors
```python
@app.context_processor
def inject_user():
return dict(current_user=current_user)
```
### Custom Filters
```python
@app.template_filter('datetime')
def datetime_filter(value, format='%Y-%m-%d %H:%M'):
return value.strftime(format) if value else ''
```
## Development Workflow
### Getting Started
1. Clone the repository
2. Create virtual environment: `python -m venv venv`
3. Activate environment: `source venv/bin/activate`
4. Install dependencies: `pip install -r requirements/development.txt`
5. Set environment variables
6. Initialize database: `flask db upgrade`
7. Run development server: `flask run`
### Development Process
1. Create feature branch from main
2. Implement changes with tests
3. Run test suite: `pytest`
4. Check code quality: `flake8`, `black`
5. Create pull request for review
6. Deploy after approval
### Code Quality Tools
- **Black** - Code formatting
- **isort** - Import sorting
- **flake8** - Linting
- **mypy** - Type checking
- **pytest** - Testing framework
- **coverage** - Test coverage