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:
parent
416fe2f180
commit
e47a9f4401
173 changed files with 11 additions and 5 deletions
293
archive/packs/flask/asw_errors.py
Normal file
293
archive/packs/flask/asw_errors.py
Normal 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'."
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue