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" },
     { name = "zortazert", email = "zortazert@matthewevan.xyz" },
 ]
 ]
 dependencies = [
 dependencies = [
-    "requests~=2.32.3",
+    "aiohttp~=3.10.3",
     "selectolax~=0.3.21",
     "selectolax~=0.3.21",
     "fastapi~=0.112.1",
     "fastapi~=0.112.1",
     "uvicorn[standard]~=0.30.6",
     "uvicorn[standard]~=0.30.6",

+ 17 - 9
requirements-dev.lock

@@ -10,15 +10,19 @@
 #   universal: true
 #   universal: true
 
 
 -e file:.
 -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
 annotated-types==0.7.0
     # via pydantic
     # via pydantic
 anyio==4.4.0
 anyio==4.4.0
     # via starlette
     # via starlette
     # via watchfiles
     # via watchfiles
-certifi==2024.7.4
+attrs==24.2.0
-    # via requests
+    # via aiohttp
-charset-normalizer==3.3.2
-    # via requests
 click==8.1.7
 click==8.1.7
     # via uvicorn
     # via uvicorn
 colorama==0.4.6 ; platform_system == 'Windows' or sys_platform == 'win32'
 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
     # via uvicorn
 fastapi==0.112.1
 fastapi==0.112.1
     # via rural-dict
     # via rural-dict
+frozenlist==1.4.1
+    # via aiohttp
+    # via aiosignal
 h11==0.14.0
 h11==0.14.0
     # via uvicorn
     # via uvicorn
 httptools==0.6.1
 httptools==0.6.1
     # via uvicorn
     # via uvicorn
 idna==3.7
 idna==3.7
     # via anyio
     # via anyio
-    # via requests
+    # via yarl
 jinja2==3.1.4
 jinja2==3.1.4
     # via rural-dict
     # via rural-dict
 markupsafe==2.1.5
 markupsafe==2.1.5
     # via jinja2
     # via jinja2
+multidict==6.0.5
+    # via aiohttp
+    # via yarl
 pydantic==2.8.2
 pydantic==2.8.2
     # via fastapi
     # via fastapi
 pydantic-core==2.20.1
 pydantic-core==2.20.1
@@ -45,8 +55,6 @@ python-dotenv==1.0.1
     # via uvicorn
     # via uvicorn
 pyyaml==6.0.2
 pyyaml==6.0.2
     # via uvicorn
     # via uvicorn
-requests==2.32.3
-    # via rural-dict
 selectolax==0.3.21
 selectolax==0.3.21
     # via rural-dict
     # via rural-dict
 sniffio==1.3.1
 sniffio==1.3.1
@@ -57,8 +65,6 @@ typing-extensions==4.12.2
     # via fastapi
     # via fastapi
     # via pydantic
     # via pydantic
     # via pydantic-core
     # via pydantic-core
-urllib3==2.2.2
-    # via requests
 uvicorn==0.30.6
 uvicorn==0.30.6
     # via rural-dict
     # via rural-dict
 uvloop==0.20.0 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'
 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
     # via uvicorn
 websockets==12.0
 websockets==12.0
     # via uvicorn
     # via uvicorn
+yarl==1.9.4
+    # via aiohttp

+ 17 - 9
requirements.lock

@@ -10,15 +10,19 @@
 #   universal: true
 #   universal: true
 
 
 -e file:.
 -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
 annotated-types==0.7.0
     # via pydantic
     # via pydantic
 anyio==4.4.0
 anyio==4.4.0
     # via starlette
     # via starlette
     # via watchfiles
     # via watchfiles
-certifi==2024.7.4
+attrs==24.2.0
-    # via requests
+    # via aiohttp
-charset-normalizer==3.3.2
-    # via requests
 click==8.1.7
 click==8.1.7
     # via uvicorn
     # via uvicorn
 colorama==0.4.6 ; platform_system == 'Windows' or sys_platform == 'win32'
 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
     # via uvicorn
 fastapi==0.112.1
 fastapi==0.112.1
     # via rural-dict
     # via rural-dict
+frozenlist==1.4.1
+    # via aiohttp
+    # via aiosignal
 h11==0.14.0
 h11==0.14.0
     # via uvicorn
     # via uvicorn
 httptools==0.6.1
 httptools==0.6.1
     # via uvicorn
     # via uvicorn
 idna==3.7
 idna==3.7
     # via anyio
     # via anyio
-    # via requests
+    # via yarl
 jinja2==3.1.4
 jinja2==3.1.4
     # via rural-dict
     # via rural-dict
 markupsafe==2.1.5
 markupsafe==2.1.5
     # via jinja2
     # via jinja2
+multidict==6.0.5
+    # via aiohttp
+    # via yarl
 pydantic==2.8.2
 pydantic==2.8.2
     # via fastapi
     # via fastapi
 pydantic-core==2.20.1
 pydantic-core==2.20.1
@@ -45,8 +55,6 @@ python-dotenv==1.0.1
     # via uvicorn
     # via uvicorn
 pyyaml==6.0.2
 pyyaml==6.0.2
     # via uvicorn
     # via uvicorn
-requests==2.32.3
-    # via rural-dict
 selectolax==0.3.21
 selectolax==0.3.21
     # via rural-dict
     # via rural-dict
 sniffio==1.3.1
 sniffio==1.3.1
@@ -57,8 +65,6 @@ typing-extensions==4.12.2
     # via fastapi
     # via fastapi
     # via pydantic
     # via pydantic
     # via pydantic-core
     # via pydantic-core
-urllib3==2.2.2
-    # via requests
 uvicorn==0.30.6
 uvicorn==0.30.6
     # via rural-dict
     # via rural-dict
 uvloop==0.20.0 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'
 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
     # via uvicorn
 websockets==12.0
 websockets==12.0
     # via uvicorn
     # via uvicorn
+yarl==1.9.4
+    # via aiohttp

+ 26 - 13
src/rural_dict/__main__.py

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