|
18 | 18 | export_state_lock, |
19 | 19 | load_export_state_from_disk, |
20 | 20 | ) |
21 | | -from utils.session_path import ( |
22 | | - get_claude_projects_dir, |
23 | | - list_projects, |
24 | | - list_sessions, |
25 | | -) |
| 21 | +from utils.session_path import get_claude_projects_dir, list_projects |
26 | 22 | from utils.jsonl_parser import parse_session |
| 23 | +from utils.exclusion_rules import is_session_excluded |
27 | 24 | from utils.session_stats import compute_stats |
28 | 25 | from utils.md_exporter import session_to_markdown |
29 | 26 | from utils.json_exporter import session_to_json |
30 | | -from utils.exclusion_rules import is_session_excluded |
31 | 27 | from utils.slugify import slugify |
32 | | -from utils.export_day_filter import collect_sessions_for_latest_activity_day |
| 28 | +from utils.export_engine import EXPORT_ERRORS as _EXPORT_ERRORS, ZipSink, run_bulk_export |
33 | 29 |
|
34 | 30 | export_bp = Blueprint("export", __name__) |
35 | 31 |
|
36 | 32 | # Tests monkeypatch this path; keep in sync with utils.export_state_store. |
37 | 33 | _STATE_FILE = EXPORT_STATE_FILE |
38 | 34 |
|
39 | | -_EXPORT_ERRORS = ( |
40 | | - json.JSONDecodeError, |
41 | | - KeyError, |
42 | | - ValueError, |
43 | | - OSError, |
44 | | - FileNotFoundError, |
45 | | -) |
46 | | - |
47 | 35 |
|
48 | 36 | def _state_lock() -> Any: |
49 | 37 | return export_state_lock(_STATE_FILE) |
@@ -120,128 +108,28 @@ def bulk_export() -> FlaskReturn: |
120 | 108 | ) |
121 | 109 |
|
122 | 110 | buf = io.BytesIO() |
123 | | - count = 0 |
124 | | - manifest: list[dict[str, Any]] = [] |
125 | | - new_sessions_map: dict[str, float] = {} |
126 | | - latest_day = None |
| 111 | + |
| 112 | + def _on_export_error(sid: str, exc: Exception) -> None: |
| 113 | + current_app.logger.warning( |
| 114 | + "Failed to export %s: %s", sid[:10], exc |
| 115 | + ) |
127 | 116 |
|
128 | 117 | with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: |
129 | | - if since == "last": |
130 | | - d, rows, _n = collect_sessions_for_latest_activity_day( |
131 | | - projects, |
132 | | - list_sessions=list_sessions, |
133 | | - parse_session=parse_session, |
134 | | - is_session_excluded=is_session_excluded, |
135 | | - rules=rules, |
136 | | - ) |
137 | | - latest_day = d |
138 | | - for project, sess_info, session, _st, _en in rows: |
139 | | - sid = sess_info["id"] |
140 | | - try: |
141 | | - stats = compute_stats(session) |
142 | | - md = session_to_markdown(session, stats) |
143 | | - title_slug = slugify(session["title"], default="session") |
144 | | - short_id = sid[:8] |
145 | | - proj_slug = slugify(project["name"], default="project") |
146 | | - ts = session["metadata"].get("first_timestamp", "") |
147 | | - ts_file = ( |
148 | | - ts[:19].replace(":", "-") |
149 | | - if ts |
150 | | - else "0000-00-00T00-00-00" |
151 | | - ) |
152 | | - rel_path = ( |
153 | | - f"{proj_slug}/{ts_file}__{title_slug}__{short_id}.md" |
154 | | - ) |
155 | | - zf.writestr(rel_path, md) |
156 | | - manifest.append( |
157 | | - { |
158 | | - "session_id": sid, |
159 | | - "title": session["title"], |
160 | | - "project": project["name"], |
161 | | - "tokens": session["metadata"]["total_input_tokens"] |
162 | | - + session["metadata"]["total_output_tokens"], |
163 | | - "tool_calls": session["metadata"][ |
164 | | - "total_tool_calls" |
165 | | - ], |
166 | | - "cost_estimate_usd": stats.get( |
167 | | - "cost_estimate_usd" |
168 | | - ), |
169 | | - } |
170 | | - ) |
171 | | - new_sessions_map[sid] = sess_info.get("modified", 0) |
172 | | - count += 1 |
173 | | - except _EXPORT_ERRORS as e: |
174 | | - current_app.logger.warning( |
175 | | - "Failed to export %s: %s", sid[:10], e |
176 | | - ) |
177 | | - continue |
178 | | - else: |
179 | | - for project in projects: |
180 | | - sessions = list_sessions(project["path"]) |
181 | | - for sess_info in sessions: |
182 | | - sid = sess_info["id"] |
183 | | - try: |
184 | | - if since == "incremental": |
185 | | - prev_mtime = last_export_sessions.get(sid, 0) |
186 | | - curr_mtime = sess_info.get("modified", 0) |
187 | | - if curr_mtime and curr_mtime <= prev_mtime: |
188 | | - continue |
189 | | - |
190 | | - session = parse_session(sess_info["path"]) |
191 | | - if session["title"] == "Untitled Session": |
192 | | - continue |
193 | | - |
194 | | - if is_session_excluded( |
195 | | - rules, |
196 | | - session, |
197 | | - project.get("display_name") or project["name"], |
198 | | - ): |
199 | | - continue |
200 | | - |
201 | | - stats = compute_stats(session) |
202 | | - md = session_to_markdown(session, stats) |
203 | | - title_slug = slugify( |
204 | | - session["title"], default="session" |
205 | | - ) |
206 | | - short_id = sid[:8] |
207 | | - proj_slug = slugify(project["name"], default="project") |
208 | | - ts = session["metadata"].get("first_timestamp", "") |
209 | | - ts_file = ( |
210 | | - ts[:19].replace(":", "-") |
211 | | - if ts |
212 | | - else "0000-00-00T00-00-00" |
213 | | - ) |
214 | | - rel_path = f"{proj_slug}/{ts_file}__{title_slug}__{short_id}.md" |
215 | | - zf.writestr(rel_path, md) |
216 | | - manifest.append( |
217 | | - { |
218 | | - "session_id": sid, |
219 | | - "title": session["title"], |
220 | | - "project": project["name"], |
221 | | - "tokens": session["metadata"][ |
222 | | - "total_input_tokens" |
223 | | - ] |
224 | | - + session["metadata"]["total_output_tokens"], |
225 | | - "tool_calls": session["metadata"][ |
226 | | - "total_tool_calls" |
227 | | - ], |
228 | | - "cost_estimate_usd": stats.get( |
229 | | - "cost_estimate_usd" |
230 | | - ), |
231 | | - } |
232 | | - ) |
233 | | - new_sessions_map[sid] = sess_info.get("modified", 0) |
234 | | - count += 1 |
235 | | - except _EXPORT_ERRORS as e: |
236 | | - current_app.logger.warning( |
237 | | - "Failed to export %s: %s", sid[:10], e |
238 | | - ) |
239 | | - continue |
240 | | - if manifest: |
241 | | - manifest_str = "\n".join( |
242 | | - json.dumps(e, default=str) for e in manifest |
243 | | - ) |
244 | | - zf.writestr("manifest.jsonl", manifest_str) |
| 118 | + result = run_bulk_export( |
| 119 | + projects=projects, |
| 120 | + since=since, |
| 121 | + rules=rules, |
| 122 | + last_export_sessions=last_export_sessions, |
| 123 | + sink=ZipSink(zf), |
| 124 | + fmt="md", |
| 125 | + path_layout="api", |
| 126 | + manifest_style="api", |
| 127 | + on_export_error=_on_export_error, |
| 128 | + ) |
| 129 | + |
| 130 | + count = result.exported_session_count |
| 131 | + new_sessions_map = result.new_sessions_map |
| 132 | + latest_day = result.latest_day |
245 | 133 |
|
246 | 134 | if count == 0: |
247 | 135 | return error_response( |
|
0 commit comments