55import os
66import zipfile
77from datetime import datetime
8+ from typing import Any
89
9- from flask import Blueprint , current_app , jsonify , request , send_file
10+ from flask import Blueprint , current_app , request , send_file
11+
12+ from api ._flask_types import FlaskReturn , json_error , json_response
13+ from models .export import ExportStateDict
1014
1115from utils .export_state_store import (
1216 EXPORT_STATE_FILE ,
3337_STATE_FILE = EXPORT_STATE_FILE
3438
3539
36- def _state_lock ():
40+ def _state_lock () -> Any :
3741 return export_state_lock (_STATE_FILE )
3842
3943
40- def _load_state_from_disk () -> dict :
44+ def _load_state_from_disk () -> ExportStateDict :
4145 return load_export_state_from_disk (_STATE_FILE )
4246
4347
44- def _atomic_write_state (state : dict ) -> None :
48+ def _atomic_write_state (state : ExportStateDict ) -> None :
4549 atomic_write_export_state (state , _STATE_FILE )
4650
4751
48- def _read_state () -> dict :
52+ def _read_state () -> ExportStateDict :
4953 with _state_lock ():
5054 return _load_state_from_disk ()
5155
5256
53- def _write_state (sessions_map : dict , count : int ) -> None :
57+ def _write_state (sessions_map : dict [ str , float ] , count : int ) -> None :
5458 """Persist merge of *sessions_map* and update last-export metadata (*count* = this run only)."""
5559 with _state_lock ():
5660 state = _load_state_from_disk ()
@@ -61,10 +65,10 @@ def _write_state(sessions_map: dict, count: int) -> None:
6165
6266
6367@export_bp .route ("/api/export/state" )
64- def get_export_state ():
68+ def get_export_state () -> FlaskReturn :
6569 state = _read_state ()
6670 n = state .get ("exportedCount" , 0 )
67- return jsonify (
71+ return json_response (
6872 {
6973 "last_export_time" : state .get ("lastExportTime" ),
7074 # Sessions exported in the last completed bulk export (not a lifetime total).
@@ -75,16 +79,16 @@ def get_export_state():
7579
7680
7781@export_bp .route ("/api/export" , methods = ["POST" ])
78- def bulk_export ():
82+ def bulk_export () -> FlaskReturn :
7983 body = request .get_json (silent = True )
8084 if body is None :
8185 body = {}
8286 if not isinstance (body , dict ):
83- return jsonify ({ "error" : " Invalid request body"}) , 400
87+ return json_error ( " Invalid request body" , 400 )
8488
8589 since = body .get ("since" , "all" )
8690 if since not in ("all" , "last" , "incremental" ):
87- return jsonify ({"error" : "Invalid since mode" , "since" : since }) , 400
91+ return json_error ({"error" : "Invalid since mode" , "since" : since }, 400 )
8892
8993 base = (
9094 current_app .config .get ("CLAUDE_PROJECTS_DIR" )
@@ -94,14 +98,14 @@ def bulk_export():
9498 rules = current_app .config .get ("EXCLUSION_RULES" ) or []
9599
96100 state = _read_state ()
97- last_export_sessions : dict = (
101+ last_export_sessions : dict [ str , float ] = (
98102 state .get ("sessions" , {}) if since == "incremental" else {}
99103 )
100104
101105 buf = io .BytesIO ()
102106 count = 0
103- manifest = []
104- new_sessions_map : dict = {}
107+ manifest : list [ dict [ str , Any ]] = []
108+ new_sessions_map : dict [ str , float ] = {}
105109 latest_day = None
106110
107111 with zipfile .ZipFile (buf , "w" , zipfile .ZIP_DEFLATED ) as zf :
@@ -226,15 +230,7 @@ def bulk_export():
226230 _write_state (new_sessions_map , count )
227231
228232 if count == 0 :
229- return (
230- jsonify (
231- {
232- "error" : "Nothing to export" ,
233- "since" : since ,
234- }
235- ),
236- 422 ,
237- )
233+ return json_error ({"error" : "Nothing to export" , "since" : since }, 422 )
238234
239235 buf .seek (0 )
240236 date_tag = datetime .now ().strftime ("%Y-%m-%d" )
@@ -251,12 +247,12 @@ def bulk_export():
251247 buf ,
252248 mimetype = "application/zip" ,
253249 as_attachment = True ,
254- download_name = f"claude-code-export{ suffix } -{ date_tag } .zip" ,
250+ download_name = f"claude-code-export{ suffix } -{ date_tag } .zip" , # type: ignore[call-arg]
255251 )
256252
257253
258254@export_bp .route ("/api/export/session/<path:project_name>/<session_id>" )
259- def export_session (project_name , session_id ) :
255+ def export_session (project_name : str , session_id : str ) -> FlaskReturn :
260256 import os
261257 from utils .session_path import safe_join
262258
@@ -267,36 +263,42 @@ def export_session(project_name, session_id):
267263 try :
268264 filepath = safe_join (base , project_name , f"{ session_id } .jsonl" )
269265 except ValueError :
270- return jsonify ({ "error" : " Invalid path"}) , 400
266+ return json_error ( " Invalid path" , 400 )
271267
272268 if not os .path .isfile (filepath ):
273- return jsonify ({ "error" : " Session not found"}) , 404
269+ return json_error ( " Session not found" , 404 )
274270
275271 fmt = request .args .get ("format" , "md" )
276- session = parse_session (filepath )
277- rules = current_app .config .get ("EXCLUSION_RULES" ) or []
278- if is_session_excluded (rules , session , project_name ):
279- return jsonify ({"error" : "Session not found" }), 404
280- stats = compute_stats (session )
281- title_slug = slugify (session ["title" ], default = "session" )
282-
283- if fmt == "json" :
284- content = session_to_json (session , stats )
285- buf = io .BytesIO (content .encode ("utf-8" ))
272+ try :
273+ session = parse_session (filepath )
274+ rules = current_app .config .get ("EXCLUSION_RULES" ) or []
275+ if is_session_excluded (rules , session , project_name ):
276+ return json_error ("Session not found" , 404 )
277+ stats = compute_stats (session )
278+ title_slug = slugify (session ["title" ], default = "session" )
279+
280+ if fmt == "json" :
281+ content = session_to_json (session , stats )
282+ buf = io .BytesIO (content .encode ("utf-8" ))
283+ buf .seek (0 )
284+ return send_file (
285+ buf ,
286+ mimetype = "application/json" ,
287+ as_attachment = True ,
288+ download_name = f"{ title_slug } .json" , # type: ignore[call-arg]
289+ )
290+
291+ md = session_to_markdown (session , stats )
292+ buf = io .BytesIO (md .encode ("utf-8" ))
286293 buf .seek (0 )
287294 return send_file (
288295 buf ,
289- mimetype = "application/json " ,
296+ mimetype = "text/markdown " ,
290297 as_attachment = True ,
291- download_name = f"{ title_slug } .json" ,
298+ download_name = f"{ title_slug } .md" , # type: ignore[call-arg]
292299 )
293-
294- md = session_to_markdown (session , stats )
295- buf = io .BytesIO (md .encode ("utf-8" ))
296- buf .seek (0 )
297- return send_file (
298- buf ,
299- mimetype = "text/markdown" ,
300- as_attachment = True ,
301- download_name = f"{ title_slug } .md" ,
302- )
300+ except Exception :
301+ current_app .logger .exception (
302+ "Failed to export session %s/%s" , project_name , session_id
303+ )
304+ return json_error ("Internal server error exporting session" , 500 )
0 commit comments