Max Saavedra commited on
Commit
7d1fec5
·
1 Parent(s): b208fdb

Codex changes

Browse files
.gitignore CHANGED
The diff for this file is too large to render. See raw diff
 
.venv/Lib/site-packages/gradio/hash_seed.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ 72a123ea2cd04be7b7f20053c7e03c99
backend/__pycache__/app.cpython-312.pyc ADDED
Binary file (673 Bytes). View file
 
backend/api/__pycache__/notebooks.cpython-312.pyc ADDED
Binary file (3.32 kB). View file
 
backend/api/notebooks.py CHANGED
@@ -1,27 +1,61 @@
1
- from fastapi import APIRouter, HTTPException
2
- from backend.models.schemas import NotebookCreate, NotebookOut
 
 
 
3
  from backend.services.storage import NotebookStore
4
 
5
  router = APIRouter()
6
  store = NotebookStore(base_dir="data")
7
 
8
 
 
 
 
 
 
 
 
 
9
  @router.post("/", response_model=NotebookOut)
10
  def create_notebook(payload: NotebookCreate) -> NotebookOut:
11
- return store.create(payload)
 
 
 
12
 
13
 
14
  @router.get("/{notebook_id}", response_model=NotebookOut)
15
- def get_notebook(notebook_id: str) -> NotebookOut:
16
- notebook = store.get(notebook_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  if not notebook:
18
  raise HTTPException(status_code=404, detail="Notebook not found")
19
  return notebook
20
 
21
 
22
  @router.delete("/{notebook_id}")
23
- def delete_notebook(notebook_id: str) -> dict:
24
- deleted = store.delete(notebook_id)
 
 
 
 
25
  if not deleted:
26
  raise HTTPException(status_code=404, detail="Notebook not found")
27
  return {"deleted": True}
 
1
+ from typing import List
2
+
3
+ from fastapi import APIRouter, HTTPException, Query
4
+
5
+ from backend.models.schemas import NotebookCreate, NotebookOut, NotebookRename
6
  from backend.services.storage import NotebookStore
7
 
8
  router = APIRouter()
9
  store = NotebookStore(base_dir="data")
10
 
11
 
12
+ @router.get("/", response_model=List[NotebookOut])
13
+ def list_notebooks(user_id: str = Query(...)) -> List[NotebookOut]:
14
+ try:
15
+ return store.list(user_id)
16
+ except ValueError as exc:
17
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
18
+
19
+
20
  @router.post("/", response_model=NotebookOut)
21
  def create_notebook(payload: NotebookCreate) -> NotebookOut:
22
+ try:
23
+ return store.create(payload)
24
+ except ValueError as exc:
25
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
26
 
27
 
28
  @router.get("/{notebook_id}", response_model=NotebookOut)
29
+ def get_notebook(notebook_id: str, user_id: str = Query(...)) -> NotebookOut:
30
+ try:
31
+ notebook = store.get(user_id, notebook_id)
32
+ except ValueError as exc:
33
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
34
+
35
+ if not notebook:
36
+ raise HTTPException(status_code=404, detail="Notebook not found")
37
+ return notebook
38
+
39
+
40
+ @router.patch("/{notebook_id}", response_model=NotebookOut)
41
+ def rename_notebook(notebook_id: str, payload: NotebookRename) -> NotebookOut:
42
+ try:
43
+ notebook = store.rename(payload.user_id, notebook_id, payload.name)
44
+ except ValueError as exc:
45
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
46
+
47
  if not notebook:
48
  raise HTTPException(status_code=404, detail="Notebook not found")
49
  return notebook
50
 
51
 
52
  @router.delete("/{notebook_id}")
53
+ def delete_notebook(notebook_id: str, user_id: str = Query(...)) -> dict:
54
+ try:
55
+ deleted = store.delete(user_id, notebook_id)
56
+ except ValueError as exc:
57
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
58
+
59
  if not deleted:
60
  raise HTTPException(status_code=404, detail="Notebook not found")
61
  return {"deleted": True}
backend/models/__pycache__/schemas.cpython-312.pyc ADDED
Binary file (882 Bytes). View file
 
backend/models/schemas.py CHANGED
@@ -6,7 +6,14 @@ class NotebookCreate(BaseModel):
6
  name: str
7
 
8
 
 
 
 
 
 
9
  class NotebookOut(BaseModel):
10
  notebook_id: str
11
  user_id: str
12
  name: str
 
 
 
6
  name: str
7
 
8
 
9
+ class NotebookRename(BaseModel):
10
+ user_id: str
11
+ name: str
12
+
13
+
14
  class NotebookOut(BaseModel):
15
  notebook_id: str
16
  user_id: str
17
  name: str
18
+ created_at: str
19
+ updated_at: str
backend/services/__pycache__/storage.cpython-312.pyc ADDED
Binary file (9.1 kB). View file
 
backend/services/storage.py CHANGED
@@ -1,7 +1,10 @@
1
  import json
 
 
2
  import uuid
 
3
  from pathlib import Path
4
- from typing import Optional
5
 
6
  from backend.models.schemas import NotebookCreate, NotebookOut
7
 
@@ -11,38 +14,135 @@ class NotebookStore:
11
  self.base_dir = Path(base_dir)
12
  self.base_dir.mkdir(parents=True, exist_ok=True)
13
 
14
- def _notebook_path(self, notebook_id: str) -> Path:
15
- return self.base_dir / notebook_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  def create(self, payload: NotebookCreate) -> NotebookOut:
 
 
 
18
  notebook_id = str(uuid.uuid4())
19
- notebook_dir = self._notebook_path(notebook_id)
20
  notebook_dir.mkdir(parents=True, exist_ok=False)
 
 
 
21
  data = {
22
  "notebook_id": notebook_id,
23
- "user_id": payload.user_id,
24
  "name": payload.name,
 
 
25
  }
26
- (notebook_dir / "meta.json").write_text(json.dumps(data, indent=2))
 
 
 
 
27
  return NotebookOut(**data)
28
 
29
- def get(self, notebook_id: str) -> Optional[NotebookOut]:
30
- notebook_dir = self._notebook_path(notebook_id)
31
- meta_path = notebook_dir / "meta.json"
 
 
 
 
32
  if not meta_path.exists():
33
  return None
34
- data = json.loads(meta_path.read_text())
 
 
35
  return NotebookOut(**data)
36
 
37
- def delete(self, notebook_id: str) -> bool:
38
- notebook_dir = self._notebook_path(notebook_id)
39
- if not notebook_dir.exists():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  return False
41
- for child in notebook_dir.rglob("*"):
42
- if child.is_file():
43
- child.unlink()
44
- for child in sorted(notebook_dir.rglob("*"), reverse=True):
45
- if child.is_dir():
46
- child.rmdir()
47
- notebook_dir.rmdir()
 
 
48
  return True
 
1
  import json
2
+ import re
3
+ import shutil
4
  import uuid
5
+ from datetime import datetime, timezone
6
  from pathlib import Path
7
+ from typing import List, Optional
8
 
9
  from backend.models.schemas import NotebookCreate, NotebookOut
10
 
 
14
  self.base_dir = Path(base_dir)
15
  self.base_dir.mkdir(parents=True, exist_ok=True)
16
 
17
+ def _validate_user_id(self, user_id: str) -> str:
18
+ if not re.fullmatch(r"[A-Za-z0-9._@-]+", user_id):
19
+ raise ValueError("Invalid user_id")
20
+ return user_id
21
+
22
+ def _users_root(self) -> Path:
23
+ return self.base_dir / "users"
24
+
25
+ def _user_root(self, user_id: str) -> Path:
26
+ return self._users_root() / self._validate_user_id(user_id)
27
+
28
+ def _notebooks_root(self, user_id: str) -> Path:
29
+ return self._user_root(user_id) / "notebooks"
30
+
31
+ def _index_path(self, user_id: str) -> Path:
32
+ return self._notebooks_root(user_id) / "index.json"
33
+
34
+ def _notebook_path(self, user_id: str, notebook_id: str) -> Path:
35
+ return self._notebooks_root(user_id) / notebook_id
36
+
37
+ def _meta_path(self, user_id: str, notebook_id: str) -> Path:
38
+ return self._notebook_path(user_id, notebook_id) / "meta.json"
39
+
40
+ def _now(self) -> str:
41
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
42
+
43
+ def _ensure_user_tree(self, user_id: str) -> None:
44
+ self._notebooks_root(user_id).mkdir(parents=True, exist_ok=True)
45
+
46
+ def _read_json(self, path: Path, default):
47
+ if not path.exists():
48
+ return default
49
+ return json.loads(path.read_text(encoding="utf-8"))
50
+
51
+ def _write_json(self, path: Path, data: object) -> None:
52
+ path.parent.mkdir(parents=True, exist_ok=True)
53
+ path.write_text(json.dumps(data, indent=2), encoding="utf-8")
54
+
55
+ def _load_index(self, user_id: str) -> List[dict]:
56
+ self._ensure_user_tree(user_id)
57
+ data = self._read_json(self._index_path(user_id), default=[])
58
+ if isinstance(data, list):
59
+ return [item for item in data if isinstance(item, dict)]
60
+ return []
61
+
62
+ def _save_index(self, user_id: str, items: List[dict]) -> None:
63
+ self._write_json(self._index_path(user_id), items)
64
+
65
+ def _initialize_notebook_dirs(self, notebook_dir: Path) -> None:
66
+ for rel in [
67
+ "files_raw",
68
+ "files_extracted",
69
+ "chroma",
70
+ "chat",
71
+ "artifacts/reports",
72
+ "artifacts/quizzes",
73
+ "artifacts/podcasts",
74
+ ]:
75
+ (notebook_dir / rel).mkdir(parents=True, exist_ok=True)
76
+ (notebook_dir / "chat" / "messages.jsonl").touch(exist_ok=True)
77
 
78
  def create(self, payload: NotebookCreate) -> NotebookOut:
79
+ user_id = self._validate_user_id(payload.user_id)
80
+ self._ensure_user_tree(user_id)
81
+
82
  notebook_id = str(uuid.uuid4())
83
+ notebook_dir = self._notebook_path(user_id, notebook_id)
84
  notebook_dir.mkdir(parents=True, exist_ok=False)
85
+ self._initialize_notebook_dirs(notebook_dir)
86
+
87
+ now = self._now()
88
  data = {
89
  "notebook_id": notebook_id,
90
+ "user_id": user_id,
91
  "name": payload.name,
92
+ "created_at": now,
93
+ "updated_at": now,
94
  }
95
+ self._write_json(notebook_dir / "meta.json", data)
96
+
97
+ index = self._load_index(user_id)
98
+ index.append(data)
99
+ self._save_index(user_id, index)
100
  return NotebookOut(**data)
101
 
102
+ def list(self, user_id: str) -> List[NotebookOut]:
103
+ self._validate_user_id(user_id)
104
+ return [NotebookOut(**item) for item in self._load_index(user_id)]
105
+
106
+ def get(self, user_id: str, notebook_id: str) -> Optional[NotebookOut]:
107
+ user_id = self._validate_user_id(user_id)
108
+ meta_path = self._meta_path(user_id, notebook_id)
109
  if not meta_path.exists():
110
  return None
111
+ data = self._read_json(meta_path, default=None)
112
+ if not isinstance(data, dict) or data.get("user_id") != user_id:
113
+ return None
114
  return NotebookOut(**data)
115
 
116
+ def rename(self, user_id: str, notebook_id: str, name: str) -> Optional[NotebookOut]:
117
+ notebook = self.get(user_id, notebook_id)
118
+ if notebook is None:
119
+ return None
120
+
121
+ updated = notebook.model_dump()
122
+ updated["name"] = name
123
+ updated["updated_at"] = self._now()
124
+ self._write_json(self._meta_path(user_id, notebook_id), updated)
125
+
126
+ index = self._load_index(user_id)
127
+ for item in index:
128
+ if item.get("notebook_id") == notebook_id:
129
+ item.update(updated)
130
+ break
131
+ self._save_index(user_id, index)
132
+ return NotebookOut(**updated)
133
+
134
+ def delete(self, user_id: str, notebook_id: str) -> bool:
135
+ user_id = self._validate_user_id(user_id)
136
+ notebook_dir = self._notebook_path(user_id, notebook_id)
137
+ if not notebook_dir.exists() or self.get(user_id, notebook_id) is None:
138
  return False
139
+
140
+ shutil.rmtree(notebook_dir)
141
+
142
+ index = [
143
+ item
144
+ for item in self._load_index(user_id)
145
+ if item.get("notebook_id") != notebook_id
146
+ ]
147
+ self._save_index(user_id, index)
148
  return True
requirements.txt CHANGED
@@ -1,4 +1,7 @@
1
- fastapi
2
- uvicorn
3
- pydantic
4
- gradio
 
 
 
 
1
+ huggingface_hub
2
+ sentence-transformers
3
+ chromadb
4
+ pypdf
5
+ python-pptx
6
+ beautifulsoup4
7
+ requests
tests/__pycache__/test_api_notebooks.cpython-312-pytest-9.0.1.pyc ADDED
Binary file (10.4 kB). View file
 
tests/__pycache__/test_storage.cpython-312-pytest-9.0.1.pyc ADDED
Binary file (10.6 kB). View file
 
tests/test_api_notebooks.py CHANGED
@@ -1,12 +1,20 @@
1
  from fastapi.testclient import TestClient
2
 
3
  from backend.app import app
 
 
4
 
5
 
6
  client = TestClient(app)
7
 
8
 
9
- def test_create_and_get_notebook():
 
 
 
 
 
 
10
  payload = {"user_id": "u1", "name": "Notebook 1"}
11
  resp = client.post("/api/notebooks/", json=payload)
12
  assert resp.status_code == 200
@@ -16,12 +24,34 @@ def test_create_and_get_notebook():
16
  assert "notebook_id" in data
17
 
18
  notebook_id = data["notebook_id"]
19
- resp_get = client.get(f"/api/notebooks/{notebook_id}")
 
 
 
 
 
 
 
20
  assert resp_get.status_code == 200
21
  data_get = resp_get.json()
22
  assert data_get["notebook_id"] == notebook_id
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
- def test_get_missing_notebook():
26
- resp = client.get("/api/notebooks/missing")
 
27
  assert resp.status_code == 404
 
1
  from fastapi.testclient import TestClient
2
 
3
  from backend.app import app
4
+ from backend.api import notebooks as notebooks_api
5
+ from backend.services.storage import NotebookStore
6
 
7
 
8
  client = TestClient(app)
9
 
10
 
11
+ def setup_function():
12
+ notebooks_api.store = NotebookStore(base_dir="tests_tmp_data")
13
+
14
+
15
+ def test_create_list_get_rename_delete_notebook(tmp_path):
16
+ notebooks_api.store = NotebookStore(base_dir=str(tmp_path))
17
+
18
  payload = {"user_id": "u1", "name": "Notebook 1"}
19
  resp = client.post("/api/notebooks/", json=payload)
20
  assert resp.status_code == 200
 
24
  assert "notebook_id" in data
25
 
26
  notebook_id = data["notebook_id"]
27
+
28
+ resp_list = client.get("/api/notebooks/", params={"user_id": "u1"})
29
+ assert resp_list.status_code == 200
30
+ listed = resp_list.json()
31
+ assert len(listed) == 1
32
+ assert listed[0]["notebook_id"] == notebook_id
33
+
34
+ resp_get = client.get(f"/api/notebooks/{notebook_id}", params={"user_id": "u1"})
35
  assert resp_get.status_code == 200
36
  data_get = resp_get.json()
37
  assert data_get["notebook_id"] == notebook_id
38
 
39
+ resp_wrong_user = client.get(f"/api/notebooks/{notebook_id}", params={"user_id": "u2"})
40
+ assert resp_wrong_user.status_code == 404
41
+
42
+ resp_rename = client.patch(
43
+ f"/api/notebooks/{notebook_id}",
44
+ json={"user_id": "u1", "name": "Renamed"},
45
+ )
46
+ assert resp_rename.status_code == 200
47
+ assert resp_rename.json()["name"] == "Renamed"
48
+
49
+ resp_delete = client.delete(f"/api/notebooks/{notebook_id}", params={"user_id": "u1"})
50
+ assert resp_delete.status_code == 200
51
+ assert resp_delete.json() == {"deleted": True}
52
+
53
 
54
+ def test_get_missing_notebook(tmp_path):
55
+ notebooks_api.store = NotebookStore(base_dir=str(tmp_path))
56
+ resp = client.get("/api/notebooks/missing", params={"user_id": "u1"})
57
  assert resp.status_code == 404
tests/test_storage.py CHANGED
@@ -1,17 +1,40 @@
1
- from backend.services.storage import NotebookStore
2
  from backend.models.schemas import NotebookCreate
 
3
 
4
 
5
- def test_create_get_delete_notebook(tmp_path):
6
  store = NotebookStore(base_dir=str(tmp_path))
 
7
  created = store.create(NotebookCreate(user_id="u1", name="Test"))
 
 
 
8
 
9
- fetched = store.get(created.notebook_id)
 
 
 
 
10
  assert fetched is not None
11
- assert fetched.notebook_id == created.notebook_id
12
- assert fetched.user_id == "u1"
13
  assert fetched.name == "Test"
14
 
15
- deleted = store.delete(created.notebook_id)
 
 
 
 
 
 
16
  assert deleted is True
17
- assert store.get(created.notebook_id) is None
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from backend.models.schemas import NotebookCreate
2
+ from backend.services.storage import NotebookStore
3
 
4
 
5
+ def test_create_list_get_rename_delete_notebook(tmp_path):
6
  store = NotebookStore(base_dir=str(tmp_path))
7
+
8
  created = store.create(NotebookCreate(user_id="u1", name="Test"))
9
+ assert created.user_id == "u1"
10
+ assert created.created_at
11
+ assert created.updated_at
12
 
13
+ notebooks = store.list("u1")
14
+ assert len(notebooks) == 1
15
+ assert notebooks[0].notebook_id == created.notebook_id
16
+
17
+ fetched = store.get("u1", created.notebook_id)
18
  assert fetched is not None
 
 
19
  assert fetched.name == "Test"
20
 
21
+ renamed = store.rename("u1", created.notebook_id, "Renamed")
22
+ assert renamed is not None
23
+ assert renamed.name == "Renamed"
24
+
25
+ assert store.get("u2", created.notebook_id) is None
26
+
27
+ deleted = store.delete("u1", created.notebook_id)
28
  assert deleted is True
29
+ assert store.get("u1", created.notebook_id) is None
30
+ assert store.list("u1") == []
31
+
32
+
33
+ def test_invalid_user_id_rejected(tmp_path):
34
+ store = NotebookStore(base_dir=str(tmp_path))
35
+
36
+ try:
37
+ store.list("../bad")
38
+ assert False, "Expected ValueError"
39
+ except ValueError:
40
+ assert True