diff --git a/python/src/notes_app/api/app.py b/python/src/notes_app/api/app.py index 5862be7..95d9123 100644 --- a/python/src/notes_app/api/app.py +++ b/python/src/notes_app/api/app.py @@ -47,6 +47,3 @@ def health() -> dict[str, str]: app = create_app() - - -app = create_app() diff --git a/python/src/notes_app/models/dataset_metadata.py b/python/src/notes_app/models/dataset_metadata.py index d9fd83d..c2d4df5 100644 --- a/python/src/notes_app/models/dataset_metadata.py +++ b/python/src/notes_app/models/dataset_metadata.py @@ -20,6 +20,8 @@ class DatasetMetadata: created: datetime modified: datetime tags: tuple[str, ...] = () + status: str = "" + priority: int = 0 format: str = "" path: str = "" size_bytes: int = 0 @@ -37,6 +39,8 @@ def from_dataset(cls, dataset: Dataset) -> "DatasetMetadata": created=dataset.created, modified=dataset.modified, tags=dataset.tags, + status=dataset.status, + priority=dataset.priority, format=dataset.format, path=cls._normalize_relative_path(dataset.path), size_bytes=dataset.size_bytes, @@ -54,6 +58,8 @@ def to_dataset(self) -> Dataset: created=self.created, modified=self.modified, tags=self.tags, + status=self.status, + priority=self.priority, format=self.format, path=self._normalize_relative_path(self.path), size_bytes=self.size_bytes, @@ -72,6 +78,8 @@ def to_dict(self) -> dict[str, object]: "created": self._to_iso(self.created), "modified": self._to_iso(self.modified), "tags": list(self.tags), + "status": self.status, + "priority": self.priority, "format": self.format, "path": self._normalize_relative_path(self.path), "sizeBytes": self.size_bytes, @@ -105,6 +113,8 @@ def from_dict(cls, data: dict[str, object]) -> "DatasetMetadata": created=cls._from_iso(data.get("created")), modified=cls._from_iso(data.get("modified"), fallback=cls._from_iso(data.get("created"))), tags=cls._normalize_tags(data.get("tags", [])), + status=str(data.get("status") or ""), + priority=cls._to_int(data.get("priority"), default=0), format=str(data.get("format") or ""), path=cls._normalize_relative_path(str(data.get("path") or "")), size_bytes=cls._to_int(data.get("sizeBytes"), default=0), diff --git a/python/src/notes_app/repositories/file_dataset_repository.py b/python/src/notes_app/repositories/file_dataset_repository.py index 8379d2e..a3e22cf 100644 --- a/python/src/notes_app/repositories/file_dataset_repository.py +++ b/python/src/notes_app/repositories/file_dataset_repository.py @@ -4,6 +4,8 @@ import io import json import logging +import os +import tempfile from dataclasses import replace from datetime import datetime from pathlib import Path @@ -44,7 +46,7 @@ def save(self, item: Dataset) -> None: """Write (or overwrite) the sidecar for *item*.""" normalized = replace(item, path=DatasetMetadata._normalize_relative_path(item.path)) sidecar_path = self._sidecar_path_for(normalized) - sidecar_path.write_text(render_sidecar(normalized), encoding="utf-8") + self._write_text_atomic(sidecar_path, render_sidecar(normalized)) def list_all(self) -> Iterable[Dataset]: results: list[Dataset] = [] @@ -130,7 +132,7 @@ def save_with_file( # Write sidecar next to the data file sidecar_path = self._datasets_dir / f"{relative_name}.dataset.yml" - sidecar_path.write_text(render_sidecar(updated), encoding="utf-8") + self._write_text_atomic(sidecar_path, render_sidecar(updated)) return updated @@ -164,7 +166,7 @@ def save_profile(self, dataset: Dataset, profile: dict[str, object]) -> None: raw_data = yaml.safe_load(sidecar_path.read_text(encoding="utf-8")) or {} data = raw_data if isinstance(raw_data, dict) else {} data["profile"] = profile - sidecar_path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") + self._write_text_atomic(sidecar_path, yaml.safe_dump(data, sort_keys=False)) except (OSError, UnicodeDecodeError, yaml.YAMLError) as exc: _LOG.warning("Unable to save profile for sidecar %s: %s", sidecar_path.name, exc) @@ -193,6 +195,27 @@ def preview(self, dataset: Dataset, limit: int) -> dict[str, object]: # Internal helpers # ------------------------------------------------------------------ + @staticmethod + def _write_text_atomic(path: Path, text: str, encoding: str = "utf-8") -> None: + """Write *text* to *path* atomically using a temp file and ``os.replace``. + + Ensures that readers never observe a partially-written sidecar file, + which would otherwise cause spurious 404 errors when a background + profiling thread rewrites the sidecar concurrently with a read request. + """ + dir_path = path.parent + fd, tmp_name = tempfile.mkstemp(dir=dir_path, suffix=".tmp") + try: + with os.fdopen(fd, "w", encoding=encoding) as fh: + fh.write(text) + os.replace(tmp_name, path) + except Exception: + try: + os.unlink(tmp_name) + except OSError: + pass + raise + def _sidecar_path_for(self, dataset: Dataset) -> Path: """Return the sidecar path that corresponds to *dataset*.""" if dataset.path: