func/docs/function-templates/python.md

380 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Python Function Developer's Guide
Python Functions allow for the direct deployment of source code as a production
service to any Kubernetes cluster with Knative installed. The request handler
method signature follows the ASGI (Asynchronous Server Gateway Interface)
standard, allowing for integration with any supporting library.
## Project Structure
When you create a Python function using `func create -l python`, you get a standard Python project structure:
```
func create -l python myfunc
tree myfunc
myfunc/
├── func.yaml # Function configuration
├── pyproject.toml # Python project metadata
├── function/
│ ├── __init__.py
│ └── func.py # Your function implementation
└── tests/
└── test_func.py # Unit tests
```
The `func.yaml` file contains build and deployment configuration. For details,
see the [func.yaml reference](../reference/func_yaml.md).
## Function Implementation
Python functions must implement a method `new()` which returns a new instance
of your Function class:
```python
def new():
"""Factory function that returns a Function instance."""
return Function()
class Function:
"""Your function implementation."""
pass
```
### Core Methods
Your function class can implement several optional methods:
#### `handle(self, scope, receive, send)`
The main request handler following ASGI protocol. This async method processes all HTTP requests except health checks.
```python
async def handle(self, scope, receive, send):
"""Handle HTTP requests."""
# Process the request
await send({
'type': 'http.response.start',
'status': 200,
'headers': [[b'content-type', b'text/plain']],
})
await send({
'type': 'http.response.body',
'body': b'Hello, World!',
})
```
#### `start(self, cfg)`
Called when a function instance starts (e.g., during scaling or updates). Receives configuration as a dictionary.
```python
def start(self, cfg):
"""Initialize function with configuration."""
self.debug = cfg.get('DEBUG', 'false').lower() == 'true'
logging.info("Function initialized")
```
#### `stop(self)`
Called when a function instance stops. Use for cleanup operations.
```python
def stop(self):
"""Clean up resources."""
# Close database connections, flush buffers, etc.
logging.info("Function shutting down")
```
#### `alive(self)` and `ready(self)`
Health check methods exposed at `/health/liveness` and `/health/readiness`:
```python
def alive(self):
"""Liveness check."""
return True, "Function is alive"
def ready(self):
"""Readiness check."""
if self.database_connected:
return True, "Ready to serve"
return False, "Database not connected"
```
## Local Development
### Running Your Function
```bash
# Build and run on the host (not in a container)
func run --builder=host
# Force rebuild even if no changes detected
func run --build
```
### Testing
Test your function with HTTP requests:
```bash
# Test the main endpoint
curl http://localhost:8080
# Check health endpoints
curl http://localhost:8080/health/liveness
curl http://localhost:8080/health/readiness
```
### Testing CloudEvent Functions
Create a CloudEvent function:
```bash
# Create a new CloudEvent function
func create -l python -t cloudevents myeventfunc
```
Test CloudEvent functions using curl with proper headers:
```bash
# Invoke with a CloudEvent
curl -X POST http://localhost:8080 \
-H "Ce-Specversion: 1.0" \
-H "Ce-Type: com.example.sampletype" \
-H "Ce-Source: example/source" \
-H "Ce-Id: 1234-5678-9101" \
-H "Ce-Subject: example-subject" \
-H "Content-Type: application/json" \
-d '{"message": "Hello CloudEvent!"}'
```
Also see `func invoke` which automates this for basic testing.
### Unit Testing
Python functions use modern Python packaging with `pyproject.toml` and include
pytest with async support for testing ASGI functions. The generated project
includes example tests in `tests/test_func.py` that demonstrate how to test the
async handler.
#### Setting Up Your Development Environment
It's best practice to use a virtual environment to isolate your function's dependencies:
```bash
# Create a virtual environment (Python 3.3+)
python3 -m venv venv
# Activate the virtual environment
# On Linux/macOS:
source venv/bin/activate
# On Windows:
# venv\Scripts\activate
# Upgrade pip to ensure you have the latest version
python -m pip install --upgrade pip
# Install the function package and its dependencies (including test dependencies)
pip install -e .
# Run tests with pytest
pytest
# Run tests with verbose output
pytest -v
# Run tests with coverage (requires pytest-cov)
pip install pytest-cov
pytest --cov=function --cov-report=term-missing
# When done, deactivate the virtual environment
deactivate
```
**Note**:
- Python 3 typically comes with `venv` module built-in
- If `python3` command is not found, try `python` instead
- The `-m pip` syntax ensures you're using the pip from your virtual environment
- Always activate your virtual environment before running tests or installing dependencies
#### Writing Tests for ASGI Functions
The test file demonstrates how to test ASGI functions by mocking the ASGI interface:
```python
import pytest
from function import new
@pytest.mark.asyncio
async def test_function_handle():
# Create function instance
f = new()
# Mock ASGI scope (request details)
scope = {
'type': 'http',
'method': 'POST',
'path': '/',
'headers': [(b'content-type', b'application/json')],
}
# Mock receive callable (for request body)
async def receive():
return {
'type': 'http.request',
'body': b'{"test": "data"}',
'more_body': False,
}
# Track sent responses
responses = []
# Mock send callable
async def send(message):
responses.append(message)
# Call the handler
await f.handle(scope, receive, send)
# Assert responses
assert len(responses) == 2
assert responses[0]['type'] == 'http.response.start'
assert responses[0]['status'] == 200
assert responses[1]['type'] == 'http.response.body'
```
#### Testing CloudEvent Functions
For CloudEvent functions, include CloudEvent headers in your test scope:
```python
@pytest.mark.asyncio
async def test_cloudevent_handler():
f = new()
# CloudEvent headers
scope = {
'type': 'http',
'method': 'POST',
'path': '/',
'headers': [
(b'ce-specversion', b'1.0'),
(b'ce-type', b'com.example.test'),
(b'ce-source', b'test/unit'),
(b'ce-id', b'test-123'),
(b'content-type', b'application/json'),
],
}
# Test with CloudEvent data
async def receive():
return {
'type': 'http.request',
'body': b'{"message": "test event"}',
'more_body': False,
}
# ... rest of test
```
#### Testing with Real HTTP Clients
For integration testing, you can use httpx with ASGI support:
```python
import httpx
import pytest
from function import new
@pytest.mark.asyncio
async def test_with_http_client():
f = new()
# Create ASGI transport with your function
transport = httpx.ASGITransport(app=f.handle)
# Make HTTP requests
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/")
assert response.status_code == 200
response = await client.post("/", json={"test": "data"})
assert response.status_code == 200
```
## Advanced Implementation Examples
### Processing Request Data
```python
async def handle(self, scope, receive, send):
"""Process POST requests with JSON data."""
if scope['method'] == 'POST':
# Receive request body
body = b''
while True:
message = await receive()
if message['type'] == 'http.request':
body += message.get('body', b'')
if not message.get('more_body', False):
break
# Process JSON data
import json
data = json.loads(body)
result = process_data(data)
# Send response
response_body = json.dumps(result).encode()
await send({
'type': 'http.response.start',
'status': 200,
'headers': [[b'content-type', b'application/json']],
})
await send({
'type': 'http.response.body',
'body': response_body,
})
```
### Environment-Based Configuration
```python
class Function:
def start(self, cfg):
"""Configure function from environment."""
self.api_key = cfg.get('API_KEY')
self.cache_ttl = int(cfg.get('CACHE_TTL', '300'))
self.log_level = cfg.get('LOG_LEVEL', 'INFO')
logging.basicConfig(level=self.log_level)
```
### CloudEvent Handling
For CloudEvent support, parse the headers and body accordingly:
```python
async def handle(self, scope, receive, send):
"""Handle CloudEvents."""
headers = dict(scope['headers'])
# Check if this is a CloudEvent
if b'ce-type' in headers:
event_type = headers[b'ce-type'].decode()
event_source = headers[b'ce-source'].decode()
# Process CloudEvent...
```
## Deployment
### Basic Deployment
```bash
# Deploy to a specific registry
func deploy --builder=host --registry docker.io/myuser
```
For all deploy options, see `func deploy --help`