From 58bf0d6b7969b4776bfb2af145dc307f9ca5ce83 Mon Sep 17 00:00:00 2001 From: Anthony Donlon Date: Tue, 16 Dec 2025 22:48:32 +0800 Subject: [PATCH] python: support type hinting for input params --- cloudflare_error_page/__init__.py | 64 ++++++++++++++++++++++++++++++- editor/server/examples.py | 3 +- editor/server/utils.py | 8 ++-- examples/flask_demo.py | 5 +-- 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/cloudflare_error_page/__init__.py b/cloudflare_error_page/__init__.py index c64c7f8..7712fc8 100644 --- a/cloudflare_error_page/__init__.py +++ b/cloudflare_error_page/__init__.py @@ -1,7 +1,15 @@ import html import secrets +import sys from datetime import datetime, timezone -from typing import Any +from typing import Any, TypedDict, Literal + +if sys.version_info >= (3, 11): + from typing import NotRequired +else: + from typing import _SpecialForm + NotRequired: _SpecialForm + from jinja2 import Environment, PackageLoader, Template, select_autoescape @@ -15,7 +23,53 @@ env = Environment( default_template: Template = env.get_template("error.html") -def render(params: dict, +class ErrorPageParams(TypedDict): + class MoreInformation(TypedDict): + hidden: NotRequired[bool] + text: NotRequired[str] + link: NotRequired[str] + for_text: NotRequired[str] # renamed to avoid Python keyword conflict + + class StatusItem(TypedDict): + status: NotRequired[Literal["ok", "error"]] + location: NotRequired[str] + name: NotRequired[str] + status_text: NotRequired[str] + status_text_color: NotRequired[str] + + class PerfSecBy(TypedDict): + text: NotRequired[str] + link: NotRequired[str] + + class CreatorInfo(TypedDict): + hidden: NotRequired[bool] + link: NotRequired[str] + text: NotRequired[str] + + html_title: NotRequired[str] + title: NotRequired[str] + error_code: NotRequired[str] + time: NotRequired[str] + + more_information: NotRequired[MoreInformation] + + browser_status: NotRequired[StatusItem] + cloudflare_status: NotRequired[StatusItem] + host_status: NotRequired[StatusItem] + + error_source: NotRequired[Literal["browser", "cloudflare", "host"]] + + what_happened: NotRequired[str] + what_can_i_do: NotRequired[str] + + ray_id: NotRequired[str] + client_ip: NotRequired[str] + + perf_sec_by: NotRequired[PerfSecBy] + creator_info: NotRequired[CreatorInfo] + + +def render(params: ErrorPageParams, allow_html: bool = True, template: Template | None = None, *args: Any, @@ -35,6 +89,12 @@ def render(params: dict, params = {**params} + more_information = params.get('more_information') + if more_information: + for_text = more_information.get('for_text') + if for_text is not None: + more_information['for'] = for_text + if not params.get('time'): utc_now = datetime.now(timezone.utc) params['time'] = utc_now.strftime("%Y-%m-%d %H:%M:%S UTC") diff --git a/editor/server/examples.py b/editor/server/examples.py index a16b9e9..e94da8e 100644 --- a/editor/server/examples.py +++ b/editor/server/examples.py @@ -11,6 +11,7 @@ from flask import ( redirect, ) +from cloudflare_error_page import ErrorPageParams from cloudflare_error_page import render as render_cf_error_page from .utils import fill_cf_template_params @@ -22,7 +23,7 @@ bp = Blueprint('examples', __name__, url_prefix='/') param_cache: dict[str, dict] = {} -def get_page_params(name: str) -> dict: +def get_page_params(name: str) -> ErrorPageParams: name = re.sub(r'[^\w]', '', name) params = param_cache.get(name) if params is not None: diff --git a/editor/server/utils.py b/editor/server/utils.py index 6959f62..f19d676 100644 --- a/editor/server/utils.py +++ b/editor/server/utils.py @@ -1,7 +1,9 @@ import json import os +from cloudflare_error_page import ErrorPageParams from flask import request + from . import root_dir @@ -31,13 +33,13 @@ def get_cf_location(loc: str): return data.get('city') -def fill_cf_template_params(params: dict): +def fill_cf_template_params(params: ErrorPageParams): # Get the real Ray ID / data center location from Cloudflare header ray_id_loc = request.headers.get('Cf-Ray') if ray_id_loc: params['ray_id'] = ray_id_loc[:16] - cf_status: dict = params.get('cloudflare_status') + cf_status = params.get('cloudflare_status') if cf_status is None: cf_status = params['cloudflare_status'] = {} if not cf_status.get('location'): @@ -61,7 +63,7 @@ def sanitize_user_link(link: str): return '#' + link -def sanitize_page_param_links(param: dict): +def sanitize_page_param_links(param: ErrorPageParams): more_info = param.get('more_information') if more_info: link = more_info.get('link') diff --git a/examples/flask_demo.py b/examples/flask_demo.py index e05367f..46e1f67 100755 --- a/examples/flask_demo.py +++ b/examples/flask_demo.py @@ -1,19 +1,18 @@ #!/usr/bin/env python3 import os -import re import sys from flask import ( Flask, request, - send_from_directory ) # Append this directory to sys.path is not required if the package is already installed examples_dir = os.path.dirname(os.path.abspath(__file__)) sys.path.append(os.path.dirname(examples_dir)) +from cloudflare_error_page import ErrorPageParams from cloudflare_error_page import render as render_cf_error_page app = Flask(__name__) @@ -21,7 +20,7 @@ app = Flask(__name__) @app.route('/') def index(): - params = { + params: ErrorPageParams = { "title": "Internal server error", "error_code": 500, "browser_status": {