CherryPy: API za 5 minút s built-in production serverom
10 min
CherryPy: API za 5 minút s built-in production serverom
"Potrebujem test API. Teraz. Nemám čas na gunicorn/uvicorn setup!" ⚡
CherryPy je tvoje riešenie.
Prečo CherryPy?
CherryPy má jednu unikátnu výhodu: built-in production server.
bash
# Iné frameworky v produkcii:
pip install flask gunicorn
gunicorn app:app
pip install fastapi uvicorn
uvicorn main:app
# CherryPy v produkcii:
pip install cherrypy
python server.py # That's it! Same as dev! 🚀Žiadny gunicorn. Žiadny uvicorn. Žiadna konfigurácia. Len spusti a funguje.
Poďme na to!
Quick Start: 5 minút od nuly po API
Krok 1: Inštalácia (1 minúta)
bash
# Virtual environment
python -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
# Nainštalovať CherryPy
pip install cherrypy
# To je všetko! Žiadne dependencies! 🎉
# Verify
python -c "import cherrypy; print(cherrypy.__version__)"
# Output: 18.8.0 (alebo novšie)Krok 2: Hello World (2 minúty)
python
# server.py
import cherrypy
class HelloAPI:
@cherrypy.expose
def index(self):
"""GET /"""
return "Hello, World! 👋"
@cherrypy.expose
def greet(self, name="Guest"):
"""GET /greet?name=John"""
return f"Hello, {name}! 🎉"
if __name__ == '__main__':
cherrypy.quickstart(HelloAPI(), '/')Spustiť:
bash
python server.py
# Output:
# [INFO] ENGINE Bus STARTING
# [INFO] ENGINE Started monitor thread 'Autoreloader'
# [INFO] ENGINE Serving on http://127.0.0.1:8080
# [INFO] ENGINE Bus STARTED
# Server beží! Production ready! ✅Testovať:
bash
# New terminal
curl http://localhost:8080/
# Output: Hello, World! 👋
curl http://localhost:8080/greet?name=John
# Output: Hello, John! 🎉
# Working! 🎉Total time: 3 minúty!
JSON API: +2 minúty
Krok 3: JSON Responses
python
# api.py
import cherrypy
class ProductAPI:
def __init__(self):
# In-memory database
self.products = [
{"id": 1, "name": "Laptop", "price": 999.99},
{"id": 2, "name": "Mouse", "price": 29.99},
{"id": 3, "name": "Keyboard", "price": 79.99}
]
@cherrypy.expose
@cherrypy.tools.json_out() # Auto JSON serialization!
def index(self):
"""GET /products - List all"""
return self.products
@cherrypy.expose
@cherrypy.tools.json_out()
def show(self, id):
"""GET /products/show/1 - Get one"""
product = next(
(p for p in self.products if p['id'] == int(id)),
None
)
if not product:
raise cherrypy.HTTPError(404, "Product not found")
return product
if __name__ == '__main__':
cherrypy.quickstart(ProductAPI(), '/products')Test:
bash
python api.py
# New terminal
curl http://localhost:8080/products
# [{"id":1,"name":"Laptop","price":999.99},...]
curl http://localhost:8080/products/show/1
# {"id":1,"name":"Laptop","price":999.99}
curl http://localhost:8080/products/show/999
# 404: Product not found
# JSON API working! 🎉Full CRUD API: +10 minút
Krok 4: Complete REST API
python
# crud_api.py
import cherrypy
from datetime import datetime
class CRUD_API:
def __init__(self):
self.items = {}
self.next_id = 1
@cherrypy.expose
@cherrypy.tools.json_out()
def index(self):
"""
GET / - List all items
Example:
curl http://localhost:8080/api/
"""
return list(self.items.values())
@cherrypy.expose
@cherrypy.tools.json_out()
def show(self, id):
"""
GET /show/1 - Get one item
Example:
curl http://localhost:8080/api/show/1
"""
item_id = int(id)
if item_id not in self.items:
raise cherrypy.HTTPError(404, "Item not found")
return self.items[item_id]
@cherrypy.expose
@cherrypy.tools.json_in()
@cherrypy.tools.json_out()
def create(self):
"""
POST /create - Create new item
Example:
curl -X POST http://localhost:8080/api/create \
-H "Content-Type: application/json" \
-d '{"name":"Test Item","description":"Test desc"}'
"""
data = cherrypy.request.json
item = {
"id": self.next_id,
"name": data['name'],
"description": data.get('description', ''),
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat()
}
self.items[self.next_id] = item
self.next_id += 1
return item
@cherrypy.expose
@cherrypy.tools.json_in()
@cherrypy.tools.json_out()
def update(self, id):
"""
PUT /update/1 - Update item
Example:
curl -X PUT http://localhost:8080/api/update/1 \
-H "Content-Type: application/json" \
-d '{"name":"Updated Name"}'
"""
item_id = int(id)
if item_id not in self.items:
raise cherrypy.HTTPError(404, "Item not found")
data = cherrypy.request.json
self.items[item_id].update(data)
self.items[item_id]['updated_at'] = datetime.now().isoformat()
return self.items[item_id]
@cherrypy.expose
@cherrypy.tools.json_out()
def delete(self, id):
"""
DELETE /delete/1 - Delete item
Example:
curl -X DELETE http://localhost:8080/api/delete/1
"""
item_id = int(id)
if item_id not in self.items:
raise cherrypy.HTTPError(404, "Item not found")
deleted_item = self.items[item_id]
del self.items[item_id]
return {
"message": "Item deleted",
"id": item_id,
"item": deleted_item
}
if __name__ == '__main__':
config = {
'global': {
'server.socket_host': '0.0.0.0',
'server.socket_port': 8080,
}
}
cherrypy.config.update(config)
cherrypy.quickstart(CRUD_API(), '/api')Test všetky operácie:
bash
# Run server
python crud_api.py
# CREATE
curl -X POST http://localhost:8080/api/create \
-H "Content-Type: application/json" \
-d '{"name":"Laptop","description":"Gaming laptop"}'
# {"id":1,"name":"Laptop",...}
curl -X POST http://localhost:8080/api/create \
-H "Content-Type: application/json" \
-d '{"name":"Mouse","description":"Wireless mouse"}'
# {"id":2,"name":"Mouse",...}
# READ ALL
curl http://localhost:8080/api/
# [{"id":1,...},{"id":2,...}]
# READ ONE
curl http://localhost:8080/api/show/1
# {"id":1,"name":"Laptop",...}
# UPDATE
curl -X PUT http://localhost:8080/api/update/1 \
-H "Content-Type: application/json" \
-d '{"name":"Gaming Laptop","description":"Updated desc"}'
# {"id":1,"name":"Gaming Laptop",...}
# DELETE
curl -X DELETE http://localhost:8080/api/delete/2
# {"message":"Item deleted","id":2,...}
# VERIFY
curl http://localhost:8080/api/
# [{"id":1,...}] # Only one left
# Full CRUD working! 🎉Database Integration: +5 minút
Krok 5: SQLite Database
python
# db_api.py
import cherrypy
import sqlite3
class DatabaseAPI:
def __init__(self):
# Initialize SQLite database
self.db_path = 'app.db'
self.init_database()
def get_db(self):
"""Get database connection"""
db = sqlite3.connect(self.db_path, check_same_thread=False)
db.row_factory = sqlite3.Row # Dict-like access
return db
def init_database(self):
"""Create tables if not exist"""
db = self.get_db()
db.execute('''
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
price REAL NOT NULL,
stock INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
db.commit()
db.close()
@cherrypy.expose
@cherrypy.tools.json_out()
def index(self):
"""GET /products - List all"""
db = self.get_db()
cursor = db.execute('SELECT * FROM products ORDER BY id DESC')
products = [dict(row) for row in cursor.fetchall()]
db.close()
return products
@cherrypy.expose
@cherrypy.tools.json_out()
def show(self, id):
"""GET /products/show/1 - Get one"""
db = self.get_db()
cursor = db.execute('SELECT * FROM products WHERE id = ?', (id,))
row = cursor.fetchone()
db.close()
if not row:
raise cherrypy.HTTPError(404, "Product not found")
return dict(row)
@cherrypy.expose
@cherrypy.tools.json_in()
@cherrypy.tools.json_out()
def create(self):
"""POST /products/create - Create new"""
data = cherrypy.request.json
db = self.get_db()
cursor = db.execute(
'''INSERT INTO products (name, description, price, stock)
VALUES (?, ?, ?, ?)''',
(
data['name'],
data.get('description', ''),
data['price'],
data.get('stock', 0)
)
)
db.commit()
# Return created product
new_id = cursor.lastrowid
cursor = db.execute('SELECT * FROM products WHERE id = ?', (new_id,))
product = dict(cursor.fetchone())
db.close()
return product
@cherrypy.expose
@cherrypy.tools.json_in()
@cherrypy.tools.json_out()
def update(self, id):
"""PUT /products/update/1 - Update"""
data = cherrypy.request.json
db = self.get_db()
# Check if exists
cursor = db.execute('SELECT id FROM products WHERE id = ?', (id,))
if not cursor.fetchone():
db.close()
raise cherrypy.HTTPError(404, "Product not found")
# Update
db.execute(
'''UPDATE products
SET name = ?, description = ?, price = ?, stock = ?
WHERE id = ?''',
(
data['name'],
data.get('description', ''),
data['price'],
data.get('stock', 0),
id
)
)
db.commit()
# Return updated product
cursor = db.execute('SELECT * FROM products WHERE id = ?', (id,))
product = dict(cursor.fetchone())
db.close()
return product
@cherrypy.expose
@cherrypy.tools.json_out()
def delete(self, id):
"""DELETE /products/delete/1 - Delete"""
db = self.get_db()
# Check if exists
cursor = db.execute('SELECT * FROM products WHERE id = ?', (id,))
row = cursor.fetchone()
if not row:
db.close()
raise cherrypy.HTTPError(404, "Product not found")
product = dict(row)
# Delete
db.execute('DELETE FROM products WHERE id = ?', (id,))
db.commit()
db.close()
return {
"message": "Product deleted",
"product": product
}
@cherrypy.expose
@cherrypy.tools.json_out()
def stats(self):
"""GET /products/stats - Statistics"""
db = self.get_db()
cursor = db.execute('''
SELECT
COUNT(*) as total,
SUM(stock) as total_stock,
AVG(price) as avg_price,
MIN(price) as min_price,
MAX(price) as max_price
FROM products
''')
stats = dict(cursor.fetchone())
db.close()
return stats
if __name__ == '__main__':
config = {
'global': {
'server.socket_host': '0.0.0.0',
'server.socket_port': 8080,
}
}
cherrypy.config.update(config)
cherrypy.quickstart(DatabaseAPI(), '/products')
# SQLite CRUD API in 150 lines! 🎉Test with database:
bash
python db_api.py
# Create products
curl -X POST http://localhost:8080/products/create \
-H "Content-Type: application/json" \
-d '{"name":"Laptop","price":999.99,"stock":10}'
curl -X POST http://localhost:8080/products/create \
-H "Content-Type: application/json" \
-d '{"name":"Mouse","price":29.99,"stock":50}'
# List all
curl http://localhost:8080/products/
# Get stats
curl http://localhost:8080/products/stats
# {"total":2,"total_stock":60,"avg_price":514.99,...}
# Database persists! Restart server, data still there! 💾Production Configuration
Krok 6: Production-Ready Setup
python
# production.py
import cherrypy
import logging
from db_api import DatabaseAPI
class ProductionConfig:
"""Production configuration"""
@staticmethod
def get_config():
return {
'global': {
# Server
'server.socket_host': '0.0.0.0',
'server.socket_port': 8080,
'server.thread_pool': 30,
'server.thread_pool_max': 50,
# Logging
'log.screen': False,
'log.access_file': 'access.log',
'log.error_file': 'error.log',
# Environment
'environment': 'production',
},
'/': {
# Sessions
'tools.sessions.on': True,
'tools.sessions.timeout': 60,
# CORS (if needed)
'tools.response_headers.on': True,
'tools.response_headers.headers': [
('Access-Control-Allow-Origin', '*'),
],
}
}
if __name__ == '__main__':
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Apply config
config = ProductionConfig.get_config()
cherrypy.config.update(config)
# Start server
print("🚀 Starting production server...")
print("📍 URL: http://0.0.0.0:8080/products")
cherrypy.quickstart(DatabaseAPI(), '/products', config)
# Run:
# python production.py
#
# Production-ready! No gunicorn! No nginx (optional)! 🎉Docker Deployment
dockerfile
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY . .
# Expose port
EXPOSE 8080
# Run
CMD ["python", "production.py"]txt
# requirements.txt
cherrypy==18.8.0bash
# Build
docker build -t my-api .
# Run
docker run -p 8080:8080 my-api
# API running in Docker! 🐳Advanced Features
Authentication
python
# auth_api.py
import cherrypy
from functools import wraps
def require_auth(f):
"""Simple token authentication decorator"""
@wraps(f)
def wrapper(*args, **kwargs):
auth = cherrypy.request.headers.get('Authorization')
if not auth or not auth.startswith('Bearer '):
raise cherrypy.HTTPError(401, "Unauthorized")
token = auth.replace('Bearer ', '')
if token != 'secret-token-123': # Simple check
raise cherrypy.HTTPError(401, "Invalid token")
return f(*args, **kwargs)
return wrapper
class ProtectedAPI:
@cherrypy.expose
@cherrypy.tools.json_out()
def public(self):
"""GET /public - No auth"""
return {"message": "Public endpoint"}
@cherrypy.expose
@cherrypy.tools.json_out()
@require_auth
def private(self):
"""GET /private - Auth required"""
return {"message": "Secret data", "user": "authenticated"}
@cherrypy.expose
@cherrypy.tools.json_out()
@require_auth
def admin(self):
"""GET /admin - Admin only"""
return {"message": "Admin panel", "level": "admin"}
if __name__ == '__main__':
cherrypy.quickstart(ProtectedAPI(), '/')
# Test:
# curl http://localhost:8080/public # Works
# curl http://localhost:8080/private # 401 Unauthorized
# curl -H "Authorization: Bearer secret-token-123" \
# http://localhost:8080/private # Works!CORS Support
python
# cors_api.py
import cherrypy
def cors():
"""Add CORS headers"""
if cherrypy.request.method == 'OPTIONS':
# Preflight request
cherrypy.response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
cherrypy.response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
cherrypy.response.headers['Access-Control-Allow-Origin'] = '*'
return True
else:
cherrypy.response.headers['Access-Control-Allow-Origin'] = '*'
class CorsAPI:
@cherrypy.expose
@cherrypy.tools.json_out()
def index(self):
return {"message": "CORS enabled"}
if __name__ == '__main__':
config = {
'/': {
'tools.cors.on': True,
}
}
cherrypy.tools.cors = cherrypy.Tool('before_handler', cors)
cherrypy.quickstart(CorsAPI(), '/', config)
# Now frontend can call API from different domain! 🌐Request Logging
python
# logging_api.py
import cherrypy
from datetime import datetime
def log_request():
"""Log all requests"""
print(f"[{datetime.now()}] {cherrypy.request.method} {cherrypy.request.path_info}")
class LoggedAPI:
@cherrypy.expose
@cherrypy.tools.json_out()
def index(self):
return {"message": "Check console for logs"}
if __name__ == '__main__':
config = {
'/': {
'tools.log_request.on': True,
}
}
cherrypy.tools.log_request = cherrypy.Tool('before_handler', log_request)
cherrypy.quickstart(LoggedAPI(), '/', config)
# Every request logged! 📝Real-World Use Cases
Use Case 1: Mock API for Frontend
python
# mock_api.py
import cherrypy
import random
import time
class MockAPI:
"""Mock API for frontend testing"""
@cherrypy.expose
@cherrypy.tools.json_out()
def users(self):
"""GET /users - Mock users"""
return [
{"id": i, "name": f"User {i}", "email": f"user{i}@example.com"}
for i in range(1, 11)
]
@cherrypy.expose
@cherrypy.tools.json_out()
def slow(self):
"""GET /slow - Simulate slow endpoint"""
time.sleep(random.uniform(2, 5))
return {"message": "Slow response"}
@cherrypy.expose
def error(self):
"""GET /error - Simulate error"""
raise cherrypy.HTTPError(500, "Simulated server error")
@cherrypy.expose
@cherrypy.tools.json_out()
def random_status(self):
"""GET /random_status - Random HTTP status"""
status = random.choice([200, 400, 500, 503])
cherrypy.response.status = status
return {"status": status}
if __name__ == '__main__':
cherrypy.quickstart(MockAPI(), '/mock')
# Perfect for frontend development before backend ready! 🎨Use Case 2: Webhook Receiver
python
# webhook_api.py
import cherrypy
import json
from datetime import datetime
class WebhookAPI:
def __init__(self):
self.webhooks = []
@cherrypy.expose
@cherrypy.tools.json_in()
@cherrypy.tools.json_out()
def receive(self):
"""POST /webhooks/receive - Receive webhook"""
data = cherrypy.request.json
webhook = {
"id": len(self.webhooks) + 1,
"data": data,
"received_at": datetime.now().isoformat(),
"headers": dict(cherrypy.request.headers)
}
self.webhooks.append(webhook)
# Log to console
print(f"\n🎣 Webhook received:")
print(json.dumps(webhook, indent=2))
return {"status": "received", "id": webhook["id"]}
@cherrypy.expose
@cherrypy.tools.json_out()
def list(self):
"""GET /webhooks/list - List all webhooks"""
return self.webhooks
@cherrypy.expose
@cherrypy.tools.json_out()
def clear(self):
"""DELETE /webhooks/clear - Clear all"""
count = len(self.webhooks)
self.webhooks = []
return {"message": f"Cleared {count} webhooks"}
if __name__ == '__main__':
cherrypy.quickstart(WebhookAPI(), '/webhooks')
# Perfect for testing webhooks locally! 🎣Záver
CherryPy jedinečná výhoda: Built-in production server!
bash
# Development
python server.py
# Production
python server.py # Same command! 🚀Žiadny gunicorn. Žiadny uvicorn. Žiadna konfigurácia.
Používaj CherryPy keď:
- ✅ Potrebuješ quick API (5 minút)
- ✅ Testing/mock server
- ✅ Prototyping
- ✅ Internal tools
- ✅ Nechceš setup gunicorn/uvicorn
Setup za 5 minút. Production ready. Zero external dependencies.
Tutorial napísal developer ktorý oceňuje simplicity. Sometimes less is more. CherryPy je proof. 🍒