initial commit

This commit is contained in:
Илья Глазунов
2025-12-05 12:57:41 +03:00
commit 1f25033d2d
16 changed files with 2554 additions and 0 deletions
+269
View File
@@ -0,0 +1,269 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASGI Mounting - pyserve</title>
<link rel="stylesheet" href="../style.css">
</head>
<body>
<div id="container">
<div id="header">
<h1>pyserve</h1>
<div class="tagline">python application orchestrator</div>
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » <a href="index.html">Guides</a> » ASGI Mounting
</div>
<div id="content">
<h2>ASGI Application Mounting (In-Process)</h2>
<p>The <code>asgi</code> 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.</p>
<div class="note">
<strong>For production use cases requiring isolation, consider
<a href="process-orchestration.html">Process Orchestration</a></strong> which runs each app
in a separate subprocess with health monitoring and auto-restart.
</div>
<h3>Overview</h3>
<p>The ASGI mounting system provides:</p>
<ul class="indent">
<li><strong>Multi-framework support</strong> — Mount FastAPI, Flask, Django, Starlette, or custom ASGI apps</li>
<li><strong>Path-based routing</strong> — Each app handles requests at its mounted path</li>
<li><strong>WSGI compatibility</strong> — Automatic WSGI-to-ASGI conversion for Flask/Django</li>
<li><strong>Factory pattern support</strong> — Create apps dynamically with arguments</li>
<li><strong>Path stripping</strong> — Optionally strip mount path from requests</li>
</ul>
<h3>Configuration</h3>
<p>ASGI applications are mounted via the <code>asgi</code> extension:</p>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">asgi</span>
<span class="directive">config:</span>
<span class="directive">mounts:</span>
- <span class="directive">path:</span> <span class="value">"/api"</span>
<span class="directive">app_path:</span> <span class="value">"myapp.api:app"</span>
<span class="directive">app_type:</span> <span class="value">asgi</span>
<span class="directive">name:</span> <span class="value">"api-app"</span>
<span class="directive">strip_path:</span> <span class="value">true</span></pre>
<h3>Mount Configuration Options</h3>
<dl>
<dt>path</dt>
<dd>URL path where the application will be mounted. Example: <code>/api</code></dd>
<dt>app_path</dt>
<dd>Python import path to the application. Format: <code>module.submodule:attribute</code></dd>
<dt>app_type</dt>
<dd>Application type: <code>asgi</code> or <code>wsgi</code>. Default: <code>asgi</code></dd>
<dt>module_path</dt>
<dd>Optional path to add to <code>sys.path</code> for module resolution</dd>
<dt>factory</dt>
<dd>If <code>true</code>, <code>app_path</code> points to a factory function. Default: <code>false</code></dd>
<dt>factory_args</dt>
<dd>Dictionary of arguments to pass to the factory function</dd>
<dt>name</dt>
<dd>Friendly name for logging. Default: uses <code>app_path</code></dd>
<dt>strip_path</dt>
<dd>Remove mount path from request URL. Default: <code>true</code></dd>
</dl>
<h3>Mounting FastAPI</h3>
<p>FastAPI applications are native ASGI:</p>
<pre><span class="comment"># myapp/api.py</span>
from fastapi import FastAPI
app = FastAPI()
@app.get("/users")
async def get_users():
return [{"id": 1, "name": "Alice"}]</pre>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">asgi</span>
<span class="directive">config:</span>
<span class="directive">mounts:</span>
- <span class="directive">path:</span> <span class="value">"/api"</span>
<span class="directive">app_path:</span> <span class="value">"myapp.api:app"</span>
<span class="directive">app_type:</span> <span class="value">asgi</span>
<span class="directive">name:</span> <span class="value">"fastapi-app"</span></pre>
<p>With this configuration:</p>
<ul class="indent">
<li><code>GET /api/users</code> → handled by FastAPI as <code>GET /users</code></li>
<li>FastAPI docs available at <code>/api/docs</code></li>
</ul>
<h3>Mounting Flask</h3>
<p>Flask applications are WSGI and will be automatically wrapped:</p>
<pre><span class="comment"># myapp/flask_api.py</span>
from flask import Flask
app = Flask(__name__)
@app.route("/hello")
def hello():
return {"message": "Hello from Flask!"}</pre>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">asgi</span>
<span class="directive">config:</span>
<span class="directive">mounts:</span>
- <span class="directive">path:</span> <span class="value">"/flask"</span>
<span class="directive">app_path:</span> <span class="value">"myapp.flask_api:app"</span>
<span class="directive">app_type:</span> <span class="value">wsgi</span>
<span class="directive">name:</span> <span class="value">"flask-app"</span></pre>
<div class="note">
<strong>Note:</strong> WSGI wrapping requires either <code>a2wsgi</code> or <code>asgiref</code>
to be installed. Install with: <code>pip install a2wsgi</code>
</div>
<h3>Mounting Django</h3>
<p>Django can be mounted using its ASGI application:</p>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">asgi</span>
<span class="directive">config:</span>
<span class="directive">mounts:</span>
- <span class="directive">path:</span> <span class="value">"/django"</span>
<span class="directive">django_settings:</span> <span class="value">"myproject.settings"</span>
<span class="directive">module_path:</span> <span class="value">"/path/to/django/project"</span>
<span class="directive">name:</span> <span class="value">"django-app"</span></pre>
<h3>Factory Pattern</h3>
<p>Use factory functions to create apps with custom configuration:</p>
<pre><span class="comment"># myapp/api.py</span>
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</pre>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">asgi</span>
<span class="directive">config:</span>
<span class="directive">mounts:</span>
- <span class="directive">path:</span> <span class="value">"/api"</span>
<span class="directive">app_path:</span> <span class="value">"myapp.api:create_app"</span>
<span class="directive">app_type:</span> <span class="value">asgi</span>
<span class="directive">factory:</span> <span class="value">true</span>
<span class="directive">factory_args:</span>
<span class="directive">debug:</span> <span class="value">true</span>
<span class="directive">prefix:</span> <span class="value">"/v2"</span></pre>
<h3>Path Stripping</h3>
<p>By default, <code>strip_path: true</code> removes the mount prefix from requests:</p>
<table class="dirindex">
<tr>
<td>Request</td>
<td><code>strip_path: true</code></td>
<td><code>strip_path: false</code></td>
</tr>
<tr>
<td><code>GET /api/users</code></td>
<td>App sees <code>/users</code></td>
<td>App sees <code>/api/users</code></td>
</tr>
<tr>
<td><code>GET /api/</code></td>
<td>App sees <code>/</code></td>
<td>App sees <code>/api/</code></td>
</tr>
</table>
<h3>Multiple Mounts</h3>
<p>Mount multiple applications at different paths:</p>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">asgi</span>
<span class="directive">config:</span>
<span class="directive">mounts:</span>
<span class="comment"># FastAPI for REST API</span>
- <span class="directive">path:</span> <span class="value">"/api"</span>
<span class="directive">app_path:</span> <span class="value">"apps.api:app"</span>
<span class="directive">app_type:</span> <span class="value">asgi</span>
<span class="comment"># Flask admin panel</span>
- <span class="directive">path:</span> <span class="value">"/admin"</span>
<span class="directive">app_path:</span> <span class="value">"apps.admin:app"</span>
<span class="directive">app_type:</span> <span class="value">wsgi</span>
<span class="comment"># Starlette websocket handler</span>
- <span class="directive">path:</span> <span class="value">"/ws"</span>
<span class="directive">app_path:</span> <span class="value">"apps.websocket:app"</span>
<span class="directive">app_type:</span> <span class="value">asgi</span>
<span class="comment"># Standard routing for static files</span>
- <span class="directive">type:</span> <span class="value">routing</span>
<span class="directive">config:</span>
<span class="directive">regex_locations:</span>
<span class="value">"__default__"</span>:
<span class="directive">root:</span> <span class="value">"./static"</span></pre>
<h3>Mount Priority</h3>
<p>Mounts are matched by path length (longest first). Given mounts at
<code>/api</code> and <code>/api/v2</code>:</p>
<ul class="indent">
<li><code>/api/v2/users</code> → matches <code>/api/v2</code> mount</li>
<li><code>/api/users</code> → matches <code>/api</code> mount</li>
</ul>
<h3>Combining with Routing</h3>
<p>ASGI mounts work alongside the routing extension. The <code>asgi</code> extension
should be listed before <code>routing</code> to handle mounted paths first:</p>
<pre><span class="directive">extensions:</span>
<span class="comment"># ASGI apps handle /api/* and /admin/*</span>
- <span class="directive">type:</span> <span class="value">asgi</span>
<span class="directive">config:</span>
<span class="directive">mounts:</span>
- <span class="directive">path:</span> <span class="value">"/api"</span>
<span class="directive">app_path:</span> <span class="value">"myapp:api"</span>
<span class="directive">app_type:</span> <span class="value">asgi</span>
<span class="comment"># Routing handles everything else</span>
- <span class="directive">type:</span> <span class="value">routing</span>
<span class="directive">config:</span>
<span class="directive">regex_locations:</span>
<span class="value">"=/health"</span>:
<span class="directive">return:</span> <span class="value">"200 OK"</span>
<span class="value">"__default__"</span>:
<span class="directive">spa_fallback:</span> <span class="value">true</span>
<span class="directive">root:</span> <span class="value">"./dist"</span></pre>
<h3>Python API</h3>
<p>For programmatic mounting, see <a href="../reference/asgi-mount.html">ASGI Mount API Reference</a>.</p>
<div class="warning">
<strong>Warning:</strong> Mounted applications share the same process.
Ensure your applications are compatible and don't have conflicting global state.
</div>
</div>
<div id="footer">
<p>pyserve &copy; 2024-2025 | MIT License</p>
</div>
</div>
</body>
</html>
+179
View File
@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Configuration - pyserve</title>
<link rel="stylesheet" href="../style.css">
</head>
<body>
<div id="container">
<div id="header">
<h1>pyserve</h1>
<div class="tagline">python application orchestrator</div>
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » <a href="/guides/">Guides</a> » Configuration
</div>
<div id="content">
<h2>Configuration Reference</h2>
<p>pyserve uses YAML configuration files. By default, it looks for
<code>config.yaml</code> in the current directory.</p>
<h3>http</h3>
<p>HTTP-related paths configuration.</p>
<dl>
<dt>static_dir</dt>
<dd>Path to static files directory. Default: <code>./static</code></dd>
<dt>templates_dir</dt>
<dd>Path to templates directory. Default: <code>./templates</code></dd>
</dl>
<h3>server</h3>
<p>Core server settings.</p>
<dl>
<dt>host</dt>
<dd>Bind address. Default: <code>0.0.0.0</code></dd>
<dt>port</dt>
<dd>Listen port. Default: <code>8080</code></dd>
<dt>backlog</dt>
<dd>Connection queue size. Default: <code>5</code></dd>
<dt>default_root</dt>
<dd>Enable default root handler. Default: <code>false</code></dd>
<dt>proxy_timeout</dt>
<dd>Default timeout for proxy requests in seconds. Default: <code>30.0</code></dd>
<dt>redirect_instructions</dt>
<dd>Dictionary of redirect rules. Format: <code>"/from": "/to"</code></dd>
</dl>
<h3>ssl</h3>
<p>SSL/TLS configuration for HTTPS.</p>
<dl>
<dt>enabled</dt>
<dd>Enable HTTPS. Default: <code>false</code></dd>
<dt>cert_file</dt>
<dd>Path to SSL certificate file. Default: <code>./ssl/cert.pem</code></dd>
<dt>key_file</dt>
<dd>Path to SSL private key file. Default: <code>./ssl/key.pem</code></dd>
</dl>
<h3>logging</h3>
<p>Logging configuration with structlog support.</p>
<dl>
<dt>level</dt>
<dd>Log level: <code>DEBUG</code>, <code>INFO</code>, <code>WARNING</code>,
<code>ERROR</code>. Default: <code>INFO</code></dd>
<dt>console_output</dt>
<dd>Output to console. Default: <code>true</code></dd>
<dt>format</dt>
<dd>Format configuration object (see below)</dd>
<dt>console</dt>
<dd>Console handler configuration</dd>
<dt>files</dt>
<dd>List of file handlers for logging to files</dd>
</dl>
<h4>logging.format</h4>
<dl>
<dt>type</dt>
<dd>Format type: <code>standard</code> or <code>json</code>. Default: <code>standard</code></dd>
<dt>use_colors</dt>
<dd>Enable colored output in console. Default: <code>true</code></dd>
<dt>show_module</dt>
<dd>Show module name in logs. Default: <code>true</code></dd>
<dt>timestamp_format</dt>
<dd>Timestamp format string. Default: <code>%Y-%m-%d %H:%M:%S</code></dd>
</dl>
<h4>logging.files[]</h4>
<dl>
<dt>path</dt>
<dd>Path to log file</dd>
<dt>level</dt>
<dd>Log level for this file handler</dd>
<dt>format</dt>
<dd>Format configuration for this file</dd>
<dt>loggers</dt>
<dd>List of logger names to include (empty = all loggers)</dd>
<dt>max_bytes</dt>
<dd>Maximum file size before rotation. Default: <code>10485760</code> (10MB)</dd>
<dt>backup_count</dt>
<dd>Number of backup files to keep. Default: <code>5</code></dd>
</dl>
<h3>extensions</h3>
<p>List of extension modules to load. See <a href="../reference/extensions.html">Extensions Reference</a>.</p>
<h3>Complete Example</h3>
<pre><span class="directive">http:</span>
<span class="directive">static_dir:</span> <span class="value">./static</span>
<span class="directive">templates_dir:</span> <span class="value">./templates</span>
<span class="directive">server:</span>
<span class="directive">host:</span> <span class="value">0.0.0.0</span>
<span class="directive">port:</span> <span class="value">8080</span>
<span class="directive">backlog:</span> <span class="value">5</span>
<span class="directive">default_root:</span> <span class="value">false</span>
<span class="directive">proxy_timeout:</span> <span class="value">30.0</span>
<span class="directive">ssl:</span>
<span class="directive">enabled:</span> <span class="value">false</span>
<span class="directive">cert_file:</span> <span class="value">./ssl/cert.pem</span>
<span class="directive">key_file:</span> <span class="value">./ssl/key.pem</span>
<span class="directive">logging:</span>
<span class="directive">level:</span> <span class="value">INFO</span>
<span class="directive">console_output:</span> <span class="value">true</span>
<span class="directive">format:</span>
<span class="directive">type:</span> <span class="value">standard</span>
<span class="directive">use_colors:</span> <span class="value">true</span>
<span class="directive">timestamp_format:</span> <span class="value">"%Y-%m-%d %H:%M:%S"</span>
<span class="directive">files:</span>
- <span class="directive">path:</span> <span class="value">./logs/pyserve.log</span>
<span class="directive">level:</span> <span class="value">DEBUG</span>
<span class="directive">max_bytes:</span> <span class="value">10485760</span>
<span class="directive">backup_count:</span> <span class="value">5</span>
<span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">routing</span>
<span class="directive">config:</span>
<span class="directive">regex_locations:</span>
<span class="value">"__default__"</span>:
<span class="directive">root:</span> <span class="value">"./static"</span>
<span class="directive">index_file:</span> <span class="value">"index.html"</span></pre>
<div class="warning">
<strong>Warning:</strong> When running in production, always use SSL
and restrict the bind address appropriately.
</div>
</div>
<div id="footer">
<p>pyserve &copy; 2024-2025 | MIT License</p>
</div>
</div>
</body>
</html>
+59
View File
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Guides - pyserve</title>
<link rel="stylesheet" href="../style.css">
</head>
<body>
<div id="container">
<div id="header">
<h1>pyserve</h1>
<div class="tagline">python application orchestrator</div>
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » Guides
</div>
<div id="content">
<h2>Guides</h2>
<p>In-depth guides for configuring and using pyserve.</p>
<table class="dirindex">
<tr>
<td class="icon"><span class="file">📄</span></td>
<td><a href="process-orchestration.html">Process Orchestration</a></td>
<td class="desc">Run apps in isolated processes with health monitoring</td>
</tr>
<tr>
<td class="icon"><span class="file">📄</span></td>
<td><a href="configuration.html">Configuration</a></td>
<td class="desc">Complete configuration reference</td>
</tr>
<tr>
<td class="icon"><span class="file">📄</span></td>
<td><a href="routing.html">Routing</a></td>
<td class="desc">URL routing and regex patterns</td>
</tr>
<tr>
<td class="icon"><span class="file">📄</span></td>
<td><a href="reverse-proxy.html">Reverse Proxy</a></td>
<td class="desc">Proxying requests to backend services</td>
</tr>
<tr>
<td class="icon"><span class="file">📄</span></td>
<td><a href="asgi-mount.html">ASGI Mounting</a></td>
<td class="desc">Mount Python web frameworks in-process</td>
</tr>
</table>
</div>
<div id="footer">
<p>pyserve &copy; 2024-2025 | MIT License</p>
</div>
</div>
</body>
</html>
+354
View File
@@ -0,0 +1,354 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Process Orchestration - pyserve</title>
<link rel="stylesheet" href="../style.css">
</head>
<body>
<div id="container">
<div id="header">
<h1>pyserve</h1>
<div class="tagline">async http server</div>
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » <a href="/guides/">Guides</a> » Process Orchestration
</div>
<div id="content">
<h2>Process Orchestration</h2>
<p>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.</p>
<h3>Overview</h3>
<p>Unlike <a href="asgi-mount.html">ASGI Mounting</a> which runs apps in-process,
Process Orchestration provides:</p>
<ul class="indent">
<li><strong>Process Isolation</strong> — Each app runs in a separate Python process</li>
<li><strong>Health Monitoring</strong> — Automatic health checks with configurable intervals</li>
<li><strong>Auto-restart</strong> — Failed processes restart with exponential backoff</li>
<li><strong>Multi-worker Support</strong> — Configure multiple uvicorn workers per app</li>
<li><strong>Dynamic Port Allocation</strong> — Automatic port assignment (9000-9999)</li>
<li><strong>WSGI Support</strong> — Flask/Django apps via automatic wrapping</li>
<li><strong>Request Tracing</strong> — X-Request-ID propagation through proxied requests</li>
</ul>
<h3>Architecture</h3>
<pre>
PyServe Gateway (:8000)
┌────────────────┼────────────────┐
▼ ▼ ▼
FastAPI Flask Starlette
:9001 :9002 :9003
/api/* /admin/* /ws/*
</pre>
<p>PyServe acts as a gateway, routing requests to the appropriate subprocess based on URL path.</p>
<h3>Basic Configuration</h3>
<pre><span class="directive">server:</span>
<span class="directive">host:</span> <span class="value">0.0.0.0</span>
<span class="directive">port:</span> <span class="value">8000</span>
<span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">process_orchestration</span>
<span class="directive">config:</span>
<span class="directive">apps:</span>
- <span class="directive">name:</span> <span class="value">api</span>
<span class="directive">path:</span> <span class="value">/api</span>
<span class="directive">app_path:</span> <span class="value">myapp.api:app</span>
- <span class="directive">name:</span> <span class="value">admin</span>
<span class="directive">path:</span> <span class="value">/admin</span>
<span class="directive">app_path:</span> <span class="value">myapp.admin:app</span></pre>
<h3>App Configuration Options</h3>
<dl>
<dt>name</dt>
<dd>Unique identifier for the application (required)</dd>
<dt>path</dt>
<dd>URL path prefix for routing requests (required)</dd>
<dt>app_path</dt>
<dd>Python import path. Format: <code>module:attribute</code> (required)</dd>
<dt>app_type</dt>
<dd>Application type: <code>asgi</code> or <code>wsgi</code>. Default: <code>asgi</code></dd>
<dt>workers</dt>
<dd>Number of uvicorn workers. Default: <code>1</code></dd>
<dt>port</dt>
<dd>Fixed port number. Default: auto-allocated from port_range</dd>
<dt>factory</dt>
<dd>If <code>true</code>, app_path points to a factory function. Default: <code>false</code></dd>
<dt>env</dt>
<dd>Environment variables to pass to the subprocess</dd>
<dt>module_path</dt>
<dd>Path to add to <code>sys.path</code> for module resolution</dd>
</dl>
<h3>Health Check Options</h3>
<dl>
<dt>health_check_enabled</dt>
<dd>Enable health monitoring. Default: <code>true</code></dd>
<dt>health_check_path</dt>
<dd>Endpoint to check for health. Default: <code>/health</code></dd>
<dt>health_check_interval</dt>
<dd>Interval between health checks in seconds. Default: <code>10.0</code></dd>
<dt>health_check_timeout</dt>
<dd>Timeout for health check requests. Default: <code>5.0</code></dd>
<dt>health_check_retries</dt>
<dd>Failed checks before restart. Default: <code>3</code></dd>
</dl>
<h3>Restart Options</h3>
<dl>
<dt>max_restart_count</dt>
<dd>Maximum restart attempts before giving up. Default: <code>5</code></dd>
<dt>restart_delay</dt>
<dd>Initial delay between restarts in seconds. Default: <code>1.0</code></dd>
<dt>shutdown_timeout</dt>
<dd>Timeout for graceful shutdown. Default: <code>30.0</code></dd>
</dl>
<h3>Global Configuration</h3>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">process_orchestration</span>
<span class="directive">config:</span>
<span class="directive">port_range:</span> <span class="value">[9000, 9999]</span>
<span class="directive">health_check_enabled:</span> <span class="value">true</span>
<span class="directive">proxy_timeout:</span> <span class="value">60.0</span>
<span class="directive">logging:</span>
<span class="directive">httpx_level:</span> <span class="value">warning</span>
<span class="directive">proxy_logs:</span> <span class="value">true</span>
<span class="directive">health_check_logs:</span> <span class="value">false</span>
<span class="directive">apps:</span>
<span class="comment"># ...</span></pre>
<dl>
<dt>port_range</dt>
<dd>Range for dynamic port allocation. Default: <code>[9000, 9999]</code></dd>
<dt>proxy_timeout</dt>
<dd>Timeout for proxied requests in seconds. Default: <code>60.0</code></dd>
<dt>logging.httpx_level</dt>
<dd>Log level for HTTP client (debug/info/warning/error). Default: <code>warning</code></dd>
<dt>logging.proxy_logs</dt>
<dd>Log proxied requests with latency. Default: <code>true</code></dd>
<dt>logging.health_check_logs</dt>
<dd>Log health check results. Default: <code>false</code></dd>
</dl>
<h3>FastAPI Example</h3>
<pre><span class="comment"># myapp/api.py</span>
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"}]</pre>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">process_orchestration</span>
<span class="directive">config:</span>
<span class="directive">apps:</span>
- <span class="directive">name:</span> <span class="value">api</span>
<span class="directive">path:</span> <span class="value">/api</span>
<span class="directive">app_path:</span> <span class="value">myapp.api:app</span>
<span class="directive">workers:</span> <span class="value">4</span>
<span class="directive">health_check_path:</span> <span class="value">/health</span></pre>
<p>Requests to <code>/api/users</code> are proxied to the FastAPI process as <code>/users</code>.</p>
<h3>Flask Example (WSGI)</h3>
<pre><span class="comment"># myapp/admin.py</span>
from flask import Flask
app = Flask(__name__)
@app.route("/health")
def health():
return {"status": "ok"}
@app.route("/dashboard")
def dashboard():
return {"page": "dashboard"}</pre>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">process_orchestration</span>
<span class="directive">config:</span>
<span class="directive">apps:</span>
- <span class="directive">name:</span> <span class="value">admin</span>
<span class="directive">path:</span> <span class="value">/admin</span>
<span class="directive">app_path:</span> <span class="value">myapp.admin:app</span>
<span class="directive">app_type:</span> <span class="value">wsgi</span>
<span class="directive">workers:</span> <span class="value">2</span></pre>
<div class="note">
<strong>Note:</strong> WSGI support requires <code>a2wsgi</code> package:
<code>pip install a2wsgi</code>
</div>
<h3>Factory Pattern</h3>
<pre><span class="comment"># myapp/api.py</span>
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</pre>
<pre><span class="directive">apps:</span>
- <span class="directive">name:</span> <span class="value">api</span>
<span class="directive">path:</span> <span class="value">/api</span>
<span class="directive">app_path:</span> <span class="value">myapp.api:create_app</span>
<span class="directive">factory:</span> <span class="value">true</span></pre>
<h3>Environment Variables</h3>
<p>Pass environment variables to subprocesses:</p>
<pre><span class="directive">apps:</span>
- <span class="directive">name:</span> <span class="value">api</span>
<span class="directive">path:</span> <span class="value">/api</span>
<span class="directive">app_path:</span> <span class="value">myapp.api:app</span>
<span class="directive">env:</span>
<span class="directive">DATABASE_URL:</span> <span class="value">"postgresql://localhost/mydb"</span>
<span class="directive">REDIS_URL:</span> <span class="value">"redis://localhost:6379"</span>
<span class="directive">DEBUG:</span> <span class="value">"false"</span></pre>
<h3>Multiple Applications</h3>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">process_orchestration</span>
<span class="directive">config:</span>
<span class="directive">port_range:</span> <span class="value">[9000, 9999]</span>
<span class="directive">apps:</span>
<span class="comment"># FastAPI REST API</span>
- <span class="directive">name:</span> <span class="value">api</span>
<span class="directive">path:</span> <span class="value">/api</span>
<span class="directive">app_path:</span> <span class="value">apps.api:app</span>
<span class="directive">workers:</span> <span class="value">4</span>
<span class="comment"># Flask Admin Panel</span>
- <span class="directive">name:</span> <span class="value">admin</span>
<span class="directive">path:</span> <span class="value">/admin</span>
<span class="directive">app_path:</span> <span class="value">apps.admin:app</span>
<span class="directive">app_type:</span> <span class="value">wsgi</span>
<span class="directive">workers:</span> <span class="value">2</span>
<span class="comment"># Starlette WebSocket Handler</span>
- <span class="directive">name:</span> <span class="value">websocket</span>
<span class="directive">path:</span> <span class="value">/ws</span>
<span class="directive">app_path:</span> <span class="value">apps.websocket:app</span>
<span class="directive">workers:</span> <span class="value">1</span></pre>
<h3>Request Tracing</h3>
<p>PyServe automatically generates and propagates <code>X-Request-ID</code> headers:</p>
<ul class="indent">
<li>If a request has <code>X-Request-ID</code>, it's preserved</li>
<li>Otherwise, a UUID is generated</li>
<li>The ID is passed to subprocesses and included in response headers</li>
<li>All logs include the request ID for tracing</li>
</ul>
<h3>Process Orchestration vs ASGI Mount</h3>
<table class="dirindex">
<tr>
<th>Feature</th>
<th>Process Orchestration</th>
<th>ASGI Mount</th>
</tr>
<tr>
<td>Isolation</td>
<td>Full process isolation</td>
<td>Shared process</td>
</tr>
<tr>
<td>Memory</td>
<td>Separate per app</td>
<td>Shared</td>
</tr>
<tr>
<td>Crash Impact</td>
<td>Only that app restarts</td>
<td>All apps affected</td>
</tr>
<tr>
<td>Health Checks</td>
<td>Yes, with auto-restart</td>
<td>No</td>
</tr>
<tr>
<td>Multi-worker</td>
<td>Yes, per app</td>
<td>No</td>
</tr>
<tr>
<td>Latency</td>
<td>HTTP proxy overhead</td>
<td>In-process (faster)</td>
</tr>
<tr>
<td>Use Case</td>
<td>Production, isolation needed</td>
<td>Development, simple setups</td>
</tr>
</table>
<div class="note">
<strong>When to use Process Orchestration:</strong>
<ul class="indent">
<li>Running multiple apps that shouldn't affect each other</li>
<li>Need automatic restart on failure</li>
<li>Different resource requirements per app</li>
<li>Production deployments</li>
</ul>
<strong>When to use ASGI Mount:</strong>
<ul class="indent">
<li>Development and testing</li>
<li>Simple setups with trusted apps</li>
<li>Minimal latency requirements</li>
</ul>
</div>
<div class="note">
<strong>See Also:</strong>
<ul class="indent">
<li><a href="asgi-mount.html">ASGI Mounting Guide</a> — In-process app mounting</li>
<li><a href="../reference/extensions.html">Extensions Reference</a> — All extension types</li>
<li><a href="configuration.html">Configuration Guide</a> — Full configuration reference</li>
</ul>
</div>
</div>
<div id="footer">
<p>pyserve &copy; 2024-2025 | MIT License</p>
</div>
</div>
</body>
</html>
+156
View File
@@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reverse Proxy - pyserve</title>
<link rel="stylesheet" href="../style.css">
</head>
<body>
<div id="container">
<div id="header">
<h1>pyserve</h1>
<div class="tagline">python application orchestrator</div>
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » <a href="/guides/">Guides</a> » Reverse Proxy
</div>
<div id="content">
<h2>Reverse Proxy</h2>
<p>pyserve can act as a reverse proxy, forwarding requests to backend services.</p>
<h3>Basic Proxy Configuration</h3>
<p>Use the <code>proxy_pass</code> directive in routing:</p>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">routing</span>
<span class="directive">config:</span>
<span class="directive">regex_locations:</span>
<span class="value">"~^/api/"</span>:
<span class="directive">proxy_pass:</span> <span class="value">"http://localhost:9001"</span></pre>
<p>All requests to <code>/api/*</code> will be forwarded to <code>http://localhost:9001/api/*</code>.</p>
<h3>Proxy Headers</h3>
<p>pyserve automatically adds standard proxy headers:</p>
<table class="dirindex">
<tr>
<td><code>X-Forwarded-For</code></td>
<td>Client's IP address</td>
</tr>
<tr>
<td><code>X-Forwarded-Proto</code></td>
<td>Original protocol (http/https)</td>
</tr>
<tr>
<td><code>X-Forwarded-Host</code></td>
<td>Original Host header</td>
</tr>
<tr>
<td><code>X-Real-IP</code></td>
<td>Client's real IP address</td>
</tr>
</table>
<h3>Custom Headers</h3>
<p>Add custom headers to proxied requests:</p>
<pre><span class="value">"~^/api/"</span>:
<span class="directive">proxy_pass:</span> <span class="value">"http://localhost:9001"</span>
<span class="directive">headers:</span>
- <span class="value">"X-Custom-Header: my-value"</span>
- <span class="value">"Authorization: Bearer token123"</span></pre>
<h3>Dynamic Headers with Captures</h3>
<p>Use regex capture groups to build dynamic headers:</p>
<pre><span class="value">"~^/api/v(?P&lt;version&gt;\\d+)/(?P&lt;service&gt;\\w+)"</span>:
<span class="directive">proxy_pass:</span> <span class="value">"http://localhost:9001"</span>
<span class="directive">headers:</span>
- <span class="value">"X-API-Version: {version}"</span>
- <span class="value">"X-Service: {service}"</span>
- <span class="value">"X-Client-IP: $remote_addr"</span></pre>
<p>Special variables:</p>
<ul class="indent">
<li><code>{capture_name}</code> — Named capture group from regex</li>
<li><code>$remote_addr</code> — Client's IP address</li>
</ul>
<h3>Proxy Timeout</h3>
<p>Configure timeout for proxy requests:</p>
<pre><span class="comment"># Global default timeout</span>
<span class="directive">server:</span>
<span class="directive">proxy_timeout:</span> <span class="value">30.0</span>
<span class="comment"># Per-route timeout</span>
<span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">routing</span>
<span class="directive">config:</span>
<span class="directive">regex_locations:</span>
<span class="value">"~^/api/slow"</span>:
<span class="directive">proxy_pass:</span> <span class="value">"http://localhost:9001"</span>
<span class="directive">timeout:</span> <span class="value">120</span> <span class="comment"># 2 minutes for slow endpoints</span></pre>
<h3>URL Rewriting</h3>
<p>The proxy preserves the original request path by default:</p>
<pre><span class="comment"># Request: GET /api/users/123</span>
<span class="comment"># Proxied: GET http://backend:9001/api/users/123</span>
<span class="value">"~^/api/"</span>:
<span class="directive">proxy_pass:</span> <span class="value">"http://backend:9001"</span></pre>
<p>To proxy to a specific path:</p>
<pre><span class="comment"># Request: GET /api/users/123</span>
<span class="comment"># Proxied: GET http://backend:9001/v2/users/123 (path preserved)</span>
<span class="value">"~^/api/"</span>:
<span class="directive">proxy_pass:</span> <span class="value">"http://backend:9001/v2"</span></pre>
<h3>Load Balancing Example</h3>
<p>Route different services to different backends:</p>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">routing</span>
<span class="directive">config:</span>
<span class="directive">regex_locations:</span>
<span class="value">"~^/api/users"</span>:
<span class="directive">proxy_pass:</span> <span class="value">"http://user-service:8001"</span>
<span class="value">"~^/api/orders"</span>:
<span class="directive">proxy_pass:</span> <span class="value">"http://order-service:8002"</span>
<span class="value">"~^/api/products"</span>:
<span class="directive">proxy_pass:</span> <span class="value">"http://product-service:8003"</span></pre>
<h3>Error Handling</h3>
<p>pyserve returns appropriate error codes for proxy failures:</p>
<table class="dirindex">
<tr>
<td><code>502 Bad Gateway</code></td>
<td>Backend connection failed or returned invalid response</td>
</tr>
<tr>
<td><code>504 Gateway Timeout</code></td>
<td>Backend did not respond within timeout</td>
</tr>
</table>
<div class="note">
<strong>Note:</strong> pyserve uses <code>httpx</code> for async HTTP requests
to backend services, supporting HTTP/1.1 and HTTP/2.
</div>
</div>
<div id="footer">
<p>pyserve &copy; 2024-2025 | MIT License</p>
</div>
</div>
</body>
</html>
+169
View File
@@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Routing - pyserve</title>
<link rel="stylesheet" href="../style.css">
</head>
<body>
<div id="container">
<div id="header">
<h1>pyserve</h1>
<div class="tagline">python application orchestrator</div>
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » <a href="/guides/">Guides</a> » Routing
</div>
<div id="content">
<h2>Routing</h2>
<p>pyserve supports nginx-style routing patterns including exact matches,
regex locations, and SPA fallback.</p>
<h3>Location Types</h3>
<table class="dirindex">
<tr>
<td><code>=</code></td>
<td>Exact match</td>
<td class="desc"><code>=/health</code> matches only <code>/health</code></td>
</tr>
<tr>
<td><code>~</code></td>
<td>Case-sensitive regex</td>
<td class="desc"><code>~^/api/v\d+/</code> matches <code>/api/v1/</code></td>
</tr>
<tr>
<td><code>~*</code></td>
<td>Case-insensitive regex</td>
<td class="desc"><code>~*\.(js|css)$</code> matches <code>.JS</code> and <code>.css</code></td>
</tr>
<tr>
<td><code>__default__</code></td>
<td>Default fallback</td>
<td class="desc">Matches when no other route matches</td>
</tr>
</table>
<h3>Match Priority</h3>
<p>Routes are processed in the following order:</p>
<ol class="indent">
<li><strong>Exact matches</strong> (<code>=</code>) — checked first</li>
<li><strong>Regex patterns</strong> (<code>~</code> and <code>~*</code>) — in definition order</li>
<li><strong>Default fallback</strong> (<code>__default__</code>) — last resort</li>
</ol>
<h3>Routing Configuration</h3>
<p>Routing is configured via the <code>routing</code> extension:</p>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">routing</span>
<span class="directive">config:</span>
<span class="directive">regex_locations:</span>
<span class="comment"># Exact match for health check</span>
<span class="value">"=/health"</span>:
<span class="directive">return:</span> <span class="value">"200 OK"</span>
<span class="directive">content_type:</span> <span class="value">"text/plain"</span>
<span class="comment"># Static files with caching</span>
<span class="value">"~*\\.(js|css|png|jpg|gif|ico)$"</span>:
<span class="directive">root:</span> <span class="value">"./static"</span>
<span class="directive">cache_control:</span> <span class="value">"public, max-age=31536000"</span>
<span class="comment"># HTML files without caching</span>
<span class="value">"~*\\.html$"</span>:
<span class="directive">root:</span> <span class="value">"./static"</span>
<span class="directive">cache_control:</span> <span class="value">"no-cache"</span>
<span class="comment"># Default fallback</span>
<span class="value">"__default__"</span>:
<span class="directive">root:</span> <span class="value">"./static"</span>
<span class="directive">index_file:</span> <span class="value">"index.html"</span></pre>
<h3>Location Directives</h3>
<dl>
<dt>root</dt>
<dd>Base directory for serving files.</dd>
<dt>index_file</dt>
<dd>Index file name for directory requests. Default: <code>index.html</code></dd>
<dt>proxy_pass</dt>
<dd>Upstream server URL for reverse proxy. See <a href="reverse-proxy.html">Reverse Proxy</a>.</dd>
<dt>return</dt>
<dd>Return a fixed response. Format: <code>"status message"</code> or <code>"status"</code></dd>
<dt>content_type</dt>
<dd>Response content type for <code>return</code> directive.</dd>
<dt>cache_control</dt>
<dd>Cache-Control header value.</dd>
<dt>headers</dt>
<dd>List of additional headers to add. Format: <code>"Header-Name: value"</code></dd>
<dt>spa_fallback</dt>
<dd>Enable SPA mode — serve index file for all routes.</dd>
<dt>exclude_patterns</dt>
<dd>URL patterns to exclude from SPA fallback.</dd>
</dl>
<h3>Named Capture Groups</h3>
<p>Regex locations support named capture groups that can be used in headers and proxy URLs:</p>
<pre><span class="value">"~^/api/v(?P&lt;version&gt;\\d+)/(?P&lt;resource&gt;\\w+)"</span>:
<span class="directive">proxy_pass:</span> <span class="value">"http://backend:9001"</span>
<span class="directive">headers:</span>
- <span class="value">"X-API-Version: {version}"</span>
- <span class="value">"X-Resource: {resource}"</span></pre>
<p>Request to <code>/api/v2/users</code> will have headers:</p>
<ul class="indent">
<li><code>X-API-Version: 2</code></li>
<li><code>X-Resource: users</code></li>
</ul>
<h3>SPA Configuration</h3>
<p>For Single Page Applications, use <code>spa_fallback</code> with <code>exclude_patterns</code>:</p>
<pre><span class="value">"__default__"</span>:
<span class="directive">spa_fallback:</span> <span class="value">true</span>
<span class="directive">root:</span> <span class="value">"./dist"</span>
<span class="directive">index_file:</span> <span class="value">"index.html"</span>
<span class="directive">exclude_patterns:</span>
- <span class="value">"/api/"</span>
- <span class="value">"/assets/"</span>
- <span class="value">"/static/"</span></pre>
<p>This will:</p>
<ul class="indent">
<li>Serve <code>index.html</code> for routes like <code>/about</code>, <code>/users/123</code></li>
<li>Return 404 for <code>/api/*</code>, <code>/assets/*</code>, <code>/static/*</code> if file not found</li>
</ul>
<h3>Static File Serving</h3>
<p>Basic static file configuration:</p>
<pre><span class="value">"~*\\.(css|js|png|jpg|gif|svg|woff2?)$"</span>:
<span class="directive">root:</span> <span class="value">"./static"</span>
<span class="directive">cache_control:</span> <span class="value">"public, max-age=86400"</span>
<span class="directive">headers:</span>
- <span class="value">"X-Content-Type-Options: nosniff"</span></pre>
<div class="note">
<strong>Note:</strong> pyserve automatically detects MIME types based on file extensions.
</div>
</div>
<div id="footer">
<p>pyserve &copy; 2024-2025 | MIT License</p>
</div>
</div>
</body>
</html>