mirror of https://github.com/knative/func.git
380 lines
9.4 KiB
Markdown
380 lines
9.4 KiB
Markdown
# 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`
|