Skip to content

Commit 87ab7b4

Browse files
authored
refactor: decouple API handlers from _-prefixed service internals (closes #73) (#83)
* fix: rename _-prefixed functions to normal * fix: remove unused param, correct doc string * fix: update all _-prefix function names in the project * fix: Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_workspace_db_special_paths.py` at line 66, The test unpacks a second value from the context manager returned by open_global_db(ws_root) into the variable name path but never uses it, causing a Ruff RUF059 lint error; update the unpacked name in the with statement (the tuple unpack from open_global_db in tests/test_workspace_db_special_paths.py) to a discard name like _path or _ to mark it intentionally unused while leaving conn unchanged. * fix: addressed reviewer's comment * fix: ci type_check error * fix: Narrow the broad except clause that catches Exception * fix: eval.md findings
1 parent 46bda64 commit 87ab7b4

23 files changed

Lines changed: 422 additions & 223 deletions

api/export_api.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,25 @@
1313
from datetime import datetime
1414
from pathlib import Path
1515

16-
from flask import Blueprint, Response, current_app, jsonify, request
16+
from flask import Blueprint, Response, jsonify, request
17+
18+
from api.flask_config import exclusion_rules
1719

1820
from utils.workspace_path import resolve_workspace_path
1921
from utils.path_helpers import to_epoch_ms
2022
from utils.text_extract import extract_text_from_bubble, slug
2123
from utils.exclusion_rules import build_searchable_text, is_excluded_by_rules
2224
from utils.cursor_md_exporter import cursor_ide_chat_to_markdown
2325
from services.workspace_db import (
24-
_build_composer_id_to_workspace_id,
25-
_collect_workspace_entries,
26+
build_composer_id_to_workspace_id,
27+
collect_workspace_entries,
2628
load_bubble_map,
2729
load_code_block_diff_map,
28-
_open_global_db,
30+
open_global_db,
2931
)
3032
from services.workspace_resolver import (
31-
_get_workspace_display_name,
32-
_create_project_name_to_workspace_id_map,
33+
create_project_name_to_workspace_id_map,
34+
lookup_workspace_display_name,
3335
)
3436

3537
bp = Blueprint("export_api", __name__)
@@ -47,8 +49,12 @@ def _get_export_state() -> dict:
4749
try:
4850
with open(state_path, "r", encoding="utf-8") as f:
4951
return json.load(f)
50-
except Exception:
51-
pass
52+
except (json.JSONDecodeError, ValueError, OSError) as e:
53+
_logger.warning(
54+
"Could not read export state from %s: %s",
55+
state_path,
56+
e,
57+
)
5258
return {}
5359

5460

@@ -96,25 +102,25 @@ def export_chats():
96102
last_export_ms = to_epoch_ms(ts_str)
97103

98104
# ── Workspace scanning via service layer ──────────────────────────────
99-
workspace_entries = _collect_workspace_entries(workspace_path)
100-
composer_id_to_ws = _build_composer_id_to_workspace_id(workspace_path, workspace_entries)
101-
project_name_map = _create_project_name_to_workspace_id_map(workspace_entries)
105+
workspace_entries = collect_workspace_entries(workspace_path)
106+
composer_id_to_ws = build_composer_id_to_workspace_id(workspace_path, workspace_entries)
107+
project_name_map = create_project_name_to_workspace_id_map(workspace_entries)
102108

103109
# Build display-name and slug maps
104110
ws_id_to_slug: dict[str, str] = {}
105111
ws_id_to_display_name: dict[str, str] = {}
106112
for e in workspace_entries:
107-
display = _get_workspace_display_name(workspace_path, e["name"])
113+
display = lookup_workspace_display_name(workspace_path, e["name"])
108114
if display != e["name"]:
109115
ws_id_to_display_name[e["name"]] = display
110116
ws_id_to_slug[e["name"]] = slug(display)
111117

112118
today = datetime.now().strftime("%Y-%m-%d")
113119
exported = []
114-
rules = current_app.config.get("EXCLUSION_RULES") or []
120+
rules = exclusion_rules()
115121

116122
# ── Database reading via service layer ────────────────────────────────
117-
with _open_global_db(workspace_path) as (global_db, global_db_path):
123+
with open_global_db(workspace_path) as (global_db, _):
118124
if global_db is None:
119125
return jsonify({"error": "Cursor global storage not found"}), 404
120126

@@ -138,7 +144,11 @@ def export_chats():
138144
if not headers:
139145
continue
140146

141-
updated_at_ms = to_epoch_ms(cd.get("lastUpdatedAt")) or to_epoch_ms(cd.get("createdAt")) or 0
147+
updated_at_ms = to_epoch_ms(cd.get("lastUpdatedAt"))
148+
if updated_at_ms is None:
149+
updated_at_ms = to_epoch_ms(cd.get("createdAt"))
150+
if updated_at_ms is None:
151+
updated_at_ms = 0
142152
if since == "last" and updated_at_ms and updated_at_ms <= last_export_ms:
143153
continue
144154

api/flask_config.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Shared Flask request/config helpers for API blueprints."""
2+
3+
from __future__ import annotations
4+
5+
from flask import current_app
6+
7+
8+
def exclusion_rules() -> list:
9+
"""Return loaded exclusion rules from app config (empty list when unset)."""
10+
return current_app.config.get("EXCLUSION_RULES") or []

api/workspaces.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
import os
1212
from datetime import datetime, timezone
1313

14-
from flask import Blueprint, current_app, jsonify
14+
from flask import Blueprint, jsonify
15+
16+
from api.flask_config import exclusion_rules
1517

1618
from utils.workspace_path import resolve_workspace_path, get_cli_chats_path
1719
from utils.cli_chat_reader import list_cli_projects
@@ -22,16 +24,10 @@
2224
)
2325
from utils.workspace_descriptor import read_json_file
2426
from services.workspace_resolver import (
25-
_infer_workspace_name_from_context,
26-
# Re-exported for back-compat with existing tests that import from api.workspaces
27-
# directly (test_invalid_workspace_aliases, test_workspace_assignment_fallback,
28-
# test_workspace_name_inference, test_models_wired_at_read_sites).
29-
# Production callers should import from services.workspace_resolver instead.
30-
_determine_project_for_conversation, # noqa: F401
31-
_infer_invalid_workspace_aliases, # noqa: F401
32-
_get_workspace_display_name, # noqa: F401
27+
infer_workspace_name_from_context,
28+
lookup_workspace_display_name,
3329
)
34-
from services.cli_tabs import _get_cli_workspace_tabs
30+
from services.cli_tabs import get_cli_workspace_tabs
3531
from services.workspace_listing import list_workspace_projects
3632
from services.workspace_tabs import assemble_workspace_tabs
3733

@@ -54,7 +50,7 @@
5450
def list_workspaces():
5551
try:
5652
workspace_path = resolve_workspace_path()
57-
rules = current_app.config.get("EXCLUSION_RULES") or []
53+
rules = exclusion_rules()
5854
projects, warnings = list_workspace_projects(workspace_path, rules)
5955
payload: dict = {"projects": projects}
6056
if warnings:
@@ -121,12 +117,12 @@ def get_workspace(workspace_id):
121117
if derived_name:
122118
workspace_name = derived_name
123119
elif workspace_name == workspace_id:
124-
inferred = _infer_workspace_name_from_context(workspace_path, workspace_id)
120+
inferred = infer_workspace_name_from_context(workspace_path, workspace_id)
125121
if inferred:
126122
workspace_name = inferred
127123
except Exception as e:
128124
warn_workspace_json_read(_logger, workspace_id, e)
129-
inferred = _infer_workspace_name_from_context(workspace_path, workspace_id)
125+
inferred = infer_workspace_name_from_context(workspace_path, workspace_id)
130126
if inferred:
131127
workspace_name = inferred
132128

@@ -150,10 +146,14 @@ def get_workspace(workspace_id):
150146
@bp.route("/api/workspaces/<workspace_id>/tabs")
151147
def get_workspace_tabs(workspace_id):
152148
if workspace_id.startswith("cli:"):
153-
return _get_cli_workspace_tabs(workspace_id)
149+
try:
150+
return get_cli_workspace_tabs(workspace_id, exclusion_rules())
151+
except Exception:
152+
_logger.exception("Failed to get CLI workspace tabs")
153+
return jsonify({"error": "Failed to get workspace tabs"}), 500
154154
try:
155155
workspace_path = resolve_workspace_path()
156-
rules = current_app.config.get("EXCLUSION_RULES") or []
156+
rules = exclusion_rules()
157157
payload, status = assemble_workspace_tabs(workspace_id, workspace_path, rules)
158158
return jsonify(payload), status
159159
except Exception:

0 commit comments

Comments
 (0)