Files
claude-gen/fission-python/template/README.md

437 lines
12 KiB
Markdown
Raw Normal View History

2026-03-18 20:21:56 +07:00
# Fission Python Template
A production-ready template for building Fission serverless Python functions with best practices for configuration, database connectivity, error handling, and testing.
## Project Structure
```
project/
├── .fission/
│ ├── deployment.json # Fission function deployment configuration
│ ├── dev-deployment.json # Development overrides
│ └── local-deployment.json # Local development overrides
├── src/
│ ├── __init__.py # Package initialization
│ ├── vault.py # Vault encryption/decryption utilities
│ ├── helpers.py # Shared utilities (DB, secrets, configs)
│ ├── exceptions.py # Custom exception hierarchy
│ ├── models.py # Pydantic models (request/response schemas)
│ ├── build.sh # Package build script
│ └── your_function.py # Your function implementations
├── test/
│ ├── __init__.py
│ ├── test_*.py # Unit tests
│ └── requirements.txt # Test dependencies
├── migrates/
│ └── schema.sql # Database migration scripts
├── manifests/ # Kubernetes manifests (optional)
├── specs/ # Generated Fission specs (created by fission CLI)
├── requirements.txt # Runtime dependencies
├── dev-requirements.txt # Development dependencies
├── .env.example # Environment variable template
├── pytest.ini # Pytest configuration
└── README.md # Project documentation
```
## Key Components
### Fission Configuration in Docstrings
Fission reads function metadata from docstrings using the ````fission` marker:
```python
def my_function(event, context):
"""
```fission
{
"name": "my-function",
"http_triggers": {
"my-trigger": {
"url": "/api/my-endpoint",
"methods": ["GET", "POST"]
}
}
}
```
"""
# Your implementation
return {"message": "Hello World"}
```
**Note:** Do not use `fission.yaml` or `fission.json`. The Fission Python builder reads the docstring annotations directly from your Python source files.
### Environment Variables & Secrets
Configuration is managed through Kubernetes Secrets and ConfigMaps:
- **Secrets**: Database credentials, API keys, encryption keys (sensitive)
- **ConfigMaps**: Non-sensitive configuration, endpoints, feature flags
Access them via helper functions:
```python
from helpers import get_secret, get_config
# Read secret (with optional default)
db_host = get_secret("PG_HOST", "localhost")
db_port = int(get_secret("PG_PORT", "5432"))
# Read config
api_endpoint = get_config("EXTERNAL_API_ENDPOINT")
```
**Placeholder variables** in `deployment.json`:
- `${PROJECT_NAME}` - Replaced with your actual project name during project creation
- Secret/configmap names follow pattern: `fission-${PROJECT_NAME}-env` and `fission-${PROJECT_NAME}-config`
### Database Connectivity
Use the provided `init_db_connection()` helper:
```python
from helpers import init_db_connection, db_rows_to_array
conn = init_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM items")
rows = db_rows_to_array(cursor, cursor.fetchall())
```
The helper automatically:
- Reads connection parameters from secrets (PG_HOST, PG_PORT, PG_DB, PG_USER, PG_PASS, PG_DBSCHEMA)
- Checks port connectivity before connecting
- Uses LoggingConnection for query logging
- Applies schema search path if PG_DBSCHEMA is set
### Error Handling
Use the exception hierarchy from `exceptions.py`:
```python
from exceptions import ValidationError, NotFoundError, ConflictError, DatabaseError
def get_item(item_id: str):
item = db.fetch_one(item_id)
if not item:
raise NotFoundError(f"Item {item_id} not found", x_user=get_user_from_headers())
return item
```
All exceptions return standardized error responses:
```json
{
"error_code": "NOT_FOUND",
"http_status": 404,
"error_msg": "Item 123 not found",
"x_user": "user-456",
"details": {"item_id": "123"}
}
```
### Validation with Pydantic
Validate request payloads using Pydantic models:
```python
from models import ItemCreateRequest
from pydantic import ValidationError as PydanticValidationError
def create_item():
try:
data = ItemCreateRequest(**request.get_json())
except PydanticValidationError as e:
raise ValidationError(str(e), details=e.errors())
```
## Development Workflow
### 1. Install Dependencies
```bash
# Install runtime and development dependencies
pip install -r dev-requirements.txt
# Or just runtime dependencies
pip install -r requirements.txt
```
### 2. Local Testing
Fission provides `fission spec` to test specs locally:
```bash
# Verify your deployment configuration
fission spec verify --file=.fission/deployment.json
# Build and test locally
fission function test --name your-function
```
### 3. Unit Testing
Run tests with pytest:
```bash
# Run all tests
pytest
# Run with coverage
pytest --cov=src
# Run specific test file
pytest test/test_my_function.py
# Verbose output
pytest -v
```
**Example test structure:**
```python
# test/test_my_function.py
import pytest
from unittest.mock import patch
from src.my_function import main
def test_my_function_success():
event = {"key": "value"}
context = {}
result = main(event, context)
assert result["status"] == "success"
@patch("helpers.init_db_connection")
def test_my_function_with_db(mock_db):
# Mock database connection
mock_conn = MagicMock()
mock_db.return_value = mock_conn
# Test function
```
### 4. Building the Package
The `build.sh` script installs dependencies and packages your code:
```bash
# From project root
./src/build.sh
# This produces a package.zip in the specs directory
# Ready for deployment with: fission deploy
```
The build script detects the OS (Debian/Alpine) and installs the correct build dependencies (gcc, libpq-dev, python3-dev).
### 5. Deployment
```bash
# Deploy to Fission
fission deploy
# Or deploy specific function
fission function update --name my-function --env your-env
```
## Deployment Configuration
### Executors
Choose between two executor types in `deployment.json`:
**poolmgr** (default) - Good for high-concurrency HTTP functions:
```json
"executor": {
"select": "poolmgr",
"poolmgr": {
"concurrency": 1,
"requestsperpod": 1,
"onceonly": false
}
}
```
**newdeploy** - Good for dedicated scaling:
```json
"executor": {
"select": "newdeploy",
"newdeploy": {
"minscale": 1,
"maxscale": 5,
"targetcpu": 80
}
}
```
### Resource Limits
Set resource allocation in `function_common`:
- `mincpu` / `maxcpu` - CPU allocation in millicores (50 = 0.05 cores)
- `minmemory` / `maxmemory` - Memory in MB
- Adjust based on your function's needs
### Environment-Specific Overrides
Use `dev-deployment.json` for development environment (different secrets, lower resources). Fission will automatically use it when `--dev` flag is passed.
## Vault Encryption
For encrypted secrets, use the vault utility functions:
```python
from vault import encrypt_vault, decrypt_vault, is_valid_vault_format
# Encrypt a value (run locally to generate vault string)
encrypted = encrypt_vault("my-secret", "your-hex-key-here")
# Result: "vault:v1:base64-encrypted-data"
# Store the encrypted string in your K8s secret
# The helper will auto-decrypt if is_valid_vault_format() returns True
```
**Important:** Set `CRYPTO_KEY` in your helpers.py (or via environment override) to your actual 32-byte key in hex format.
## Testing Strategies
### Unit Tests
- Mock external dependencies (database, HTTP calls)
- Test business logic isolation
- Use `pytest-mock` for convenient mocking
### Integration Tests
- Use a test database
- Clean up test data after each run
- Consider using `pytest.fixtures` for setup/teardown
### Local Development
- Use `.fission/local-deployment.json` for local Fission setup
- Override secrets/configmaps for local environment
- Run with: `fission function test --local`
## Migrations
Place SQL migration scripts in `migrates/`:
```sql
-- migrates/001_create_items_table.sql
CREATE TABLE items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
status VARCHAR(50) DEFAULT 'active',
created TIMESTAMP DEFAULT NOW(),
modified TIMESTAMP DEFAULT NOW()
);
```
Apply migrations manually via psql or using a migration tool like `alembic`.
## Best Practices
1. **Keep functions small** - Single responsibility per function
2. **Use Pydantic** - Validate all inputs with request models
3. **Standardize errors** - Use the provided exception classes
4. **Log appropriately** - Use `logger` from helpers (already configured)
5. **Track users** - Use `get_user_from_headers()` for audit trails
6. **Write tests** - Aim for high coverage of business logic
7. **Document functions** - Add docstrings with fission config block
8. **Avoid global state** - Functions should be stateless and idempotent
## Continuous Integration
The template includes `.gitea/workflows/` for CI/CD:
- `install-dispatch.yaml` - Triggered on installation events
- `uninstall-dispatch.yaml` - Cleanup on uninstall
- `dev-deployment.yaml` - Development environment updates
- `analystic-dispatch.yaml` - Analytics processing
Adapt these workflows for your deployment pipeline (GitHub Actions, GitLab CI, etc.).
## Troubleshooting
### Spec Generation Fails
- Ensure all function files have proper fission config in docstrings
- Run: `python -m py_compile src/*.py` to check syntax
- Verify `build.sh` is executable: `chmod +x src/build.sh`
### Cannot Connect to Database
- Check that secrets are mounted correctly: `kubectl exec <pod> -- ls /secrets/default/`
- Verify PG_HOST, PG_PORT are correct
- Use `check_port_open()` debug output
- Test connection manually: `psql -h $PG_HOST -p $PG_PORT -U $PG_USER $PG_DB`
### Missing Dependencies
- Ensure `requirements.txt` includes ALL dependencies (Flask is required!)
- Check build logs for pip errors
- Rebuild package: `./src/build.sh`
## Example Implementations
### CRUD Operation
```python
from flask import request
from helpers import init_db_connection, format_error_response
from exceptions import ValidationError, NotFoundError, DatabaseError
from models import ItemCreateRequest, ItemResponse
def create_item(event, context):
"""Create a new item."""
try:
# Validate input
data = ItemCreateRequest(**request.get_json())
except Exception as e:
raise ValidationError(str(e))
conn = init_db_connection()
try:
cursor = conn.cursor()
cursor.execute(
"INSERT INTO items (name, description, status) VALUES (%s, %s, %s) RETURNING id, created, modified",
(data.name, data.description, data.status.value)
)
row = cursor.fetchone()
conn.commit()
item = db_row_to_dict(cursor, row)
return item
except Exception as e:
conn.rollback()
raise DatabaseError(str(e))
finally:
conn.close()
```
### Webhook Receiver
```python
def webhook_handler(event, context):
"""Process incoming webhook."""
# Webhook data is in event
payload = event.get("body", {})
signature = request.headers.get("X-Webhook-Signature")
# Verify signature
if not verify_signature(payload, signature):
raise ValidationError("Invalid signature")
# Process webhook
process_webhook(payload)
return {"status": "processed"}
```
## Next Steps
1. Replace placeholder values in `.fission/deployment.json`
2. Update `SECRET_NAME` and `CONFIG_NAME` in `helpers.py` (or use create-project.sh)
3. Implement your business logic in new function files
4. Write tests for your functions
5. Deploy to Kubernetes cluster with Fission
## Resources
- [Fission Documentation](https://fission.io/docs/)
- [Fission Python Builder](https://github.com/fission/fission-python-builder)
- [Pydantic Documentation](https://docs.pydantic.dev/)
- [Flask Documentation](https://flask.palletsprojects.com/)