diff --git a/backend/main.py b/backend/main.py index 12597af..29489af 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,5 +1,6 @@ import asyncio import json +import logging import sys from typing import Any, List, Optional @@ -7,8 +8,23 @@ from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel +from utils.cache import ( + cache_result, + github_commits_cache, + github_repos_cache, + make_github_commits_key, + make_github_repos_key, + make_social_key, + social_cache, +) from utils.security import is_safe_url +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(name)s] %(levelname)s: %(message)s", + datefmt="%H:%M:%S", +) + # Resilient Windows subprocess execution configuration for asyncio if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) @@ -74,13 +90,22 @@ async def handle_scan(request: ScanRequest): raise HTTPException(status_code=400, detail="Target cannot be empty.") if _is_github_input(input_data): - git_data: dict | None = await analyze_github_target(input_data) + git_data: dict | None = await cache_result( + github_repos_cache, + make_github_repos_key(input_data), + analyze_github_target, + input_data, + ) if "error" in git_data: raise HTTPException(status_code=400, detail=git_data["error"]) - social_result = await scan_username( - git_data["username"], include_variations=request.include_variations + social_result = await cache_result( + social_cache, + make_social_key(git_data["username"], request.include_variations), + scan_username, + git_data["username"], + include_variations=request.include_variations, ) owner_name = git_data.get("username", "") @@ -103,8 +128,12 @@ async def handle_scan(request: ScanRequest): } else: - social_result = await scan_username( - input_data, include_variations=request.include_variations + social_result = await cache_result( + social_cache, + make_social_key(input_data, request.include_variations), + scan_username, + input_data, + include_variations=request.include_variations, ) if "error" in social_result: @@ -121,7 +150,16 @@ async def handle_scan(request: ScanRequest): git_data = None if github_targets: - git_tasks = [analyze_github_target(user) for user in github_targets if user] + git_tasks = [ + cache_result( + github_repos_cache, + make_github_repos_key(user), + analyze_github_target, + user, + ) + for user in github_targets + if user + ] git_results = await asyncio.gather(*git_tasks) aggregated_interesting = [] @@ -190,7 +228,13 @@ async def handle_scan_commits(request: CommitRequest): status_code=400, detail="Repository name and username are required." ) - result = await fetch_repo_commits(username, repo_name) + result = await cache_result( + github_commits_cache, + make_github_commits_key(username, repo_name), + fetch_repo_commits, + username, + repo_name, + ) if "error" in result: raise HTTPException(status_code=400, detail=result["error"]) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index de1bc14..99f3022 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -5,6 +5,7 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.13" dependencies = [ + "cachetools>=5.5.0", "curl-cffi>=0.15.0", "fastapi[standard]>=0.138.0", "httpx>=0.28.1", diff --git a/backend/requirements.txt b/backend/requirements.txt index c6e1bec..5ff04e1 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,3 +1,4 @@ +cachetools>=5.5.0 fastapi[standard] uvicorn httpx diff --git a/backend/utils/cache.py b/backend/utils/cache.py new file mode 100644 index 0000000..4147a21 --- /dev/null +++ b/backend/utils/cache.py @@ -0,0 +1,48 @@ +import logging +from typing import Any + +from cachetools import TTLCache + +logger = logging.getLogger(__name__) + +SOCIAL_TTL = 600 +REPOS_TTL = 1800 +COMMITS_TTL = 3600 + +social_cache: TTLCache = TTLCache(maxsize=256, ttl=SOCIAL_TTL) +github_repos_cache: TTLCache = TTLCache(maxsize=128, ttl=REPOS_TTL) +github_commits_cache: TTLCache = TTLCache(maxsize=256, ttl=COMMITS_TTL) + + +def make_social_key(username: str, include_variations: bool) -> tuple: + return ("social", username, include_variations) + + +def make_github_repos_key(target_input: str) -> tuple: + return ("repos", target_input) + + +def make_github_commits_key(username: str, repo_name: str) -> tuple: + return ("commits", username, repo_name) + + +async def cache_result(cache: TTLCache, key: tuple, func, *args, **kwargs) -> Any: + if key in cache: + logger.info("Cache HIT for %s", key) + return cache[key] + + result = await func(*args, **kwargs) + + if isinstance(result, dict) and "error" in result: + logger.info("Cache MISS for %s (not caching — error response)", key) + return result + + cache[key] = result + logger.info("Cache MISS for %s", key) + return result + + +def clear_all_caches() -> None: + social_cache.clear() + github_repos_cache.clear() + github_commits_cache.clear() diff --git a/backend/uv.lock b/backend/uv.lock index 42d0df2..5ad9db3 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -41,6 +41,7 @@ name = "backend" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "cachetools" }, { name = "curl-cffi" }, { name = "fastapi", extra = ["standard"] }, { name = "httpx" }, @@ -52,6 +53,7 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "cachetools", specifier = ">=5.5.0" }, { name = "curl-cffi", specifier = ">=0.15.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.138.0" }, { name = "httpx", specifier = ">=0.28.1" }, @@ -61,6 +63,15 @@ requires-dist = [ { name = "uvicorn", specifier = ">=0.49.0" }, ] +[[package]] +name = "cachetools" +version = "7.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085, upload-time = "2026-05-21T22:40:43.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" }, +] + [[package]] name = "certifi" version = "2026.6.17"