Browse Source

feat: use aiohttp instead of requests

Now app is fully asynchronous.
Zubarev Grigoriy 7 months ago
parent
commit
1a04015f03
4 changed files with 61 additions and 32 deletions
  1. 1 1
      pyproject.toml
  2. 17 9
      requirements-dev.lock
  3. 17 9
      requirements.lock
  4. 26 13
      src/rural_dict/__main__.py

+ 1 - 1
pyproject.toml

@@ -11,7 +11,7 @@ authors = [
     { name = "zortazert", email = "zortazert@matthewevan.xyz" },
 ]
 dependencies = [
-    "requests~=2.32.3",
+    "aiohttp~=3.10.3",
     "selectolax~=0.3.21",
     "fastapi~=0.112.1",
     "uvicorn[standard]~=0.30.6",

+ 17 - 9
requirements-dev.lock

@@ -10,15 +10,19 @@
 #   universal: true
 
 -e file:.
+aiohappyeyeballs==2.3.6
+    # via aiohttp
+aiohttp==3.10.3
+    # via rural-dict
+aiosignal==1.3.1
+    # via aiohttp
 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
+attrs==24.2.0
+    # via aiohttp
 click==8.1.7
     # via uvicorn
 colorama==0.4.6 ; platform_system == 'Windows' or sys_platform == 'win32'
@@ -26,17 +30,23 @@ colorama==0.4.6 ; platform_system == 'Windows' or sys_platform == 'win32'
     # via uvicorn
 fastapi==0.112.1
     # via rural-dict
+frozenlist==1.4.1
+    # via aiohttp
+    # via aiosignal
 h11==0.14.0
     # via uvicorn
 httptools==0.6.1
     # via uvicorn
 idna==3.7
     # via anyio
-    # via requests
+    # via yarl
 jinja2==3.1.4
     # via rural-dict
 markupsafe==2.1.5
     # via jinja2
+multidict==6.0.5
+    # via aiohttp
+    # via yarl
 pydantic==2.8.2
     # via fastapi
 pydantic-core==2.20.1
@@ -45,8 +55,6 @@ 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
@@ -57,8 +65,6 @@ typing-extensions==4.12.2
     # via fastapi
     # via pydantic
     # via pydantic-core
-urllib3==2.2.2
-    # via requests
 uvicorn==0.30.6
     # via rural-dict
 uvloop==0.20.0 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'
@@ -67,3 +73,5 @@ watchfiles==0.23.0
     # via uvicorn
 websockets==12.0
     # via uvicorn
+yarl==1.9.4
+    # via aiohttp

+ 17 - 9
requirements.lock

@@ -10,15 +10,19 @@
 #   universal: true
 
 -e file:.
+aiohappyeyeballs==2.3.6
+    # via aiohttp
+aiohttp==3.10.3
+    # via rural-dict
+aiosignal==1.3.1
+    # via aiohttp
 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
+attrs==24.2.0
+    # via aiohttp
 click==8.1.7
     # via uvicorn
 colorama==0.4.6 ; platform_system == 'Windows' or sys_platform == 'win32'
@@ -26,17 +30,23 @@ colorama==0.4.6 ; platform_system == 'Windows' or sys_platform == 'win32'
     # via uvicorn
 fastapi==0.112.1
     # via rural-dict
+frozenlist==1.4.1
+    # via aiohttp
+    # via aiosignal
 h11==0.14.0
     # via uvicorn
 httptools==0.6.1
     # via uvicorn
 idna==3.7
     # via anyio
-    # via requests
+    # via yarl
 jinja2==3.1.4
     # via rural-dict
 markupsafe==2.1.5
     # via jinja2
+multidict==6.0.5
+    # via aiohttp
+    # via yarl
 pydantic==2.8.2
     # via fastapi
 pydantic-core==2.20.1
@@ -45,8 +55,6 @@ 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
@@ -57,8 +65,6 @@ typing-extensions==4.12.2
     # via fastapi
     # via pydantic
     # via pydantic-core
-urllib3==2.2.2
-    # via requests
 uvicorn==0.30.6
     # via rural-dict
 uvloop==0.20.0 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'
@@ -67,3 +73,5 @@ watchfiles==0.23.0
     # via uvicorn
 websockets==12.0
     # via uvicorn
+yarl==1.9.4
+    # via aiohttp

+ 26 - 13
src/rural_dict/__main__.py

@@ -1,20 +1,30 @@
 import logging
-import re
 import sys
+from contextlib import asynccontextmanager
+from json import JSONDecodeError
 from pathlib import Path
 
-import requests
+import aiohttp
 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
 
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+    global session
+    session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(10))
+    yield
+    await session.close()
+
+
 ROOT_PATH = Path(__file__).parent
-app = FastAPI(docs_url=None, redoc_url=None)
+app = FastAPI(lifespan=lifespan, docs_url=None, redoc_url=None)
 app.mount("/static", StaticFiles(directory=ROOT_PATH / "static"), name="static")
 templates = Jinja2Templates(directory=ROOT_PATH / "templates")
+session: aiohttp.ClientSession | None = None
 
 
 def remove_classes(node: Node) -> Node:
@@ -29,24 +39,27 @@ def remove_classes(node: Node) -> Node:
 @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?://[^/]+/+", "", str(response.url))
-    url = f"https://www.urbandictionary.com/{path_without_host}"
-
-    data = requests.get(url, timeout=10)
+    path_without_host = (
+        f"{response.url.path}{f'?{response.url.query}' if response.url.query else ''}"
+    )
+    url = f"https://www.urbandictionary.com{path_without_host}"
 
-    if data.history:
-        return RedirectResponse(re.sub(r"https?://[^/]+", "", data.url), status_code=301)
+    async with session.get(url) as dict_response:
+        if dict_response.history:
+            return RedirectResponse(dict_response.url.relative(), status_code=301)
+        html = await dict_response.text()
 
     results = []
-    parser = HTMLParser(data.text)
+    parser = HTMLParser(html)
     definitions = parser.css("div[data-defid]")
     try:
         thumbs_api_url = (
             f'https://api.urbandictionary.com/v0/uncacheable?ids='
             f'{",".join(d.attributes["data-defid"] for d in definitions)}'
         )
-        thumbs_json = requests.get(thumbs_api_url, timeout=10).json()["thumbs"]
-        thumbs_data = {el["defid"]: el for el in thumbs_json}
+        async with session.get(thumbs_api_url) as thumbs_response:
+            thumbs_json = await thumbs_response.json()
+            thumbs_data = {el["defid"]: el for el in thumbs_json["thumbs"]}
     except (KeyError, JSONDecodeError):
         thumbs_data = {}