diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 309b99d..0000000 --- a/Dockerfile +++ /dev/null @@ -1,136 +0,0 @@ -FROM ubuntu:noble AS build - -# The following does not work in Podman unless you build in Docker -# compatibility mode: -# You can manually prepend every RUN script with `set -ex` too. -# SHELL ["sh", "-exc"] - -### Start build prep. -### This should be a separate build container for better reuse. - -RUN < src/fastapi_dynamic_response/__about__.py -touch README.md -uv sync \ - --locked \ - --no-dev \ - --no-install-project -EOT - -# Now install the APPLICATION from `/src` without any dependencies. -# `/src` will NOT be copied into the runtime container. -# LEAVE THIS OUT if your application is NOT a proper Python package. -# As of uv 0.4.11, you can also use -# `cd /src && uv sync --locked --no-dev --no-editable` instead. -COPY . /src -RUN --mount=type=cache,target=/root/.cache \ - uv pip install \ - --python=$UV_PROJECT_ENVIRONMENT \ - --no-deps \ - /src - - -########################################################################## - -FROM ubuntu:noble -# SHELL ["sh", "-exc"] - -# Optional: add the application virtualenv to search path. -ENV PATH=/app/bin:$PATH - -# Don't run your app as root. -RUN <. -STOPSIGNAL SIGINT - -# Note how the runtime dependencies differ from build-time ones. -# Notably, there is no uv either! -RUN <=0.115.0", "html2text>=2024.2.26", - "itsdangerous>=2.2.0", "jinja2>=3.1.4", "markdown>=3.7", "pillow>=10.4.0", - "pydantic-settings>=2.5.2", "pydyf==0.8.0", "python-levenshtein>=0.25.1", "rich>=13.9.2", "selenium>=4.25.0", - "structlog>=24.4.0", - "typer>=0.12.5", "uvicorn>=0.31.1", "weasyprint>=61.2", ] @@ -47,9 +43,6 @@ Documentation = "https://github.com/U.N. Owen/fastapi-dynamic-response#readme" Issues = "https://github.com/U.N. Owen/fastapi-dynamic-response/issues" Source = "https://github.com/U.N. Owen/fastapi-dynamic-response" -[project.scripts] -fdr_app = "fastapi_dynamic_response.cli.cli:app" - [tool.hatch.version] path = "src/fastapi_dynamic_response/__about__.py" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2b01b01..0000000 --- a/requirements.txt +++ /dev/null @@ -1,131 +0,0 @@ -# This file was autogenerated by uv via the following command: -# uv pip compile pyproject.toml -o requirements.txt -annotated-types==0.7.0 - # via pydantic -anyio==4.6.2.post1 - # via starlette -attrs==24.2.0 - # via - # outcome - # trio -brotli==1.1.0 - # via fonttools -certifi==2024.8.30 - # via selenium -cffi==1.17.1 - # via weasyprint -click==8.1.7 - # via uvicorn -cssselect2==0.7.0 - # via weasyprint -fastapi==0.115.3 - # via fastapi-dynamic-response (pyproject.toml) -fonttools==4.54.1 - # via weasyprint -h11==0.14.0 - # via - # uvicorn - # wsproto -html2text==2024.2.26 - # via fastapi-dynamic-response (pyproject.toml) -html5lib==1.1 - # via weasyprint -idna==3.10 - # via - # anyio - # trio -itsdangerous==2.2.0 - # via fastapi-dynamic-response (pyproject.toml) -jinja2==3.1.4 - # via fastapi-dynamic-response (pyproject.toml) -levenshtein==0.26.0 - # via python-levenshtein -markdown==3.7 - # via fastapi-dynamic-response (pyproject.toml) -markdown-it-py==3.0.0 - # via rich -markupsafe==3.0.2 - # via jinja2 -mdurl==0.1.2 - # via markdown-it-py -outcome==1.3.0.post0 - # via trio -pillow==11.0.0 - # via - # fastapi-dynamic-response (pyproject.toml) - # weasyprint -pycparser==2.22 - # via cffi -pydantic==2.9.2 - # via - # fastapi - # pydantic-settings -pydantic-core==2.23.4 - # via pydantic -pydantic-settings==2.6.0 - # via fastapi-dynamic-response (pyproject.toml) -pydyf==0.8.0 - # via - # fastapi-dynamic-response (pyproject.toml) - # weasyprint -pygments==2.18.0 - # via rich -pyphen==0.16.0 - # via weasyprint -pysocks==1.7.1 - # via urllib3 -python-dotenv==1.0.1 - # via pydantic-settings -python-levenshtein==0.26.0 - # via fastapi-dynamic-response (pyproject.toml) -rapidfuzz==3.10.0 - # via levenshtein -rich==13.9.3 - # via fastapi-dynamic-response (pyproject.toml) -selenium==4.25.0 - # via fastapi-dynamic-response (pyproject.toml) -six==1.16.0 - # via html5lib -sniffio==1.3.1 - # via - # anyio - # trio -sortedcontainers==2.4.0 - # via trio -starlette==0.41.0 - # via fastapi -structlog==24.4.0 - # via fastapi-dynamic-response (pyproject.toml) -tinycss2==1.3.0 - # via - # cssselect2 - # weasyprint -trio==0.27.0 - # via - # selenium - # trio-websocket -trio-websocket==0.11.1 - # via selenium -typing-extensions==4.12.2 - # via - # fastapi - # pydantic - # pydantic-core - # selenium -urllib3==2.2.3 - # via selenium -uvicorn==0.32.0 - # via fastapi-dynamic-response (pyproject.toml) -weasyprint==61.2 - # via fastapi-dynamic-response (pyproject.toml) -webencodings==0.5.1 - # via - # cssselect2 - # html5lib - # tinycss2 -websocket-client==1.8.0 - # via selenium -wsproto==1.2.0 - # via trio-websocket -zopfli==0.2.3.post1 - # via fonttools diff --git a/sitemap.html b/sitemap.html deleted file mode 100644 index 189bbf6..0000000 --- a/sitemap.html +++ /dev/null @@ -1,612 +0,0 @@ - - - - - - Sitemap - - - - - -
- -
- -
- -

Sitemap

- - -
  • /livez
  • - -
  • /readyz
  • - -
  • /healthz
  • - -
  • /example
  • - -
  • /another-example
  • - -
  • /message
  • - -
  • /static
  • - -
  • /sitemap
  • - - -
    - -
    -

    © 2024 FastApi Dynamic Response

    -
    - - diff --git a/sitemap.pdf b/sitemap.pdf deleted file mode 100644 index e69de29..0000000 diff --git a/sitemap.png b/sitemap.png deleted file mode 100644 index cc770d5..0000000 Binary files a/sitemap.png and /dev/null differ diff --git a/src/fastapi_dynamic_response/auth.py b/src/fastapi_dynamic_response/auth.py deleted file mode 100644 index 1502dc9..0000000 --- a/src/fastapi_dynamic_response/auth.py +++ /dev/null @@ -1,117 +0,0 @@ -from fastapi import HTTPException, Request -from functools import wraps -from starlette.authentication import AuthCredentials, AuthenticationBackend, SimpleUser -from typing import Dict - -# In-memory user database for demonstration purposes -AUTH_DB: Dict[str, str] = { - "user1": "password123", - "user2": "securepassword", - "user3": "supersecurepassword", -} - -SCOPES = { - "authenticated": "Authenticated users", - "admin": "Admin users", - "superuser": "Superuser", -} - -USER_SCOPES = { - "user1": ["authenticated"], - "user2": ["authenticated", "admin"], - "user3": ["authenticated", "admin", "superuser"], -} - - -def authenticated(func: callable): - @wraps(func) - async def wrapper(request: Request, *args, **kwargs): - if not request.user.is_authenticated: - raise HTTPException(status_code=401, detail="Authentication required") - return await func(request, *args, **kwargs) - - return wrapper - - -def admin(func: callable): - @wraps(func) - async def wrapper(request: Request, *args, **kwargs): - if not request.user.is_authenticated: - raise HTTPException(status_code=401, detail="Authentication required") - if "admin" not in request.user.scopes: - raise HTTPException(status_code=403, detail="Admin access required") - return await func(request, *args, **kwargs) - - return wrapper - - -def has_scope(scope: str): - def decorator(func: callable): - @wraps(func) - async def wrapper(request: Request, *args, **kwargs): - if not request.user.is_authenticated: - raise HTTPException(status_code=401, detail="Authentication required") - if scope not in request.auth.scopes: - raise HTTPException(status_code=403, detail="Access denied") - return await func(request, *args, **kwargs) - - return wrapper - - return decorator - - -class BasicAuthBackend(AuthenticationBackend): - """Custom authentication backend that validates Basic auth credentials.""" - - async def authenticate(self, request: Request): - # Extract the 'Authorization' header from the request - auth_header = request.headers.get("Authorization") - - if not auth_header: - return None # No credentials provided - - try: - # Basic authentication: "Basic :" - auth_type, credentials = auth_header.split(" ", 1) - if auth_type != "Basic": - return None # Unsupported auth type - - username, password = credentials.split(":") - except ValueError: - raise HTTPException(status_code=400, detail="Invalid Authorization format") - - # Validate credentials against the in-memory AUTH_DB - if AUTH_DB.get(username) != password: - raise HTTPException(status_code=401, detail="Invalid credentials") - - # If valid, return user object and auth credentials - return AuthCredentials(USER_SCOPES[username]), SimpleUser(username) - - -# # Initialize FastAPI app -# app = FastAPI() -# -# # Add AuthenticationMiddleware to FastAPI with the custom backend -# app.add_middleware(AuthenticationMiddleware, backend=BasicAuthBackend()) -# -# # Add SessionMiddleware with a secret key -# app.add_middleware(SessionMiddleware, secret_key="your-secret-key") - -# @app.get("/") -# async def public(): -# """Public route.""" -# return {"message": "This route is accessible to everyone!"} -# -# @app.get("/private") -# async def private(request: Request): -# """Private route that requires authentication.""" -# if not request.user.is_authenticated: -# raise HTTPException(status_code=401, detail="Authentication required") -# -# return {"message": f"Welcome, {request.user.display_name}!"} -# -# @app.get("/session") -# async def session_info(request: Request): -# """Access session data.""" -# request.session["example"] = "This is session data" -# return {"session_data": request.session} diff --git a/src/fastapi_dynamic_response/base/router.py b/src/fastapi_dynamic_response/base/router.py index eedb953..5112381 100644 --- a/src/fastapi_dynamic_response/base/router.py +++ b/src/fastapi_dynamic_response/base/router.py @@ -1,6 +1,7 @@ -from fastapi import APIRouter, Depends, Request +from fastapi import APIRouter +from fastapi import Depends +from fastapi import Request -from fastapi_dynamic_response.auth import admin, authenticated, has_scope from fastapi_dynamic_response.base.schema import Message from fastapi_dynamic_response.dependencies import get_content_type @@ -16,46 +17,6 @@ async def get_example( return {"message": "Hello, this is an example", "data": [1, 2, 3, 4]} -@router.get("/private") -@authenticated -async def get_private( - request: Request, - content_type: str = Depends(get_content_type), -): - request.state.template_name = "example.html" - return {"message": "This page is private", "data": [1, 2, 3, 4]} - - -@router.get("/admin") -@admin -async def get_admin( - request: Request, - content_type: str = Depends(get_content_type), -): - request.state.template_name = "example.html" - return {"message": "This is only for admin users", "data": [1, 2, 3, 4]} - - -@router.get("/superuser") -@has_scope("superuser") -async def get_superuser( - request: Request, - content_type: str = Depends(get_content_type), -): - request.state.template_name = "example.html" - return {"message": "This is only for superusers", "data": [1, 2, 3, 4]} - - -@router.get("/error") -async def get_error( - request: Request, - content_type: str = Depends(get_content_type), -): - request.state.template_name = "example.html" - 0 / 0 - return {"message": "Hello, this is an example", "data": [1, 2, 3, 4]} - - @router.get("/another-example") async def another_example( request: Request, @@ -69,16 +30,6 @@ async def another_example( } -@router.get("/message") -async def message( - request: Request, - message_id: int, - content_type: str = Depends(get_content_type), -): - request.state.template_name = "post_message.html" - return {"message": message.message} - - @router.post("/message") async def message( request: Request, diff --git a/src/fastapi_dynamic_response/cli/.null-ls_858127_app.py b/src/fastapi_dynamic_response/cli/.null-ls_858127_app.py deleted file mode 100644 index 71e9e28..0000000 --- a/src/fastapi_dynamic_response/cli/.null-ls_858127_app.py +++ /dev/null @@ -1,26 +0,0 @@ -import typer -import uvicorn - -from fastapi_dynamic_response import settings - - -app_app = typer.Typer() - - -@app_app.callback() -def app(): - "model cli" - - -@app_app.command() -def run( - env: str = typer.Option( - "local", - help="the environment to use", - ), -): - uvicorn.run(**settings.api_server.dict()) - - -if __name__ == "__main__": - app_app() diff --git a/src/fastapi_dynamic_response/cli/app.py b/src/fastapi_dynamic_response/cli/app.py deleted file mode 100644 index 417d622..0000000 --- a/src/fastapi_dynamic_response/cli/app.py +++ /dev/null @@ -1,26 +0,0 @@ -import typer -import uvicorn - -from fastapi_dynamic_response.settings import settings - - -app_app = typer.Typer() - - -@app_app.callback() -def app(): - "model cli" - - -@app_app.command() -def run( - env: str = typer.Option( - "local", - help="the environment to use", - ), -): - uvicorn.run(**settings.api_server.dict()) - - -if __name__ == "__main__": - app_app() diff --git a/src/fastapi_dynamic_response/cli/cli.py b/src/fastapi_dynamic_response/cli/cli.py deleted file mode 100644 index 27be712..0000000 --- a/src/fastapi_dynamic_response/cli/cli.py +++ /dev/null @@ -1,7 +0,0 @@ -import typer - -from fastapi_dynamic_response.cli.app import app_app - -app = typer.Typer() - -app.add_typer(app_app, name="app") diff --git a/src/fastapi_dynamic_response/constant.py b/src/fastapi_dynamic_response/constant.py index 4bb76f6..e25d785 100644 --- a/src/fastapi_dynamic_response/constant.py +++ b/src/fastapi_dynamic_response/constant.py @@ -1,32 +1,21 @@ ACCEPT_TYPES = { - "application/html": "html", - "application/html-partial": "html", "application/json": "JSON", - "application/markdown": "markdown", - "application/md": "markdown", - "application/pdf": "pdf", - "application/plain": "text", + "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", - "html": "html", - "image/png": "png", + "application/markdown": "markdown", + "text/markdown": "markdown", + "text/x-markdown": "markdown", "json": "JSON", + "html": "html", + "rtf": "rtf", + "plain": "text", "markdown": "markdown", "md": "markdown", - "pdf": "pdf", - "plain": "text", - "png": "png", - "rich": "rtf", - "richtext": "rtf", - "richtextformat": "rtf", - "rtf": "rtf", - "text": "text", - "text/html": "html", - "text/html-partial": "html", - "text/markdown": "markdown", - "text/md": "markdown", - "text/plain": "text", - "text/rich": "rtf", - "text/rtf": "rtf", - "text/x-markdown": "markdown", } diff --git a/src/fastapi_dynamic_response/logging_config.py b/src/fastapi_dynamic_response/logging_config.py deleted file mode 100644 index 80f0a3f..0000000 --- a/src/fastapi_dynamic_response/logging_config.py +++ /dev/null @@ -1,73 +0,0 @@ -# logging_config.py - - -import logging - -from fastapi_dynamic_response.settings import settings -import structlog - -logger = structlog.get_logger() - - -def configure_logging(): - # Clear existing loggers - logging.config.dictConfig( - { - "version": 1, - "disable_existing_loggers": False, - } - ) - - if settings.ENV == "local": - # Local development logging configuration - processors = [ - # structlog.processors.TimeStamper(fmt="iso"), - structlog.dev.ConsoleRenderer(colors=False), - ] - logging_level = logging.DEBUG - - # Enable rich tracebacks - from rich.traceback import install - - install(show_locals=True) - - # Use RichHandler for pretty console logs - from rich.logging import RichHandler - - logging.basicConfig( - level=logging_level, - format="%(message)s", - datefmt="[%X]", - handlers=[RichHandler()], - ) - else: - # Production logging configuration - processors = [ - structlog.processors.TimeStamper(fmt="iso"), - structlog.processors.JSONRenderer(), - ] - logging_level = logging.INFO - - # Standard logging configuration - logging.basicConfig( - format="%(message)s", - level=logging_level, - handlers=[logging.StreamHandler()], - ) - - structlog.configure( - processors=processors, - wrapper_class=structlog.make_filtering_bound_logger(logging_level), - context_class=dict, - logger_factory=structlog.stdlib.LoggerFactory(), - cache_logger_on_first_use=True, - ) - - # Redirect uvicorn loggers to structlog - for logger_name in ("uvicorn", "uvicorn.error", "uvicorn.access"): - logger = logging.getLogger(logger_name) - logger.handlers = [] - logger.propagate = True - - logger.info("Logging configured") - logger.info(f"Environment: {settings.ENV}") diff --git a/src/fastapi_dynamic_response/main.py b/src/fastapi_dynamic_response/main.py index b6bad64..e395df1 100644 --- a/src/fastapi_dynamic_response/main.py +++ b/src/fastapi_dynamic_response/main.py @@ -1,27 +1,18 @@ -from fastapi import FastAPI +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.zpages.router import router as zpages_router - -from fastapi_dynamic_response.settings import settings - -from fastapi_dynamic_response.logging_config import configure_logging from fastapi_dynamic_response.middleware import ( Sitemap, - add_process_time_header, catch_exceptions_middleware, - log_requests, respond_based_on_content_type, - set_bound_logger, set_prefers, - set_span_id, ) +from fastapi_dynamic_response.zpages.router import router as zpages_router -configure_logging() app = FastAPI( title="FastAPI Dynamic Response", version=__version__, @@ -30,35 +21,20 @@ app = FastAPI( openapi_url=None, # openapi_tags=tags_metadata, # exception_handlers=exception_handlers, - debug=settings.DEBUG, + debug=True, dependencies=[ - # Depends(set_prefers), - # Depends(set_span_id), - # Depends(log_request_state), + Depends(set_prefers), ], ) - -# configure_tracing(app) - app.include_router(zpages_router) app.include_router(base_router) -app.middleware("http")(respond_based_on_content_type) -app.middleware("http")(add_process_time_header) -app.middleware("http")(log_requests) app.middleware("http")(Sitemap(app)) -app.middleware("http")(set_prefers) -app.middleware("http")(set_span_id) app.middleware("http")(catch_exceptions_middleware) -app.middleware("http")(set_bound_logger) +app.middleware("http")(respond_based_on_content_type) app.mount("/static", StaticFiles(directory="static"), name="static") -from fastapi import Depends, Request -from fastapi_dynamic_response.auth import BasicAuthBackend -from starlette.middleware.authentication import AuthenticationMiddleware -from starlette.middleware.sessions import SessionMiddleware -app.add_middleware(AuthenticationMiddleware, backend=BasicAuthBackend()) -app.add_middleware(SessionMiddleware, secret_key="your-secret-key") +# Flag to indicate if the application is ready @app.on_event("startup") diff --git a/src/fastapi_dynamic_response/middleware.py b/src/fastapi_dynamic_response/middleware.py index 7cf923d..d80e317 100644 --- a/src/fastapi_dynamic_response/middleware.py +++ b/src/fastapi_dynamic_response/middleware.py @@ -1,13 +1,14 @@ from difflib import get_close_matches -from fastapi_dynamic_response.settings import settings from io import BytesIO import json -import time import traceback from typing import Any, Dict -from uuid import uuid4 from fastapi import Request, Response +from fastapi.exceptions import ( + HTTPException as StarletteHTTPException, + RequestValidationError, +) from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse import html2text from pydantic import BaseModel, model_validator @@ -16,17 +17,11 @@ from rich.markdown import Markdown from rich.panel import Panel from selenium import webdriver from selenium.webdriver.chrome.options import Options +from weasyprint import HTML as WEAZYHTML -import base64 from fastapi_dynamic_response.constant import ACCEPT_TYPES from fastapi_dynamic_response.globals import templates -import structlog - -logger = structlog.get_logger() - -console = Console() - class Prefers(BaseModel): JSON: bool = False @@ -35,15 +30,6 @@ class Prefers(BaseModel): text: bool = False markdown: bool = False partial: bool = False - png: bool = False - pdf: bool = False - - def __repr__(self): - _repr = [] - for key, value in self.dict().items(): - if value: - _repr.append(key + "=True") - return f'Prefers({", ".join(_repr)})' @property def textlike(self) -> bool: @@ -51,136 +37,48 @@ class Prefers(BaseModel): @model_validator(mode="after") def check_one_true(self) -> Dict[str, Any]: - format_flags = [ - self.JSON, - self.html, - self.rtf, - self.text, - self.markdown, - self.png, - self.pdf, - ] + 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) - return self - - -def log_request_state(request: Request): - console.log(request.state.span_id) - console.log(request.url.path) - console.log(request.state.prefers) - - -async def add_process_time_header(request: Request, call_next): - start_time = time.perf_counter() - response = await call_next(request) - process_time = time.perf_counter() - start_time - if str(response.status_code)[0] in "123": - response.headers["X-Process-Time"] = str(process_time) - return response - - -def set_bound_logger(request: Request, call_next): - request.state.bound_logger = logger.bind() - return call_next(request) - - -async def set_span_id(request: Request, call_next): - span_id = uuid4() - request.state.span_id = span_id - request.state.bound_logger = logger.bind(span_id=request.state.span_id) - - response = await call_next(request) - - if str(response.status_code)[0] in "123": - response.headers["x-request-id"] = str(span_id) - response.headers["x-span-id"] = str(span_id) - return response def set_prefers( request: Request, - call_next, ): content_type = ( - request.query_params.get( - "content-type", - request.query_params.get( - "content_type", - request.query_params.get("accept"), - ), - ) - or request.headers.get( - "content-type", - request.headers.get( - "content_type", - request.headers.get("accept"), - ), - ) + 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() - referer = request.headers.get("referer", "") - - if content_type and "," in content_type: - content_type = content_type.split(",")[0] - - request.state.bound_logger.info( - "content_type set", - content_type=content_type, - hx_request_header=hx_request_header, - user_agent=user_agent, - referer=referer, - ) - - if content_type == "*/*": - content_type = None - if ("/docs" in referer or "/redoc" in referer) and content_type is None: - content_type = "application/json" - elif is_browser_request(user_agent) and content_type is None: - request.state.bound_logger.info("browser agent request") - content_type = "text/html" - elif is_rtf_request(user_agent) and content_type is None: - request.state.bound_logger.info("rtf agent request") - content_type = "application/rtf" - elif content_type is None: - request.state.bound_logger.info("no content type request") - content_type = content_type or "application/json" if hx_request_header == "true": - content_type = "text/html-partial" - # request.state.prefers = Prefers(html=True, partial=True) - # content_type = "text/html" + request.state.prefers = Prefers(html=True, partial=True) + return - elif is_browser_request(user_agent) and content_type is None: + 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" - # else: - # content_type = "application/json" + elif content_type is None: + content_type = "application/json" - partial = "partial" in content_type # if content_type in ACCEPT_TYPES: - # for accept_type, accept_value in ACCEPT_TYPES.items(): - # if accept_type in content_type: - if content_type in ACCEPT_TYPES: - request.state.prefers = Prefers( - **{ACCEPT_TYPES[content_type]: True}, partial=partial - ) - else: - request.state.prefers = Prefers(JSON=True, partial=partial) - - request.state.content_type = content_type - request.state.bound_logger = request.state.bound_logger.bind( - # content_type=request.state.content_type, - prefers=request.state.prefers, - ) - return call_next(request) + 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: @@ -235,41 +133,6 @@ def get_screenshot(html_content: str) -> BytesIO: return buffer -def get_pdf(html_content: str, scale: float = 1.0) -> BytesIO: - chrome_options = Options() - chrome_options.add_argument("--headless") - chrome_options.add_argument("--disable-gpu") - chrome_options.add_argument("--no-sandbox") - chrome_options.add_argument("--window-size=1280x1024") - chrome_options.add_argument("--disable-dev-shm-usage") # Helps avoid memory issues - - driver = webdriver.Chrome(options=chrome_options) - driver.get("data:text/html;charset=utf-8," + html_content) - - # Generate PDF - pdf = driver.execute_cdp_cmd( - "Page.printToPDF", - { - "printBackground": True, # Include CSS backgrounds in the PDF - "paperWidth": 8.27, # A4 paper size width in inches - "paperHeight": 11.69, # A4 paper size height in inches - "marginTop": 0, - "marginBottom": 0, - "marginLeft": 0, - "marginRight": 0, - "scale": scale, - }, - )["data"] - - driver.quit() - - # Convert base64 PDF to BytesIO - pdf_buffer = BytesIO() - pdf_buffer.write(base64.b64decode(pdf)) - pdf_buffer.seek(0) - return pdf_buffer.getvalue() - - def format_json_as_plain_text(data: dict) -> str: """Convert JSON to human-readable plain text format with indentation and bullet points.""" @@ -293,8 +156,8 @@ def format_json_as_plain_text(data: dict) -> str: def format_json_as_rich_text(data: dict, template_name: str) -> str: """Convert JSON to a human-readable rich text format using rich.""" - # pretty_data = Pretty(data, indent_guides=True) console = Console() + # pretty_data = Pretty(data, indent_guides=True) template = templates.get_template(template_name) html_content = template.render(data=data) @@ -312,6 +175,19 @@ def format_json_as_rich_text(data: dict, template_name: str) -> str: return capture.get() +async def respond_based_on_content_type( + request: Request, + call_next, + content_type: str, + data: str, +): + requested_path = request.url.path + 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] @@ -335,75 +211,78 @@ def handle_not_found(request: Request, call_next, data: str): async def respond_based_on_content_type(request: Request, call_next): requested_path = request.url.path - if requested_path in ["/docs", "/redoc", "/openapi.json", "/static/app.css"]: - request.state.bound_logger.info( - "protected route returning non-dynamic response" - ) + if requested_path in ["/docs", "/redoc", "/openapi.json"]: return await call_next(request) try: response = await call_next(request) + user_agent = request.headers.get("user-agent", "").lower() + referer = request.headers.get("referer", "") + content_type = request.query_params.get( + "content_type", + request.headers.get("content-type", request.headers.get("Accept")), + ) + if "raw" in content_type: + return await call_next(request) + if content_type == "*/*": + content_type = None + if ("/docs" in referer or "/redoc" in referer) and content_type is None: + content_type = "application/json" + elif 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 = "application/rtf" + elif content_type is None: + content_type = content_type or "application/json" + + body = b"".join([chunk async for chunk in response.body_iterator]) + + data = body.decode("utf-8") + if response.status_code == 404: - request.state.bound_logger.info("404 not found") - body = b"".join([chunk async for chunk in response.body_iterator]) - data = body.decode("utf-8") - response = handle_not_found( + return handle_not_found( request=request, call_next=call_next, data=data, ) - elif str(response.status_code)[0] not in "123": - request.state.bound_logger.info(f"non-200 response {response.status_code}") - # return await handle_response(request, response, data) + if response.status_code == 422: + return response + if str(response.status_code)[0] not in "123": return response - else: - body = b"".join([chunk async for chunk in response.body_iterator]) - data = body.decode("utf-8") - return await handle_response(request, response, data) + return await handle_response(request, data) + # except TemplateNotFound: + # return HTMLResponse(content="Template Not Found ", status_code=404) + except StarletteHTTPException as exc: + return HTMLResponse( + content=f"Error {exc.status_code}: {exc.detail}", + status_code=exc.status_code, + ) + except RequestValidationError as exc: + return JSONResponse(status_code=422, content={"detail": exc.errors()}) except Exception as e: - request.state.bound_logger.info("internal server error") - # print(traceback.format_exc()) - raise e - if settings.ENV == "local": - return HTMLResponse( - content=f"Internal Server Error: {e!s} {traceback.format_exc()}", - status_code=500, - ) - else: - return HTMLResponse( - content=f"Internal Server Error: {e!s}", status_code=500 - ) + print(traceback.format_exc()) + return HTMLResponse(content=f"Internal Server Error: {e!s}", status_code=500) -async def handle_response( - request: Request, - response: Response, - data: 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: - request.state.bound_logger = logger.bind(template_name=template_name) template_name = "partial_" + template_name + content_type = request.state.prefers if request.state.prefers.JSON: - request.state.bound_logger.info("returning JSON") - return JSONResponse( - content=json_data, - ) + return JSONResponse(content=json_data) - if request.state.prefers.html: - request.state.bound_logger.info("returning html") + elif request.state.prefers.html: return templates.TemplateResponse( - template_name, - {"request": request, "data": json_data}, + template_name, {"request": request, "data": json_data} ) - if request.state.prefers.markdown: - request.state.bound_logger.info("returning markdown") + elif request.state.prefers.markdown: import html2text template = templates.get_template(template_name) @@ -411,75 +290,24 @@ async def handle_response( markdown_content = html2text.html2text(html_content) return PlainTextResponse(content=markdown_content) - if request.state.prefers.text: - request.state.bound_logger.info("returning plain text") + elif request.state.prefers.text: plain_text_content = format_json_as_plain_text(json_data) - return PlainTextResponse( - content=plain_text_content, - ) + return PlainTextResponse(content=plain_text_content) - if request.state.prefers.rtf: - request.state.bound_logger.info("returning rich text") + elif request.state.prefers.rtf: rich_text_content = format_json_as_rich_text(json_data, template_name) - return PlainTextResponse( - content=rich_text_content, - ) + return PlainTextResponse(content=rich_text_content) - if request.state.prefers.png: - request.state.bound_logger.info("returning PNG") + elif content_type == "image/png": template = templates.get_template(template_name) html_content = template.render(data=json_data) screenshot = get_screenshot(html_content) - return Response( - content=screenshot.getvalue(), - media_type="image/png", - ) + return Response(content=screenshot.getvalue(), media_type="image/png") - if request.state.prefers.pdf: - request.state.bound_logger.info("returning PDF") + elif content_type == "application/pdf": template = templates.get_template(template_name) html_content = template.render(data=json_data) - scale = float( - request.headers.get("scale", request.query_params.get("scale", 1.0)) - ) - console.log(f"Scale: {scale}") - pdf = get_pdf(html_content, scale) + pdf = WEAZYHTML(string=html_content).write_pdf() + return Response(content=pdf, media_type="application/pdf") - return Response( - content=pdf, - media_type="application/pdf", - ) - - request.state.bound_logger.info("returning DEFAULT JSON") - return JSONResponse( - content=json_data, - ) - - -# Initialize the logger -async def log_requests(request: Request, call_next): - # Log request details - request.state.bound_logger = logger.bind( - method=request.method, path=request.url.path - ) - request.state.bound_logger.info( - "Request received", - ) - # logger.info( - # headers=dict(request.headers), - # prefers=request.state.prefers, - # ) - - # Process the request - response = await call_next(request) - - # Log response details - # logger.info( - # "Response sent", - # span_id=request.state.span_id, - # method=request.method, - # status_code=response.status_code, - # headers=dict(response.headers), - # ) - - return response + return JSONResponse(content=json_data) diff --git a/src/fastapi_dynamic_response/settings.py b/src/fastapi_dynamic_response/settings.py deleted file mode 100644 index ff41d04..0000000 --- a/src/fastapi_dynamic_response/settings.py +++ /dev/null @@ -1,31 +0,0 @@ -from pydantic import BaseModel, model_validator -from pydantic_settings import BaseSettings - - -class ApiServer(BaseModel): - app: str = "fastapi_dynamic_response.main:app" - port: int = 8000 - reload: bool = True - log_level: str = "info" - host: str = "0.0.0.0" - workers: int = 1 - forwarded_allow_ips: str = "*" - proxy_headers: bool = True - - -class Settings(BaseSettings): - ENV: str = "local" - DEBUG: bool = False - api_server: ApiServer = ApiServer() - - class Config: - env_file = "config.env" - - @model_validator(mode="after") - def validate_debug(self): - if self.ENV == "local" and self.DEBUG is False: - self.DEBUG = True - return self - - -settings = Settings() diff --git a/src/fastapi_dynamic_response/tracing.py b/src/fastapi_dynamic_response/tracing.py deleted file mode 100644 index d73fa73..0000000 --- a/src/fastapi_dynamic_response/tracing.py +++ /dev/null @@ -1,35 +0,0 @@ -# tracing.py - -from fastapi_dynamic_response.settings import Settings -from opentelemetry import trace -from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter - -# from opentelemetry.exporter.richconsole import RichConsoleSpanExporter -from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor - - -def configure_tracing(app): - settings = Settings() - trace.set_tracer_provider(TracerProvider()) - tracer_provider = trace.get_tracer_provider() - - if settings.ENV == "local": - # Use console exporter for local development - # span_exporter = RichConsoleSpanExporter() - # span_processor = SimpleSpanProcessor(span_exporter) - # span_exporter = OTLPSpanExporter() - span_exporter = OTLPSpanExporter( - endpoint="http://localhost:4317", insecure=True - ) - span_processor = BatchSpanProcessor(span_exporter) - else: - # Use OTLP exporter for production - span_exporter = OTLPSpanExporter() - span_processor = BatchSpanProcessor(span_exporter) - - tracer_provider.add_span_processor(span_processor) - - # Instrument FastAPI - FastAPIInstrumentor.instrument_app(app) diff --git a/static/app.css b/static/app.css index 41a1a8c..99827ca 100644 --- a/static/app.css +++ b/static/app.css @@ -593,19 +593,6 @@ video { margin-right: auto; } -.my-4 { - margin-top: 1rem; - margin-bottom: 1rem; -} - -.ml-8 { - margin-left: 2rem; -} - -.mt-4 { - margin-top: 1rem; -} - .mt-auto { margin-top: auto; } @@ -622,10 +609,6 @@ video { min-height: 100vh; } -.list-disc { - list-style-type: disc; -} - .flex-col { flex-direction: column; } @@ -666,11 +649,6 @@ video { text-align: center; } -.text-2xl { - font-size: 1.5rem; - line-height: 2rem; -} - .text-xl { font-size: 1.25rem; line-height: 1.75rem; @@ -685,16 +663,6 @@ video { color: rgb(229 231 235 / var(--tw-text-opacity)); } -.text-gray-300 { - --tw-text-opacity: 1; - color: rgb(209 213 219 / var(--tw-text-opacity)); -} - -.text-gray-400 { - --tw-text-opacity: 1; - color: rgb(156 163 175 / var(--tw-text-opacity)); -} - .text-teal-400 { --tw-text-opacity: 1; color: rgb(45 212 191 / var(--tw-text-opacity)); diff --git a/templates/another_example.html b/templates/another_example.html index 34109dc..d313c56 100644 --- a/templates/another_example.html +++ b/templates/another_example.html @@ -3,16 +3,16 @@ {% block title %}Another Example{% endblock %} {% block content %} -

    Example

    +

    Example

    {{ data.message }}

    -

    Items

    -

    +

    Items

    +

    there are {{ data.get('items', [])|length }} items in the list

    -
      +
        {% for item in data.get('items', []) %}
      • {{ item }}
      • {% endfor %} diff --git a/templates/base.html b/templates/base.html index ae038fe..86c3e9d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,9 +4,8 @@ {% block title %}FastAPI Dynamic Response{% endblock %} - - - + +
        diff --git a/uv.lock b/uv.lock index 11c4369..e9df9c5 100644 --- a/uv.lock +++ b/uv.lock @@ -57,10 +57,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/c4/65456561d89d3c49f46b7fbeb8fe6e449f13bdc8ea7791832c5d476b2faf/Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", size = 2809981 }, { url = "https://files.pythonhosted.org/packages/05/1b/cf49528437bae28abce5f6e059f0d0be6fecdcc1d3e33e7c54b3ca498425/Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", size = 2935297 }, { url = "https://files.pythonhosted.org/packages/81/ff/190d4af610680bf0c5a09eb5d1eac6e99c7c8e216440f9c7cfd42b7adab5/Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", size = 2930735 }, - { url = "https://files.pythonhosted.org/packages/80/7d/f1abbc0c98f6e09abd3cad63ec34af17abc4c44f308a7a539010f79aae7a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", size = 2933107 }, - { url = "https://files.pythonhosted.org/packages/34/ce/5a5020ba48f2b5a4ad1c0522d095ad5847a0be508e7d7569c8630ce25062/Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", size = 2845400 }, - { url = "https://files.pythonhosted.org/packages/44/89/fa2c4355ab1eecf3994e5a0a7f5492c6ff81dfcb5f9ba7859bd534bb5c1a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", size = 3031985 }, - { url = "https://files.pythonhosted.org/packages/af/a4/79196b4a1674143d19dca400866b1a4d1a089040df7b93b88ebae81f3447/Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", size = 2927099 }, { url = "https://files.pythonhosted.org/packages/e9/54/1c0278556a097f9651e657b873ab08f01b9a9ae4cac128ceb66427d7cd20/Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", size = 333172 }, { url = "https://files.pythonhosted.org/packages/f7/65/b785722e941193fd8b571afd9edbec2a9b838ddec4375d8af33a50b8dab9/Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", size = 357255 }, { url = "https://files.pythonhosted.org/packages/96/12/ad41e7fadd5db55459c4c401842b47f7fee51068f86dd2894dd0dcfc2d2a/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", size = 873068 }, @@ -73,14 +69,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/d6/0bd38d758d1afa62a5524172f0b18626bb2392d717ff94806f741fcd5ee9/Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", size = 2813051 }, { url = "https://files.pythonhosted.org/packages/14/56/48859dd5d129d7519e001f06dcfbb6e2cf6db92b2702c0c2ce7d97e086c1/Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", size = 2938172 }, { url = "https://files.pythonhosted.org/packages/3d/77/a236d5f8cd9e9f4348da5acc75ab032ab1ab2c03cc8f430d24eea2672888/Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", size = 2933023 }, - { url = "https://files.pythonhosted.org/packages/f1/87/3b283efc0f5cb35f7f84c0c240b1e1a1003a5e47141a4881bf87c86d0ce2/Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", size = 2935871 }, - { url = "https://files.pythonhosted.org/packages/f3/eb/2be4cc3e2141dc1a43ad4ca1875a72088229de38c68e842746b342667b2a/Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", size = 2847784 }, - { url = "https://files.pythonhosted.org/packages/66/13/b58ddebfd35edde572ccefe6890cf7c493f0c319aad2a5badee134b4d8ec/Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", size = 3034905 }, - { url = "https://files.pythonhosted.org/packages/84/9c/bc96b6c7db824998a49ed3b38e441a2cae9234da6fa11f6ed17e8cf4f147/Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", size = 2929467 }, { url = "https://files.pythonhosted.org/packages/e7/71/8f161dee223c7ff7fea9d44893fba953ce97cf2c3c33f78ba260a91bcff5/Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", size = 333169 }, { url = "https://files.pythonhosted.org/packages/02/8a/fece0ee1057643cb2a5bbf59682de13f1725f8482b2c057d4e799d7ade75/Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", size = 357253 }, - { url = "https://files.pythonhosted.org/packages/5c/d0/5373ae13b93fe00095a58efcbce837fd470ca39f703a235d2a999baadfbc/Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", size = 815693 }, - { url = "https://files.pythonhosted.org/packages/8e/48/f6e1cdf86751300c288c1459724bfa6917a80e30dbfc326f92cea5d3683a/Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", size = 422489 }, { url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081 }, { url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244 }, { url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505 }, @@ -91,24 +81,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452 }, { url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751 }, { url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757 }, - { url = "https://files.pythonhosted.org/packages/13/f0/358354786280a509482e0e77c1a5459e439766597d280f28cb097642fc26/Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", size = 2936146 }, - { url = "https://files.pythonhosted.org/packages/80/f7/daf538c1060d3a88266b80ecc1d1c98b79553b3f117a485653f17070ea2a/Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", size = 2848055 }, - { url = "https://files.pythonhosted.org/packages/ad/cf/0eaa0585c4077d3c2d1edf322d8e97aabf317941d3a72d7b3ad8bce004b0/Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", size = 3035102 }, - { url = "https://files.pythonhosted.org/packages/d8/63/1c1585b2aa554fe6dbce30f0c18bdbc877fa9a1bf5ff17677d9cca0ac122/Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", size = 2930029 }, { url = "https://files.pythonhosted.org/packages/5f/3b/4e3fd1893eb3bbfef8e5a80d4508bec17a57bb92d586c85c12d28666bb13/Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", size = 333276 }, { url = "https://files.pythonhosted.org/packages/3d/d5/942051b45a9e883b5b6e98c041698b1eb2012d25e5948c58d6bf85b1bb43/Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", size = 357255 }, - { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681 }, - { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475 }, - { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173 }, - { url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803 }, - { url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946 }, - { url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707 }, - { url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231 }, - { url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157 }, - { url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122 }, - { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206 }, - { url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804 }, - { url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517 }, { url = "https://files.pythonhosted.org/packages/34/1b/16114a20c0a43c20331f03431178ed8b12280b12c531a14186da0bc5b276/Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3", size = 873053 }, { url = "https://files.pythonhosted.org/packages/36/49/2afe4aa5a23a13dad4c7160ae574668eec58b3c80b56b74a826cebff7ab8/Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208", size = 446211 }, { url = "https://files.pythonhosted.org/packages/10/9d/6463edb80a9e0a944f70ed0c4d41330178526626d7824f729e81f78a3f24/Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7", size = 2904604 }, @@ -119,10 +93,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b1/53/110657f4017d34a2e9a96d9630a388ad7e56092023f1d46d11648c6c0bce/Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a", size = 2809968 }, { url = "https://files.pythonhosted.org/packages/3f/2a/fbc95429b45e4aa4a3a3a815e4af11772bfd8ef94e883dcff9ceaf556662/Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088", size = 2935402 }, { url = "https://files.pythonhosted.org/packages/4e/52/02acd2992e5a2c10adf65fa920fad0c29e11e110f95eeb11bcb20342ecd2/Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596", size = 2931208 }, - { url = "https://files.pythonhosted.org/packages/6b/35/5d258d1aeb407e1fc6fcbbff463af9c64d1ecc17042625f703a1e9d22ec5/Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7", size = 2933171 }, - { url = "https://files.pythonhosted.org/packages/cc/58/b25ca26492da9880e517753967685903c6002ddc2aade93d6e56df817b30/Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5", size = 2845347 }, - { url = "https://files.pythonhosted.org/packages/12/cf/91b84beaa051c9376a22cc38122dc6fbb63abcebd5a4b8503e9c388de7b1/Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943", size = 3031668 }, - { url = "https://files.pythonhosted.org/packages/38/05/04a57ba75aed972be0c6ad5f2f5ea34c83f5fecf57787cc6e54aac21a323/Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a", size = 2926949 }, { url = "https://files.pythonhosted.org/packages/c9/2f/fbe6938f33d2cd9b7d7fb591991eb3fb57ffa40416bb873bbbacab60a381/Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b", size = 333179 }, { url = "https://files.pythonhosted.org/packages/39/a5/9322c8436072e77b8646f6bde5e19ee66f62acf7aa01337ded10777077fa/Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0", size = 357254 }, { url = "https://files.pythonhosted.org/packages/1b/aa/aa6e0c9848ee4375514af0b27abf470904992939b7363ae78fc8aca8a9a8/Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a", size = 873048 }, @@ -135,10 +105,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/1f/be9443995821c933aad7159803f84ef4923c6f5b72c2affd001192b310fc/Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c", size = 2809728 }, { url = "https://files.pythonhosted.org/packages/76/2f/213bab6efa902658c80a1247142d42b138a27ccdd6bade49ca9cd74e714a/Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d", size = 2935043 }, { url = "https://files.pythonhosted.org/packages/27/89/bbb14fa98e895d1e601491fba54a5feec167d262f0d3d537a3b0d4cd0029/Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59", size = 2930639 }, - { url = "https://files.pythonhosted.org/packages/14/87/03a6d6e1866eddf9f58cc57e35befbeb5514da87a416befe820150cae63f/Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419", size = 2932834 }, - { url = "https://files.pythonhosted.org/packages/a4/d5/e5f85e04f75144d1a89421ba432def6bdffc8f28b04f5b7d540bbd03362c/Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2", size = 2845213 }, - { url = "https://files.pythonhosted.org/packages/99/bf/25ef07add7afbb1aacd4460726a1a40370dfd60c0810b6f242a6d3871d7e/Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f", size = 3031573 }, - { url = "https://files.pythonhosted.org/packages/55/22/948a97bda5c9dc9968d56b9ed722d9727778db43739cf12ef26ff69be94d/Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb", size = 2926885 }, { url = "https://files.pythonhosted.org/packages/31/ba/e53d107399b535ef89deb6977dd8eae468e2dde7b1b74c6cbe2c0e31fda2/Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64", size = 333171 }, { url = "https://files.pythonhosted.org/packages/99/b3/f7b3af539f74b82e1c64d28685a5200c631cc14ae751d37d6ed819655627/Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467", size = 357258 }, ] @@ -325,17 +291,13 @@ source = { editable = "." } dependencies = [ { name = "fastapi" }, { name = "html2text" }, - { name = "itsdangerous" }, { name = "jinja2" }, { name = "markdown" }, { name = "pillow" }, - { name = "pydantic-settings" }, { name = "pydyf" }, { name = "python-levenshtein" }, { name = "rich" }, { name = "selenium" }, - { name = "structlog" }, - { name = "typer" }, { name = "uvicorn" }, { name = "weasyprint" }, ] @@ -344,17 +306,13 @@ dependencies = [ requires-dist = [ { name = "fastapi", specifier = ">=0.115.0" }, { name = "html2text", specifier = ">=2024.2.26" }, - { name = "itsdangerous", specifier = ">=2.2.0" }, { name = "jinja2", specifier = ">=3.1.4" }, { name = "markdown", specifier = ">=3.7" }, { name = "pillow", specifier = ">=10.4.0" }, - { name = "pydantic-settings", specifier = ">=2.5.2" }, { name = "pydyf", specifier = "==0.8.0" }, { name = "python-levenshtein", specifier = ">=0.25.1" }, { name = "rich", specifier = ">=13.9.2" }, { name = "selenium", specifier = ">=4.25.0" }, - { name = "structlog", specifier = ">=24.4.0" }, - { name = "typer", specifier = ">=0.12.5" }, { name = "uvicorn", specifier = ">=0.31.1" }, { name = "weasyprint", specifier = ">=61.2" }, ] @@ -460,23 +418,14 @@ wheels = [ [[package]] name = "importlib-metadata" -version = "8.4.0" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/bd/fa8ce65b0a7d4b6d143ec23b0f5fd3f7ab80121078c465bc02baeaab22dc/importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5", size = 54320 } +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/14/362d31bf1076b21e1bcdcb0dc61944822ff263937b804a79231df2774d28/importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1", size = 26269 }, -] - -[[package]] -name = "itsdangerous" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, ] [[package]] @@ -903,19 +852,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1d/d9/1d7ecb98318da4cb96986daaf0e20d66f1651d0aeb9e2d4435b916ce031d/pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e", size = 1920855 }, ] -[[package]] -name = "pydantic-settings" -version = "2.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/68/27/0bed9dd26b93328b60a1402febc780e7be72b42847fa8b5c94b7d0aeb6d1/pydantic_settings-2.5.2.tar.gz", hash = "sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0", size = 70938 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/8d/29e82e333f32d9e2051c10764b906c2a6cd140992910b5f49762790911ba/pydantic_settings-2.5.2-py3-none-any.whl", hash = "sha256:2c912e55fd5794a59bf8c832b9de832dcfdf4778d79ff79b708744eed499a907", size = 26864 }, -] - [[package]] name = "pydyf" version = "0.8.0" @@ -952,15 +888,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725 }, ] -[[package]] -name = "python-dotenv" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, -] - [[package]] name = "python-levenshtein" version = "0.25.1" @@ -1119,15 +1046,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/85/fa44f23dd5d5066a72f7c4304cce4b5ff9a6e7fd92431a48b2c63fbf63ec/selenium-4.25.0-py3-none-any.whl", hash = "sha256:3798d2d12b4a570bc5790163ba57fef10b2afee958bf1d80f2a3cf07c4141f33", size = 9693127 }, ] -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, -] - [[package]] name = "six" version = "1.16.0" @@ -1168,15 +1086,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/9c/93f7bc03ff03199074e81974cc148908ead60dcf189f68ba1761a0ee35cf/starlette-0.38.6-py3-none-any.whl", hash = "sha256:4517a1409e2e73ee4951214ba012052b9e16f60e90d73cfb06192c19203bbb05", size = 71451 }, ] -[[package]] -name = "structlog" -version = "24.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/a3/e811a94ac3853826805253c906faa99219b79951c7d58605e89c79e65768/structlog-24.4.0.tar.gz", hash = "sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4", size = 1348634 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/65/813fc133609ebcb1299be6a42e5aea99d6344afb35ccb43f67e7daaa3b92/structlog-24.4.0-py3-none-any.whl", hash = "sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610", size = 67180 }, -] - [[package]] name = "tinycss2" version = "1.3.0" @@ -1221,21 +1130,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/be/a9ae5f50cad5b6f85bd2574c2c923730098530096e170c1ce7452394d7aa/trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638", size = 17408 }, ] -[[package]] -name = "typer" -version = "0.12.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/58/a79003b91ac2c6890fc5d90145c662fd5771c6f11447f116b63300436bc9/typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722", size = 98953 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/2b/886d13e742e514f704c33c4caa7df0f3b89e5a25ef8db02aa9ca3d9535d5/typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b", size = 47288 }, -] - [[package]] name = "typing-extensions" version = "4.12.2"