ref: up
This commit is contained in:
433
fission-python/template/examples/example_crud.py
Normal file
433
fission-python/template/examples/example_crud.py
Normal file
@@ -0,0 +1,433 @@
|
||||
"""
|
||||
Example: Basic CRUD operations for a resource.
|
||||
|
||||
This demonstrates:
|
||||
- Pydantic request validation
|
||||
- Database operations with helpers
|
||||
- Standard error handling
|
||||
- Proper Fission docstring configuration
|
||||
"""
|
||||
|
||||
from flask import request
|
||||
from helpers import (
|
||||
init_db_connection,
|
||||
db_row_to_dict,
|
||||
db_rows_to_array,
|
||||
get_user_from_headers,
|
||||
format_error_response,
|
||||
)
|
||||
from exceptions import ValidationError, NotFoundError, ConflictError, DatabaseError
|
||||
from models import ItemResponse, ItemCreateRequest, ItemUpdateRequest
|
||||
|
||||
# Pool manager executor, one request at a time
|
||||
def create(event, context):
|
||||
"""
|
||||
```fission
|
||||
{
|
||||
"name": "create-item",
|
||||
"http_triggers": {
|
||||
"create": {
|
||||
"url": "/api/items",
|
||||
"methods": ["POST"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Create a new item.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "string (required, 1-255 chars)",
|
||||
"description": "string (optional)",
|
||||
"status": "active|inactive|pending",
|
||||
"metadata": {}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
- 200: Item created successfully
|
||||
- 400: Validation error
|
||||
- 409: Conflict (e.g., duplicate name)
|
||||
- 500: Database error
|
||||
"""
|
||||
# Get user for audit trail
|
||||
x_user = get_user_from_headers()
|
||||
|
||||
# Validate request payload
|
||||
try:
|
||||
data = ItemCreateRequest(**request.get_json())
|
||||
except Exception as e:
|
||||
raise ValidationError(f"Invalid request: {str(e)}", x_user=x_user)
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = init_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check for conflicts (example)
|
||||
cursor.execute(
|
||||
"SELECT id FROM items WHERE name = %s",
|
||||
(data.name,)
|
||||
)
|
||||
if cursor.fetchone():
|
||||
raise ConflictError(
|
||||
f"Item with name '{data.name}' already exists",
|
||||
x_user=x_user,
|
||||
details={"name": data.name}
|
||||
)
|
||||
|
||||
# Insert new item
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO items (name, description, status, metadata)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
RETURNING id, name, description, status, metadata, created, modified
|
||||
""",
|
||||
(data.name, data.description, data.status.value, data.metadata)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
conn.commit()
|
||||
|
||||
# Build response
|
||||
item = db_row_to_dict(cursor, row)
|
||||
return item
|
||||
|
||||
except (ValidationError, NotFoundError, ConflictError, DatabaseError):
|
||||
# Re-raise our own exceptions
|
||||
raise
|
||||
except Exception as e:
|
||||
if conn:
|
||||
conn.rollback()
|
||||
raise DatabaseError(f"Database error: {str(e)}", x_user=x_user)
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def list_items(event, context):
|
||||
"""
|
||||
```fission
|
||||
{
|
||||
"name": "list-items",
|
||||
"http_triggers": {
|
||||
"list": {
|
||||
"url": "/api/items",
|
||||
"methods": ["GET"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
List items with optional filtering and pagination.
|
||||
|
||||
**Query Parameters:**
|
||||
- `page` (int): Page number, zero-based (default: 0)
|
||||
- `size` (int): Items per page (default: 10, max: 100)
|
||||
- `asc` (bool): Sort ascending (default: true)
|
||||
- `filter[ids]` (string[]): Filter by specific IDs
|
||||
- `filter[keyword]` (string): Search in name/description
|
||||
- `filter[status]` (string[]): Filter by status values
|
||||
- `filter[created_from]` (datetime): Filter created after
|
||||
- `filter[created_to]` (datetime): Filter created before
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [...],
|
||||
"page": 0,
|
||||
"size": 10,
|
||||
"total": 42
|
||||
}
|
||||
```
|
||||
"""
|
||||
from helpers import str_to_bool
|
||||
|
||||
# Parse pagination
|
||||
page = int(request.args.get("page", 0))
|
||||
size = int(request.args.get("size", 10))
|
||||
asc = str_to_bool(request.args.get("asc", "true"))
|
||||
|
||||
# Parse filters
|
||||
ids = request.args.getlist("filter[ids]")
|
||||
keyword = request.args.get("filter[keyword]")
|
||||
statuses = request.args.getlist("filter[status]")
|
||||
|
||||
# Build query
|
||||
conditions = []
|
||||
params = []
|
||||
|
||||
if ids:
|
||||
conditions.append(f"id IN ({', '.join(['%s'] * len(ids))})")
|
||||
params.extend(ids)
|
||||
if keyword:
|
||||
conditions.append("(name ILIKE %s OR description ILIKE %s)")
|
||||
params.extend([f"%{keyword}%", f"%{keyword}%"])
|
||||
if statuses:
|
||||
conditions.append(f"status IN ({', '.join(['%s'] * len(statuses))})")
|
||||
params.extend(statuses)
|
||||
|
||||
where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = init_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get total count
|
||||
count_sql = f"SELECT COUNT(*) FROM items {where_clause}"
|
||||
cursor.execute(count_sql, params)
|
||||
total = cursor.fetchone()[0]
|
||||
|
||||
# Get paginated data
|
||||
offset = page * size
|
||||
data_sql = f"""
|
||||
SELECT id, name, description, status, metadata, created, modified
|
||||
FROM items
|
||||
{where_clause}
|
||||
ORDER BY created {'ASC' if asc else 'DESC'}
|
||||
LIMIT %s OFFSET %s
|
||||
"""
|
||||
cursor.execute(data_sql, params + [size, offset])
|
||||
rows = cursor.fetchall()
|
||||
items = [db_row_to_dict(cursor, row) for row in rows]
|
||||
|
||||
return {
|
||||
"data": items,
|
||||
"page": page,
|
||||
"size": size,
|
||||
"total": total
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise DatabaseError(f"Failed to list items: {str(e)}")
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_item(event, context):
|
||||
"""
|
||||
```fission
|
||||
{
|
||||
"name": "get-item",
|
||||
"http_triggers": {
|
||||
"get": {
|
||||
"url": "/api/items/:id",
|
||||
"methods": ["GET"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Get a specific item by ID.
|
||||
|
||||
**URL Parameters:**
|
||||
- `id` (string): Item UUID
|
||||
|
||||
**Response:**
|
||||
- 200: Item found
|
||||
- 404: Item not found
|
||||
- 500: Database error
|
||||
"""
|
||||
# Extract item ID from path (Fission passes path params differently depending on trigger)
|
||||
# For HTTP triggers, the ID would come from the URL path
|
||||
item_id = request.view_args.get('id') if hasattr(request, 'view_args') else None
|
||||
if not item_id:
|
||||
# Fallback: parse from query or request path
|
||||
item_id = request.path.rstrip('/').split('/')[-1]
|
||||
|
||||
if not item_id:
|
||||
raise ValidationError("Item ID is required")
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = init_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, name, description, status, metadata, created, modified
|
||||
FROM items WHERE id = %s
|
||||
""",
|
||||
(item_id,)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
raise NotFoundError(f"Item {item_id} not found")
|
||||
|
||||
return db_row_to_dict(cursor, row)
|
||||
|
||||
except NotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise DatabaseError(f"Failed to fetch item: {str(e)}")
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def update_item(event, context):
|
||||
"""
|
||||
```fission
|
||||
{
|
||||
"name": "update-item",
|
||||
"http_triggers": {
|
||||
"update": {
|
||||
"url": "/api/items/:id",
|
||||
"methods": ["PUT", "PATCH"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Update an existing item.
|
||||
|
||||
**URL Parameters:**
|
||||
- `id` (string): Item UUID
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "string (optional)",
|
||||
"description": "string (optional)",
|
||||
"status": "active|inactive|pending (optional)"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
- 200: Item updated successfully
|
||||
- 404: Item not found
|
||||
- 409: Conflict (duplicate name)
|
||||
- 400: Validation error
|
||||
- 500: Database error
|
||||
"""
|
||||
x_user = get_user_from_headers()
|
||||
|
||||
# Extract item ID
|
||||
item_id = request.view_args.get('id') if hasattr(request, 'view_args') else None
|
||||
if not item_id:
|
||||
item_id = request.path.rstrip('/').split('/')[-1]
|
||||
|
||||
if not item_id:
|
||||
raise ValidationError("Item ID is required")
|
||||
|
||||
# Validate request
|
||||
try:
|
||||
data = ItemUpdateRequest(**request.get_json())
|
||||
except Exception as e:
|
||||
raise ValidationError(f"Invalid request: {str(e)}", x_user=x_user)
|
||||
|
||||
# Build update statement dynamically
|
||||
updates = []
|
||||
params = []
|
||||
|
||||
if data.name is not None:
|
||||
updates.append("name = %s")
|
||||
params.append(data.name)
|
||||
if data.description is not None:
|
||||
updates.append("description = %s")
|
||||
params.append(data.description)
|
||||
if data.status is not None:
|
||||
updates.append("status = %s")
|
||||
params.append(data.status.value)
|
||||
if data.metadata is not None:
|
||||
updates.append("metadata = %s")
|
||||
params.append(data.metadata)
|
||||
|
||||
if not updates:
|
||||
raise ValidationError("No update fields provided", x_user=x_user)
|
||||
|
||||
updates.append("modified = NOW()")
|
||||
params.append(item_id)
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = init_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check for name conflict if name is being updated
|
||||
if data.name:
|
||||
cursor.execute(
|
||||
"SELECT id FROM items WHERE name = %s AND id != %s",
|
||||
(data.name, item_id)
|
||||
)
|
||||
if cursor.fetchone():
|
||||
raise ConflictError(
|
||||
f"Another item with name '{data.name}' already exists",
|
||||
x_user=x_user,
|
||||
details={"name": data.name}
|
||||
)
|
||||
|
||||
# Execute update
|
||||
sql = f"UPDATE items SET {', '.join(updates)} WHERE id = %s RETURNING *"
|
||||
cursor.execute(sql, params)
|
||||
row = cursor.fetchone()
|
||||
conn.commit()
|
||||
|
||||
if not row:
|
||||
raise NotFoundError(f"Item {item_id} not found", x_user=x_user)
|
||||
|
||||
return db_row_to_dict(cursor, row)
|
||||
|
||||
except (ValidationError, NotFoundError, ConflictError, DatabaseError):
|
||||
raise
|
||||
except Exception as e:
|
||||
if conn:
|
||||
conn.rollback()
|
||||
raise DatabaseError(f"Failed to update item: {str(e)}", x_user=x_user)
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_item(event, context):
|
||||
"""
|
||||
```fission
|
||||
{
|
||||
"name": "delete-item",
|
||||
"http_triggers": {
|
||||
"delete": {
|
||||
"url": "/api/items/:id",
|
||||
"methods": ["DELETE"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Delete an item.
|
||||
|
||||
**URL Parameters:**
|
||||
- `id` (string): Item UUID
|
||||
|
||||
**Response:**
|
||||
- 204: Item deleted successfully
|
||||
- 404: Item not found
|
||||
- 500: Database error
|
||||
"""
|
||||
x_user = get_user_from_headers()
|
||||
|
||||
# Extract item ID
|
||||
item_id = request.view_args.get('id') if hasattr(request, 'view_args') else None
|
||||
if not item_id:
|
||||
item_id = request.path.rstrip('/').split('/')[-1]
|
||||
|
||||
if not item_id:
|
||||
raise ValidationError("Item ID is required", x_user=x_user)
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = init_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM items WHERE id = %s", (item_id,))
|
||||
conn.commit()
|
||||
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"Item {item_id} not found", x_user=x_user)
|
||||
|
||||
return None # 204 No Content
|
||||
|
||||
except NotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
if conn:
|
||||
conn.rollback()
|
||||
raise DatabaseError(f"Failed to delete item: {str(e)}", x_user=x_user)
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
Reference in New Issue
Block a user