Obsah článku / Contents
CherryPy: API za 5 minút s built-in production serverom

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.0
bash
# 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. 🍒

Všetky články