diff options
Diffstat (limited to 'fatcat_scholar/web.py')
-rw-r--r-- | fatcat_scholar/web.py | 94 |
1 files changed, 78 insertions, 16 deletions
diff --git a/fatcat_scholar/web.py b/fatcat_scholar/web.py index dfebe01..8725ce0 100644 --- a/fatcat_scholar/web.py +++ b/fatcat_scholar/web.py @@ -1,17 +1,24 @@ - """ -This file is the FastAPI web application. +This contains the FastAPI web application and RESTful API. + +So far there are few endpoints, so we just put them all here! """ from enum import Enum -from fastapi import FastAPI, APIRouter, Request, Depends +import babel.support +from fastapi import FastAPI, APIRouter, Request, Depends, Header from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates +from fatcat_scholar.hacks import Jinja2Templates from fastapi.responses import HTMLResponse +from pydantic import BaseModel I18N_LANG_DEFAULT = "en" -I18N_LANG_OPTIONS = ["en", "de", "zh"] +I18N_LANG_TRANSLATIONS = ["de", "zh"] +I18N_LANG_OPTIONS = I18N_LANG_TRANSLATIONS + [I18N_LANG_DEFAULT,] + +class SearchParams(BaseModel): + q: str = "" class LangPrefix: """ @@ -46,34 +53,89 @@ class ContentNegotiation: api = APIRouter() @api.get("/", operation_id="get_home") -async def home(request: Request): +async def home(): return {"endpoints": {"/": "this", "/search": "fulltext search"}} @api.get("/search", operation_id="get_search") -async def search(request: Request): +async def search(query: SearchParams = Depends(SearchParams)): return {"message": "search results would go here, I guess"} web = APIRouter() -templates = Jinja2Templates(directory="fatcat_scholar/templates") + +def locale_gettext(translations): + def gt(s): + return translations.ugettext(s) + return gt + +def locale_ngettext(translations): + def ngt(s, n): + return translations.ungettext(s) + return ngt + +def load_i18n_templates(): + """ + This is a hack to work around lack of per-request translation + (babel/gettext) locale switching in FastAPI and Starlette. Flask (and + presumably others) get around this using global context (eg, in + Flask-Babel). + + See related issues: + + - https://github.com/encode/starlette/issues/279 + - https://github.com/aio-libs/aiohttp-jinja2/issues/187 + """ + + d = dict() + for lang_opt in I18N_LANG_OPTIONS: + translations = babel.support.Translations.load( + dirname="fatcat_scholar/translations", + locales=[lang_opt], + ) + templates = Jinja2Templates( + directory="fatcat_scholar/templates", + extensions=["jinja2.ext.i18n"], + ) + templates.env.install_gettext_translations(translations, newstyle=True) + templates.env.install_gettext_callables( + locale_gettext(translations), + locale_ngettext(translations), + newstyle=True, + ) + d[lang_opt] = templates + return d + +i18n_templates = load_i18n_templates() @web.get("/", include_in_schema=False) async def web_home(request: Request, lang: LangPrefix = Depends(LangPrefix), content: ContentNegotiation = Depends(ContentNegotiation)): if content.mimetype == "application/json": - return await api_home(request) - return templates.TemplateResponse("home.html", {"request": request}) + return await home() + return i18n_templates[lang.code].TemplateResponse("home.html", {"request": request, "locale": lang.code, "lang_prefix": lang.prefix}) @web.get("/search", include_in_schema=False) -async def web_search(request: Request): +async def web_search(request: Request, query: SearchParams = Depends(SearchParams), lang: LangPrefix = Depends(LangPrefix), content: ContentNegotiation = Depends(ContentNegotiation)): if content.mimetype == "application/json": - return await api_search(request) - return templates.TemplateResponse("search.html", {"request": request}) - -app = FastAPI() - + return await search(query) + return i18n_templates[lang.code].TemplateResponse("search.html", {"request": request}) + +app = FastAPI( + title="Fatcat Scholar", + description="Fulltext search interface for scholarly web content in the Fatcat catalog. An Internet Archive project.", + version="0.1.0-dev", + openapi_url="/api/openapi.json", + redoc_url="/api/redoc", + docs_url="/api/docs", +) app.include_router(web) for lang_option in I18N_LANG_OPTIONS: app.include_router(web, prefix=f"/{lang_option}") + +# Becasue we are mounting 'api' after 'web', the web routes will take +# precedence. Requests get passed through the API handlers based on content +# negotiation. This is counter-intuitive here in the code, but does seem to +# work, and results in the OpenAPI docs looking correct. app.include_router(api) + app.mount("/static", StaticFiles(directory="fatcat_scholar/static"), name="static") |