asgi/wsgi mounting implemented

This commit is contained in:
Илья Глазунов
2025-12-03 12:10:28 +03:00
parent 831eee5d01
commit 0d0d1aec80
13 changed files with 2528 additions and 7 deletions
+1
View File
@@ -0,0 +1 @@
"""Example applications package."""
+194
View File
@@ -0,0 +1,194 @@
"""
Example custom ASGI application for PyServe ASGI mounting.
This demonstrates how to create a raw ASGI application without
any framework - similar to Python's http.server but async.
"""
from typing import Dict, Any, List, Callable, Awaitable, Optional
import json
Scope = Dict[str, Any]
Receive = Callable[[], Awaitable[Dict[str, Any]]]
Send = Callable[[Dict[str, Any]], Awaitable[None]]
class SimpleASGIApp:
def __init__(self):
self.routes: Dict[str, Callable] = {}
self._setup_routes()
def _setup_routes(self) -> None:
self.routes = {
"/": self._handle_root,
"/health": self._handle_health,
"/echo": self._handle_echo,
"/info": self._handle_info,
"/headers": self._handle_headers,
}
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http":
return
path = scope.get("path", "/")
method = scope.get("method", "GET")
handler = self.routes.get(path)
if handler is None:
if path.startswith("/echo/"):
handler = self._handle_echo_path
else:
await self._send_response(
send,
status=404,
body={"error": "Not found", "path": path}
)
return
await handler(scope, receive, send)
async def _handle_root(self, scope: Scope, receive: Receive, send: Send) -> None:
await self._send_response(
send,
body={
"message": "Welcome to Custom ASGI App mounted in PyServe!",
"description": "This is a raw ASGI application without any framework",
"endpoints": list(self.routes.keys()) + ["/echo/{message}"],
}
)
async def _handle_health(self, scope: Scope, receive: Receive, send: Send) -> None:
await self._send_response(
send,
body={"status": "healthy", "app": "custom-asgi"}
)
async def _handle_echo(self, scope: Scope, receive: Receive, send: Send) -> None:
method = scope.get("method", "GET")
if method == "POST":
body = await self._read_body(receive)
await self._send_response(
send,
body={"echo": body.decode("utf-8") if body else ""}
)
else:
await self._send_response(
send,
body={"message": "Send a POST request to echo data"}
)
async def _handle_echo_path(self, scope: Scope, receive: Receive, send: Send) -> None:
path = scope.get("path", "")
message = path.replace("/echo/", "", 1)
await self._send_response(
send,
body={"echo": message}
)
async def _handle_info(self, scope: Scope, receive: Receive, send: Send) -> None:
await self._send_response(
send,
body={
"method": scope.get("method"),
"path": scope.get("path"),
"query_string": scope.get("query_string", b"").decode("utf-8"),
"root_path": scope.get("root_path", ""),
"scheme": scope.get("scheme", "http"),
"server": list(scope.get("server", ())),
"asgi": scope.get("asgi", {}),
}
)
async def _handle_headers(self, scope: Scope, receive: Receive, send: Send) -> None:
headers = {}
for name, value in scope.get("headers", []):
headers[name.decode("utf-8")] = value.decode("utf-8")
await self._send_response(
send,
body={"headers": headers}
)
async def _read_body(self, receive: Receive) -> bytes:
body = b""
more_body = True
while more_body:
message = await receive()
body += message.get("body", b"")
more_body = message.get("more_body", False)
return body
async def _send_response(
self,
send: Send,
status: int = 200,
body: Any = None,
content_type: str = "application/json",
headers: Optional[List[tuple]] = None,
) -> None:
response_headers = [
(b"content-type", content_type.encode("utf-8")),
]
if headers:
response_headers.extend(headers)
if body is not None:
if content_type == "application/json":
body_bytes = json.dumps(body, ensure_ascii=False).encode("utf-8")
elif isinstance(body, bytes):
body_bytes = body
else:
body_bytes = str(body).encode("utf-8")
else:
body_bytes = b""
response_headers.append(
(b"content-length", str(len(body_bytes)).encode("utf-8"))
)
await send({
"type": "http.response.start",
"status": status,
"headers": response_headers,
})
await send({
"type": "http.response.body",
"body": body_bytes,
})
app = SimpleASGIApp()
async def simple_asgi_app(scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http":
return
response_body = json.dumps({
"message": "Hello from minimal ASGI app!",
"path": scope.get("path", "/"),
}).encode("utf-8")
await send({
"type": "http.response.start",
"status": 200,
"headers": [
(b"content-type", b"application/json"),
(b"content-length", str(len(response_body)).encode("utf-8")),
],
})
await send({
"type": "http.response.body",
"body": response_body,
})
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8004)
+118
View File
@@ -0,0 +1,118 @@
"""
Example FastAPI application for PyServe ASGI mounting.
This demonstrates how to create a FastAPI application that can be
mounted at a specific path in PyServe.
"""
from typing import Optional, Dict, Any
try:
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
except ImportError:
raise ImportError(
"FastAPI is not installed. Install with: pip install fastapi"
)
app = FastAPI(
title="Example FastAPI App",
description="This is an example FastAPI application mounted in PyServe",
version="1.0.0",
)
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
class Message(BaseModel):
message: str
items_db: Dict[int, Dict[str, Any]] = {
1: {"name": "Item 1", "description": "First item", "price": 10.5, "tax": 1.05},
2: {"name": "Item 2", "description": "Second item", "price": 20.0, "tax": 2.0},
}
@app.get("/")
async def root():
return {"message": "Welcome to FastAPI mounted in PyServe!"}
@app.get("/health")
async def health_check():
return {"status": "healthy", "app": "fastapi"}
@app.get("/items")
async def list_items():
return {"items": list(items_db.values()), "count": len(items_db)}
@app.get("/items/{item_id}")
async def get_item(item_id: int):
if item_id not in items_db:
raise HTTPException(status_code=404, detail="Item not found")
return items_db[item_id]
@app.post("/items", response_model=Message)
async def create_item(item: Item):
new_id = max(items_db.keys()) + 1 if items_db else 1
items_db[new_id] = item.model_dump()
return {"message": f"Item created with ID {new_id}"}
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
if item_id not in items_db:
raise HTTPException(status_code=404, detail="Item not found")
items_db[item_id] = item.model_dump()
return {"message": f"Item {item_id} updated"}
@app.delete("/items/{item_id}")
async def delete_item(item_id: int):
if item_id not in items_db:
raise HTTPException(status_code=404, detail="Item not found")
del items_db[item_id]
return {"message": f"Item {item_id} deleted"}
def create_app(debug: bool = False, **kwargs) -> FastAPI:
application = FastAPI(
title="Example FastAPI App (Factory)",
description="FastAPI application created via factory function",
version="2.0.0",
debug=debug,
)
@application.get("/")
async def factory_root():
return {
"message": "Welcome to FastAPI (factory) mounted in PyServe!",
"debug": debug,
"config": kwargs,
}
@application.get("/health")
async def factory_health():
return {"status": "healthy", "app": "fastapi-factory", "debug": debug}
@application.get("/echo/{message}")
async def echo(message: str):
return {"echo": message}
return application
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001)
+112
View File
@@ -0,0 +1,112 @@
"""
Example Flask application for PyServe ASGI mounting.
This demonstrates how to create a Flask application that can be
mounted at a specific path in PyServe (via WSGI-to-ASGI adapter).
"""
from typing import Optional
try:
from flask import Flask, jsonify, request
except ImportError:
raise ImportError(
"Flask is not installed. Install with: pip install flask"
)
app = Flask(__name__)
users_db = {
1: {"id": 1, "name": "Alice", "email": "alice@example.com"},
2: {"id": 2, "name": "Bob", "email": "bob@example.com"},
}
@app.route("/")
def root():
return jsonify({"message": "Welcome to Flask mounted in PyServe!"})
@app.route("/health")
def health_check():
return jsonify({"status": "healthy", "app": "flask"})
@app.route("/users")
def list_users():
return jsonify({"users": list(users_db.values()), "count": len(users_db)})
@app.route("/users/<int:user_id>")
def get_user(user_id: int):
if user_id not in users_db:
return jsonify({"error": "User not found"}), 404
return jsonify(users_db[user_id])
@app.route("/users", methods=["POST"])
def create_user():
data = request.get_json()
if not data or "name" not in data:
return jsonify({"error": "Name is required"}), 400
new_id = max(users_db.keys()) + 1 if users_db else 1
users_db[new_id] = {
"id": new_id,
"name": data["name"],
"email": data.get("email", ""),
}
return jsonify({"message": f"User created with ID {new_id}", "user": users_db[new_id]}), 201
@app.route("/users/<int:user_id>", methods=["PUT"])
def update_user(user_id: int):
if user_id not in users_db:
return jsonify({"error": "User not found"}), 404
data = request.get_json()
if data:
if "name" in data:
users_db[user_id]["name"] = data["name"]
if "email" in data:
users_db[user_id]["email"] = data["email"]
return jsonify({"message": f"User {user_id} updated", "user": users_db[user_id]})
@app.route("/users/<int:user_id>", methods=["DELETE"])
def delete_user(user_id: int):
if user_id not in users_db:
return jsonify({"error": "User not found"}), 404
del users_db[user_id]
return jsonify({"message": f"User {user_id} deleted"})
def create_app(config: Optional[dict] = None) -> Flask:
application = Flask(__name__)
if config:
application.config.update(config)
@application.route("/")
def factory_root():
return jsonify({
"message": "Welcome to Flask (factory) mounted in PyServe!",
"config": config or {},
})
@application.route("/health")
def factory_health():
return jsonify({"status": "healthy", "app": "flask-factory"})
@application.route("/echo/<message>")
def echo(message: str):
return jsonify({"echo": message})
return application
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8002, debug=True)
+112
View File
@@ -0,0 +1,112 @@
"""
Example Starlette application for PyServe ASGI mounting.
This demonstrates how to create a Starlette application that can be
mounted at a specific path in PyServe.
"""
try:
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
from starlette.requests import Request
except ImportError:
raise ImportError(
"Starlette is not installed. Install with: pip install starlette"
)
tasks_db = {
1: {"id": 1, "title": "Task 1", "completed": False},
2: {"id": 2, "title": "Task 2", "completed": True},
}
async def homepage(request: Request) -> JSONResponse:
return JSONResponse({
"message": "Welcome to Starlette mounted in PyServe!"
})
async def health_check(request: Request) -> JSONResponse:
return JSONResponse({"status": "healthy", "app": "starlette"})
async def list_tasks(request: Request) -> JSONResponse:
return JSONResponse({
"tasks": list(tasks_db.values()),
"count": len(tasks_db)
})
async def get_task(request: Request) -> JSONResponse:
task_id = int(request.path_params["task_id"])
if task_id not in tasks_db:
return JSONResponse({"error": "Task not found"}, status_code=404)
return JSONResponse(tasks_db[task_id])
async def create_task(request: Request) -> JSONResponse:
data = await request.json()
if not data or "title" not in data:
return JSONResponse({"error": "Title is required"}, status_code=400)
new_id = max(tasks_db.keys()) + 1 if tasks_db else 1
tasks_db[new_id] = {
"id": new_id,
"title": data["title"],
"completed": data.get("completed", False),
}
return JSONResponse(
{"message": f"Task created with ID {new_id}", "task": tasks_db[new_id]},
status_code=201
)
async def update_task(request: Request) -> JSONResponse:
task_id = int(request.path_params["task_id"])
if task_id not in tasks_db:
return JSONResponse({"error": "Task not found"}, status_code=404)
data = await request.json()
if data:
if "title" in data:
tasks_db[task_id]["title"] = data["title"]
if "completed" in data:
tasks_db[task_id]["completed"] = data["completed"]
return JSONResponse({
"message": f"Task {task_id} updated",
"task": tasks_db[task_id]
})
async def delete_task(request: Request) -> JSONResponse:
task_id = int(request.path_params["task_id"])
if task_id not in tasks_db:
return JSONResponse({"error": "Task not found"}, status_code=404)
del tasks_db[task_id]
return JSONResponse({"message": f"Task {task_id} deleted"})
routes = [
Route("/", homepage),
Route("/health", health_check),
Route("/tasks", list_tasks, methods=["GET"]),
Route("/tasks", create_task, methods=["POST"]),
Route("/tasks/{task_id:int}", get_task, methods=["GET"]),
Route("/tasks/{task_id:int}", update_task, methods=["PUT"]),
Route("/tasks/{task_id:int}", delete_task, methods=["DELETE"]),
]
app = Starlette(debug=True, routes=routes)
def create_app(debug: bool = False) -> Starlette:
return Starlette(debug=debug, routes=routes)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8003)
+103
View File
@@ -0,0 +1,103 @@
# Example configuration for ASGI application mounts
# This demonstrates how to mount various Python web frameworks
http:
static_dir: ./static
templates_dir: ./templates
server:
host: 0.0.0.0
port: 8080
backlog: 5
proxy_timeout: 30.0
logging:
level: DEBUG
console_output: true
format:
type: standard
use_colors: true
extensions:
# ASGI Application Mount Extension
- type: asgi
config:
mounts:
# FastAPI application
- path: "/api"
app_path: "examples.apps.fastapi_app:app"
app_type: asgi
name: "fastapi-api"
strip_path: true
# FastAPI with factory pattern
- path: "/api/v2"
app_path: "examples.apps.fastapi_app:create_app"
app_type: asgi
factory: true
factory_args:
debug: true
name: "fastapi-api-v2"
strip_path: true
# Flask application (WSGI wrapped to ASGI)
- path: "/flask"
app_path: "examples.apps.flask_app:app"
app_type: wsgi
name: "flask-app"
strip_path: true
# Flask with factory pattern
- path: "/flask-v2"
app_path: "examples.apps.flask_app:create_app"
app_type: wsgi
factory: true
name: "flask-app-factory"
strip_path: true
# Django application
# Uncomment and configure for your Django project
# - path: "/django"
# django_settings: "myproject.settings"
# module_path: "/path/to/django/project"
# name: "django-app"
# strip_path: true
# Starlette application
- path: "/starlette"
app_path: "examples.apps.starlette_app:app"
app_type: asgi
name: "starlette-app"
strip_path: true
# Custom ASGI application (http.server style)
- path: "/custom"
app_path: "examples.apps.custom_asgi:app"
app_type: asgi
name: "custom-asgi"
strip_path: true
# Standard routing for other paths
- type: routing
config:
regex_locations:
# Health check
"=/health":
return: "200 OK"
content_type: "text/plain"
# Static files
"~*\\.(js|css|png|jpg|gif|ico|svg|woff2?)$":
root: "./static"
cache_control: "public, max-age=31536000"
# Root path
"=/":
root: "./static"
index_file: "index.html"
# Default fallback
"__default__":
spa_fallback: true
root: "./static"
index_file: "index.html"