Skip to content

Commit 2b1f228

Browse files
authored
feat: Refactor backend to a rest api (#346)
* refactor: Refactor backend to a rest api and make weak changes to the front to make it works * fix: remove result bool in Success response object since it's useless, remove sensitive data from the Success Response object, make the integration unit test compatible * fix: clean query processor return types, remove deprecated fields, better handling for few errors * fix: unit tests, remove deprecated is_index from jinja templates --------- Co-authored-by: ix <n.guintini@protonmail.com>
1 parent f8d397e commit 2b1f228

File tree

15 files changed

+467
-364
lines changed

15 files changed

+467
-364
lines changed

src/server/main.py

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@
66
from pathlib import Path
77

88
from dotenv import load_dotenv
9-
from fastapi import FastAPI, Request
9+
from fastapi import FastAPI
1010
from fastapi.responses import FileResponse, HTMLResponse
1111
from fastapi.staticfiles import StaticFiles
1212
from slowapi.errors import RateLimitExceeded
1313
from starlette.middleware.trustedhost import TrustedHostMiddleware
1414

15-
from server.routers import download, dynamic, index
16-
from server.server_config import templates
15+
from server.routers import dynamic, index, ingest
1716
from server.server_utils import lifespan, limiter, rate_limit_exception_handler
1817

1918
# Load environment variables from .env file
@@ -58,7 +57,7 @@ async def health_check() -> dict[str, str]:
5857
return {"status": "healthy"}
5958

6059

61-
@app.head("/")
60+
@app.head("/", include_in_schema=False)
6261
async def head_root() -> HTMLResponse:
6362
"""Respond to HTTP HEAD requests for the root URL.
6463
@@ -73,26 +72,7 @@ async def head_root() -> HTMLResponse:
7372
return HTMLResponse(content=None, headers={"content-type": "text/html; charset=utf-8"})
7473

7574

76-
@app.get("/api/", response_class=HTMLResponse)
77-
@app.get("/api", response_class=HTMLResponse)
78-
async def api_docs(request: Request) -> HTMLResponse:
79-
"""Render the API documentation page.
80-
81-
Parameters
82-
----------
83-
request : Request
84-
The incoming HTTP request.
85-
86-
Returns
87-
-------
88-
HTMLResponse
89-
A rendered HTML page displaying API documentation.
90-
91-
"""
92-
return templates.TemplateResponse("api.jinja", {"request": request})
93-
94-
95-
@app.get("/robots.txt")
75+
@app.get("/robots.txt", include_in_schema=False)
9676
async def robots() -> FileResponse:
9777
"""Serve the ``robots.txt`` file to guide search engine crawlers.
9878
@@ -120,5 +100,5 @@ async def llm_txt() -> FileResponse:
120100

121101
# Include routers for modular endpoints
122102
app.include_router(index)
123-
app.include_router(download)
103+
app.include_router(ingest)
124104
app.include_router(dynamic)

src/server/models.py

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,116 @@
22

33
from __future__ import annotations
44

5-
from pydantic import BaseModel
5+
from enum import Enum
6+
from typing import Union
7+
8+
from pydantic import BaseModel, Field, field_validator
69

710
# needed for type checking (pydantic)
811
from server.form_types import IntForm, OptStrForm, StrForm # noqa: TC001 (typing-only-first-party-import)
912

1013

14+
class PatternType(str, Enum):
15+
"""Enumeration for pattern types used in file filtering."""
16+
17+
INCLUDE = "include"
18+
EXCLUDE = "exclude"
19+
20+
21+
class IngestRequest(BaseModel):
22+
"""Request model for the /api/ingest endpoint.
23+
24+
Attributes
25+
----------
26+
input_text : str
27+
The Git repository URL or slug to ingest.
28+
max_file_size : int
29+
Maximum file size slider position (0-500) for filtering files.
30+
pattern_type : PatternType
31+
Type of pattern to use for file filtering (include or exclude).
32+
pattern : str
33+
Glob/regex pattern string for file filtering.
34+
token : str | None
35+
GitHub personal access token (PAT) for accessing private repositories.
36+
37+
"""
38+
39+
input_text: str = Field(..., description="Git repository URL or slug to ingest")
40+
max_file_size: int = Field(..., ge=0, le=500, description="File size slider position (0-500)")
41+
pattern_type: PatternType = Field(default=PatternType.EXCLUDE, description="Pattern type for file filtering")
42+
pattern: str = Field(default="", description="Glob/regex pattern for file filtering")
43+
token: str | None = Field(default=None, description="GitHub PAT for private repositories")
44+
45+
@field_validator("input_text")
46+
@classmethod
47+
def validate_input_text(cls, v: str) -> str:
48+
"""Validate that input_text is not empty."""
49+
if not v.strip():
50+
err = "input_text cannot be empty"
51+
raise ValueError(err)
52+
return v.strip()
53+
54+
@field_validator("pattern")
55+
@classmethod
56+
def validate_pattern(cls, v: str) -> str:
57+
"""Validate pattern field."""
58+
return v.strip()
59+
60+
61+
class IngestSuccessResponse(BaseModel):
62+
"""Success response model for the /api/ingest endpoint.
63+
64+
Attributes
65+
----------
66+
repo_url : str
67+
The original repository URL that was processed.
68+
short_repo_url : str
69+
Short form of repository URL (user/repo).
70+
summary : str
71+
Summary of the ingestion process including token estimates.
72+
tree : str
73+
File tree structure of the repository.
74+
content : str
75+
Processed content from the repository files.
76+
default_max_file_size : int
77+
The file size slider position used.
78+
pattern_type : str
79+
The pattern type used for filtering.
80+
pattern : str
81+
The pattern used for filtering.
82+
83+
"""
84+
85+
repo_url: str = Field(..., description="Original repository URL")
86+
short_repo_url: str = Field(..., description="Short repository URL (user/repo)")
87+
summary: str = Field(..., description="Ingestion summary with token estimates")
88+
tree: str = Field(..., description="File tree structure")
89+
content: str = Field(..., description="Processed file content")
90+
default_max_file_size: int = Field(..., description="File size slider position used")
91+
pattern_type: str = Field(..., description="Pattern type used")
92+
pattern: str = Field(..., description="Pattern used")
93+
94+
95+
class IngestErrorResponse(BaseModel):
96+
"""Error response model for the /api/ingest endpoint.
97+
98+
Attributes
99+
----------
100+
error : str
101+
Error message describing what went wrong.
102+
repo_url : str
103+
The repository URL that failed to process.
104+
105+
"""
106+
107+
error: str = Field(..., description="Error message")
108+
repo_url: str = Field(..., description="Repository URL that failed")
109+
110+
111+
# Union type for API responses
112+
IngestResponse = Union[IngestSuccessResponse, IngestErrorResponse]
113+
114+
11115
class QueryForm(BaseModel):
12116
"""Form data for the query.
13117

src/server/query_processor.py

Lines changed: 22 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,35 @@
22

33
from __future__ import annotations
44

5-
from functools import partial
65
from pathlib import Path
7-
from typing import TYPE_CHECKING, cast
6+
from typing import cast
87

98
from gitingest.clone import clone_repo
109
from gitingest.ingestion import ingest_query
1110
from gitingest.query_parser import IngestionQuery, parse_query
1211
from gitingest.utils.git_utils import validate_github_token
12+
from server.models import IngestErrorResponse, IngestResponse, IngestSuccessResponse
1313
from server.server_config import (
14-
DEFAULT_FILE_SIZE_KB,
15-
EXAMPLE_REPOS,
14+
DEFAULT_MAX_FILE_SIZE_KB,
1615
MAX_DISPLAY_SIZE,
17-
templates,
1816
)
1917
from server.server_utils import Colors, log_slider_to_size
2018

21-
if TYPE_CHECKING:
22-
from fastapi import Request
23-
from starlette.templating import _TemplateResponse
24-
2519

2620
async def process_query(
27-
request: Request,
28-
*,
2921
input_text: str,
3022
slider_position: int,
3123
pattern_type: str = "exclude",
3224
pattern: str = "",
33-
is_index: bool = False,
3425
token: str | None = None,
35-
) -> _TemplateResponse:
26+
) -> IngestResponse:
3627
"""Process a query by parsing input, cloning a repository, and generating a summary.
3728
3829
Handle user input, process Git repository data, and prepare
3930
a response for rendering a template with the processed results or an error message.
4031
4132
Parameters
4233
----------
43-
request : Request
44-
The HTTP request object.
4534
input_text : str
4635
Input text provided by the user, typically a Git repository URL or slug.
4736
slider_position : int
@@ -50,15 +39,13 @@ async def process_query(
5039
Type of pattern to use (either "include" or "exclude") (default: ``"exclude"``).
5140
pattern : str
5241
Pattern to include or exclude in the query, depending on the pattern type.
53-
is_index : bool
54-
Flag indicating whether the request is for the index page (default: ``False``).
5542
token : str | None
5643
GitHub personal access token (PAT) for accessing private repositories.
5744
5845
Returns
5946
-------
60-
_TemplateResponse
61-
Rendered template response containing the processed results or an error message.
47+
IngestResponse
48+
A union type, corresponding to IngestErrorResponse or IngestSuccessResponse
6249
6350
Raises
6451
------
@@ -79,21 +66,10 @@ async def process_query(
7966
if token:
8067
validate_github_token(token)
8168

82-
template = "index.jinja" if is_index else "git.jinja"
83-
template_response = partial(templates.TemplateResponse, name=template)
8469
max_file_size = log_slider_to_size(slider_position)
8570

86-
context = {
87-
"request": request,
88-
"repo_url": input_text,
89-
"examples": EXAMPLE_REPOS if is_index else [],
90-
"default_file_size": slider_position,
91-
"pattern_type": pattern_type,
92-
"pattern": pattern,
93-
"token": token,
94-
}
95-
9671
query: IngestionQuery | None = None
72+
short_repo_url = ""
9773

9874
try:
9975
query = await parse_query(
@@ -107,7 +83,7 @@ async def process_query(
10783
query.ensure_url()
10884

10985
# Sets the "<user>/<repo>" for the page title
110-
context["short_repo_url"] = f"{query.user_name}/{query.repo_name}"
86+
short_repo_url = f"{query.user_name}/{query.repo_name}"
11187

11288
clone_config = query.extract_clone_config()
11389
await clone_repo(clone_config, token=token)
@@ -126,10 +102,10 @@ async def process_query(
126102
print(f"{Colors.BROWN}WARN{Colors.END}: {Colors.RED}<- {Colors.END}", end="")
127103
print(f"{Colors.RED}{exc}{Colors.END}")
128104

129-
context["error_message"] = f"Error: {exc}"
130-
if "405" in str(exc):
131-
context["error_message"] = "Repository not found. Please make sure it is public."
132-
return template_response(context=context)
105+
return IngestErrorResponse(
106+
error="Repository not found. Please make sure it is public." if "405" in str(exc) else "",
107+
repo_url=short_repo_url,
108+
)
133109

134110
if len(content) > MAX_DISPLAY_SIZE:
135111
content = (
@@ -148,18 +124,17 @@ async def process_query(
148124
summary=summary,
149125
)
150126

151-
context.update(
152-
{
153-
"result": True,
154-
"summary": summary,
155-
"tree": tree,
156-
"content": content,
157-
"ingest_id": query.id,
158-
},
127+
return IngestSuccessResponse(
128+
repo_url=input_text,
129+
short_repo_url=short_repo_url,
130+
summary=summary,
131+
tree=tree,
132+
content=content,
133+
default_max_file_size=slider_position,
134+
pattern_type=pattern_type,
135+
pattern=pattern,
159136
)
160137

161-
return template_response(context=context)
162-
163138

164139
def _print_query(url: str, max_file_size: int, pattern_type: str, pattern: str) -> None:
165140
"""Print a formatted summary of the query details for debugging.
@@ -177,7 +152,7 @@ def _print_query(url: str, max_file_size: int, pattern_type: str, pattern: str)
177152
178153
"""
179154
print(f"{Colors.WHITE}{url:<20}{Colors.END}", end="")
180-
if int(max_file_size / 1024) != DEFAULT_FILE_SIZE_KB:
155+
if int(max_file_size / 1024) != DEFAULT_MAX_FILE_SIZE_KB:
181156
print(
182157
f" | {Colors.YELLOW}Size: {int(max_file_size / 1024)}kb{Colors.END}",
183158
end="",

src/server/routers/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Module containing the routers for the FastAPI application."""
22

3-
from server.routers.download import router as download
43
from server.routers.dynamic import router as dynamic
54
from server.routers.index import router as index
5+
from server.routers.ingest import router as ingest
66

7-
__all__ = ["download", "dynamic", "index"]
7+
__all__ = ["dynamic", "index", "ingest"]

0 commit comments

Comments
 (0)