diff --git a/services/api-server/src/api_server/main.py b/services/api-server/src/api_server/main.py index ce109a6..226866d 100644 --- a/services/api-server/src/api_server/main.py +++ b/services/api-server/src/api_server/main.py @@ -1620,10 +1620,7 @@ async def api_health() -> dict[str, str]: @app.get("/html5") async def html5_index() -> Response: - return Response( - render_html5_index(_project_summaries()), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_index(_project_summaries())) @app.post("/html5/projects") @@ -1633,10 +1630,7 @@ async def html5_create_project(request: Request) -> Response: if not project_id: raise HTTPException(status_code=400, detail="project_id is required.") await create_project(ProjectCreateRequest(project_id=project_id, name=_form_value(form, "name"))) - return Response( - render_html5_project_rows(_project_summaries()), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_project_rows(_project_summaries())) @app.post("/html5/projects/{project_id}/delete") @@ -1644,31 +1638,25 @@ async def html5_delete_project(project_id: str, request: Request) -> Response: form = await _html5_form_data(request) confirmation = _form_value(form, "confirmation") or "" await delete_project(project_id, ProjectDeleteRequest(confirmation=confirmation)) - return Response( - render_html5_project_rows(_project_summaries()), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_project_rows(_project_summaries())) @app.get("/html5/operations") async def html5_operations(project_id: str = "", status: str = "", kind: str = "") -> Response: - return Response( + return _html5_response( render_html5_operations( _html5_operation_jobs(project_id=project_id, status=status, kind=kind), project_id=project_id, status=status, kind=kind, - ), - media_type="text/html; charset=utf-8", + ) ) @app.get("/html5/operations/jobs") async def html5_operation_jobs(project_id: str = "", status: str = "", kind: str = "") -> Response: - return Response( - render_html5_operation_rows(_html5_operation_jobs(project_id=project_id, status=status, kind=kind)), - media_type="text/html; charset=utf-8", - ) + jobs = _html5_operation_jobs(project_id=project_id, status=status, kind=kind) + return _html5_response(render_html5_operation_rows(jobs)) @app.get("/html5/operations/jobs/{job_id}/detail") @@ -1676,18 +1664,13 @@ async def html5_operation_job_detail(job_id: str) -> Response: job = _operations.jobs.get(job_id) if job is None: raise HTTPException(status_code=404, detail=f"Unknown operation job: {job_id}") - return Response( - render_html5_operation_detail(job), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_operation_detail(job)) @app.get("/html5/operations/summary") async def html5_operation_summary(project_id: str = "", status: str = "", kind: str = "") -> Response: - return Response( - render_html5_operation_summary(_html5_operation_jobs(project_id=project_id, status=status, kind=kind)), - media_type="text/html; charset=utf-8", - ) + jobs = _html5_operation_jobs(project_id=project_id, status=status, kind=kind) + return _html5_response(render_html5_operation_summary(jobs)) @app.get("/html5/operations/events") @@ -1708,11 +1691,7 @@ async def html5_operations_events( break time.sleep(3) - return StreamingResponse( - stream_operations(), - media_type="text/event-stream", - headers=_html5_sse_headers(), - ) + return _html5_sse_response(stream_operations()) @app.get("/html5/projects/{project_id}/editor") @@ -1733,7 +1712,7 @@ async def html5_project_editor(project_id: str, q: str = "") -> Response: error=str(error.detail), q=q, ) - return Response(html, media_type="text/html; charset=utf-8") + return _html5_response(html) @app.get("/html5/projects/{project_id}/events") @@ -1774,29 +1753,19 @@ async def html5_project_events(project_id: str, once: bool = False) -> Streaming break await asyncio.sleep(5) - return StreamingResponse( - stream_status(), - media_type="text/event-stream", - headers=_html5_sse_headers(), - ) + return _html5_sse_response(stream_status()) @app.get("/html5/projects/{project_id}/symbols") async def html5_project_symbols(project_id: str, q: str = "") -> Response: snapshot = _project_snapshot_or_404(project_id) - return Response( - render_html5_symbols(snapshot, q, project_id), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_symbols(snapshot, q, project_id)) @app.get("/html5/projects/{project_id}/symbols/{lineage_id}/detail") async def html5_project_symbol_detail(project_id: str, lineage_id: str) -> Response: references = await project_symbol_references(project_id, lineage_id, direction="both") - return Response( - render_html5_symbol_detail(project_id, references), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_symbol_detail(project_id, references)) @app.get("/html5/projects/{project_id}/source/by-path") @@ -1812,10 +1781,7 @@ async def html5_project_source_by_path(project_id: str, path: str) -> Response: ) if node is None: raise HTTPException(status_code=404, detail=f"Source not found: {path}") - return Response( - render_html5_source(node), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_source(node)) @app.get("/html5/projects/{project_id}/source/{lineage_id}") @@ -1824,28 +1790,19 @@ async def html5_project_source(project_id: str, lineage_id: str) -> Response: node = _find_snapshot_node(snapshot, lineage_id) if node is None: raise HTTPException(status_code=404, detail=f"Lineage not found: {lineage_id}") - return Response( - render_html5_source(node), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_source(node)) @app.get("/html5/projects/{project_id}/report") async def html5_project_report(project_id: str) -> Response: report = await project_report(project_id) - return Response( - render_html5_project_report(project_id, report), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_project_report(project_id, report)) @app.get("/html5/projects/{project_id}/review") async def html5_project_review(project_id: str) -> Response: findings = await get_review(project_id) - return Response( - render_html5_review(project_id, findings), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_review(project_id, findings)) @app.get("/html5/projects/{project_id}/flowchart") @@ -1856,9 +1813,8 @@ async def html5_project_flowchart( limit: int = 80, ) -> Response: flowchart = await project_flowchart(project_id, focus=focus, depth=depth, limit=limit) - return Response( + return _html5_response( render_html5_flowchart(project_id, flowchart, focus=focus, depth=depth), - media_type="text/html; charset=utf-8", ) @@ -1902,25 +1858,22 @@ async def html5_project_object_context(project_id: str, object_name: str, mode: oob=True, ) review_context = render_html5_review(project_id, focused_findings, title="Review объекта", oob=True) - return Response( + return _html5_response( object_context + flowchart_context + source_context + symbol_context + report_context + review_context, - media_type="text/html; charset=utf-8", ) @app.get("/html5/projects/{project_id}/authoring/changes") async def html5_project_authoring_changes(project_id: str) -> Response: - return Response( + return _html5_response( render_html5_authoring_changes(project_id, _authoring_change_summaries(project_id)), - media_type="text/html; charset=utf-8", ) @app.get("/html5/projects/{project_id}/authoring/changes/{change_id}") async def html5_project_authoring_change_detail(project_id: str, change_id: str) -> Response: - return Response( + return _html5_response( render_html5_authoring_change_detail(project_id, _authoring_rollback_preview(project_id, change_id)), - media_type="text/html; charset=utf-8", ) @@ -1939,7 +1892,7 @@ async def html5_project_authoring_apply_rollback(project_id: str, change_id: str html = render_html5_authoring_rollback_result(project_id, result) except HTTPException as error: html = render_html5_authoring_rollback_result(project_id, error=str(error.detail)) - return Response(html, media_type="text/html; charset=utf-8") + return _html5_response(html) @app.post("/html5/projects/{project_id}/authoring/completion-preview") @@ -1963,7 +1916,7 @@ async def html5_project_authoring_completion_preview(project_id: str, request: R html = render_html5_authoring_preview_result(project_id, preview) except HTTPException as error: html = render_html5_authoring_preview_result(project_id, error=str(error.detail)) - return Response(html, media_type="text/html; charset=utf-8") + return _html5_response(html) @app.post("/html5/projects/{project_id}/authoring/semantic-diff-preview") @@ -1995,7 +1948,7 @@ async def html5_project_authoring_semantic_diff_preview(project_id: str, request ) except HTTPException as error: html = render_html5_authoring_diff_result(project_id, error=str(error.detail)) - return Response(html, media_type="text/html; charset=utf-8") + return _html5_response(html) @app.post("/html5/projects/{project_id}/authoring/apply-change-set") @@ -2018,7 +1971,7 @@ async def html5_project_authoring_apply_change_set(project_id: str, request: Req html = render_html5_authoring_apply_result(project_id, result) except HTTPException as error: html = render_html5_authoring_apply_result(project_id, error=str(error.detail)) - return Response(html, media_type="text/html; charset=utf-8") + return _html5_response(html) @app.post("/html5/projects/{project_id}/authoring/metadata-object-preview") @@ -2032,7 +1985,7 @@ async def html5_project_authoring_metadata_object_preview(project_id: str, reque except (HTTPException, ValueError) as error: detail = getattr(error, "detail", str(error)) html = render_html5_metadata_preview_result(project_id, error=str(detail)) - return Response(html, media_type="text/html; charset=utf-8") + return _html5_response(html) @app.post("/html5/projects/{project_id}/authoring/apply-metadata-object") @@ -2051,25 +2004,21 @@ async def html5_project_authoring_apply_metadata_object(project_id: str, request except (HTTPException, ValueError) as error: detail = getattr(error, "detail", str(error)) html = render_html5_metadata_apply_result(project_id, error=str(detail)) - return Response(html, media_type="text/html; charset=utf-8") + return _html5_response(html) @app.get("/html5/projects/{project_id}/setup") async def html5_project_setup(project_id: str) -> Response: setup = _project_setup_response(project_id) - return Response( + return _html5_response( render_html5_project_setup(project_id=project_id, projects=_project_summaries(), setup=setup), - media_type="text/html; charset=utf-8", ) @app.get("/html5/projects/{project_id}/setup/summary") async def html5_project_setup_summary(project_id: str) -> Response: setup = _project_setup_response(project_id) - return Response( - render_html5_setup_summary(project_id, setup), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_setup_summary(project_id, setup)) @app.get("/html5/projects/{project_id}/setup/events") @@ -2100,11 +2049,7 @@ async def html5_project_setup_events(project_id: str, once: bool = False) -> Str break await asyncio.sleep(2) - return StreamingResponse( - stream_setup(), - media_type="text/event-stream", - headers=_html5_sse_headers(), - ) + return _html5_sse_response(stream_setup()) @app.post("/html5/projects/{project_id}/setup/source") @@ -2114,10 +2059,7 @@ async def html5_project_setup_source(project_id: str, request: Request) -> Respo current = _project_setup_response(project_id) settings = current.settings.model_copy(update={"structure_source": source}) setup = await save_project_settings(project_id, settings) - return Response( - render_html5_setup_summary(project_id, setup), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_setup_summary(project_id, setup)) @app.post("/html5/projects/{project_id}/setup/settings") @@ -2132,10 +2074,7 @@ async def html5_project_setup_settings(project_id: str, request: Request) -> Res } ) setup = await save_project_settings(project_id, settings) - return Response( - render_html5_settings_panel(project_id, setup, saved=True), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_settings_panel(project_id, setup, saved=True)) @app.post("/html5/projects/{project_id}/setup/check") @@ -2143,10 +2082,7 @@ async def html5_project_setup_check(project_id: str, request: Request) -> Respon form = await _html5_form_data(request) source = ImportSourceKind(_form_value(form, "source") or _current_import_source(project_id).value) check = _import_check_response(project_id, source, ImportRequest(source=source)) - return Response( - render_html5_import_check(project_id, check), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_import_check(project_id, check)) @app.post("/html5/projects/{project_id}/setup/import") @@ -2156,10 +2092,7 @@ async def html5_project_setup_import(project_id: str, request: Request) -> Respo structure_only = _form_value(form, "structure_only") in {"1", "true", "on", "yes"} _execute_import_project(project_id, ImportRequest(source=source, structure_only=structure_only)) setup = _project_setup_response(project_id) - return Response( - render_html5_setup_summary(project_id, setup), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_setup_summary(project_id, setup)) @app.post("/html5/projects/{project_id}/setup/import-job") @@ -2167,10 +2100,7 @@ async def html5_project_setup_import_job(project_id: str, request: Request) -> R form = await _html5_form_data(request) source = ImportSourceKind(_form_value(form, "source") or _current_import_source(project_id).value) job = await start_project_import_job(project_id, source, ImportRequest(source=source)) - return Response( - render_html5_import_job(project_id, job), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_import_job(project_id, job)) @app.get("/html5/projects/{project_id}/setup/jobs/{job_id}") @@ -2178,20 +2108,14 @@ async def html5_project_setup_job(project_id: str, job_id: str) -> Response: job = _operations.jobs.get(job_id) if job is None or job.payload.get("project_id") != project_id: raise HTTPException(status_code=404, detail=f"Unknown import job: {job_id}") - return Response( - render_html5_import_job(project_id, job), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_import_job(project_id, job)) @app.post("/html5/projects/{project_id}/setup/reindex") async def html5_project_setup_reindex(project_id: str) -> Response: await reindex_project(project_id) setup = _project_setup_response(project_id) - return Response( - render_html5_setup_summary(project_id, setup), - media_type="text/html; charset=utf-8", - ) + return _html5_response(render_html5_setup_summary(project_id, setup)) @app.get("/version") @@ -8517,6 +8441,18 @@ def _html5_sse_headers() -> dict[str, str]: } +def _html5_response(fragment: str) -> Response: + return Response(fragment, media_type="text/html; charset=utf-8") + + +def _html5_sse_response(content: Any) -> StreamingResponse: + return StreamingResponse( + content, + media_type="text/event-stream", + headers=_html5_sse_headers(), + ) + + def _html5_sse_if_changed(last_fragments: dict[str, str], event: str, fragment: str): if last_fragments.get(event) == fragment: return