Spaces:
Runtime error
Runtime error
tech-envision
commited on
Commit
·
6da6be7
1
Parent(s):
94490a0
Add FastAPI backend with document management
Browse files- src/api.py +73 -0
- src/document_service.py +60 -0
src/api.py
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from __future__ import annotations
|
2 |
+
|
3 |
+
from typing import List
|
4 |
+
from datetime import datetime
|
5 |
+
|
6 |
+
from fastapi import FastAPI, UploadFile, File, HTTPException
|
7 |
+
from pydantic import BaseModel
|
8 |
+
from fastapi.responses import PlainTextResponse
|
9 |
+
|
10 |
+
from .document_service import (
|
11 |
+
save_document,
|
12 |
+
list_documents,
|
13 |
+
get_document,
|
14 |
+
read_content,
|
15 |
+
)
|
16 |
+
from .log import get_logger
|
17 |
+
|
18 |
+
|
19 |
+
class DocumentInfo(BaseModel):
|
20 |
+
id: int
|
21 |
+
original_name: str
|
22 |
+
file_path: str
|
23 |
+
created_at: datetime
|
24 |
+
|
25 |
+
class Config:
|
26 |
+
from_attributes = True
|
27 |
+
|
28 |
+
|
29 |
+
class DocumentDetail(DocumentInfo):
|
30 |
+
content: str
|
31 |
+
|
32 |
+
|
33 |
+
def create_app() -> FastAPI:
|
34 |
+
app = FastAPI(title="LLM Backend API")
|
35 |
+
log = get_logger(__name__)
|
36 |
+
|
37 |
+
@app.post("/users/{username}/documents", response_model=DocumentInfo)
|
38 |
+
async def upload(username: str, file: UploadFile = File(...)) -> DocumentInfo:
|
39 |
+
log.info("Uploading document %s for %s", file.filename, username)
|
40 |
+
doc = save_document(username, file)
|
41 |
+
return DocumentInfo.model_validate(doc.__data__)
|
42 |
+
|
43 |
+
@app.get("/users/{username}/documents", response_model=List[DocumentInfo])
|
44 |
+
async def list_docs(username: str) -> List[DocumentInfo]:
|
45 |
+
docs = list_documents(username)
|
46 |
+
return [DocumentInfo.model_validate(d.__data__) for d in docs]
|
47 |
+
|
48 |
+
@app.get("/users/{username}/documents/{doc_id}", response_model=DocumentDetail)
|
49 |
+
async def inspect(username: str, doc_id: int) -> DocumentDetail:
|
50 |
+
doc = get_document(username, doc_id)
|
51 |
+
if not doc:
|
52 |
+
raise HTTPException(status_code=404, detail="Document not found")
|
53 |
+
content = read_content(doc)
|
54 |
+
data = DocumentDetail.model_validate(doc.__data__)
|
55 |
+
data.content = content
|
56 |
+
return data
|
57 |
+
|
58 |
+
@app.get("/users/{username}/documents/{doc_id}/raw", response_class=PlainTextResponse)
|
59 |
+
async def download(username: str, doc_id: int) -> PlainTextResponse:
|
60 |
+
doc = get_document(username, doc_id)
|
61 |
+
if not doc:
|
62 |
+
raise HTTPException(status_code=404, detail="Document not found")
|
63 |
+
return PlainTextResponse(read_content(doc))
|
64 |
+
|
65 |
+
return app
|
66 |
+
|
67 |
+
|
68 |
+
app = create_app()
|
69 |
+
|
70 |
+
if __name__ == "__main__":
|
71 |
+
import uvicorn
|
72 |
+
|
73 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
src/document_service.py
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from __future__ import annotations
|
2 |
+
|
3 |
+
from pathlib import Path
|
4 |
+
import shutil
|
5 |
+
from typing import List, Optional
|
6 |
+
|
7 |
+
from fastapi import UploadFile
|
8 |
+
|
9 |
+
from .config import UPLOAD_DIR
|
10 |
+
from .db import Document, User, init_db
|
11 |
+
|
12 |
+
|
13 |
+
def _ensure_user_dir(username: str) -> Path:
|
14 |
+
path = Path(UPLOAD_DIR) / username
|
15 |
+
path.mkdir(parents=True, exist_ok=True)
|
16 |
+
return path
|
17 |
+
|
18 |
+
|
19 |
+
def save_document(username: str, file: UploadFile) -> Document:
|
20 |
+
"""Persist an uploaded file and return its database entry."""
|
21 |
+
init_db()
|
22 |
+
user, _ = User.get_or_create(username=username)
|
23 |
+
dest_dir = _ensure_user_dir(username)
|
24 |
+
dest = dest_dir / file.filename
|
25 |
+
with dest.open('wb') as buffer:
|
26 |
+
shutil.copyfileobj(file.file, buffer)
|
27 |
+
doc = Document.create(user=user, file_path=str(dest), original_name=file.filename)
|
28 |
+
return doc
|
29 |
+
|
30 |
+
|
31 |
+
def list_documents(username: str) -> List[Document]:
|
32 |
+
"""Return all documents for ``username`` sorted by creation time."""
|
33 |
+
init_db()
|
34 |
+
try:
|
35 |
+
user = User.get(User.username == username)
|
36 |
+
except User.DoesNotExist:
|
37 |
+
return []
|
38 |
+
docs = Document.select().where(Document.user == user).order_by(Document.created_at)
|
39 |
+
return list(docs)
|
40 |
+
|
41 |
+
|
42 |
+
def get_document(username: str, doc_id: int) -> Optional[Document]:
|
43 |
+
"""Retrieve a single document for ``username`` by id."""
|
44 |
+
init_db()
|
45 |
+
try:
|
46 |
+
user = User.get(User.username == username)
|
47 |
+
except User.DoesNotExist:
|
48 |
+
return None
|
49 |
+
try:
|
50 |
+
return Document.get(Document.id == doc_id, Document.user == user)
|
51 |
+
except Document.DoesNotExist:
|
52 |
+
return None
|
53 |
+
|
54 |
+
|
55 |
+
def read_content(doc: Document) -> str:
|
56 |
+
"""Read and return the text content of ``doc``. Errors yield empty string."""
|
57 |
+
try:
|
58 |
+
return Path(doc.file_path).read_text(encoding='utf-8', errors='replace')
|
59 |
+
except Exception:
|
60 |
+
return ''
|