add tailwindcss
This commit is contained in:
parent
a426df12c0
commit
16e207000f
20 changed files with 2421 additions and 100 deletions
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
21
src/fastapi_dynamic_response/constant.py
Normal file
21
src/fastapi_dynamic_response/constant.py
Normal 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",
|
||||
}
|
||||
|
|
@ -2,3 +2,4 @@ from fastapi.templating import Jinja2Templates
|
|||
|
||||
is_ready = False
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
routes = []
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue