Skip to content

Commit 693ff27

Browse files
committed
move utils in server
1 parent 5ceac5e commit 693ff27

File tree

8 files changed

+148
-150
lines changed

8 files changed

+148
-150
lines changed

src/gitingest/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
MAX_FILES = 10_000 # Maximum number of files to process
88
MAX_TOTAL_SIZE_BYTES = 500 * 1024 * 1024 # 500 MB
99

10+
OUTPUT_FILE_PATH = "digest.txt"
1011
TMP_BASE_PATH = Path("/tmp/gitingest")

src/server/main.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
from slowapi.errors import RateLimitExceeded
1111
from starlette.middleware.trustedhost import TrustedHostMiddleware
1212

13+
from gitingest.config import TMP_BASE_PATH
1314
from server.routers import download, dynamic, index
14-
from server.server_config import DELETE_REPO_AFTER, TMP_BASE_PATH
15-
from server.server_utils import limiter
15+
from server.server_config import DELETE_REPO_AFTER, templates
16+
from server.server_utils import lifespan, limiter, rate_limit_exception_handler
1617

1718
# Load environment variables from .env file
1819
load_dotenv()
@@ -43,9 +44,6 @@
4344
# Add middleware to enforce allowed hosts
4445
app.add_middleware(TrustedHostMiddleware, allowed_hosts=allowed_hosts)
4546

46-
# Set up template rendering
47-
templates = Jinja2Templates(directory="server/templates")
48-
4947

5048
@app.get("/health")
5149
async def health_check() -> dict[str, str]:

src/server/routers/download.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from fastapi import APIRouter, HTTPException
44
from fastapi.responses import Response
55

6-
from server.server_config import TMP_BASE_PATH
6+
from gitingest.config import TMP_BASE_PATH
77

88
router = APIRouter()
99

src/server/routers/dynamic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
from fastapi.responses import HTMLResponse
55

66
from server.query_processor import process_query
7+
from server.server_config import templates
78
from server.server_utils import limiter
89

910
router = APIRouter()
10-
templates = Jinja2Templates(directory="server/templates")
1111

1212

1313
@router.get("/{full_path:path}")

src/server/server_config.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
MAX_DISPLAY_SIZE: int = 300_000
88
DELETE_REPO_AFTER: int = 60 * 60 # In seconds
99

10-
OUTPUT_FILE_PATH = "digest.txt"
11-
TMP_BASE_PATH = Path("/tmp/gitingest")
1210

1311
EXAMPLE_REPOS: list[dict[str, str]] = [
1412
{"name": "Gitingest", "url": "https://github.com/cyclotruc/gitingest"},
@@ -18,4 +16,4 @@
1816
{"name": "ApiAnalytics", "url": "https://github.com/tom-draper/api-analytics"},
1917
]
2018

21-
templates = Jinja2Templates(directory="templates")
19+
templates = Jinja2Templates(directory="server/templates")

src/server/server_utils.py

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,153 @@
11
""" Utility functions for the server. """
22

3+
import asyncio
34
import math
5+
import shutil
6+
import time
7+
from contextlib import asynccontextmanager
8+
from pathlib import Path
49

5-
from slowapi import Limiter
10+
from fastapi import FastAPI, Request
11+
from fastapi.responses import Response
12+
from slowapi import Limiter, _rate_limit_exceeded_handler
13+
from slowapi.errors import RateLimitExceeded
614
from slowapi.util import get_remote_address
715

16+
from gitingest.config import TMP_BASE_PATH
17+
from server.server_config import DELETE_REPO_AFTER
18+
819
# Initialize a rate limiter
920
limiter = Limiter(key_func=get_remote_address)
1021

1122

23+
24+
25+
26+
27+
async def rate_limit_exception_handler(request: Request, exc: Exception) -> Response:
28+
"""
29+
Custom exception handler for rate-limiting errors.
30+
31+
Parameters
32+
----------
33+
request : Request
34+
The incoming HTTP request.
35+
exc : Exception
36+
The exception raised, expected to be RateLimitExceeded.
37+
38+
Returns
39+
-------
40+
Response
41+
A response indicating that the rate limit has been exceeded.
42+
43+
Raises
44+
------
45+
exc
46+
If the exception is not a RateLimitExceeded error, it is re-raised.
47+
"""
48+
if isinstance(exc, RateLimitExceeded):
49+
# Delegate to the default rate limit handler
50+
return _rate_limit_exceeded_handler(request, exc)
51+
# Re-raise other exceptions
52+
raise exc
53+
54+
55+
@asynccontextmanager
56+
async def lifespan(_: FastAPI):
57+
"""
58+
Lifecycle manager for handling startup and shutdown events for the FastAPI application.
59+
60+
Parameters
61+
----------
62+
_ : FastAPI
63+
The FastAPI application instance (unused).
64+
65+
Yields
66+
-------
67+
None
68+
Yields control back to the FastAPI application while the background task runs.
69+
"""
70+
task = asyncio.create_task(_remove_old_repositories())
71+
72+
yield
73+
# Cancel the background task on shutdown
74+
task.cancel()
75+
try:
76+
await task
77+
except asyncio.CancelledError:
78+
pass
79+
80+
81+
async def _remove_old_repositories():
82+
"""
83+
Periodically remove old repository folders.
84+
85+
Background task that runs periodically to clean up old repository directories.
86+
87+
This task:
88+
- Scans the TMP_BASE_PATH directory every 60 seconds
89+
- Removes directories older than DELETE_REPO_AFTER seconds
90+
- Before deletion, logs repository URLs to history.txt if a matching .txt file exists
91+
- Handles errors gracefully if deletion fails
92+
93+
The repository URL is extracted from the first .txt file in each directory,
94+
assuming the filename format: "owner-repository.txt"
95+
"""
96+
while True:
97+
try:
98+
if not TMP_BASE_PATH.exists():
99+
await asyncio.sleep(60)
100+
continue
101+
102+
current_time = time.time()
103+
104+
for folder in TMP_BASE_PATH.iterdir():
105+
if folder.is_dir():
106+
continue
107+
108+
# Skip if folder is not old enough
109+
if current_time - folder.stat().st_ctime <= DELETE_REPO_AFTER:
110+
continue
111+
112+
await _process_folder(folder)
113+
114+
except Exception as e:
115+
print(f"Error in _remove_old_repositories: {e}")
116+
117+
await asyncio.sleep(60)
118+
119+
120+
async def _process_folder(folder: Path) -> None:
121+
"""
122+
Process a single folder for deletion and logging.
123+
124+
Parameters
125+
----------
126+
folder : Path
127+
The path to the folder to be processed.
128+
"""
129+
# Try to log repository URL before deletion
130+
try:
131+
txt_files = [f for f in folder.iterdir() if f.suffix == ".txt"]
132+
133+
# Extract owner and repository name from the filename
134+
if txt_files and "-" in (filename := txt_files[0].stem):
135+
owner, repo = filename.split("-", 1)
136+
repo_url = f"{owner}/{repo}"
137+
138+
with open("history.txt", mode="a", encoding="utf-8") as history:
139+
history.write(f"{repo_url}\n")
140+
141+
except Exception as e:
142+
print(f"Error logging repository URL for {folder}: {e}")
143+
144+
# Delete the folder
145+
try:
146+
shutil.rmtree(folder)
147+
except Exception as e:
148+
print(f"Error deleting {folder}: {e}")
149+
150+
12151
def log_slider_to_size(position: int) -> int:
13152
"""
14153
Convert a slider position to a file size in bytes using a logarithmic scale.

src/utils.py

Lines changed: 0 additions & 138 deletions
This file was deleted.

tests/test_cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
from click.testing import CliRunner
66

7-
from config import MAX_FILE_SIZE, OUTPUT_FILE_PATH
87
from gitingest.cli import main
8+
from gitingest.config import MAX_FILE_SIZE, OUTPUT_FILE_PATH
99

1010

1111
def test_cli_with_default_options():

0 commit comments

Comments
 (0)