From a23cad2e457516f6b1bef0f428de89a3bf6575cf Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Sun, 10 Aug 2025 09:09:20 +0200 Subject: [PATCH] add Quart utilities add_i18n(), negotiate() add_rest() --- CHANGELOG.md | 1 + src/suou/flask.py | 4 ++- src/suou/http.py | 24 +++++++++++++++++ src/suou/quart.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/suou/http.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7815fb1..20f0a6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ + Add `redact` module with `redact_url_password()` + Add more exceptions: `NotFoundError()`, `BabelTowerError()` + Add `sass` module ++ Add `quart` module with `negotiate()`, `add_rest()`, `add_i18n()`, `WantsContentType` ## 0.4.0 diff --git a/src/suou/flask.py b/src/suou/flask.py index a2ce4f9..f097c8e 100644 --- a/src/suou/flask.py +++ b/src/suou/flask.py @@ -67,10 +67,11 @@ def get_flask_conf(key: str, default = None, *, app: Flask | None = None) -> Any app = current_app return app.config.get(key, default) -## XXX UNTESTED! def harden(app: Flask): """ Make common "dork" endpoints unavailable + + XXX UNTESTED! """ i = 1 for ep in SENSITIVE_ENDPOINTS: @@ -81,6 +82,7 @@ def harden(app: Flask): return app + # Optional dependency: do not import into __init__.py __all__ = ('add_context_from_config', 'add_i18n', 'get_flask_conf', 'harden') diff --git a/src/suou/http.py b/src/suou/http.py new file mode 100644 index 0000000..30c4c50 --- /dev/null +++ b/src/suou/http.py @@ -0,0 +1,24 @@ +""" +Framework-agnostic utilities for web app development. + +--- + +Copyright (c) 2025 Sakuragasaki46. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +See LICENSE for the specific language governing permissions and +limitations under the License. + +This software is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +from __future__ import annotations +import enum + +class WantsContentType(enum.Enum): + PLAIN = 'text/plain' + JSON = 'application/json' + HTML = 'text/html' + diff --git a/src/suou/quart.py b/src/suou/quart.py index 9caaee1..6f393bc 100644 --- a/src/suou/quart.py +++ b/src/suou/quart.py @@ -14,4 +14,70 @@ This software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. """ -# TODO everything \ No newline at end of file +from __future__ import annotations + +from flask import current_app +from quart import Quart, request, g +from quart_schema import QuartSchema + +from suou.http import WantsContentType + +from .i18n import I18n +from .itertools import makelist + +def add_i18n(app: Quart, i18n: I18n, var_name: str = 'T', *, + query_arg: str = 'lang', default_lang = 'en'): + ''' + Integrate a I18n() object with a Quart application: + - set g.lang + - add T() to Jinja templates + + XXX UNTESTED + ''' + def _get_lang(): + lang = request.args.get(query_arg) + if not lang: + for lp in request.headers.get('accept-language', 'en').split(','): + l = lp.split(';')[0] + lang = l + break + else: + lang = default_lang + return lang + + @app.context_processor + def _add_i18n(): + return {var_name: i18n.lang(_get_lang()).t} + + @app.before_request + def _add_language_code(): + g.lang = _get_lang() + + return app + + +def negotiate() -> WantsContentType: + """ + Return an appropriate MIME type for content negotiation. + """ + if 'application/json' in request.accept_mimetypes or any(request.path.startswith(f'/{p.strip('/')}/') for p in current_app.config.get('REST_PATHS')): + return WantsContentType.JSON + elif request.user_agent.string.startswith('Mozilla/'): + return WantsContentType.HTML + else: + return WantsContentType.PLAIN + + +def add_rest(app: Quart, *bases: str, **kwargs) -> QuartSchema: + """ + Construct a REST ... + + The rest of ... + """ + + schema = QuartSchema(app, **kwargs) + app.config['REST_PATHS'] = makelist(bases, wrap=False) + return schema + + +__all__ = ('add_i18n', 'negotiate', 'add_rest') \ No newline at end of file