Spaces:
Sleeping
Sleeping
Max Saavedra commited on
Commit ·
7d1fec5
1
Parent(s): b208fdb
Codex changes
Browse files- .gitignore +0 -0
- .venv/Lib/site-packages/gradio/hash_seed.txt +1 -0
- backend/__pycache__/app.cpython-312.pyc +0 -0
- backend/api/__pycache__/notebooks.cpython-312.pyc +0 -0
- backend/api/notebooks.py +41 -7
- backend/models/__pycache__/schemas.cpython-312.pyc +0 -0
- backend/models/schemas.py +7 -0
- backend/services/__pycache__/storage.cpython-312.pyc +0 -0
- backend/services/storage.py +120 -20
- requirements.txt +7 -4
- tests/__pycache__/test_api_notebooks.cpython-312-pytest-9.0.1.pyc +0 -0
- tests/__pycache__/test_storage.cpython-312-pytest-9.0.1.pyc +0 -0
- tests/test_api_notebooks.py +34 -4
- tests/test_storage.py +30 -7
.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
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
|
| 14 |
@router.get("/{notebook_id}", response_model=NotebookOut)
|
| 15 |
-
def get_notebook(notebook_id: str) -> NotebookOut:
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 24 |
"name": payload.name,
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
-
(notebook_dir / "meta.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
return NotebookOut(**data)
|
| 28 |
|
| 29 |
-
def
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
if not meta_path.exists():
|
| 33 |
return None
|
| 34 |
-
data =
|
|
|
|
|
|
|
| 35 |
return NotebookOut(**data)
|
| 36 |
|
| 37 |
-
def
|
| 38 |
-
|
| 39 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
return False
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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
|
| 6 |
store = NotebookStore(base_dir=str(tmp_path))
|
|
|
|
| 7 |
created = store.create(NotebookCreate(user_id="u1", name="Test"))
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|