asw-v01: archive deferred content (packs, site, lab, legacy examples)

- 2.1: packs/ -> archive/packs/
- 2.2: site/ -> archive/site/
- 2.3: src/lab/ -> archive/lab/
- 2.4: examples/ -> archive/examples-legacy/ (SSI-based)
This commit is contained in:
exe.dev user 2026-06-07 10:39:21 +02:00
parent 416fe2f180
commit e47a9f4401
173 changed files with 11 additions and 5 deletions

View file

@ -0,0 +1,293 @@
#!/usr/bin/env python3
"""
ASW Flask/FastAPI Error Pack styled error responses for Python web frameworks.
Drop this into your project directory and call register_asw_errors(app).
Usage (Flask):
from flask import Flask
from asw_errors import register_asw_errors
app = Flask(__name__)
register_asw_errors(app)
Usage (FastAPI):
from fastapi import FastAPI
from asw_errors import register_asw_errors
app = FastAPI()
register_asw_errors(app)
Optional: link to a hosted asw.css instead of inline styles
register_asw_errors(app, css_url="/static/asw.css")
register_asw_errors(app, css_url="https://cdn.example.com/asw.css")
"""
import html as _html
# ── Inline styles ─────────────────────────────────────────────────────────────
# Minimal ASW tokens — no external dependency. Matches asw.css dark theme.
_ASW_INLINE = """
:root {
--asw-bg: #0d1117;
--asw-bg-elevated: #161b22;
--asw-bg-overlay: #1c2128;
--asw-text: #e6edf3;
--asw-text-secondary: #8b949e;
--asw-text-muted: #484f58;
--asw-accent: #3fb950;
--asw-border: #30363d;
--asw-font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
--asw-font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', ui-monospace, monospace;
}
*, *::before, *::after { box-sizing: border-box; }
html { font-size: 16px; }
body {
background: var(--asw-bg);
color: var(--asw-text);
font-family: var(--asw-font-body);
font-size: 1rem;
line-height: 1.6;
margin: 0;
display: flex;
flex-direction: column;
min-height: 100vh;
}
nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1.5rem;
border-bottom: 1px solid var(--asw-border);
background: var(--asw-bg-elevated);
}
nav a { color: var(--asw-text); text-decoration: none; font-weight: 600; }
nav a:hover { color: var(--asw-accent); }
.badge {
font-family: var(--asw-font-mono);
font-size: 0.75rem;
color: var(--asw-text-muted);
background: var(--asw-bg-overlay);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
border: 1px solid var(--asw-border);
}
main {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.error-card {
max-width: 42ch;
text-align: center;
}
.error-code {
font-family: var(--asw-font-mono);
font-size: 6rem;
font-weight: 700;
line-height: 1;
color: var(--asw-text-muted);
letter-spacing: -0.05em;
margin: 0 0 0.5rem;
}
h1 {
margin: 0 0 1rem;
font-size: 1.5rem;
color: var(--asw-text);
}
p { margin: 0 0 0.75rem; color: var(--asw-text-secondary); }
a.button {
display: inline-block;
margin-top: 1.5rem;
padding: 0.5rem 1.25rem;
background: transparent;
color: var(--asw-accent);
border: 1px solid var(--asw-accent);
border-radius: 0.375rem;
text-decoration: none;
font-size: 0.875rem;
transition: background 0.15s;
}
a.button:hover { background: rgba(63, 185, 80, 0.1); }
code {
font-family: var(--asw-font-mono);
font-size: 0.85em;
background: var(--asw-bg-overlay);
padding: 0.1em 0.35em;
border-radius: 0.2rem;
color: var(--asw-text-secondary);
}
footer {
text-align: center;
padding: 1rem;
font-size: 0.75rem;
color: var(--asw-text-muted);
border-top: 1px solid var(--asw-border);
}
footer a { color: var(--asw-text-muted); }
"""
_ERROR_MESSAGES = {
400: ("Bad Request", "The server couldn't understand the request."),
401: ("Unauthorized", "Authentication required. Please sign in."),
403: ("Forbidden", "You don't have permission to access this resource."),
404: ("Not Found", "The page or resource you requested doesn't exist."),
405: ("Method Not Allowed", "That HTTP method isn't supported for this endpoint."),
408: ("Request Timeout", "The request took too long. Please try again."),
409: ("Conflict", "The request conflicts with the current state of the server."),
410: ("Gone", "This resource has been permanently removed."),
415: ("Unsupported Media Type", "The Content-Type you sent isn't accepted."),
422: ("Unprocessable Entity", "The request was well-formed but contains invalid data."),
429: ("Too Many Requests", "You've sent too many requests. Please slow down."),
500: ("Internal Server Error", "Something went wrong on the server."),
501: ("Not Implemented", "This feature hasn't been implemented yet."),
502: ("Bad Gateway", "The server received an invalid response from upstream."),
503: ("Service Unavailable", "The server is temporarily unavailable. Try again soon."),
504: ("Gateway Timeout", "The upstream server didn't respond in time."),
}
# Codes that receive an extra hint about the path in the response
_SHOW_PATH_CODES = {404, 403, 405}
def _make_style_tag(css_url=None):
if css_url:
return f'<link rel="stylesheet" href="{_html.escape(css_url)}">'
return f"<style>{_ASW_INLINE}</style>"
def error_html(code: int, path: str = "", css_url=None, app_name: str = "API") -> str:
"""
Generate an ASW-styled error page as an HTML string.
Args:
code: HTTP status code
path: Request path (shown for 403/404/405)
css_url: Optional URL to link instead of inlining styles
app_name: Shown in the nav bar (default: "API")
"""
title, message = _ERROR_MESSAGES.get(code, ("Error", "An error occurred."))
style_block = _make_style_tag(css_url)
path_hint = ""
if path and code in _SHOW_PATH_CODES:
escaped = _html.escape(path)
path_hint = f'<p>Path: <code>{escaped}</code></p>'
return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{code} {title}</title>
{style_block}
</head>
<body>
<nav>
<a href="/">{_html.escape(app_name)}</a>
<span class="badge">{code}</span>
</nav>
<main>
<div class="error-card">
<p class="error-code">{code}</p>
<h1>{title}</h1>
<p>{message}</p>
{path_hint}
<a class="button" href="/"> Back</a>
</div>
</main>
<footer>Powered by <a href="https://github.com/trentuna/agentic-semantic-web">agentic-semantic-web</a></footer>
</body>
</html>"""
# ── Flask registration ────────────────────────────────────────────────────────
def _register_flask(app, css_url, app_name):
"""Register error handlers on a Flask application."""
from flask import request as flask_request
from flask import Response as FlaskResponse
codes = list(_ERROR_MESSAGES.keys())
def make_handler(code):
def handler(e):
path = getattr(flask_request, "path", "")
body = error_html(code, path=path, css_url=css_url, app_name=app_name)
return FlaskResponse(body, status=code, mimetype="text/html")
handler.__name__ = f"asw_error_{code}"
return handler
for code in codes:
app.register_error_handler(code, make_handler(code))
# ── FastAPI / Starlette registration ──────────────────────────────────────────
def _register_fastapi(app, css_url, app_name):
"""Register exception handlers on a FastAPI application."""
from starlette.requests import Request
from starlette.responses import HTMLResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
code = exc.status_code
path = request.url.path
body = error_html(code, path=path, css_url=css_url, app_name=app_name)
return HTMLResponse(content=body, status_code=code)
# Catch all Starlette HTTP exceptions (covers 4xx and 5xx)
app.add_exception_handler(StarletteHTTPException, http_exception_handler)
# Also catch FastAPI's RequestValidationError (unprocessable entity — 422)
try:
from fastapi.exceptions import RequestValidationError
async def validation_exception_handler(request: Request, exc: RequestValidationError):
path = request.url.path
body = error_html(422, path=path, css_url=css_url, app_name=app_name)
return HTMLResponse(content=body, status_code=422)
app.add_exception_handler(RequestValidationError, validation_exception_handler)
except ImportError:
pass # Pure Starlette without FastAPI — skip
# ── Public API ────────────────────────────────────────────────────────────────
def register_asw_errors(app, css_url=None, app_name=None):
"""
Register ASW-styled HTTP error handlers on a Flask or FastAPI application.
Args:
app: Flask or FastAPI application instance
css_url: Optional URL to link asw.css instead of inlining styles.
Use this when your app already serves asw.css:
register_asw_errors(app, css_url="/static/asw.css")
app_name: Label shown in the nav bar. Defaults to the app's name.
Raises:
TypeError: If the app type is not recognized as Flask or FastAPI.
"""
app_type = type(app).__name__
# Resolve app_name from app if not provided
if app_name is None:
if app_type == "Flask":
app_name = app.name or "Flask App"
elif app_type == "FastAPI":
app_name = app.title or "FastAPI"
else:
app_name = "API"
if app_type == "Flask":
_register_flask(app, css_url, app_name)
elif app_type == "FastAPI":
_register_fastapi(app, css_url, app_name)
else:
raise TypeError(
f"Unsupported app type: {app_type!r}. "
"Expected 'Flask' or 'FastAPI'."
)