add tailwindcss

This commit is contained in:
Waylon S. Walker 2024-10-14 19:56:41 -05:00
parent a426df12c0
commit 16e207000f
20 changed files with 2421 additions and 100 deletions

View file

@ -1,8 +1,10 @@
from fastapi import APIRouter, Depends, Request
from fastapi import APIRouter
from fastapi import Depends
from fastapi import Request
from fastapi_dynamic_response.base.schema import Message
from fastapi_dynamic_response.dependencies import get_content_type
router = APIRouter()

View file

@ -0,0 +1,21 @@
ACCEPT_TYPES = {
"application/json": "JSON",
"text/html": "html",
"application/html": "html",
"text/html-partial": "html",
"text/html-fragment": "html",
"text/rich": "rtf",
"application/rtf": "rtf",
"text/rtf": "rtf",
"text/plain": "text",
"application/text": "text",
"application/markdown": "markdown",
"text/markdown": "markdown",
"text/x-markdown": "markdown",
"json": "JSON",
"html": "html",
"rtf": "rtf",
"plain": "text",
"markdown": "markdown",
"md": "markdown",
}

View file

@ -2,3 +2,4 @@ from fastapi.templating import Jinja2Templates
is_ready = False
templates = Jinja2Templates(directory="templates")
routes = []

View file

@ -1,19 +1,37 @@
from fastapi import Depends, FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi_dynamic_response import globals
from fastapi_dynamic_response.__about__ import __version__
from fastapi_dynamic_response.base.router import router as base_router
from fastapi_dynamic_response.dependencies import get_content_type
from fastapi_dynamic_response.middleware import (
Sitemap,
catch_exceptions_middleware,
respond_based_on_content_type,
set_prefers,
)
from fastapi_dynamic_response.zpages.router import router as zpages_router
app = FastAPI(debug=True)
app = FastAPI(
title="FastAPI Dynamic Response",
version=__version__,
docs_url=None,
redoc_url=None,
openapi_url=None,
# openapi_tags=tags_metadata,
# exception_handlers=exception_handlers,
debug=True,
dependencies=[
Depends(set_prefers),
],
)
app.include_router(zpages_router)
app.include_router(base_router)
app.middleware("http")(Sitemap(app))
app.middleware("http")(catch_exceptions_middleware)
app.middleware("http")(respond_based_on_content_type)
app.mount("/static", StaticFiles(directory="static"), name="static")
# Flag to indicate if the application is ready
@ -24,6 +42,7 @@ async def startup_event():
# Perform startup actions, e.g., database connections
# If all startup actions are successful, set is_ready to True
globals.is_ready = True
globals.routes = [route.path for route in app.router.routes if route.path]
@app.get("/sitemap")

View file

@ -1,4 +1,9 @@
from difflib import get_close_matches
from io import BytesIO
import json
import traceback
from typing import Any, Dict
from fastapi import Request, Response
from fastapi.exceptions import (
HTTPException as StarletteHTTPException,
@ -6,19 +11,87 @@ from fastapi.exceptions import (
)
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
import html2text
from io import BytesIO
import json
from pydantic import BaseModel, model_validator
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import traceback
from weasyprint import HTML as WeasyHTML
from weasyprint import HTML as WEAZYHTML
from fastapi_dynamic_response.constant import ACCEPT_TYPES
from fastapi_dynamic_response.globals import templates
class Prefers(BaseModel):
JSON: bool = False
html: bool = False
rtf: bool = False
text: bool = False
markdown: bool = False
partial: bool = False
@property
def textlike(self) -> bool:
return self.rtf or self.text or self.markdown
@model_validator(mode="after")
def check_one_true(self) -> Dict[str, Any]:
format_flags = [self.JSON, self.html, self.rtf, self.text, self.markdown]
if format_flags.count(True) != 1:
message = "Exactly one of JSON, html, rtf, text, or markdown must be True."
raise ValueError(message)
def set_prefers(
request: Request,
):
content_type = (
request.query_params.get("content_type")
or request.headers.get("content-type")
or request.headers.get("accept", None)
).lower()
if content_type == "*/*":
content_type = None
hx_request_header = request.headers.get("hx-request")
user_agent = request.headers.get("user-agent", "").lower()
if hx_request_header == "true":
request.state.prefers = Prefers(html=True, partial=True)
return
if is_browser_request(user_agent) and content_type is None:
content_type = "text/html"
elif is_rtf_request(user_agent) and content_type is None:
content_type = "text/rtf"
elif content_type is None:
content_type = "application/json"
# if content_type in ACCEPT_TYPES:
for accept_type, accept_value in ACCEPT_TYPES.items():
if accept_type in content_type:
request.state.prefers = Prefers(**{ACCEPT_TYPES[accept_value]: True})
print("content_type:", content_type)
print("prefers:", request.state.prefers)
return
request.state.prefers = Prefers(JSON=True, partial=False)
print("prefers:", request.state.prefers)
print("content_type:", content_type)
class Sitemap:
def __init__(self, app):
self.app = app
async def __call__(self, request: Request, call_next):
request.state.routes = [
route.path for route in self.app.router.routes if route.path
]
return await call_next(request)
async def catch_exceptions_middleware(request: Request, call_next):
try:
return await call_next(request)
@ -102,17 +175,34 @@ def format_json_as_rich_text(data: dict, template_name: str) -> str:
return capture.get()
def handle_not_found(request: Request, data: str):
async def respond_based_on_content_type(
request: Request,
call_next,
content_type: str,
data: str,
):
requested_path = request.url.path
available_routes = [route.path for route in app.router.routes if route.path]
suggestions = get_close_matches(requested_path, available_routes, n=3, cutoff=0.5)
if requested_path in ["/docs", "/redoc", "/openapi.json"]:
return await call_next(request)
return await call_next(request)
def handle_not_found(request: Request, call_next, data: str):
requested_path = request.url.path
# available_routes = [route.path for route in app.router.routes if route.path]
suggestions = get_close_matches(
requested_path, request.state.routes, n=3, cutoff=0.5
)
request.state.template_name = "404.html"
return templates.TemplateResponse(
"404.html",
{
"request": request,
"data": json.loads(data),
"available_routes": available_routes,
"available_routes": request.state.routes,
"requested_path": requested_path,
"suggestions": suggestions,
},
@ -151,13 +241,17 @@ async def respond_based_on_content_type(request: Request, call_next):
data = body.decode("utf-8")
if response.status_code == 404:
return handle_not_found(request, data)
return handle_not_found(
request=request,
call_next=call_next,
data=data,
)
if response.status_code == 422:
return response
if str(response.status_code)[0] not in "123":
return response
return await handle_response(request, data, content_type)
return await handle_response(request, data)
# except TemplateNotFound:
# return HTMLResponse(content="Template Not Found ", status_code=404)
except StarletteHTTPException as exc:
@ -169,32 +263,26 @@ async def respond_based_on_content_type(request: Request, call_next):
return JSONResponse(status_code=422, content={"detail": exc.errors()})
except Exception as e:
print(traceback.format_exc())
return HTMLResponse(content=f"Internal Server Error: {str(e)}", status_code=500)
return HTMLResponse(content=f"Internal Server Error: {e!s}", status_code=500)
async def handle_response(request: Request, data: str, content_type: str):
async def handle_response(request: Request, data: str):
json_data = json.loads(data)
template_name = getattr(request.state, "template_name", "default_template.html")
if request.state.prefers.partial:
template_name = "partial_" + template_name
content_type = request.state.prefers
if content_type == "application/json":
if request.state.prefers.JSON:
return JSONResponse(content=json_data)
elif content_type == "text/html":
elif request.state.prefers.html:
return templates.TemplateResponse(
template_name, {"request": request, "data": json_data}
)
elif content_type == "text/html-partial":
return templates.TemplateResponse(
template_name, {"request": request, "data": json_data}
)
elif (
content_type == "text/markdown"
or content_type == "md"
or content_type == "markdown"
):
elif request.state.prefers.markdown:
import html2text
template = templates.get_template(template_name)
@ -202,15 +290,11 @@ async def handle_response(request: Request, data: str, content_type: str):
markdown_content = html2text.html2text(html_content)
return PlainTextResponse(content=markdown_content)
elif content_type == "text/plain":
elif request.state.prefers.text:
plain_text_content = format_json_as_plain_text(json_data)
return PlainTextResponse(content=plain_text_content)
elif (
content_type == "text/rich"
or content_type == "text/rtf"
or content_type == "application/rtf"
):
elif request.state.prefers.rtf:
rich_text_content = format_json_as_rich_text(json_data, template_name)
return PlainTextResponse(content=rich_text_content)
@ -223,7 +307,7 @@ async def handle_response(request: Request, data: str, content_type: str):
elif content_type == "application/pdf":
template = templates.get_template(template_name)
html_content = template.render(data=json_data)
pdf = WeasyHTML(string=html_content).write_pdf()
pdf = WEAZYHTML(string=html_content).write_pdf()
return Response(content=pdf, media_type="application/pdf")
return JSONResponse(content=json_data)

View file

@ -1,4 +1,5 @@
from fastapi import APIRouter, HTTPException, Request
from fastapi_dynamic_response import globals
router = APIRouter()
@ -36,7 +37,7 @@ async def healthz(request: Request):
Returns 503 Service Unavailable if not healthy.
"""
request.state.template_name = "status.html"
if is_ready:
if globals.is_ready:
return {"status": "healthy"}
else:
raise HTTPException(status_code=503, detail="Unhealthy")