commit 1f25033d2dc5c617a72021648badbd29ccae5448 Author: Илья Глазунов Date: Fri Dec 5 12:57:41 2025 +0300 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..515a80a --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# docs.pyserve.org + +This repository contains the source files for the documentation of the PyServe project, which can be found at [docs.pyserve.org](https://docs.pyserve.org). diff --git a/getting-started/index.html b/getting-started/index.html new file mode 100644 index 0000000..b011dc8 --- /dev/null +++ b/getting-started/index.html @@ -0,0 +1,44 @@ + + + + + + Getting Started - pyserve + + + +
+ + + + +
+

Getting Started

+ +

Get up and running with pyserve quickly.

+ + + + + + + + + + + + +
📄InstallationDownload and install pyserve
📄Quick StartGet up and running in 5 minutes
+
+ + +
+ + diff --git a/getting-started/installation.html b/getting-started/installation.html new file mode 100644 index 0000000..94de947 --- /dev/null +++ b/getting-started/installation.html @@ -0,0 +1,79 @@ + + + + + + Installation - pyserve + + + +
+ + + + +
+

Installation

+ +

Requirements

+
    +
  • Python 3.12 or higher
  • +
  • pip (Python package manager)
  • +
+ +

Install from Release (Recommended)

+

Download the latest wheel file from GitHub Releases and install it:

+ +
# Download the wheel file from releases
+# Example: pyserve-0.7.0-py3-none-any.whl
+
+pip install pyserve-0.7.0-py3-none-any.whl
+ +

After installation, the pyserve command will be available in your terminal:

+
pyserve --version
+ +

Install from Source

+

For development or if you want the latest changes:

+ +
# Clone the repository
+git clone https://github.com/ShiftyX1/PyServe.git
+cd PyServe
+
+# Install with Poetry (recommended for development)
+make init
+
+# Or build and install the package
+make build
+pip install dist/pyserve-*.whl
+ +

Verify Installation

+

Check that pyserve is installed correctly:

+
pyserve --version
+# Output: pyserve 0.7.0
+ +

Dependencies

+

pyserve automatically installs the following dependencies:

+
    +
  • starlette — ASGI framework
  • +
  • uvicorn — ASGI server
  • +
  • pyyaml — YAML configuration parsing
  • +
  • structlog — Structured logging
  • +
  • httpx — HTTP client for reverse proxy
  • +
+ +
+ Next: Continue to Quick Start to run your first server. +
+
+ + +
+ + diff --git a/getting-started/quickstart.html b/getting-started/quickstart.html new file mode 100644 index 0000000..f3c0326 --- /dev/null +++ b/getting-started/quickstart.html @@ -0,0 +1,121 @@ + + + + + + Quick Start - pyserve + + + +
+ + + + +
+

Quick Start

+ +

Get pyserve running in under 5 minutes.

+ +

1. Create Configuration File

+

Create a file named config.yaml in your project directory:

+ +
http:
+  static_dir: ./static
+  templates_dir: ./templates
+
+server:
+  host: 0.0.0.0
+  port: 8080
+
+logging:
+  level: INFO
+  console_output: true
+
+extensions:
+  - type: routing
+    config:
+      regex_locations:
+        "__default__":
+          root: "./static"
+          index_file: "index.html"
+ +

2. Create Static Directory

+

Create a static folder and add an index.html:

+ +
mkdir -p static
+echo '<h1>Hello from pyserve!</h1>' > static/index.html
+ +

3. Start the Server

+
pyserve
+ +

You should see output like:

+
Starting PyServe server on 0.0.0.0:8080
+ +

4. Open in Browser

+

Navigate to http://localhost:8080 — you should see your page!

+ +

Using CLI Options

+

Override configuration via command line:

+ +
# Use a different config file
+pyserve -c /path/to/config.yaml
+
+# Override host and port
+pyserve --host 127.0.0.1 --port 9000
+
+# Enable debug mode (verbose logging)
+pyserve --debug
+ +

Example: Serve Documentation

+

Serve a documentation directory with proper caching:

+ +
http:
+  static_dir: ./docs
+
+server:
+  host: 0.0.0.0
+  port: 8000
+
+extensions:
+  - type: routing
+    config:
+      regex_locations:
+        "=/":
+          root: "./docs"
+          index_file: "index.html"
+        
+        "~*\\.(css|js)$":
+          root: "./docs"
+          cache_control: "public, max-age=3600"
+        
+        "~*\\.html$":
+          root: "./docs"
+          cache_control: "no-cache"
+        
+        "__default__":
+          root: "./docs"
+          index_file: "index.html"
+ +
+ Next Steps: + +
+
+ + +
+ + diff --git a/guides/asgi-mount.html b/guides/asgi-mount.html new file mode 100644 index 0000000..2d0894d --- /dev/null +++ b/guides/asgi-mount.html @@ -0,0 +1,269 @@ + + + + + + ASGI Mounting - pyserve + + + +
+ + + + +
+

ASGI Application Mounting (In-Process)

+ +

The asgi extension mounts ASGI and WSGI applications directly in the pyserve process. + This is simpler and has lower latency, but all apps share the same process.

+ +
+ For production use cases requiring isolation, consider + Process Orchestration which runs each app + in a separate subprocess with health monitoring and auto-restart. +
+ +

Overview

+

The ASGI mounting system provides:

+
    +
  • Multi-framework support — Mount FastAPI, Flask, Django, Starlette, or custom ASGI apps
  • +
  • Path-based routing — Each app handles requests at its mounted path
  • +
  • WSGI compatibility — Automatic WSGI-to-ASGI conversion for Flask/Django
  • +
  • Factory pattern support — Create apps dynamically with arguments
  • +
  • Path stripping — Optionally strip mount path from requests
  • +
+ +

Configuration

+

ASGI applications are mounted via the asgi extension:

+ +
extensions:
+  - type: asgi
+    config:
+      mounts:
+        - path: "/api"
+          app_path: "myapp.api:app"
+          app_type: asgi
+          name: "api-app"
+          strip_path: true
+ +

Mount Configuration Options

+
+
path
+
URL path where the application will be mounted. Example: /api
+ +
app_path
+
Python import path to the application. Format: module.submodule:attribute
+ +
app_type
+
Application type: asgi or wsgi. Default: asgi
+ +
module_path
+
Optional path to add to sys.path for module resolution
+ +
factory
+
If true, app_path points to a factory function. Default: false
+ +
factory_args
+
Dictionary of arguments to pass to the factory function
+ +
name
+
Friendly name for logging. Default: uses app_path
+ +
strip_path
+
Remove mount path from request URL. Default: true
+
+ +

Mounting FastAPI

+

FastAPI applications are native ASGI:

+ +
# myapp/api.py
+from fastapi import FastAPI
+
+app = FastAPI()
+
+@app.get("/users")
+async def get_users():
+    return [{"id": 1, "name": "Alice"}]
+ +
extensions:
+  - type: asgi
+    config:
+      mounts:
+        - path: "/api"
+          app_path: "myapp.api:app"
+          app_type: asgi
+          name: "fastapi-app"
+ +

With this configuration:

+
    +
  • GET /api/users → handled by FastAPI as GET /users
  • +
  • FastAPI docs available at /api/docs
  • +
+ +

Mounting Flask

+

Flask applications are WSGI and will be automatically wrapped:

+ +
# myapp/flask_api.py
+from flask import Flask
+
+app = Flask(__name__)
+
+@app.route("/hello")
+def hello():
+    return {"message": "Hello from Flask!"}
+ +
extensions:
+  - type: asgi
+    config:
+      mounts:
+        - path: "/flask"
+          app_path: "myapp.flask_api:app"
+          app_type: wsgi
+          name: "flask-app"
+ +
+ Note: WSGI wrapping requires either a2wsgi or asgiref + to be installed. Install with: pip install a2wsgi +
+ +

Mounting Django

+

Django can be mounted using its ASGI application:

+ +
extensions:
+  - type: asgi
+    config:
+      mounts:
+        - path: "/django"
+          django_settings: "myproject.settings"
+          module_path: "/path/to/django/project"
+          name: "django-app"
+ +

Factory Pattern

+

Use factory functions to create apps with custom configuration:

+ +
# myapp/api.py
+from fastapi import FastAPI
+
+def create_app(debug: bool = False, prefix: str = "/v1") -> FastAPI:
+    app = FastAPI(debug=debug)
+    
+    @app.get(f"{prefix}/status")
+    async def status():
+        return {"debug": debug}
+    
+    return app
+ +
extensions:
+  - type: asgi
+    config:
+      mounts:
+        - path: "/api"
+          app_path: "myapp.api:create_app"
+          app_type: asgi
+          factory: true
+          factory_args:
+            debug: true
+            prefix: "/v2"
+ +

Path Stripping

+

By default, strip_path: true removes the mount prefix from requests:

+ + + + + + + + + + + + + + + + + +
Requeststrip_path: truestrip_path: false
GET /api/usersApp sees /usersApp sees /api/users
GET /api/App sees /App sees /api/
+ +

Multiple Mounts

+

Mount multiple applications at different paths:

+ +
extensions:
+  - type: asgi
+    config:
+      mounts:
+        # FastAPI for REST API
+        - path: "/api"
+          app_path: "apps.api:app"
+          app_type: asgi
+        
+        # Flask admin panel
+        - path: "/admin"
+          app_path: "apps.admin:app"
+          app_type: wsgi
+        
+        # Starlette websocket handler
+        - path: "/ws"
+          app_path: "apps.websocket:app"
+          app_type: asgi
+
+  # Standard routing for static files
+  - type: routing
+    config:
+      regex_locations:
+        "__default__":
+          root: "./static"
+ +

Mount Priority

+

Mounts are matched by path length (longest first). Given mounts at + /api and /api/v2:

+
    +
  • /api/v2/users → matches /api/v2 mount
  • +
  • /api/users → matches /api mount
  • +
+ +

Combining with Routing

+

ASGI mounts work alongside the routing extension. The asgi extension + should be listed before routing to handle mounted paths first:

+ +
extensions:
+  # ASGI apps handle /api/* and /admin/*
+  - type: asgi
+    config:
+      mounts:
+        - path: "/api"
+          app_path: "myapp:api"
+          app_type: asgi
+  
+  # Routing handles everything else
+  - type: routing
+    config:
+      regex_locations:
+        "=/health":
+          return: "200 OK"
+        "__default__":
+          spa_fallback: true
+          root: "./dist"
+ +

Python API

+

For programmatic mounting, see ASGI Mount API Reference.

+ +
+ Warning: Mounted applications share the same process. + Ensure your applications are compatible and don't have conflicting global state. +
+
+ + +
+ + diff --git a/guides/configuration.html b/guides/configuration.html new file mode 100644 index 0000000..0a6a841 --- /dev/null +++ b/guides/configuration.html @@ -0,0 +1,179 @@ + + + + + + Configuration - pyserve + + + +
+ + + + +
+

Configuration Reference

+ +

pyserve uses YAML configuration files. By default, it looks for + config.yaml in the current directory.

+ +

http

+

HTTP-related paths configuration.

+
+
static_dir
+
Path to static files directory. Default: ./static
+ +
templates_dir
+
Path to templates directory. Default: ./templates
+
+ +

server

+

Core server settings.

+
+
host
+
Bind address. Default: 0.0.0.0
+ +
port
+
Listen port. Default: 8080
+ +
backlog
+
Connection queue size. Default: 5
+ +
default_root
+
Enable default root handler. Default: false
+ +
proxy_timeout
+
Default timeout for proxy requests in seconds. Default: 30.0
+ +
redirect_instructions
+
Dictionary of redirect rules. Format: "/from": "/to"
+
+ +

ssl

+

SSL/TLS configuration for HTTPS.

+
+
enabled
+
Enable HTTPS. Default: false
+ +
cert_file
+
Path to SSL certificate file. Default: ./ssl/cert.pem
+ +
key_file
+
Path to SSL private key file. Default: ./ssl/key.pem
+
+ +

logging

+

Logging configuration with structlog support.

+
+
level
+
Log level: DEBUG, INFO, WARNING, + ERROR. Default: INFO
+ +
console_output
+
Output to console. Default: true
+ +
format
+
Format configuration object (see below)
+ +
console
+
Console handler configuration
+ +
files
+
List of file handlers for logging to files
+
+ +

logging.format

+
+
type
+
Format type: standard or json. Default: standard
+ +
use_colors
+
Enable colored output in console. Default: true
+ +
show_module
+
Show module name in logs. Default: true
+ +
timestamp_format
+
Timestamp format string. Default: %Y-%m-%d %H:%M:%S
+
+ +

logging.files[]

+
+
path
+
Path to log file
+ +
level
+
Log level for this file handler
+ +
format
+
Format configuration for this file
+ +
loggers
+
List of logger names to include (empty = all loggers)
+ +
max_bytes
+
Maximum file size before rotation. Default: 10485760 (10MB)
+ +
backup_count
+
Number of backup files to keep. Default: 5
+
+ +

extensions

+

List of extension modules to load. See Extensions Reference.

+ +

Complete Example

+
http:
+  static_dir: ./static
+  templates_dir: ./templates
+
+server:
+  host: 0.0.0.0
+  port: 8080
+  backlog: 5
+  default_root: false
+  proxy_timeout: 30.0
+
+ssl:
+  enabled: false
+  cert_file: ./ssl/cert.pem
+  key_file: ./ssl/key.pem
+
+logging:
+  level: INFO
+  console_output: true
+  format:
+    type: standard
+    use_colors: true
+    timestamp_format: "%Y-%m-%d %H:%M:%S"
+  files:
+    - path: ./logs/pyserve.log
+      level: DEBUG
+      max_bytes: 10485760
+      backup_count: 5
+
+extensions:
+  - type: routing
+    config:
+      regex_locations:
+        "__default__":
+          root: "./static"
+          index_file: "index.html"
+ +
+ Warning: When running in production, always use SSL + and restrict the bind address appropriately. +
+
+ + +
+ + diff --git a/guides/index.html b/guides/index.html new file mode 100644 index 0000000..725b04e --- /dev/null +++ b/guides/index.html @@ -0,0 +1,59 @@ + + + + + + Guides - pyserve + + + +
+ + + + +
+

Guides

+ +

In-depth guides for configuring and using pyserve.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
📄Process OrchestrationRun apps in isolated processes with health monitoring
📄ConfigurationComplete configuration reference
📄RoutingURL routing and regex patterns
📄Reverse ProxyProxying requests to backend services
📄ASGI MountingMount Python web frameworks in-process
+
+ + +
+ + diff --git a/guides/process-orchestration.html b/guides/process-orchestration.html new file mode 100644 index 0000000..b6f9a14 --- /dev/null +++ b/guides/process-orchestration.html @@ -0,0 +1,354 @@ + + + + + + Process Orchestration - pyserve + + + +
+ + + + +
+

Process Orchestration

+ +

Process Orchestration is pyserve's flagship feature for running multiple Python web + applications with full process isolation. Each application runs in its own subprocess + with independent lifecycle, health monitoring, and automatic restart on failure.

+ +

Overview

+

Unlike ASGI Mounting which runs apps in-process, + Process Orchestration provides:

+
    +
  • Process Isolation — Each app runs in a separate Python process
  • +
  • Health Monitoring — Automatic health checks with configurable intervals
  • +
  • Auto-restart — Failed processes restart with exponential backoff
  • +
  • Multi-worker Support — Configure multiple uvicorn workers per app
  • +
  • Dynamic Port Allocation — Automatic port assignment (9000-9999)
  • +
  • WSGI Support — Flask/Django apps via automatic wrapping
  • +
  • Request Tracing — X-Request-ID propagation through proxied requests
  • +
+ +

Architecture

+
+                    PyServe Gateway (:8000)
+                           │
+          ┌────────────────┼────────────────┐
+          ▼                ▼                ▼
+      FastAPI          Flask           Starlette
+       :9001           :9002             :9003
+      /api/*          /admin/*           /ws/*
+        
+

PyServe acts as a gateway, routing requests to the appropriate subprocess based on URL path.

+ +

Basic Configuration

+
server:
+  host: 0.0.0.0
+  port: 8000
+
+extensions:
+  - type: process_orchestration
+    config:
+      apps:
+        - name: api
+          path: /api
+          app_path: myapp.api:app
+          
+        - name: admin
+          path: /admin
+          app_path: myapp.admin:app
+ +

App Configuration Options

+
+
name
+
Unique identifier for the application (required)
+ +
path
+
URL path prefix for routing requests (required)
+ +
app_path
+
Python import path. Format: module:attribute (required)
+ +
app_type
+
Application type: asgi or wsgi. Default: asgi
+ +
workers
+
Number of uvicorn workers. Default: 1
+ +
port
+
Fixed port number. Default: auto-allocated from port_range
+ +
factory
+
If true, app_path points to a factory function. Default: false
+ +
env
+
Environment variables to pass to the subprocess
+ +
module_path
+
Path to add to sys.path for module resolution
+
+ +

Health Check Options

+
+
health_check_enabled
+
Enable health monitoring. Default: true
+ +
health_check_path
+
Endpoint to check for health. Default: /health
+ +
health_check_interval
+
Interval between health checks in seconds. Default: 10.0
+ +
health_check_timeout
+
Timeout for health check requests. Default: 5.0
+ +
health_check_retries
+
Failed checks before restart. Default: 3
+
+ +

Restart Options

+
+
max_restart_count
+
Maximum restart attempts before giving up. Default: 5
+ +
restart_delay
+
Initial delay between restarts in seconds. Default: 1.0
+ +
shutdown_timeout
+
Timeout for graceful shutdown. Default: 30.0
+
+ +

Global Configuration

+
extensions:
+  - type: process_orchestration
+    config:
+      port_range: [9000, 9999]
+      health_check_enabled: true
+      proxy_timeout: 60.0
+      logging:
+        httpx_level: warning
+        proxy_logs: true
+        health_check_logs: false
+      apps:
+        # ...
+ +
+
port_range
+
Range for dynamic port allocation. Default: [9000, 9999]
+ +
proxy_timeout
+
Timeout for proxied requests in seconds. Default: 60.0
+ +
logging.httpx_level
+
Log level for HTTP client (debug/info/warning/error). Default: warning
+ +
logging.proxy_logs
+
Log proxied requests with latency. Default: true
+ +
logging.health_check_logs
+
Log health check results. Default: false
+
+ +

FastAPI Example

+
# myapp/api.py
+from fastapi import FastAPI
+
+app = FastAPI()
+
+@app.get("/health")
+async def health():
+    return {"status": "ok"}
+
+@app.get("/users")
+async def get_users():
+    return [{"id": 1, "name": "Alice"}]
+ +
extensions:
+  - type: process_orchestration
+    config:
+      apps:
+        - name: api
+          path: /api
+          app_path: myapp.api:app
+          workers: 4
+          health_check_path: /health
+ +

Requests to /api/users are proxied to the FastAPI process as /users.

+ +

Flask Example (WSGI)

+
# myapp/admin.py
+from flask import Flask
+
+app = Flask(__name__)
+
+@app.route("/health")
+def health():
+    return {"status": "ok"}
+
+@app.route("/dashboard")
+def dashboard():
+    return {"page": "dashboard"}
+ +
extensions:
+  - type: process_orchestration
+    config:
+      apps:
+        - name: admin
+          path: /admin
+          app_path: myapp.admin:app
+          app_type: wsgi
+          workers: 2
+ +
+ Note: WSGI support requires a2wsgi package: + pip install a2wsgi +
+ +

Factory Pattern

+
# myapp/api.py
+from fastapi import FastAPI
+
+def create_app(debug: bool = False) -> FastAPI:
+    app = FastAPI(debug=debug)
+    
+    @app.get("/health")
+    async def health():
+        return {"status": "ok", "debug": debug}
+    
+    return app
+ +
apps:
+  - name: api
+    path: /api
+    app_path: myapp.api:create_app
+    factory: true
+ +

Environment Variables

+

Pass environment variables to subprocesses:

+
apps:
+  - name: api
+    path: /api
+    app_path: myapp.api:app
+    env:
+      DATABASE_URL: "postgresql://localhost/mydb"
+      REDIS_URL: "redis://localhost:6379"
+      DEBUG: "false"
+ +

Multiple Applications

+
extensions:
+  - type: process_orchestration
+    config:
+      port_range: [9000, 9999]
+      apps:
+        # FastAPI REST API
+        - name: api
+          path: /api
+          app_path: apps.api:app
+          workers: 4
+        
+        # Flask Admin Panel
+        - name: admin
+          path: /admin
+          app_path: apps.admin:app
+          app_type: wsgi
+          workers: 2
+        
+        # Starlette WebSocket Handler
+        - name: websocket
+          path: /ws
+          app_path: apps.websocket:app
+          workers: 1
+ +

Request Tracing

+

PyServe automatically generates and propagates X-Request-ID headers:

+
    +
  • If a request has X-Request-ID, it's preserved
  • +
  • Otherwise, a UUID is generated
  • +
  • The ID is passed to subprocesses and included in response headers
  • +
  • All logs include the request ID for tracing
  • +
+ +

Process Orchestration vs ASGI Mount

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureProcess OrchestrationASGI Mount
IsolationFull process isolationShared process
MemorySeparate per appShared
Crash ImpactOnly that app restartsAll apps affected
Health ChecksYes, with auto-restartNo
Multi-workerYes, per appNo
LatencyHTTP proxy overheadIn-process (faster)
Use CaseProduction, isolation neededDevelopment, simple setups
+ +
+ When to use Process Orchestration: +
    +
  • Running multiple apps that shouldn't affect each other
  • +
  • Need automatic restart on failure
  • +
  • Different resource requirements per app
  • +
  • Production deployments
  • +
+ When to use ASGI Mount: +
    +
  • Development and testing
  • +
  • Simple setups with trusted apps
  • +
  • Minimal latency requirements
  • +
+
+ +
+ See Also: + +
+
+ + +
+ + diff --git a/guides/reverse-proxy.html b/guides/reverse-proxy.html new file mode 100644 index 0000000..255d14f --- /dev/null +++ b/guides/reverse-proxy.html @@ -0,0 +1,156 @@ + + + + + + Reverse Proxy - pyserve + + + +
+ + + + +
+

Reverse Proxy

+ +

pyserve can act as a reverse proxy, forwarding requests to backend services.

+ +

Basic Proxy Configuration

+

Use the proxy_pass directive in routing:

+ +
extensions:
+  - type: routing
+    config:
+      regex_locations:
+        "~^/api/":
+          proxy_pass: "http://localhost:9001"
+ +

All requests to /api/* will be forwarded to http://localhost:9001/api/*.

+ +

Proxy Headers

+

pyserve automatically adds standard proxy headers:

+ + + + + + + + + + + + + + + + + + +
X-Forwarded-ForClient's IP address
X-Forwarded-ProtoOriginal protocol (http/https)
X-Forwarded-HostOriginal Host header
X-Real-IPClient's real IP address
+ +

Custom Headers

+

Add custom headers to proxied requests:

+ +
"~^/api/":
+  proxy_pass: "http://localhost:9001"
+  headers:
+    - "X-Custom-Header: my-value"
+    - "Authorization: Bearer token123"
+ +

Dynamic Headers with Captures

+

Use regex capture groups to build dynamic headers:

+ +
"~^/api/v(?P<version>\\d+)/(?P<service>\\w+)":
+  proxy_pass: "http://localhost:9001"
+  headers:
+    - "X-API-Version: {version}"
+    - "X-Service: {service}"
+    - "X-Client-IP: $remote_addr"
+ +

Special variables:

+
    +
  • {capture_name} — Named capture group from regex
  • +
  • $remote_addr — Client's IP address
  • +
+ +

Proxy Timeout

+

Configure timeout for proxy requests:

+ +
# Global default timeout
+server:
+  proxy_timeout: 30.0
+
+# Per-route timeout
+extensions:
+  - type: routing
+    config:
+      regex_locations:
+        "~^/api/slow":
+          proxy_pass: "http://localhost:9001"
+          timeout: 120  # 2 minutes for slow endpoints
+ +

URL Rewriting

+

The proxy preserves the original request path by default:

+ +
# Request: GET /api/users/123
+# Proxied: GET http://backend:9001/api/users/123
+"~^/api/":
+  proxy_pass: "http://backend:9001"
+ +

To proxy to a specific path:

+ +
# Request: GET /api/users/123
+# Proxied: GET http://backend:9001/v2/users/123 (path preserved)
+"~^/api/":
+  proxy_pass: "http://backend:9001/v2"
+ +

Load Balancing Example

+

Route different services to different backends:

+ +
extensions:
+  - type: routing
+    config:
+      regex_locations:
+        "~^/api/users":
+          proxy_pass: "http://user-service:8001"
+        
+        "~^/api/orders":
+          proxy_pass: "http://order-service:8002"
+        
+        "~^/api/products":
+          proxy_pass: "http://product-service:8003"
+ +

Error Handling

+

pyserve returns appropriate error codes for proxy failures:

+ + + + + + + + + + +
502 Bad GatewayBackend connection failed or returned invalid response
504 Gateway TimeoutBackend did not respond within timeout
+ +
+ Note: pyserve uses httpx for async HTTP requests + to backend services, supporting HTTP/1.1 and HTTP/2. +
+
+ + +
+ + diff --git a/guides/routing.html b/guides/routing.html new file mode 100644 index 0000000..982c808 --- /dev/null +++ b/guides/routing.html @@ -0,0 +1,169 @@ + + + + + + Routing - pyserve + + + +
+ + + + +
+

Routing

+ +

pyserve supports nginx-style routing patterns including exact matches, + regex locations, and SPA fallback.

+ +

Location Types

+ + + + + + + + + + + + + + + + + + + + + + +
=Exact match=/health matches only /health
~Case-sensitive regex~^/api/v\d+/ matches /api/v1/
~*Case-insensitive regex~*\.(js|css)$ matches .JS and .css
__default__Default fallbackMatches when no other route matches
+ +

Match Priority

+

Routes are processed in the following order:

+
    +
  1. Exact matches (=) — checked first
  2. +
  3. Regex patterns (~ and ~*) — in definition order
  4. +
  5. Default fallback (__default__) — last resort
  6. +
+ +

Routing Configuration

+

Routing is configured via the routing extension:

+ +
extensions:
+  - type: routing
+    config:
+      regex_locations:
+        # Exact match for health check
+        "=/health":
+          return: "200 OK"
+          content_type: "text/plain"
+        
+        # Static files with caching
+        "~*\\.(js|css|png|jpg|gif|ico)$":
+          root: "./static"
+          cache_control: "public, max-age=31536000"
+        
+        # HTML files without caching
+        "~*\\.html$":
+          root: "./static"
+          cache_control: "no-cache"
+        
+        # Default fallback
+        "__default__":
+          root: "./static"
+          index_file: "index.html"
+ +

Location Directives

+ +
+
root
+
Base directory for serving files.
+ +
index_file
+
Index file name for directory requests. Default: index.html
+ +
proxy_pass
+
Upstream server URL for reverse proxy. See Reverse Proxy.
+ +
return
+
Return a fixed response. Format: "status message" or "status"
+ +
content_type
+
Response content type for return directive.
+ +
cache_control
+
Cache-Control header value.
+ +
headers
+
List of additional headers to add. Format: "Header-Name: value"
+ +
spa_fallback
+
Enable SPA mode — serve index file for all routes.
+ +
exclude_patterns
+
URL patterns to exclude from SPA fallback.
+
+ +

Named Capture Groups

+

Regex locations support named capture groups that can be used in headers and proxy URLs:

+ +
"~^/api/v(?P<version>\\d+)/(?P<resource>\\w+)":
+  proxy_pass: "http://backend:9001"
+  headers:
+    - "X-API-Version: {version}"
+    - "X-Resource: {resource}"
+ +

Request to /api/v2/users will have headers:

+
    +
  • X-API-Version: 2
  • +
  • X-Resource: users
  • +
+ +

SPA Configuration

+

For Single Page Applications, use spa_fallback with exclude_patterns:

+ +
"__default__":
+  spa_fallback: true
+  root: "./dist"
+  index_file: "index.html"
+  exclude_patterns:
+    - "/api/"
+    - "/assets/"
+    - "/static/"
+ +

This will:

+
    +
  • Serve index.html for routes like /about, /users/123
  • +
  • Return 404 for /api/*, /assets/*, /static/* if file not found
  • +
+ +

Static File Serving

+

Basic static file configuration:

+ +
"~*\\.(css|js|png|jpg|gif|svg|woff2?)$":
+  root: "./static"
+  cache_control: "public, max-age=86400"
+  headers:
+    - "X-Content-Type-Options: nosniff"
+ +
+ Note: pyserve automatically detects MIME types based on file extensions. +
+
+ + +
+ + diff --git a/index.html b/index.html new file mode 100644 index 0000000..660c934 --- /dev/null +++ b/index.html @@ -0,0 +1,118 @@ + + + + + + pyserve - Documentation + + + +
+ + +
+

About

+

+ pyserve is a Python application orchestrator and HTTP server. + Built on top of Starlette and Uvicorn, + it manages multiple ASGI/WSGI applications through a single entry point + with process isolation, health monitoring, and auto-restart. +

+ +

Key Features

+
    +
  • Process Orchestration — Run multiple apps in isolated subprocesses with health checks
  • +
  • Auto-restart — Failed processes restart automatically with exponential backoff
  • +
  • Multi-worker Support — Configure workers per application
  • +
  • nginx-style Routing — Regex patterns with exact, prefix, and case-insensitive matching
  • +
  • Reverse Proxy — Forward requests to backend services with header manipulation
  • +
  • Static File Serving — Efficient serving with correct MIME types
  • +
  • SPA Support — Single Page Application fallback routing
  • +
  • SSL/HTTPS — Secure connections with certificate configuration
  • +
  • ASGI/WSGI Support — FastAPI, Flask, Django, Starlette and more
  • +
  • Request Tracing — X-Request-ID propagation through proxied requests
  • +
+ +

Documentation

+ +

Getting Started

+ + + + + + + + + + + +
📄InstallationDownload and install pyserve
📄Quick StartGet up and running in 5 minutes
+ +

Guides

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
📄Process OrchestrationRun apps in isolated processes with health monitoring
📄ConfigurationComplete configuration reference
📄RoutingURL routing and regex patterns
📄Reverse ProxyProxying requests to backend services
📄ASGI MountingMount Python web frameworks in-process
+ +

Reference

+ + + + + + + + + + + +
📄CLI ReferenceCommand-line interface options
📄ExtensionsBuilt-in extension modules
+ +

Resources

+ + +

Version

+

Current version: 0.9.10

+ +

Requirements

+
    +
  • Python 3.12 or higher
  • +
+
+ + +
+ + diff --git a/reference/asgi-mount.html b/reference/asgi-mount.html new file mode 100644 index 0000000..1a8a97e --- /dev/null +++ b/reference/asgi-mount.html @@ -0,0 +1,251 @@ + + + + + + ASGI Mount API - pyserve + + + +
+ + + + +
+

ASGI Mount API Reference

+ +

The pyserve.asgi_mount module provides a Python API for mounting + ASGI and WSGI applications programmatically.

+ +

Classes

+ +

ASGIAppLoader

+

Loads and manages ASGI/WSGI applications from Python import paths.

+ +
from pyserve import ASGIAppLoader
+
+loader = ASGIAppLoader()
+
+# Load an ASGI app
+app = loader.load_app(
+    app_path="mymodule:app",
+    app_type="asgi",
+    module_path="/path/to/project",
+    factory=False,
+    factory_args=None
+)
+ +
Methods
+
+
load_app(app_path, app_type="asgi", module_path=None, factory=False, factory_args=None)
+
+ Load an application from an import path. +
    +
  • app_path: Import path in format module:attribute
  • +
  • app_type: "asgi" or "wsgi"
  • +
  • module_path: Optional path to add to sys.path
  • +
  • factory: If True, call the attribute as a factory function
  • +
  • factory_args: Dict of arguments for factory function
  • +
+ Returns the loaded ASGI application or None on error. +
+ +
get_app(app_path)
+
Get a previously loaded application by its path.
+ +
reload_app(app_path, **kwargs)
+
Reload an application, useful for development hot-reloading.
+
+ +

MountedApp

+

Represents an application mounted at a specific path.

+ +
from pyserve import MountedApp
+
+mount = MountedApp(
+    path="/api",
+    app=my_asgi_app,
+    name="my-api",
+    strip_path=True
+)
+ +
Attributes
+
+
path: str
+
The mount path (without trailing slash).
+ +
app: ASGIApp
+
The ASGI application.
+ +
name: str
+
Friendly name for logging.
+ +
strip_path: bool
+
Whether to strip the mount path from requests.
+
+ +
Methods
+
+
matches(request_path) → bool
+
Check if a request path matches this mount.
+ +
get_modified_path(original_path) → str
+
Get the modified path after stripping mount prefix.
+
+ +

ASGIMountManager

+

Manages multiple mounted applications and routes requests.

+ +
from pyserve import ASGIMountManager
+
+manager = ASGIMountManager()
+
+# Mount using app instance
+manager.mount(path="/api", app=my_app)
+
+# Mount using import path
+manager.mount(
+    path="/flask",
+    app_path="myapp:flask_app",
+    app_type="wsgi"
+)
+ +
Methods
+
+
mount(path, app=None, app_path=None, app_type="asgi", module_path=None, factory=False, factory_args=None, name="", strip_path=True) → bool
+
+ Mount an application at a path. Either app or app_path must be provided. + Returns True on success. +
+ +
unmount(path) → bool
+
Remove a mounted application. Returns True if found and removed.
+ +
get_mount(request_path) → Optional[MountedApp]
+
Get the mount that matches a request path.
+ +
handle_request(scope, receive, send) → bool
+
Handle an ASGI request. Returns True if handled by a mounted app.
+ +
list_mounts() → List[Dict]
+
Get a list of all mounts with their configuration.
+
+ +
Properties
+
+
mounts: List[MountedApp]
+
Copy of the current mounts list (sorted by path length, longest first).
+
+ +

Helper Functions

+

Convenience functions for loading specific framework applications:

+ +

create_fastapi_app()

+
from pyserve import create_fastapi_app
+
+app = create_fastapi_app(
+    app_path="myapp.api:app",
+    module_path=None,
+    factory=False,
+    factory_args=None
+)
+ +

create_flask_app()

+
from pyserve import create_flask_app
+
+app = create_flask_app(
+    app_path="myapp.web:app",
+    module_path=None,
+    factory=False,
+    factory_args=None
+)
+

Automatically wraps the WSGI app for ASGI compatibility.

+ +

create_django_app()

+
from pyserve import create_django_app
+
+app = create_django_app(
+    settings_module="myproject.settings",
+    module_path="/path/to/project"
+)
+

Sets DJANGO_SETTINGS_MODULE and returns Django's ASGI application.

+ +

create_starlette_app()

+
from pyserve import create_starlette_app
+
+app = create_starlette_app(
+    app_path="myapp:starlette_app",
+    module_path=None,
+    factory=False,
+    factory_args=None
+)
+ +

Usage Example

+

Complete example mounting multiple applications:

+ +
from pyserve import (
+    PyServeServer,
+    ASGIMountManager,
+    create_fastapi_app,
+    create_flask_app
+)
+
+# Create mount manager
+mounts = ASGIMountManager()
+
+# Mount FastAPI
+api_app = create_fastapi_app("myapp.api:app")
+if api_app:
+    mounts.mount("/api", app=api_app, name="api")
+
+# Mount Flask
+admin_app = create_flask_app("myapp.admin:app")
+if admin_app:
+    mounts.mount("/admin", app=admin_app, name="admin")
+
+# List mounts
+for mount in mounts.list_mounts():
+    print(f"Mounted {mount['name']} at {mount['path']}")
+ +

Error Handling

+

All loader functions return None on failure and log errors. + Check the return value before using:

+ +
app = create_fastapi_app("nonexistent:app")
+if app is None:
+    # Handle error - check logs for details
+    print("Failed to load application")
+ +

WSGI Compatibility

+

For WSGI applications, pyserve uses adapters in this priority:

+
    +
  1. a2wsgi.WSGIMiddleware (recommended)
  2. +
  3. asgiref.wsgi.WsgiToAsgi (fallback)
  4. +
+ +

Install an adapter:

+
pip install a2wsgi  # recommended
+# or
+pip install asgiref
+ +
+ See Also: + +
+
+ + +
+ + diff --git a/reference/cli.html b/reference/cli.html new file mode 100644 index 0000000..89111ef --- /dev/null +++ b/reference/cli.html @@ -0,0 +1,141 @@ + + + + + + CLI Reference - pyserve + + + +
+ + + + +
+

CLI Reference

+ +

pyserve provides a command-line interface for server management.

+ +

Synopsis

+
pyserve [OPTIONS]
+ +

Options

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
-c, --config FILEPath to configuration fileDefault: config.yaml
--host HOSTBind addressOverrides config value
--port PORTListen portOverrides config value
--debugEnable debug modeSets log level to DEBUG
--versionShow version and exit
--helpShow help message and exit
+ +

Examples

+ +

Start with default configuration:

+
pyserve
+ +

Start with custom config file:

+
pyserve -c /path/to/config.yaml
+ +

Override host and port:

+
pyserve --host 127.0.0.1 --port 9000
+ +

Enable debug mode:

+
pyserve --debug
+ +

Show version:

+
pyserve --version
+# Output: pyserve 0.7.0
+ +

Configuration Priority

+

Settings are applied in the following order (later overrides earlier):

+
    +
  1. Default values
  2. +
  3. Configuration file (config.yaml)
  4. +
  5. Command-line options
  6. +
+ +

Default Configuration

+

If no configuration file is found, pyserve uses default settings:

+
    +
  • Host: 0.0.0.0
  • +
  • Port: 8080
  • +
  • Log level: INFO
  • +
+ +

Exit Codes

+ + + + + + + + + +
0Success / Clean shutdown
1Configuration error or startup failure
+ +

Signals

+

pyserve handles the following signals:

+ + + + + + + + + +
SIGINT (Ctrl+C)Graceful shutdown
SIGTERMGraceful shutdown
+ +

Development Commands (Makefile)

+

When working with the source repository, use make commands:

+ +
make run           # Start in development mode
+make run-prod      # Start in production mode
+make test          # Run tests
+make test-cov      # Tests with coverage
+make lint          # Check code with linters
+make format        # Format code
+make build         # Build wheel package
+make clean         # Clean temporary files
+make help          # Show all commands
+
+ + +
+ + diff --git a/reference/extensions.html b/reference/extensions.html new file mode 100644 index 0000000..1c1cd04 --- /dev/null +++ b/reference/extensions.html @@ -0,0 +1,325 @@ + + + + + + Extensions - pyserve + + + +
+ + + + +
+

Extensions

+ +

pyserve uses a modular extension system for adding functionality. Extensions + are loaded in order and can process requests and modify responses.

+ +

Built-in Extensions

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
process_orchestrationRun ASGI/WSGI apps in isolated processes with health monitoring
routingnginx-style URL routing with regex patterns
asgiMount ASGI/WSGI applications in-process
securitySecurity headers and IP filtering
cachingResponse caching (in development)
monitoringRequest metrics and statistics
+ +

Extension Configuration

+

Extensions are configured in the extensions section:

+ +
extensions:
+  - type: routing
+    config:
+      # extension-specific configuration
+  
+  - type: security
+    config:
+      # ...
+ +

Routing Extension

+

The primary extension for URL routing. See Routing Guide for full documentation.

+ +
- type: routing
+  config:
+    regex_locations:
+      "=/health":
+        return: "200 OK"
+      "~^/api/":
+        proxy_pass: "http://backend:9001"
+      "__default__":
+        root: "./static"
+ +

Security Extension

+

Adds security headers and IP-based access control.

+ +

Configuration Options

+
+
security_headers
+
Dictionary of security headers to add to all responses
+ +
allowed_ips
+
List of allowed IP addresses (whitelist mode)
+ +
blocked_ips
+
List of blocked IP addresses (blacklist mode)
+
+ +
- type: security
+  config:
+    security_headers:
+      X-Frame-Options: DENY
+      X-Content-Type-Options: nosniff
+      X-XSS-Protection: "1; mode=block"
+      Strict-Transport-Security: "max-age=31536000"
+    blocked_ips:
+      - "192.168.1.100"
+      - "10.0.0.50"
+ +

Default security headers if not specified:

+
    +
  • X-Content-Type-Options: nosniff
  • +
  • X-Frame-Options: DENY
  • +
  • X-XSS-Protection: 1; mode=block
  • +
+ +

Caching Extension

+

Response caching for improved performance. (Currently in development)

+ +

Configuration Options

+
+
cache_patterns
+
URL patterns to cache
+ +
cache_ttl
+
Default cache TTL in seconds. Default: 3600
+
+ +
- type: caching
+  config:
+    cache_ttl: 3600
+    cache_patterns:
+      - "/api/public/*"
+ +

Monitoring Extension

+

Collects request metrics and provides statistics.

+ +

Configuration Options

+
+
enable_metrics
+
Enable metrics collection. Default: true
+
+ +
- type: monitoring
+  config:
+    enable_metrics: true
+ +

Collected metrics (available at /metrics):

+
    +
  • request_count — Total number of requests
  • +
  • error_count — Number of requests with 4xx/5xx status
  • +
  • error_rate — Error rate (errors / total)
  • +
  • avg_response_time — Average response time in seconds
  • +
+ +

Built-in Endpoints

+

pyserve provides built-in endpoints regardless of extensions:

+ + + + + + + + + + +
/healthHealth check endpoint, returns 200 OK
/metricsJSON metrics from all extensions
+ +

Extension Processing Order

+

Extensions process requests in the order they are defined:

+
    +
  1. Request comes in
  2. +
  3. Each extension's process_request is called in order
  4. +
  5. First extension to return a response wins
  6. +
  7. Response passes through each extension's process_response
  8. +
  9. Response is sent to client
  10. +
+ +
+ Note: Place the routing extension last if you want + other extensions (like security) to process requests first. +
+ +

ASGI Extension

+

Mount external ASGI/WSGI applications (FastAPI, Flask, Django, etc.) at specified paths.

+ +

Configuration Options

+
+
mounts
+
List of mount configurations (see below)
+
+ +

Mount Configuration

+
+
path
+
URL path where the app will be mounted. Example: /api
+ +
app_path
+
Python import path. Format: module:attribute
+ +
app_type
+
Application type: asgi or wsgi. Default: asgi
+ +
module_path
+
Optional path to add to sys.path
+ +
factory
+
If true, call as factory function. Default: false
+ +
factory_args
+
Arguments to pass to factory function
+ +
name
+
Friendly name for logging
+ +
strip_path
+
Remove mount path from request URL. Default: true
+ +
django_settings
+
Django settings module (for Django apps only)
+
+ +
- type: asgi
+  config:
+    mounts:
+      # FastAPI application
+      - path: "/api"
+        app_path: "myapp.api:app"
+        app_type: asgi
+        name: "api"
+      
+      # Flask application (WSGI)
+      - path: "/admin"
+        app_path: "myapp.admin:app"
+        app_type: wsgi
+        name: "admin"
+      
+      # Factory pattern with arguments
+      - path: "/api/v2"
+        app_path: "myapp.api:create_app"
+        factory: true
+        factory_args:
+          debug: true
+          version: "2.0"
+ +

Supported frameworks:

+
    +
  • FastAPI — Native ASGI (app_type: asgi)
  • +
  • Starlette — Native ASGI (app_type: asgi)
  • +
  • Flask — WSGI, auto-wrapped (app_type: wsgi)
  • +
  • Django — Use django_settings parameter
  • +
  • Custom ASGI — Any ASGI-compatible application
  • +
+ +
+ Note: For WSGI applications, install a2wsgi or asgiref: + pip install a2wsgi +
+ +

See ASGI Mounting Guide for detailed documentation.

+ +

Process Orchestration Extension

+

The flagship extension for running apps in isolated subprocesses. Recommended for production.

+ +

Key Features

+
    +
  • Process isolation — each app runs in its own subprocess
  • +
  • Health monitoring with automatic restart
  • +
  • Multi-worker support per application
  • +
  • Dynamic port allocation (9000-9999)
  • +
  • Request tracing with X-Request-ID
  • +
+ +
- type: process_orchestration
+  config:
+    port_range: [9000, 9999]
+    health_check_enabled: true
+    proxy_timeout: 60.0
+    logging:
+      httpx_level: warning
+      proxy_logs: true
+      health_check_logs: false
+    apps:
+      - name: api
+        path: /api
+        app_path: myapp.api:app
+        workers: 4
+        health_check_path: /health
+      
+      - name: admin
+        path: /admin
+        app_path: myapp.admin:app
+        app_type: wsgi
+        workers: 2
+ +

App Configuration

+
+
name
+
Unique identifier (required)
+ +
path
+
URL path prefix (required)
+ +
app_path
+
Python import path (required)
+ +
app_type
+
asgi or wsgi. Default: asgi
+ +
workers
+
Number of uvicorn workers. Default: 1
+ +
health_check_path
+
Health endpoint. Default: /health
+ +
max_restart_count
+
Max restart attempts. Default: 5
+
+ +

See Process Orchestration Guide for full documentation.

+
+ + +
+ + diff --git a/reference/index.html b/reference/index.html new file mode 100644 index 0000000..74209ee --- /dev/null +++ b/reference/index.html @@ -0,0 +1,49 @@ + + + + + + Reference - pyserve + + + +
+ + + + +
+

Reference

+ +

API and CLI reference documentation.

+ + + + + + + + + + + + + + + + + +
📄CLI ReferenceCommand-line interface options
📄ExtensionsBuilt-in extension modules
📄ASGI Mount APIPython API for mounting ASGI/WSGI applications
+
+ + +
+ + diff --git a/style.css b/style.css new file mode 100644 index 0000000..7e8fdd9 --- /dev/null +++ b/style.css @@ -0,0 +1,237 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Arial, sans-serif; + font-size: 13px; + line-height: 1.5; + color: #c9c9c9; + background: #1a1a1a; +} + +#container { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} + +#header { + border-bottom: 2px solid #2e8b57; + padding-bottom: 10px; + margin-bottom: 20px; +} + +#header h1 { + font-size: 24px; + font-weight: normal; + color: #3cb371; + margin: 0; +} + +#header .tagline { + font-size: 12px; + color: #888; + font-style: italic; +} + +#content { + margin-bottom: 30px; +} + +h2 { + font-size: 16px; + font-weight: bold; + color: #e0e0e0; + margin: 20px 0 10px 0; + padding-bottom: 5px; + border-bottom: 1px solid #333; +} + +h3 { + font-size: 14px; + font-weight: bold; + color: #d0d0d0; + margin: 15px 0 8px 0; +} + +p { + margin: 10px 0; +} + +a { + color: #5fba7d; + text-decoration: none; +} + +a:hover { + text-decoration: underline; + color: #7ccd9a; +} + +a:visited { + color: #4a9a6a; +} + +/* Directory listing table */ +table.dirindex { + width: 100%; + border-collapse: collapse; + margin: 15px 0; +} + +table.dirindex td { + padding: 4px 8px; + border-bottom: 1px solid #2a2a2a; + vertical-align: top; +} + +table.dirindex tr:hover { + background: #252525; +} + +table.dirindex .icon { + width: 20px; + text-align: center; +} + +table.dirindex .desc { + color: #888; + font-size: 12px; +} + +/* Plain list */ +ul.plain { + list-style: none; + margin: 10px 0; + padding-left: 0; +} + +ul.plain li { + padding: 3px 0; +} + +ul.plain li:before { + content: "» "; + color: #555; +} + +/* Indented list */ +ul.indent { + margin: 10px 0; + padding-left: 25px; +} + +ul.indent li { + padding: 2px 0; +} + +/* Code blocks */ +pre { + background: #0d0d0d; + border: 1px solid #333; + padding: 10px; + overflow-x: auto; + font-family: 'Monaco', 'Menlo', 'Consolas', monospace; + font-size: 12px; + line-height: 1.4; + margin: 10px 0; + color: #b0b0b0; +} + +code { + font-family: 'Monaco', 'Menlo', 'Consolas', monospace; + font-size: 12px; + background: #0d0d0d; + padding: 1px 4px; + color: #b0b0b0; +} + +/* Inline code in text */ +p code, li code, td code { + border: 1px solid #333; +} + +/* Configuration block */ +.config { + background: #1f1f1a; + border: 1px solid #3a3a30; + padding: 10px; + margin: 10px 0; +} + +/* Note/warning blocks */ +.note { + background: #1a2a1a; + border-left: 3px solid #2e8b57; + padding: 10px; + margin: 15px 0; +} + +.note strong { + color: #3cb371; +} + +.warning { + background: #2a2a1a; + border-left: 3px solid #b8860b; + padding: 10px; + margin: 15px 0; +} + +.warning strong { + color: #daa520; +} + +/* Definition list */ +dl { + margin: 10px 0; +} + +dt { + font-weight: bold; + margin-top: 10px; + color: #e0e0e0; +} + +dd { + margin-left: 20px; + color: #999; +} + +/* Navigation breadcrumb */ +.breadcrumb { + font-size: 12px; + color: #888; + margin-bottom: 15px; +} + +.breadcrumb a { + color: #5fba7d; +} + +/* Footer */ +#footer { + border-top: 1px solid #333; + padding-top: 10px; + font-size: 11px; + color: #666; + text-align: center; +} + +/* Syntax highlighting for config examples */ +.directive { + color: #5fba7d; + font-weight: bold; +} + +.value { + color: #87ceeb; +} + +.comment { + color: #666; + font-style: italic; +}