diff --git a/docs/api/core/requests.md b/docs/api/core/requests.md index e2d6ba4..ce997b1 100644 --- a/docs/api/core/requests.md +++ b/docs/api/core/requests.md @@ -113,15 +113,6 @@ if "application/json" in request.content_type: pass ``` -#### `content_length: int` - -The size of the request body in bytes. Returns 0 if `Content-Length` header is missing or invalid. - -```python -if request.content_length > 10_000_000: - return "413 PAYLOAD TOO LARGE", "Request too large" -``` - ### Connection Information #### `client_ip: str` diff --git a/docs/api/core/server.md b/docs/api/core/server.md index 45d6274..6237496 100644 --- a/docs/api/core/server.md +++ b/docs/api/core/server.md @@ -1,16 +1,23 @@ # Server -The `RunServer` class provides a simple WSGI server implementation for running Wiverno applications. It uses Python's built-in `wsgiref.simple_server` for request handling. +The `RunServer` class provides a production-ready WSGI server implementation for running Wiverno applications. It uses Python's built-in `wsgiref.simple_server` with enhanced features for stability and production use. ## Module: `wiverno.core.server` ## Overview -`RunServer` is a lightweight WSGI server wrapper that's suitable for development and testing. For production deployments, use external WSGI servers like Gunicorn, uWSGI, or Waitress. +`RunServer` is an improved WSGI server that includes: + +- **Graceful shutdown** - Handles SIGINT and SIGTERM signals properly +- **Enhanced logging** - Detailed startup and error information +- **Error handling** - Better exception handling and recovery +- **Configurable queue size** - Tune for your workload + +While suitable for light to medium traffic production environments, for high-traffic applications consider using dedicated WSGI servers like Gunicorn, uWSGI, or Waitress. ## Constructor -### `RunServer(application, host="localhost", port=8000)` +### `RunServer(application, host="localhost", port=8000, request_queue_size=5)` Creates a new server instance. @@ -19,6 +26,7 @@ Creates a new server instance. - `application` (Callable): A WSGI-compatible application - `host` (str, optional): Hostname to bind to. Defaults to `"localhost"` - `port` (int, optional): Port number to bind to. Defaults to `8000` +- `request_queue_size` (int, optional): Maximum number of queued connections. Defaults to `5` **Returns:** `RunServer` instance @@ -32,8 +40,8 @@ app = Wiverno() def home(request): return "200 OK", "Hello" -# Create server -server = RunServer(app, host="localhost", port=8000) +# Create server with custom queue size +server = RunServer(app, host="localhost", port=8000, request_queue_size=10) ``` ## Attributes @@ -65,21 +73,60 @@ server = RunServer(app) print(server.application) # Output: ``` +### `request_queue_size: int` + +Maximum number of queued connections. + +```python +server = RunServer(app, request_queue_size=10) +print(server.request_queue_size) # Output: 10 +``` + ## Methods ### `start() -> None` Starts the WSGI server and serves the application forever. -The server will run indefinitely until interrupted by `KeyboardInterrupt` (Ctrl+C). This method blocks and doesn't return unless the server is stopped. +The server will run indefinitely until interrupted by `KeyboardInterrupt` (Ctrl+C) or SIGTERM signal. This method blocks and doesn't return unless the server is stopped. + +Features: + +- Handles SIGINT (Ctrl+C) and SIGTERM signals gracefully +- Completes current requests before shutting down +- Logs startup information and errors +- Automatically cleans up resources **Raises:** -- `KeyboardInterrupt` when user presses Ctrl+C (gracefully handled) +- `OSError`: If unable to bind to the specified host:port +- `Exception`: For unexpected errors during server operation ```python +import logging + +logging.basicConfig(level=logging.INFO) + server = RunServer(app, host="0.0.0.0", port=8000) -server.start() # Blocks until Ctrl+C is pressed +server.start() # Blocks until interrupted + +# Logs: +# INFO: Wiverno server started on http://0.0.0.0:8000 +# INFO: Request queue size: 5 +# INFO: Press Ctrl+C to stop the server +``` + +### `stop() -> None` + +Stops the server gracefully. + +Shuts down the server, allowing current requests to complete before stopping. This method is called automatically on SIGINT/SIGTERM or can be called programmatically. + +```python +server = RunServer(app) + +# In another thread or signal handler: +server.stop() # Graceful shutdown ``` ## Usage Examples @@ -102,6 +149,7 @@ if __name__ == "__main__": ``` Run with: + ```bash python run.py # Server running at http://localhost:8000 @@ -119,37 +167,243 @@ server.start() # Now accessible at http://0.0.0.0:5000 ``` -### Development Server Setup +### Production Setup with Logging ```python +import logging from wiverno.main import Wiverno from wiverno.core.server import RunServer +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +) + app = Wiverno() @app.get("/") def home(request): return "200 OK", "Home" -@app.get("/about") -def about(request): - return "200 OK", "About" +@app.get("/api/health") +def health(request): + return "200 OK", '{"status": "ok"}' if __name__ == "__main__": - print("Starting development server...") - print("Server running at http://localhost:8000") - print("Press Ctrl+C to stop") + server = RunServer( + app, + host="0.0.0.0", + port=8000, + request_queue_size=10, # Handle more concurrent connections + ) + + try: + server.start() + except OSError as e: + logging.error(f"Failed to start server: {e}") + except KeyboardInterrupt: + logging.info("Server stopped by user") +``` - server = RunServer(app) +### Error Handling + +```python +from wiverno.core.server import RunServer + +try: + # Try to bind to privileged port + server = RunServer(app, host="0.0.0.0", port=80) server.start() +except OSError as e: + if "Permission denied" in str(e): + print("Error: Port 80 requires root privileges") + print("Try: sudo python run.py") + print("Or use port >= 1024") + elif "Address already in use" in str(e): + print("Error: Port 80 is already in use") + print("Kill the process or use a different port") ``` +### Programmatic Stop + +```python +import threading +import time +from wiverno.core.server import RunServer + +server = RunServer(app, host="localhost", port=8000) + +# Start server in a thread +server_thread = threading.Thread(target=server.start, daemon=True) +server_thread.start() + +# Do some work... +time.sleep(60) + +# Stop server gracefully +server.stop() +``` + +## Comparison with Other Servers + +### RunServer (Built-in) + +**Pros:** + +- No additional dependencies +- Graceful shutdown +- Good for light to medium traffic + +- Easy to configure + +**Cons:** + +- Single-threaded +- Not optimized for high traffic +- Limited performance tuning + +**Best for:** Small to medium applications, prototypes, internal tools + +### Gunicorn (Recommended for Production) + +```bash +pip install gunicorn + +gunicorn myapp:app --workers 4 --bind 0.0.0.0:8000 +``` + +**Pros:** + +- Multi-worker support + +- High performance +- Production-proven +- Many configuration options + +**Cons:** + +- Unix/Linux only +- Additional dependency + +### Waitress (Cross-platform) + +```bash +pip install waitress +waitress-serve --host=0.0.0.0 --port=8000 myapp:app +``` + +**Pros:** + +- Cross-platform (Windows support) +- Multi-threaded +- Production-ready + +**Cons:** + +- Additional dependency +- Fewer features than Gunicorn + +## Best Practices + +### Development + +Use DevServer for development with hot reload: + +```python +from wiverno.dev.dev_server import DevServer + +DevServer.serve(app_module="myapp", port=8000) +``` + +See [Running Your Application](../../guide/running.md) for details. + +### Production + +For production, choose based on traffic: + +**Light Traffic** (< 100 req/sec): + +```python +# Use RunServer +server = RunServer(app, host="0.0.0.0", port=8000, request_queue_size=10) +server.start() + +``` + +**Medium to High Traffic**: + +```bash +# Use Gunicorn +gunicorn myapp:app --workers 4 --worker-class sync --bind 0.0.0.0:8000 +``` + +**Very High Traffic**: + +```bash +# Use Gunicorn with multiple workers + nginx reverse proxy +gunicorn myapp:app \ + --workers 8 \ + --worker-class sync \ + --max-requests 1000 \ + --max-requests-jitter 100 \ + --bind 127.0.0.1:8000 +``` + +## Troubleshooting + +### Port Already in Use + +```bash +# Find process using the port +lsof -i :8000 + +# Kill the process +kill -9 + +# Or use a different port +server = RunServer(app, port=8001) +``` + +### Permission Denied (Port < 1024) + +```bash +# Option 1: Use sudo (not recommended) +sudo python run.py + +# Option 2: Use port >= 1024 (recommended) +server = RunServer(app, port=8000) + +# Option 3: Use authbind (Linux) +authbind --deep python run.py +``` + +### Server Not Accessible from External Network + +```python +# Wrong: Only accessible from localhost +server = RunServer(app, host="localhost", port=8000) + +# Correct: Accessible from external network +server = RunServer(app, host="0.0.0.0", port=8000) +``` + +## Related Documentation + +- [Running Your Application](../../guide/running.md) - Comprehensive guide on development and production servers +- [CLI Commands](../cli.md) - Command-line interface for server management +- [Application](application.md) - Wiverno application class + ## CLI Alternative The Wiverno CLI provides a convenient way to start the server: ```bash -wiverno run dev app:app --host 0.0.0.0 --port 5000 +# Run production server +wiverno run prod --host 0.0.0.0 --port 8000 + +# Run development server with hot reload +wiverno run dev --host 0.0.0.0 --port 8000 ``` This automatically creates a `RunServer` and starts it. diff --git a/docs/api/dev/dev-server.md b/docs/api/dev/dev-server.md new file mode 100644 index 0000000..b102f21 --- /dev/null +++ b/docs/api/dev/dev-server.md @@ -0,0 +1,469 @@ +# DevServer + +The `DevServer` class provides a development server with hot reload functionality. It automatically restarts your application when Python source files are modified, making development faster and more convenient. + +## Module: `wiverno.dev.dev_server` + +## Overview + +`DevServer` is specifically designed for development environments and includes: + +- **Hot Reload** - Automatically restarts when `.py` files change +- **Debouncing** - Prevents excessive restarts by grouping changes +- **File Watching** - Uses watchdog to monitor specified directories +- **Rich UI** - Beautiful console output with progress indicators +- **Configurable Ignoring** - Exclude specific files/directories from watching + +**⚠️ Important:** DevServer is for development only. Do not use in production! + +## Constructor + +### `DevServer(app_module, app_name="app", host="localhost", port=8000, watch_dirs=None, ignore_patterns=None, debounce_seconds=1.0)` + +Creates a new development server instance. + +**Parameters:** + +- `app_module` (str): Module path containing the WSGI application (e.g., `'run'`, `'myapp.main'`) +- `app_name` (str, optional): Name of the application variable in the module. Defaults to `"app"` +- `host` (str, optional): Server host address. Defaults to `"localhost"` +- `port` (int, optional): Server port. Defaults to `8000` +- `watch_dirs` (list[str], optional): Directories to watch for changes. If None, watches current directory +- `ignore_patterns` (list[str], optional): Patterns to ignore. Defaults to common patterns (see below) +- `debounce_seconds` (float, optional): Time to wait before restarting after file changes. Defaults to `1.0` + +**Default Ignore Patterns:** + +- `__pycache__/` +- `.venv/`, `venv/` +- `.git/` +- `.pytest_cache/`, `.mypy_cache/`, `.ruff_cache/` +- `tests/` +- `htmlcov/`, `.coverage` + +```python +from wiverno.dev.dev_server import DevServer + +# Basic usage +server = DevServer( + app_module="myapp", + app_name="app", + host="localhost", + port=8000, +) +``` + +## Attributes + +### `app_module: str` + +Module path containing the WSGI application. + +```python +server = DevServer(app_module="myapp.main") +print(server.app_module) # Output: myapp.main +``` + +### `app_name: str` + +Name of the application variable. + +```python +server = DevServer(app_module="run", app_name="application") +print(server.app_name) # Output: application +``` + +### `host: str` + +Server host address. + +```python +server = DevServer(app_module="run", host="0.0.0.0") +print(server.host) # Output: 0.0.0.0 +``` + +### `port: int` + +Server port number. + +```python +server = DevServer(app_module="run", port=8080) +print(server.port) # Output: 8080 +``` + +### `watch_dirs: list[str]` + +List of directories to watch for changes. + +```python +server = DevServer( + app_module="run", + watch_dirs=["./myapp", "./shared"], +) +print(server.watch_dirs) # Output: ['./myapp', './shared'] +``` + +### `ignore_patterns: list[str]` + +List of patterns to ignore when watching for changes. + +```python +server = DevServer( + app_module="run", + ignore_patterns=["__pycache__", "*.pyc", ".venv"], +) +print(server.ignore_patterns) +``` + +### `debounce_seconds: float` + +Time to wait before restarting after file changes. + +```python +server = DevServer(app_module="run", debounce_seconds=2.0) +print(server.debounce_seconds) # Output: 2.0 +``` + +## Methods + +### `serve(app_module="run", app_name="app", host="localhost", port=8000)` [static] + +Main entry point for running the development server. This is the recommended way to start the server. + +**Parameters:** + +- `app_module` (str, optional): Module path containing the WSGI application. Defaults to `"run"` +- `app_name` (str, optional): Name of the application variable. Defaults to `"app"` +- `host` (str, optional): Server host address. Defaults to `"localhost"` +- `port` (int, optional): Server port. Defaults to `8000` + +**Returns:** None (blocks until interrupted) + +```python +from wiverno.dev.dev_server import DevServer + +# Quick start +DevServer.serve() + +# Custom configuration +DevServer.serve( + app_module="myapp", + app_name="application", + host="0.0.0.0", + port=8080, +) +``` + +### `stop() -> None` + +Stop the development server and file watcher. + +Gracefully shuts down the server process and stops file watching. + +```python +server = DevServer(app_module="run") + +# In another thread or signal handler +server.stop() +``` + +## Usage Examples + +### Quick Start + +```python +from wiverno.dev.dev_server import DevServer + +# Simplest form - uses defaults (run.py with 'app' variable) +DevServer.serve() +``` + +### Custom Configuration + +```python +DevServer.serve( + app_module="myproject.api", + app_name="application", + host="0.0.0.0", + port=5000, +) +``` + +For more detailed examples and project structure, see the [Running Guide](../../guide/running.md). + +### Advanced Configuration + +```python +from wiverno.dev.dev_server import DevServer + +server = DevServer( + app_module="myapp", + app_name="app", + host="0.0.0.0", + port=8000, + watch_dirs=[ + "./myapp", # Watch application code + "./shared", # Watch shared utilities + "./config", # Watch configuration + ], + ignore_patterns=[ + "__pycache__", + "*.pyc", + ".venv", + "venv", + "tests", + "*.log", + ".git", + ], + debounce_seconds=1.5, # Wait 1.5 seconds before restart +) + +# Use serve() static method for actual startup +# DevServer.serve(...) +``` + +### Project Structure Example + +``` +myproject/ +├── myapp/ +│ ├── __init__.py +│ ├── main.py # Contains 'app' +│ └── routes.py +├── dev.py # Development server script +└── run.py # Production server script +``` + +`dev.py`: + +```python +from wiverno.dev.dev_server import DevServer + +if __name__ == "__main__": + DevServer.serve( + app_module="myapp.main", + app_name="app", + host="127.0.0.1", + port=8000, + ) +``` + +Run with: + +```bash +uv run python dev.py +``` + +### Hot Reload in Action + +When you save a file: + +``` +WARNING: File changed: /path/to/myapp/routes.py +>> Server restarting... +╭────────────────────────────────────────╮ +│ Wiverno Development Server │ +│ │ +│ Server: http://localhost:8000 │ +│ Debug Mode: ON │ +│ Restart: #2 │ +│ Press Ctrl+C to stop │ +╰────────────────────────────────────────╯ +``` + +## How Hot Reload Works + +DevServer uses the watchdog library to monitor file changes: + +1. Monitors `.py` files in specified directories +2. Debounces changes (waits `debounce_seconds` before restart) +3. Terminates old process gracefully +4. Spawns new process with updated code + +See the [Running Guide](../../guide/running.md#how-hot-reload-works) for detailed explanation. + +## CLI Alternative + +Use the Wiverno CLI for quick development server starts: + +```bash +# Start dev server with defaults +wiverno run dev + +# Custom configuration +wiverno run dev --host 0.0.0.0 --port 8080 + +# Specify module and app +wiverno run dev --app-module myapp --app-name application +``` + +## Best Practices + +### Use for Development Only + +```python +# ✅ Good - Development +if __name__ == "__main__": + from wiverno.dev.dev_server import DevServer + DevServer.serve() + +# ❌ Bad - Don't use in production +if __name__ == "__main__": + from wiverno.dev.dev_server import DevServer + DevServer.serve(host="0.0.0.0") # Exposed to internet! +``` + +### Configure Ignore Patterns + +Ignore files that shouldn't trigger restarts: + +```python +DevServer.serve( + app_module="myapp", + ignore_patterns=[ + "__pycache__", + "*.pyc", + ".venv", + "tests", # Don't restart on test changes + "*.log", # Ignore log files + "static/", # Ignore static assets + "migrations/", # Ignore database migrations + ], +) +``` + +### Separate Dev and Production Scripts + +```python +# dev.py - Development only +from wiverno.dev.dev_server import DevServer + +if __name__ == "__main__": + DevServer.serve(app_module="myapp") + +# run.py - Production +from wiverno.core.server import RunServer +from myapp import app + +if __name__ == "__main__": + server = RunServer(app, host="0.0.0.0", port=8000) + server.start() +``` + +### Adjust Debounce Time + +For different workflows: + +```python +# Fast restarts (may restart too often) +DevServer.serve(debounce_seconds=0.5) + +# Slower restarts (better for large projects) +DevServer.serve(debounce_seconds=2.0) + +# Default (recommended) +DevServer.serve(debounce_seconds=1.0) +``` + +## Troubleshooting + +### Hot Reload Not Working + +**Problem:** Files change but server doesn't restart. + +**Solutions:** + +1. Check file is not in ignore patterns +2. Verify file has `.py` extension +3. Ensure file is in watched directories +4. Check console for error messages + +```python +# Debug: Print watch directories +server = DevServer( + app_module="myapp", + watch_dirs=["./myapp"], # Explicit watch directory +) +print(f"Watching: {server.watch_dirs}") +``` + +### Too Many Restarts + +**Problem:** Server restarts constantly. + +**Solutions:** + +1. Increase debounce time +2. Add patterns to ignore list +3. Check for auto-save or backup files + +```python +DevServer.serve( + debounce_seconds=2.0, # Wait longer + ignore_patterns=[ + "__pycache__", + "*.swp", # Vim swap files + "*.tmp", # Temp files + "*~", # Backup files + ], +) +``` + +### Import Errors + +**Problem:** `ImportError: No module named 'myapp'` + +**Solutions:** + +1. Verify module path is correct +2. Check PYTHONPATH +3. Ensure `__init__.py` exists in package directories + +```python +# Wrong +DevServer.serve(app_module="myapp") # Looking for myapp.py + +# Correct +DevServer.serve(app_module="myapp.main") # Looking for myapp/main.py +``` + +### Port Already in Use + +**Problem:** `OSError: Address already in use` + +**Solutions:** + +```bash +# Find and kill process +lsof -i :8000 +kill -9 + +# Or use different port +DevServer.serve(port=8001) +``` + +## Related Documentation + +- [Running Your Application](../../guide/running.md) - Complete guide on development and production servers +- [RunServer](../core/server.md) - Production server documentation +- [CLI Commands](../cli.md) - Command-line interface + +## Comparison with RunServer + +| Feature | DevServer | RunServer | +| --------------------- | ----------------- | --------------------- | +| **Hot Reload** | ✅ Yes | ❌ No | +| **Purpose** | Development | Production | +| **Performance** | Lower | Higher | +| **File Watching** | ✅ Yes | ❌ No | +| **Graceful Shutdown** | ✅ Yes | ✅ Yes | +| **Dependencies** | watchdog, rich | None | +| **Use Case** | Local development | Production deployment | + +## Summary + +- Use `DevServer` for all development work +- It automatically restarts when code changes +- Configure ignore patterns to avoid unnecessary restarts +- **Never use in production** - use RunServer or Gunicorn instead +- Main entry point is `DevServer.serve()` static method diff --git a/docs/guide/index.md b/docs/guide/index.md index 47e050b..0ec31d4 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -18,6 +18,7 @@ Learn the fundamental concepts of Wiverno: - [**Routing**](routing.md) - Define URL patterns and handlers - [**Requests**](requests.md) - Handle incoming HTTP requests +- [**Running Your Application**](running.md) - Development and production servers - [**HTTP Status Codes**](status-codes.md) - Working with HTTP status codes ### Advanced Topics diff --git a/docs/guide/running.md b/docs/guide/running.md new file mode 100644 index 0000000..673b5ba --- /dev/null +++ b/docs/guide/running.md @@ -0,0 +1,497 @@ +# Running Your Application + +This guide explains how to run your Wiverno application in different environments, from development to production. + +## Overview + +Wiverno provides two server options: + +- **DevServer** - Development server with hot reload (automatic restart on code changes) +- **RunServer** - Production-ready WSGI server for deploying applications + +## Development Server (DevServer) + +The `DevServer` is designed for local development and includes hot reload functionality. When you modify your Python files, the server automatically restarts to reflect the changes. + +### Basic Usage + +```python +from wiverno.dev.dev_server import DevServer + +# Quick start with defaults +DevServer.serve() +``` + +This starts the server with default settings: + +- **Module**: `run` (looks for `run.py`) +- **App name**: `app` (looks for `app` variable) +- **Host**: `localhost` +- **Port**: `8000` + +### Custom Configuration + +```python +DevServer.serve( + app_module="myapp", # Module containing your app + app_name="application", # Variable name of your app + host="0.0.0.0", # Bind to all interfaces + port=8080 # Custom port +) +``` + +### Advanced Usage + +For more control, create a DevServer instance: + +```python +from wiverno.dev.dev_server import DevServer + +server = DevServer( + app_module="myapp", + app_name="app", + host="localhost", + port=8000, + watch_dirs=["./myapp", "./shared"], # Watch multiple directories + ignore_patterns=[ # Custom ignore patterns + "__pycache__", + ".venv", + "tests", + "*.pyc", + ], + debounce_seconds=1.0, # Wait time before restart +) + +# For advanced usage, instantiate and configure before serving +# Then use serve() static method for actual startup +``` + +### How Hot Reload Works + +1. **Watchdog** monitors `.py` files in specified directories +2. When a file is modified, a `FileSystemEvent` is triggered +3. **Debounce mechanism** waits 1 second to collect all changes +4. After the delay, server restart is initiated +5. Old server process is properly terminated +6. New process starts with updated code +7. Restart counter increments for tracking + +### Ignored Patterns + +By default, DevServer ignores: + +- `__pycache__/` - Python cache directories +- `.venv/`, `venv/` - Virtual environments +- `.git/` - Git repository +- `.pytest_cache/`, `.mypy_cache/`, `.ruff_cache/` - Tool caches +- `tests/` - Test directories +- `htmlcov/`, `.coverage` - Coverage files + +### Example Development Setup + +Create a `dev.py` file in your project: + +```python +from wiverno.dev.dev_server import DevServer + +if __name__ == "__main__": + DevServer.serve( + app_module="app", + app_name="app", + host="0.0.0.0", + port=8000, + ) +``` + +Run it: + +```bash +uv run python dev.py +``` + +### Using with CLI + +The CLI provides a convenient way to run the dev server: + +```bash +# Start development server +wiverno run dev + +# With custom host and port +wiverno run dev --host 0.0.0.0 --port 8000 + +# With custom module +wiverno run dev --app-module myapp --app-name application +``` + +## Production Server (RunServer) + +The `RunServer` is an improved WSGI server suitable for production deployments. It includes graceful shutdown, better error handling, and production-ready features. + +### Basic Usage + +```python +from wiverno.core.server import RunServer +from myapp import app + +server = RunServer(app, host="0.0.0.0", port=8000) +server.start() +``` + +### Configuration Options + +```python +server = RunServer( + application=app, # Your WSGI application + host="0.0.0.0", # Bind to all interfaces + port=8000, # Port number + request_queue_size=5, # Max queued connections +) +``` + +### Features + +#### Graceful Shutdown + +RunServer handles SIGINT (Ctrl+C) and SIGTERM signals gracefully: + +```python +server = RunServer(app) +server.start() # Server runs until interrupted + +# On Ctrl+C or kill signal: +# 1. Current requests are completed +# 2. Server shuts down cleanly +# 3. Resources are released +``` + +You can also stop the server programmatically: + +```python +server.stop() # Graceful shutdown +``` + +#### Enhanced Logging + +RunServer provides detailed logging: + +```python +import logging + +logging.basicConfig(level=logging.INFO) + +server = RunServer(app, host="0.0.0.0", port=8000) +server.start() + +# Logs: +# INFO: Wiverno server started on http://0.0.0.0:8000 +# INFO: Request queue size: 5 +# INFO: Press Ctrl+C to stop the server +``` + +#### Error Handling + +The server handles common errors: + +```python +try: + server = RunServer(app, host="0.0.0.0", port=80) + server.start() +except OSError as e: + print(f"Cannot bind to port 80: {e}") + # Permission denied or port already in use +``` + +### Example Production Setup + +Create a `run.py` file: + +```python +import logging +from wiverno.core.server import RunServer +from myapp import app + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +if __name__ == "__main__": + server = RunServer( + app, + host="0.0.0.0", + port=8000, + request_queue_size=10, # Handle more concurrent connections + ) + + try: + server.start() + except KeyboardInterrupt: + print("Server stopped") +``` + +Run it: + +```bash +uv run python run.py +``` + +### Using with CLI + +```bash +# Start production server +wiverno run prod + +# With custom configuration +wiverno run prod --host 0.0.0.0 --port 8000 +``` + +## Production Deployment Recommendations + +### For Light to Medium Traffic + +RunServer is suitable for light to medium traffic applications: + +```python +server = RunServer( + app, + host="0.0.0.0", + port=8000, + request_queue_size=10, +) +server.start() +``` + +**Pros**: + +- Built-in, no extra dependencies +- Graceful shutdown +- Easy to configure +- Good error handling + +**Cons**: + +- Single-threaded (no concurrency) +- Not optimized for high traffic +- Limited performance tuning options + +### For High Traffic (Recommended) + +For production environments with high traffic, use dedicated WSGI servers: + +#### Gunicorn (Linux/Unix) + +```bash +pip install gunicorn +gunicorn myapp:app --workers 4 --bind 0.0.0.0:8000 +``` + +#### Waitress (Cross-platform) + +```bash +pip install waitress +waitress-serve --host=0.0.0.0 --port=8000 myapp:app +``` + +#### uWSGI (Linux/Unix) + +```bash +pip install uwsgi +uwsgi --http :8000 --wsgi-file myapp.py --callable app --processes 4 +``` + +## Comparison Table + +| Feature | DevServer | RunServer | Gunicorn/uWSGI | +| --------------------- | -------------- | ---------------- | ---------------- | +| **Purpose** | Development | Light production | Heavy production | +| **Hot Reload** | ✅ Yes | ❌ No | ❌ No | +| **Multi-process** | ❌ No | ❌ No | ✅ Yes | +| **Graceful Shutdown** | ✅ Yes | ✅ Yes | ✅ Yes | +| **Performance** | Low | Medium | High | +| **Ease of Use** | Very Easy | Easy | Moderate | +| **Dependencies** | watchdog, rich | None | Extra package | + +## Best Practices + +### Development + +1. **Use DevServer** for all development work +2. **Don't use DevServer in production** - it's not designed for it +3. **Configure ignore patterns** to avoid unnecessary restarts +4. **Use appropriate debounce time** (1 second is usually good) + +Example: + +```python +# dev.py +from wiverno.dev.dev_server import DevServer + +if __name__ == "__main__": + DevServer.serve( + app_module="myapp", + host="127.0.0.1", # Only localhost in dev + port=8000, + ) +``` + +### Production + +1. **Test with RunServer first** before moving to Gunicorn/uWSGI +2. **Use RunServer for small applications** or prototypes +3. **Use Gunicorn/uWSGI/Waitress for production** applications +4. **Enable proper logging** for debugging +5. **Use reverse proxy** (nginx, Apache) in front of your WSGI server +6. **Monitor resource usage** and adjust workers/threads accordingly + +Example: + +```python +# run.py +import logging +from wiverno.core.server import RunServer +from myapp import app + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s [%(levelname)s] %(message)s', +) + +if __name__ == "__main__": + server = RunServer(app, host="0.0.0.0", port=8000) + server.start() +``` + +### Systemd Service (Linux) + +Create `/etc/systemd/system/wiverno-app.service`: + +```ini +[Unit] +Description=Wiverno Application +After=network.target + +[Service] +Type=simple +User=www-data +WorkingDirectory=/var/www/myapp +Environment="PATH=/var/www/myapp/.venv/bin" +ExecStart=/var/www/myapp/.venv/bin/python run.py +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +Enable and start: + +```bash +sudo systemctl enable wiverno-app +sudo systemctl start wiverno-app +sudo systemctl status wiverno-app +``` + +## Docker Deployment + +### Dockerfile Example + +```dockerfile +FROM python:3.12-slim + +WORKDIR /app + +# Install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application +COPY . . + +# Expose port +EXPOSE 8000 + +# Run with RunServer (for simple apps) +CMD ["python", "run.py"] + +# Or use Gunicorn for production +# CMD ["gunicorn", "myapp:app", "--workers=4", "--bind=0.0.0.0:8000"] +``` + +### Docker Compose Example + +```yaml +version: "3.8" + +services: + web: + build: . + ports: + - "8000:8000" + environment: + - PYTHONUNBUFFERED=1 + volumes: + - .:/app # For development with hot reload + # Remove volumes in production +``` + +## Troubleshooting + +### Port Already in Use + +```python +# Error: OSError: [Errno 98] Address already in use + +# Solution: Use a different port or kill the process using the port +# Find process: +# lsof -i :8000 +# Kill process: +# kill -9 +``` + +### Hot Reload Not Working + +1. Check that you're using DevServer, not RunServer +2. Verify file patterns are not in ignore list +3. Make sure files are being saved properly +4. Check console for error messages + +### Permission Denied on Port 80/443 + +```bash +# Ports below 1024 require root privileges +# Option 1: Use port >= 1024 (recommended) +DevServer.serve(port=8000) + +# Option 2: Use sudo (not recommended) +sudo python dev.py + +# Option 3: Use reverse proxy (best for production) +# nginx -> localhost:8000 +``` + +### High Memory Usage in Production + +RunServer is single-threaded, but if you see high memory usage: + +1. Check for memory leaks in your application code +2. Monitor with tools like `htop` or `ps` +3. Consider using Gunicorn with multiple workers: + +```bash +gunicorn myapp:app --workers 4 --max-requests 1000 --max-requests-jitter 100 +``` + +## Summary + +- **DevServer**: Development with hot reload - use `DevServer.serve()` +- **RunServer**: Light production - use for small apps or prototypes +- **Gunicorn/uWSGI/Waitress**: Heavy production - use for production applications +- Always use proper logging and monitoring in production +- Test thoroughly before deploying to production + +## Next Steps + +- Learn about [CLI commands](cli.md) for quick server management +- Explore [Routing](routing.md) to define your application endpoints +- Read about [Requests](requests.md) to handle incoming data diff --git a/docs/guide/status-codes.md b/docs/guide/status-codes.md index 1165b1c..c4eba92 100644 --- a/docs/guide/status-codes.md +++ b/docs/guide/status-codes.md @@ -159,14 +159,14 @@ Wiverno supports all standard HTTP status codes: - 410 Gone - 411 Length Required - 412 Precondition Failed -- 413 Payload Too Large -- 414 URI Too Long +- 413 Content Too Large or Request Entity Too Larg +- 414 URI Too Long or Request-URI Too Long - 415 Unsupported Media Type -- 416 Range Not Satisfiable +- 416 Range Not Satisfiable or Requested Range Not Satisfiable - 417 Expectation Failed - 418 I'm a Teapot - 421 Misdirected Request -- 422 Unprocessable Entity +- 422 Unprocessable Entity or Unprocessable Content - 423 Locked - 424 Failed Dependency - 425 Too Early diff --git a/mkdocs.yml b/mkdocs.yml index 7f1da16..bddca2d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -168,6 +168,7 @@ nav: - Core Concepts: - guide/routing.md - guide/requests.md + - guide/running.md - HTTP Status Codes: guide/status-codes.md - guide/cli.md @@ -178,6 +179,8 @@ nav: - Requests: api/core/requests.md - Router: api/core/router.md - Server: api/core/server.md + - Development: + - DevServer: api/dev/dev-server.md - Templating: - Templator: api/templating/templator.md - Views: diff --git a/tests/unit/test_http_validator.py b/tests/unit/test_http_validator.py index 29a0a36..73a602a 100644 --- a/tests/unit/test_http_validator.py +++ b/tests/unit/test_http_validator.py @@ -176,11 +176,8 @@ def test_normalize_status_client_error_codes(self): (408, "408 Request Timeout"), (409, "409 Conflict"), (410, "410 Gone"), - (413, "413 Request Entity Too Large"), - (414, "414 Request-URI Too Long"), (415, "415 Unsupported Media Type"), (418, "418 I'm a Teapot"), - (422, "422 Unprocessable Entity"), (429, "429 Too Many Requests"), ] for code, expected in test_cases: diff --git a/tests/unit/test_server.py b/tests/unit/test_server.py index 027df7e..05c0c4c 100644 --- a/tests/unit/test_server.py +++ b/tests/unit/test_server.py @@ -27,6 +27,7 @@ def test_server_initialization_default_values(self): assert server.application is app assert server.host == "localhost" assert server.port == 8000 + assert server.request_queue_size == 5 def test_server_initialization_custom_host_port(self): """Test: Custom host and port can be set.""" @@ -70,7 +71,7 @@ def test_start_creates_wsgi_server(self, mock_make_server): # Configure mock mock_httpd = MagicMock() - mock_make_server.return_value.__enter__.return_value = mock_httpd + mock_make_server.return_value = mock_httpd # Interrupt serve_forever so test doesn't hang mock_httpd.serve_forever.side_effect = KeyboardInterrupt @@ -79,7 +80,8 @@ def test_start_creates_wsgi_server(self, mock_make_server): server.start() # Check that make_server was called with correct parameters - mock_make_server.assert_called_once_with("localhost", 8080, app) + from wsgiref.simple_server import WSGIServer + mock_make_server.assert_called_once_with("localhost", 8080, app, server_class=WSGIServer) @patch("wiverno.core.server.make_server") def test_start_calls_serve_forever(self, mock_make_server): @@ -89,7 +91,7 @@ def test_start_calls_serve_forever(self, mock_make_server): # Configure mock mock_httpd = MagicMock() - mock_make_server.return_value.__enter__.return_value = mock_httpd + mock_make_server.return_value = mock_httpd mock_httpd.serve_forever.side_effect = KeyboardInterrupt # Start server @@ -106,7 +108,7 @@ def test_start_handles_keyboard_interrupt(self, mock_make_server): # Configure mock to simulate Ctrl+C mock_httpd = MagicMock() - mock_make_server.return_value.__enter__.return_value = mock_httpd + mock_make_server.return_value = mock_httpd mock_httpd.serve_forever.side_effect = KeyboardInterrupt # Start and check that no exception was raised @@ -124,14 +126,14 @@ def test_start_logs_server_info(self, mock_logger, mock_make_server): # Configure mock mock_httpd = MagicMock() - mock_make_server.return_value.__enter__.return_value = mock_httpd + mock_make_server.return_value = mock_httpd mock_httpd.serve_forever.side_effect = KeyboardInterrupt # Start server server.start() # Check logging - mock_logger.info.assert_any_call("Serving on http://127.0.0.1:3000 ...") + mock_logger.info.assert_any_call("Wiverno server started on http://127.0.0.1:3000") @patch("wiverno.core.server.make_server") @patch("wiverno.core.server.logger") @@ -142,7 +144,7 @@ def test_start_logs_shutdown_message(self, mock_logger, mock_make_server): # Configure mock mock_httpd = MagicMock() - mock_make_server.return_value.__enter__.return_value = mock_httpd + mock_make_server.return_value = mock_httpd mock_httpd.serve_forever.side_effect = KeyboardInterrupt # Start server @@ -176,14 +178,15 @@ def simple_app(environ, start_response): # Configure mock mock_httpd = MagicMock() - mock_make_server.return_value.__enter__.return_value = mock_httpd + mock_make_server.return_value = mock_httpd mock_httpd.serve_forever.side_effect = KeyboardInterrupt # Start server server.start() # Check that application was passed to make_server - mock_make_server.assert_called_once_with("0.0.0.0", 8080, simple_app) + from wsgiref.simple_server import WSGIServer + mock_make_server.assert_called_once_with("0.0.0.0", 8080, simple_app, server_class=WSGIServer) @patch("wiverno.core.server.make_server") def test_server_with_multiple_instances(self, mock_make_server): diff --git a/wiverno/core/server.py b/wiverno/core/server.py index efe1db5..b9a8d17 100644 --- a/wiverno/core/server.py +++ b/wiverno/core/server.py @@ -1,23 +1,42 @@ import logging +import signal +import sys from collections.abc import Callable from typing import Any -from wsgiref.simple_server import make_server +from wsgiref.simple_server import WSGIServer, make_server logger = logging.getLogger(__name__) class RunServer: """ - Simple WSGI server to run a Wiverno application. + WSGI server to run a Wiverno application. + + This server is built on Python's wsgiref.simple_server and includes + improvements for better production readiness: + - Graceful shutdown handling + - Enhanced logging + - Error handling + - Configurable request queue size + + Note: + While this server is improved for stability, for high-traffic production + environments, consider using dedicated WSGI servers like Gunicorn, uWSGI, + or Waitress which offer better performance and concurrency. Attributes: application (Callable): A WSGI-compatible application. host (str): The hostname to bind the server to. port (int): The port number to bind the server to. + request_queue_size (int): Maximum number of queued connections. """ def __init__( - self, application: Callable[..., Any], host: str = "localhost", port: int = 8000 + self, + application: Callable[..., Any], + host: str = "localhost", + port: int = 8000, + request_queue_size: int = 5, ) -> None: """ Initializes the server with application, host, and port. @@ -26,20 +45,83 @@ def __init__( application (Callable): A WSGI-compatible application. host (str, optional): Hostname for the server. Defaults to 'localhost'. port (int, optional): Port for the server. Defaults to 8000. + request_queue_size (int, optional): Max queued connections. Defaults to 5. """ self.host: str = host self.port: int = port self.application: Callable[..., Any] = application + self.request_queue_size: int = request_queue_size + self._httpd: WSGIServer | None = None + self._setup_signal_handlers() + + def _setup_signal_handlers(self) -> None: + """Setup signal handlers for graceful shutdown.""" + signal.signal(signal.SIGINT, self._handle_shutdown) + signal.signal(signal.SIGTERM, self._handle_shutdown) + + def _handle_shutdown(self, signum: int, frame: Any) -> None: + """ + Handle shutdown signals gracefully. + + Args: + signum: Signal number. + frame: Current stack frame. + """ + signal_name = signal.Signals(signum).name + logger.info(f"Received {signal_name}, initiating graceful shutdown...") + self.stop() + sys.exit(0) def start(self) -> None: """ Starts the WSGI server and serves the application forever. - The server will continue running until interrupted by KeyboardInterrupt (Ctrl+C). + The server will continue running until interrupted by SIGINT (Ctrl+C) + or SIGTERM signal. Implements graceful shutdown to finish processing + current requests before stopping. + + Raises: + OSError: If the server cannot bind to the specified host:port. """ try: - with make_server(self.host, self.port, self.application) as httpd: - logger.info(f"Serving on http://{self.host}:{self.port} ...") - httpd.serve_forever() + self._httpd = make_server( + self.host, + self.port, + self.application, + server_class=WSGIServer, + ) + self._httpd.request_queue_size = self.request_queue_size + + logger.info( + f"Wiverno server started on http://{self.host}:{self.port}" + ) + logger.info( + f"Request queue size: {self.request_queue_size}" + ) + logger.info("Press Ctrl+C to stop the server") + + self._httpd.serve_forever() + + except OSError as e: + logger.error(f"Failed to start server: {e}") + raise except KeyboardInterrupt: logger.info("Server stopped by user.") + self.stop() + except Exception as e: + logger.exception(f"Unexpected error in server: {e}") + self.stop() + raise + + def stop(self) -> None: + """ + Stop the server gracefully. + + Shuts down the server, allowing current requests to complete. + """ + if self._httpd: + logger.info("Shutting down server...") + self._httpd.shutdown() + self._httpd.server_close() + logger.info("Server stopped successfully.") + self._httpd = None diff --git a/wiverno/dev/dev_server.py b/wiverno/dev/dev_server.py index 51d26f8..2f4b4db 100644 --- a/wiverno/dev/dev_server.py +++ b/wiverno/dev/dev_server.py @@ -239,9 +239,9 @@ def _restart_server(self) -> None: self._stop_server_process(show_restart_message=False) self._start_server_process() - def start(self) -> None: + def _start(self) -> None: """ - Start the development server with hot reload. + Start the development server with hot reload (internal method). This method starts the server and sets up file watching for automatic restarts when Python files are modified. @@ -289,26 +289,33 @@ def stop(self) -> None: self._stop_server_process(show_restart_message=False) console.print("[green]>> Server stopped successfully[/green]") + @staticmethod + def serve( + app_module: str = "run", + app_name: str = "app", + host: str = "localhost", + port: int = 8000, + ) -> None: + """ + Run the development server with hot reload. -def run_dev_server( - app_module: str = "run", - app_name: str = "app", - host: str = "localhost", - port: int = 8000, -) -> None: - """ - Convenience function to run the development server. + This is the main entry point for starting the development server. + Use this method to quickly start a development server with automatic + reloading when source files change. - Args: - app_module: Module path containing the WSGI application. - app_name: Name of the application variable in the module. - host: Server host address. - port: Server port. - """ - dev_server = DevServer( - app_module=app_module, - app_name=app_name, - host=host, - port=port, - ) - dev_server.start() + Example: + >>> DevServer.serve(app_module="myapp", port=8080) + + Args: + app_module: Module path containing the WSGI application. + app_name: Name of the application variable in the module. + host: Server host address. + port: Server port. + """ + dev_server = DevServer( + app_module=app_module, + app_name=app_name, + host=host, + port=port, + ) + dev_server._start()