Browse Source

feat: use FastAPI instead of Flask

Note that when run as python module access log is disabled,
but when run as development server with `rye run dev` logs are
enabled.
Zubarev Grigoriy 7 months ago
parent
commit
169b0cb11d
4 changed files with 106 additions and 39 deletions
  1. 7 3
      pyproject.toml
  2. 38 12
      requirements-dev.lock
  3. 38 12
      requirements.lock
  4. 23 12
      src/rural_dict/__main__.py

+ 7 - 3
pyproject.toml

@@ -12,9 +12,10 @@ authors = [
 ]
 dependencies = [
     "requests~=2.32.3",
-    "flask~=3.0.3",
-    "waitress~=3.0.0",
-    "selectolax>=0.3.21",
+    "selectolax~=0.3.21",
+    "fastapi~=0.112.1",
+    "uvicorn[standard]~=0.30.6",
+    "jinja2~=3.1.4",
 ]
 dynamic = ["version"]
 
@@ -31,6 +32,9 @@ managed = true
 universal = true
 dev-dependencies = []
 
+[tool.rye.scripts]
+dev = "uvicorn src.rural_dict.__main__:app --port 8080 --reload --reload-dir src/rural_dict"
+
 [tool.hatch.version]
 path = "src/rural_dict/__init__.py"
 

+ 38 - 12
requirements-dev.lock

@@ -10,34 +10,60 @@
 #   universal: true
 
 -e file:.
-blinker==1.8.2
-    # via flask
+annotated-types==0.7.0
+    # via pydantic
+anyio==4.4.0
+    # via starlette
+    # via watchfiles
 certifi==2024.7.4
     # via requests
 charset-normalizer==3.3.2
     # via requests
 click==8.1.7
-    # via flask
-colorama==0.4.6 ; platform_system == 'Windows'
+    # via uvicorn
+colorama==0.4.6 ; platform_system == 'Windows' or sys_platform == 'win32'
     # via click
-flask==3.0.3
+    # via uvicorn
+fastapi==0.112.1
     # via rural-dict
+h11==0.14.0
+    # via uvicorn
+httptools==0.6.1
+    # via uvicorn
 idna==3.7
+    # via anyio
     # via requests
-itsdangerous==2.2.0
-    # via flask
 jinja2==3.1.4
-    # via flask
+    # via rural-dict
 markupsafe==2.1.5
     # via jinja2
-    # via werkzeug
+pydantic==2.8.2
+    # via fastapi
+pydantic-core==2.20.1
+    # via pydantic
+python-dotenv==1.0.1
+    # via uvicorn
+pyyaml==6.0.2
+    # via uvicorn
 requests==2.32.3
     # via rural-dict
 selectolax==0.3.21
     # via rural-dict
+sniffio==1.3.1
+    # via anyio
+starlette==0.38.2
+    # via fastapi
+typing-extensions==4.12.2
+    # via fastapi
+    # via pydantic
+    # via pydantic-core
 urllib3==2.2.2
     # via requests
-waitress==3.0.0
+uvicorn==0.30.6
     # via rural-dict
-werkzeug==3.0.3
-    # via flask
+uvloop==0.20.0 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'
+    # via uvicorn
+watchfiles==0.23.0
+    # via uvicorn
+websockets==12.0
+    # via uvicorn

+ 38 - 12
requirements.lock

@@ -10,34 +10,60 @@
 #   universal: true
 
 -e file:.
-blinker==1.8.2
-    # via flask
+annotated-types==0.7.0
+    # via pydantic
+anyio==4.4.0
+    # via starlette
+    # via watchfiles
 certifi==2024.7.4
     # via requests
 charset-normalizer==3.3.2
     # via requests
 click==8.1.7
-    # via flask
-colorama==0.4.6 ; platform_system == 'Windows'
+    # via uvicorn
+colorama==0.4.6 ; platform_system == 'Windows' or sys_platform == 'win32'
     # via click
-flask==3.0.3
+    # via uvicorn
+fastapi==0.112.1
     # via rural-dict
+h11==0.14.0
+    # via uvicorn
+httptools==0.6.1
+    # via uvicorn
 idna==3.7
+    # via anyio
     # via requests
-itsdangerous==2.2.0
-    # via flask
 jinja2==3.1.4
-    # via flask
+    # via rural-dict
 markupsafe==2.1.5
     # via jinja2
-    # via werkzeug
+pydantic==2.8.2
+    # via fastapi
+pydantic-core==2.20.1
+    # via pydantic
+python-dotenv==1.0.1
+    # via uvicorn
+pyyaml==6.0.2
+    # via uvicorn
 requests==2.32.3
     # via rural-dict
 selectolax==0.3.21
     # via rural-dict
+sniffio==1.3.1
+    # via anyio
+starlette==0.38.2
+    # via fastapi
+typing-extensions==4.12.2
+    # via fastapi
+    # via pydantic
+    # via pydantic-core
 urllib3==2.2.2
     # via requests
-waitress==3.0.0
+uvicorn==0.30.6
     # via rural-dict
-werkzeug==3.0.3
-    # via flask
+uvloop==0.20.0 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'
+    # via uvicorn
+watchfiles==0.23.0
+    # via uvicorn
+websockets==12.0
+    # via uvicorn

+ 23 - 12
src/rural_dict/__main__.py

@@ -1,36 +1,41 @@
 import logging
 import re
 import sys
+from pathlib import Path
 
 import requests
-from flask import Flask, redirect, render_template, request
+from fastapi import FastAPI, Request
+from fastapi.responses import HTMLResponse, RedirectResponse
+from fastapi.staticfiles import StaticFiles
+from fastapi.templating import Jinja2Templates
 from requests import JSONDecodeError
 from selectolax.parser import HTMLParser, Node
 
-app = Flask(__name__, template_folder="templates", static_folder="static")
+ROOT_PATH = Path(__file__).parent
+app = FastAPI(docs_url=None, redoc_url=None)
+app.mount("/static", StaticFiles(directory=ROOT_PATH / "static"), name="static")
+templates = Jinja2Templates(directory=ROOT_PATH / "templates")
 
 
 def remove_classes(node: Node) -> Node:
     """Remove all classes from all nodes recursively."""
     if "class" in node.attributes:
         del node.attrs["class"]
-
     for child in node.iter():
         remove_classes(child)
     return node
 
 
-@app.route("/", defaults={"path": ""})
-@app.route("/<path:path>")
-def root_route(path):
+@app.get("/{path:path}", response_class=HTMLResponse)
+async def catch_all(response: Request):
     """Check all routes on Urban Dictionary and redirect if needed."""
-    path_without_host = re.sub(r"https?://[^/]+/", "", request.url)
+    path_without_host = re.sub(r"https?://[^/]+/+", "", str(response.url))
     url = f"https://www.urbandictionary.com/{path_without_host}"
 
     data = requests.get(url, timeout=10)
 
     if data.history:
-        return redirect(re.sub(r"https?://[^/]+", "", data.url), 302)
+        return RedirectResponse(re.sub(r"https?://[^/]+", "", data.url), status_code=301)
 
     results = []
     parser = HTMLParser(data.text)
@@ -62,13 +67,19 @@ def root_route(path):
         pagination.attrs["class"] = "pagination"
         pagination = pagination.html
 
-    return render_template(
-        "index.html", results=results, pagination=pagination, term=request.args.get("term")
+    return templates.TemplateResponse(
+        "index.html",
+        {
+            "request": response,
+            "results": results,
+            "pagination": pagination,
+            "term": response.query_params.get("term"),
+        },
     )
 
 
 if __name__ == "__main__":
-    from waitress import serve
+    import uvicorn
 
     logging.basicConfig(level=logging.INFO, stream=sys.stdout)
-    serve(app, host="0.0.0.0", port=8080)  # noqa: S104
+    uvicorn.run(app, host="0.0.0.0", port=8080, access_log=False)  # noqa: S104