Skip to content

Commit c97a37c

Browse files
docs: add Google-style docstrings to public api/services/utils/models… (#119)
* docs: add Google-style docstrings to public api/services/utils/models surface * docs: correct inaccurate API and model docstrings from PR review * docs: address PR 119 review — Google-style sections and doc accuracy * docs: complete validate_path and get_workspace_tab docstrings * revert(workspaces): restore lookup_workspace_display_name import for #118
1 parent ae468e9 commit c97a37c

13 files changed

Lines changed: 284 additions & 1 deletion

api/composers.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ def _read_json_file(path: str) -> Any:
3030

3131
@bp.route("/api/composers")
3232
def list_composers() -> tuple[Response, int] | Response:
33+
"""List all composers across workspace databases (GET /api/composers).
34+
35+
Returns:
36+
JSON array of composer dicts sorted by ``lastUpdatedAt`` descending.
37+
500 on failure.
38+
"""
3339
try:
3440
workspace_path = resolve_workspace_path()
3541
composers = []
@@ -120,8 +126,22 @@ def list_composers() -> tuple[Response, int] | Response:
120126
except Exception:
121127
_logger.exception("Failed to get composers")
122128
return json_response({"error": "Failed to get composers"}, 500)
129+
130+
123131
@bp.route("/api/composers/<composer_id>")
124132
def get_composer(composer_id: str) -> tuple[Response, int] | Response:
133+
"""Fetch one composer by ID (GET /api/composers/<composer_id>).
134+
135+
Args:
136+
composer_id: Composer UUID.
137+
138+
Returns:
139+
Composer JSON from per-workspace storage or global fallback. Per-workspace
140+
schema drift is logged and skipped before global fallback is attempted.
141+
404 when the composer is absent from both stores (``{"error": "Composer not found"}``)
142+
or when the global row fails validation (``{"error": "Composer schema drift"}``).
143+
500 on unexpected failure.
144+
"""
125145
try:
126146
workspace_path = resolve_workspace_path()
127147

api/config_api.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424

2525
@bp.route("/api/detect-environment")
2626
def detect_environment() -> Response:
27+
"""Detect runtime OS, WSL, and SSH-remote context (GET /api/detect-environment).
28+
29+
Returns:
30+
JSON with ``os``, ``isWSL``, and ``isRemote``. Falls back to safe defaults
31+
on detection errors.
32+
"""
2733
try:
2834
is_wsl = False
2935
is_remote = bool(
@@ -59,7 +65,22 @@ def detect_environment() -> Response:
5965

6066
@bp.route("/api/validate-path", methods=["POST"])
6167
def validate_path() -> tuple[Response, int] | Response:
62-
"""Same path rules as POST /api/set-workspace: realpath, markers (issue #15)."""
68+
"""Validate a workspace storage path without persisting it (POST /api/validate-path).
69+
70+
Uses the same rules as :func:`set_workspace` (realpath, Cursor markers; issue #15).
71+
72+
Args:
73+
path: Workspace storage root from JSON body ``{"path": "..."}``.
74+
75+
Returns:
76+
JSON with ``valid``, ``workspaceCount``, and canonical ``path`` on success.
77+
``valid`` is ``false`` when the path fails validation or contains no
78+
workspace folders with ``state.vscdb``. Invalid JSON body returns
79+
``{"valid": false, "error": "invalid JSON body", "workspaceCount": 0}``.
80+
Path validation errors return ``{"valid": false, "error": "...", "workspaceCount": 0}``.
81+
500 with ``{"valid": false, "error": "Failed to validate path"}`` on
82+
unexpected failure.
83+
"""
6384
try:
6485
body = request.get_json(silent=True) or {}
6586
if not isinstance(body, dict):
@@ -96,8 +117,21 @@ def validate_path() -> tuple[Response, int] | Response:
96117
exc_info=True,
97118
)
98119
return json_response({"valid": False, "error": "Failed to validate path"}, 500)
120+
121+
99122
@bp.route("/api/set-workspace", methods=["POST"])
100123
def set_workspace() -> tuple[Response, int] | Response:
124+
"""Persist a validated workspace storage path (POST /api/set-workspace).
125+
126+
Args:
127+
path: Workspace storage root from JSON body ``{"path": "..."}``.
128+
Canonicalized via :func:`utils.path_validation.validate_workspace_path`
129+
before storing the thread-safe module override.
130+
131+
Returns:
132+
``{"success": true, "path": "..."}`` on success. 400 for invalid path or
133+
body; 500 when override storage fails.
134+
"""
101135
# Reject non-dict JSON bodies (array / string / number / null). Without
102136
# this, get_json returns the value directly, the truthy fallback `or {}`
103137
# is bypassed, and `body.get("path", "")` raises AttributeError — which
@@ -127,6 +161,12 @@ def set_workspace() -> tuple[Response, int] | Response:
127161

128162
@bp.route("/api/get-username")
129163
def get_username() -> Response:
164+
"""Return the detected Windows/WSL username (GET /api/get-username).
165+
166+
Returns:
167+
JSON ``{"username": "..."}``. Falls back to ``YOUR_USERNAME`` when
168+
detection fails.
169+
"""
130170
try:
131171
username = "YOUR_USERNAME"
132172

api/logs.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ def _extract_chat_id_from_bubble_key(key: str) -> str | None:
3030

3131
@bp.route("/api/logs")
3232
def get_logs() -> tuple[Response, int] | Response:
33+
"""List chat logs from global and per-workspace storage (GET /api/logs).
34+
35+
Returns:
36+
JSON ``{"logs": [<log summary>, ...]}`` where each summary has ``id``,
37+
``title``, ``timestamp``, etc. 500 with ``{"error": ..., "logs": []}`` on
38+
unexpected failure.
39+
"""
3340
try:
3441
workspace_path = resolve_workspace_path()
3542
logs = []

api/pdf.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ def _safe_text(text: str) -> str:
4747

4848
@bp.route("/api/generate-pdf", methods=["POST"])
4949
def generate_pdf() -> tuple[Response, int] | Response:
50+
"""Render markdown chat content as a PDF download (POST /api/generate-pdf).
51+
52+
Args:
53+
markdown: Markdown source text (optional; defaults to ``""``).
54+
title: Document title (optional; defaults to ``"Chat"``).
55+
56+
Returns:
57+
``application/pdf`` attachment on success. 500 JSON error on failure.
58+
"""
5059
try:
5160
body = request.get_json(silent=True) or {}
5261
markdown_text = body.get("markdown", "")
@@ -55,10 +64,14 @@ def generate_pdf() -> tuple[Response, int] | Response:
5564
from fpdf import FPDF
5665

5766
class PDFDoc(FPDF):
67+
"""Minimal fpdf2 document with page numbers in the footer."""
68+
5869
def header(self) -> None:
70+
"""No running header (title is rendered in body)."""
5971
pass
6072

6173
def footer(self) -> None:
74+
"""Render centered page ``n/total`` at the bottom."""
6275
self.set_y(-15)
6376
self.set_font("Helvetica", "I", 8)
6477
self.cell(0, 10, f"Page {self.page_no()}/{{nb}}", align="C")

api/search.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@
2525

2626
@bp.route("/api/search")
2727
def search() -> tuple[Response, int] | Response:
28+
"""Search chats, composers, and CLI sessions across Cursor storage.
29+
30+
Args:
31+
q: Search query string (required; 400 when empty).
32+
type: Filter scope — ``all`` (default), ``chat``, or ``composer``.
33+
34+
Returns:
35+
JSON ``{"results": [...]}`` with optional ``warnings``. 400 when ``q`` is
36+
empty; 500 with ``{"error": ..., "results": []}`` on unexpected failure.
37+
"""
2838
try:
2939
query = request.args.get("q", "").strip()
3040
search_type = request.args.get("type", "all")

api/workspaces.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ def _request_nocache() -> bool:
5757

5858
@bp.route("/api/workspaces")
5959
def list_workspaces() -> tuple[Response, int] | Response:
60+
"""List workspace projects for the sidebar (GET /api/workspaces).
61+
62+
Args:
63+
nocache: When ``1`` or ``true``, bypass the summary disk cache.
64+
65+
Returns:
66+
JSON with ``projects`` and optional ``warnings``. 500 on failure.
67+
"""
6068
try:
6169
workspace_path = resolve_workspace_path()
6270
rules = exclusion_rules()
@@ -70,12 +78,23 @@ def list_workspaces() -> tuple[Response, int] | Response:
7078
except Exception:
7179
_logger.exception("Failed to get workspaces")
7280
return json_response({"error": "Failed to get workspaces"}, 500)
81+
82+
7383
# ---------------------------------------------------------------------------
7484
# GET /api/workspaces/<id>
7585
# ---------------------------------------------------------------------------
7686

7787
@bp.route("/api/workspaces/<workspace_id>")
7888
def get_workspace(workspace_id: str) -> tuple[Response, int] | Response:
89+
"""Return metadata for one workspace, global bucket, or CLI project.
90+
91+
Args:
92+
workspace_id: Storage folder name, ``global``, or ``cli:<project_id>``.
93+
94+
Returns:
95+
Workspace JSON (id, name, path, folder, lastModified). 404 when not found;
96+
500 on unexpected failure.
97+
"""
7998
try:
8099
if workspace_id == "global":
81100
return json_response({
@@ -144,12 +163,28 @@ def get_workspace(workspace_id: str) -> tuple[Response, int] | Response:
144163
except Exception:
145164
_logger.exception("Failed to get workspace")
146165
return json_response({"error": "Failed to get workspace"}, 500)
166+
167+
147168
# ---------------------------------------------------------------------------
148169
# GET /api/workspaces/<id>/tabs
149170
# ---------------------------------------------------------------------------
150171

151172
@bp.route("/api/workspaces/<workspace_id>/tabs")
152173
def get_workspace_tabs(workspace_id: str) -> tuple[Response, int] | Response:
174+
"""List conversation tabs for a workspace (GET /api/workspaces/<id>/tabs).
175+
176+
Args:
177+
workspace_id: Storage folder name, ``global`` for unassigned chats, or
178+
``cli:<project_id>``.
179+
summary: When ``1`` or ``true``, return lightweight tab headers only.
180+
nocache: When ``1`` or ``true``, bypass cache on summary requests.
181+
182+
Returns:
183+
Tabs payload from :func:`services.workspace_tabs` helpers (typically
184+
``{"tabs": [...]}`` with optional ``warnings``). May return 200 with an
185+
empty ``tabs`` list when upstream SQLite reads fail silently; 404 when
186+
global storage is missing; 500 on unexpected route-level failure.
187+
"""
153188
if workspace_id.startswith("cli:"):
154189
try:
155190
return get_cli_workspace_tabs(workspace_id, exclusion_rules())
@@ -170,12 +205,27 @@ def get_workspace_tabs(workspace_id: str) -> tuple[Response, int] | Response:
170205
except Exception:
171206
_logger.exception("Failed to get workspace tabs")
172207
return json_response({"error": "Failed to get workspace tabs"}, 500)
208+
209+
173210
# ---------------------------------------------------------------------------
174211
# GET /api/workspaces/<id>/tabs/<composer_id>
175212
# ---------------------------------------------------------------------------
176213

177214
@bp.route("/api/workspaces/<workspace_id>/tabs/<composer_id>")
178215
def get_workspace_tab(workspace_id: str, composer_id: str) -> tuple[Response, int] | Response:
216+
"""Lazy-load one conversation tab (GET /api/workspaces/<id>/tabs/<composer_id>).
217+
218+
Args:
219+
workspace_id: Storage folder name, ``global`` for unassigned chats, or
220+
``cli:<project_id>`` (CLI workspaces return 400).
221+
composer_id: Composer UUID to load.
222+
223+
Returns:
224+
Single-tab JSON from :func:`services.workspace_tabs.assemble_single_tab`
225+
(typically ``{"tab": {...}}`` with optional ``warnings``). 400 for CLI
226+
workspaces; 404 when global storage is missing, the composer is not found,
227+
or it is not assigned to *workspace_id*; 500 on unexpected failure.
228+
"""
179229
if workspace_id.startswith("cli:"):
180230
return json_response({"error": "Per-tab lazy load is not supported for CLI workspaces"}, 400)
181231
try:

models/cli_session.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ class CliSessionMeta:
1717

1818
@classmethod
1919
def from_dict(cls, raw: dict[str, Any]) -> "CliSessionMeta":
20+
"""Parse CLI session ``meta`` JSON into a validated descriptor.
21+
22+
Args:
23+
raw: Decoded meta object from a CLI chat session.
24+
25+
Returns:
26+
Validated :class:`CliSessionMeta`.
27+
28+
Raises:
29+
SchemaError: When ``latestRootBlobId`` is missing, falsey (e.g.
30+
empty string or ``None``), or not a string.
31+
"""
2032
raw = require_dict(raw, model="CliSessionMeta", field="meta")
2133
latest = require_truthy(
2234
raw.get("latestRootBlobId"),

models/conversation.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ class Composer:
3030

3131
@classmethod
3232
def from_dict(cls, raw: dict[str, Any], *, composer_id: str) -> "Composer":
33+
"""Parse a global ``composerData`` row into a validated composer.
34+
35+
Args:
36+
raw: Decoded JSON object from cursorDiskKV.
37+
composer_id: Composer UUID from the storage key.
38+
39+
Returns:
40+
Validated :class:`Composer` with required headers and timestamps.
41+
42+
Raises:
43+
SchemaError: When required fields are missing or malformed.
44+
"""
3345
raw = require_dict(raw, model="Composer", field="composerData")
3446
require_non_empty_str(composer_id, model="Composer", field="composerId")
3547
require_key(raw, "fullConversationHeadersOnly", model="Composer")
@@ -164,6 +176,17 @@ class WorkspaceLocalComposer:
164176

165177
@classmethod
166178
def from_dict(cls, raw: dict[str, Any]) -> "WorkspaceLocalComposer":
179+
"""Parse one ``allComposers`` entry from per-workspace state.
180+
181+
Args:
182+
raw: Composer summary dict from ``composer.composerData``.
183+
184+
Returns:
185+
Validated local composer row.
186+
187+
Raises:
188+
SchemaError: When ``composerId`` is missing or invalid.
189+
"""
167190
raw = require_dict(raw, model="WorkspaceLocalComposer", field="composer")
168191
composer_id = require_non_empty_str_field(
169192
raw, "composerId", model="WorkspaceLocalComposer"
@@ -187,6 +210,18 @@ class Bubble:
187210

188211
@classmethod
189212
def from_dict(cls, raw: dict[str, Any], *, bubble_id: str) -> "Bubble":
213+
"""Parse one ``bubbleId:*`` KV value into a validated bubble.
214+
215+
Args:
216+
raw: Decoded bubble JSON (``bubble_id`` comes from the key, not value).
217+
bubble_id: Bubble UUID from the storage key suffix.
218+
219+
Returns:
220+
Validated :class:`Bubble`.
221+
222+
Raises:
223+
SchemaError: When the payload or *bubble_id* is invalid.
224+
"""
190225
raw = require_dict(raw, model="Bubble", field="bubble")
191226
require_non_empty_str(bubble_id, model="Bubble", field="bubbleId")
192227
return cls(bubble_id=bubble_id, raw=raw)

models/export.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ class ExportEntry:
3131

3232
@classmethod
3333
def from_dict(cls, raw: dict[str, Any]) -> "ExportEntry":
34+
"""Parse one manifest.jsonl row into a validated export entry.
35+
36+
Args:
37+
raw: Decoded JSON object for a single manifest line.
38+
39+
Returns:
40+
Validated :class:`ExportEntry`.
41+
42+
Raises:
43+
SchemaError: When ``log_id``, ``title``, or ``workspace`` are missing.
44+
"""
3445
raw = require_dict(raw, model="ExportEntry", field="entry")
3546
require_non_empty_str_fields(
3647
raw,

models/workspace.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ class Workspace:
1616

1717
@classmethod
1818
def from_dict(cls, raw: dict[str, Any], *, workspace_id: str) -> "Workspace":
19+
"""Parse ``workspace.json`` into a validated workspace descriptor.
20+
21+
Args:
22+
raw: Decoded workspace.json object.
23+
workspace_id: Workspace storage folder name.
24+
25+
Returns:
26+
Validated :class:`Workspace` (``folder`` may be ``None`` for CLI-only).
27+
28+
Raises:
29+
SchemaError: When required fields are missing or malformed.
30+
"""
1931
raw = require_dict(raw, model="Workspace", field="workspace.json")
2032
require_non_empty_str(workspace_id, model="Workspace", field="workspaceId")
2133
folder = require_optional_str(raw.get("folder"), model="Workspace", field="folder")

0 commit comments

Comments
 (0)