diff --git a/services/api-server/src/api_server/html5_setup_controller.py b/services/api-server/src/api_server/html5_setup_controller.py new file mode 100644 index 0000000..656db7c --- /dev/null +++ b/services/api-server/src/api_server/html5_setup_controller.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import asyncio +from collections.abc import AsyncIterator, Callable, Iterable +from typing import Any + +from fastapi import HTTPException + +from api_server.html5_forms import form_value +from api_server.html5_setup import ( + render_html5_import_check, + render_html5_import_job, + render_html5_project_setup, + render_html5_settings_panel, + render_html5_setup_summary, +) +from api_server.html5_sse import html5_sse_comment, html5_sse_if_changed + + +def html5_setup_page( + *, + project_id: str, + projects: Iterable[object], + setup: object, +) -> str: + return render_html5_project_setup(project_id=project_id, projects=projects, setup=setup) + + +def html5_setup_summary(*, project_id: str, setup: object) -> str: + return render_html5_setup_summary(project_id, setup) + + +async def html5_setup_event_stream( + *, + project_id: str, + setup_response: Callable[[str], object], + latest_import_job: Callable[[str], object | None], + once: bool = False, +) -> AsyncIterator[str]: + last_fragments: dict[str, str] = {} + while True: + yield html5_sse_comment(f"setup {project_id} heartbeat") + try: + setup = setup_response(project_id) + except HTTPException as error: + setup_error = f'

{error.detail}

' + for event_text in html5_sse_if_changed(last_fragments, "setup-summary", setup_error): + yield event_text + if once: + break + await asyncio.sleep(2) + continue + for event_text in html5_sse_if_changed(last_fragments, "setup-summary", render_html5_setup_summary(project_id, setup)): + yield event_text + for event_text in html5_sse_if_changed( + last_fragments, + "setup-import-job", + render_html5_import_job(project_id, latest_import_job(project_id)), + ): + yield event_text + if once: + break + await asyncio.sleep(2) + + +async def html5_setup_source( + *, + project_id: str, + form: dict[str, list[str]], + import_source_kind: Callable[[str], Any], + setup_response: Callable[[str], object], + save_settings: Callable[[str, object], Any], +) -> str: + source = import_source_kind(form_value(form, "source") or "XML_DUMP") + current = setup_response(project_id) + settings = current.settings.model_copy(update={"structure_source": source}) + setup = await save_settings(project_id, settings) + return render_html5_setup_summary(project_id, setup) + + +async def html5_setup_settings( + *, + project_id: str, + form: dict[str, list[str]], + setup_response: Callable[[str], object], + save_settings: Callable[[str, object], Any], +) -> str: + current = setup_response(project_id) + settings = current.settings.model_copy( + update={ + "name": form_value(form, "name") or current.settings.name, + "platform_version": form_value(form, "platform_version"), + "compatibility_mode": form_value(form, "compatibility_mode"), + } + ) + setup = await save_settings(project_id, settings) + return render_html5_settings_panel(project_id, setup, saved=True) + + +def html5_setup_check( + *, + project_id: str, + form: dict[str, list[str]], + import_source_kind: Callable[[str], Any], + import_request: Callable[..., object], + current_import_source: Callable[[str], object], + import_check: Callable[[str, object, object], object], +) -> str: + source = import_source_kind(form_value(form, "source") or current_import_source(project_id).value) + check = import_check(project_id, source, import_request(source=source)) + return render_html5_import_check(project_id, check) + + +def html5_setup_import( + *, + project_id: str, + form: dict[str, list[str]], + import_source_kind: Callable[[str], Any], + import_request: Callable[..., object], + current_import_source: Callable[[str], object], + execute_import: Callable[[str, object], object], + setup_response: Callable[[str], object], +) -> str: + source = import_source_kind(form_value(form, "source") or current_import_source(project_id).value) + structure_only = form_value(form, "structure_only") in {"1", "true", "on", "yes"} + execute_import(project_id, import_request(source=source, structure_only=structure_only)) + return render_html5_setup_summary(project_id, setup_response(project_id)) + + +async def html5_setup_import_job( + *, + project_id: str, + form: dict[str, list[str]], + import_source_kind: Callable[[str], Any], + import_request: Callable[..., object], + current_import_source: Callable[[str], object], + start_import_job: Callable[[str, object, object], Any], +) -> str: + source = import_source_kind(form_value(form, "source") or current_import_source(project_id).value) + job = await start_import_job(project_id, source, import_request(source=source)) + return render_html5_import_job(project_id, job) + + +def html5_setup_job( + *, + project_id: str, + job_id: str, + jobs_by_id: dict[str, object], +) -> str: + job = jobs_by_id.get(job_id) + if job is None or (getattr(job, "payload", {}) or {}).get("project_id") != project_id: + raise HTTPException(status_code=404, detail=f"Unknown import job: {job_id}") + return render_html5_import_job(project_id, job) + + +async def html5_setup_reindex( + *, + project_id: str, + reindex: Callable[[str], Any], + setup_response: Callable[[str], object], +) -> str: + await reindex(project_id) + return render_html5_setup_summary(project_id, setup_response(project_id)) diff --git a/services/api-server/src/api_server/main.py b/services/api-server/src/api_server/main.py index 752fad5..7310727 100644 --- a/services/api-server/src/api_server/main.py +++ b/services/api-server/src/api_server/main.py @@ -90,12 +90,17 @@ from api_server.html5_operations_controller import ( html5_operations_event_stream as _html5_operations_event_stream, html5_operations_page as _html5_operations_page, ) -from api_server.html5_setup import ( - render_html5_import_check, - render_html5_import_job, - render_html5_project_setup, - render_html5_settings_panel, - render_html5_setup_summary, +from api_server.html5_setup_controller import ( + html5_setup_check as _html5_setup_check, + html5_setup_event_stream as _html5_setup_event_stream, + html5_setup_import as _html5_setup_import, + html5_setup_import_job as _html5_setup_import_job, + html5_setup_job as _html5_setup_job, + html5_setup_page as _html5_setup_page, + html5_setup_reindex as _html5_setup_reindex, + html5_setup_settings as _html5_setup_settings, + html5_setup_source as _html5_setup_source, + html5_setup_summary as _html5_setup_summary, ) from impact_engine import object_impact, routine_impact from incremental_indexer import rebuild_changed_file @@ -2019,113 +2024,115 @@ async def html5_project_authoring_apply_metadata_object(project_id: str, request @app.get("/html5/projects/{project_id}/setup") async def html5_project_setup(project_id: str) -> Response: - setup = _project_setup_response(project_id) return _html5_response( - render_html5_project_setup(project_id=project_id, projects=_project_summaries(), setup=setup), + _html5_setup_page(project_id=project_id, projects=_project_summaries(), setup=_project_setup_response(project_id)), ) @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 _html5_response(render_html5_setup_summary(project_id, setup)) + return _html5_response(_html5_setup_summary(project_id=project_id, setup=_project_setup_response(project_id))) @app.get("/html5/projects/{project_id}/setup/events") async def html5_project_setup_events(project_id: str, once: bool = False) -> StreamingResponse: - async def stream_setup(): - last_fragments: dict[str, str] = {} - while True: - yield _html5_sse_comment(f"setup {project_id} heartbeat") - try: - setup = _project_setup_response(project_id) - except HTTPException as error: - setup_error = f'

{error.detail}

' - for event_text in _html5_sse_if_changed(last_fragments, "setup-summary", setup_error): - yield event_text - if once: - break - await asyncio.sleep(2) - continue - for event_text in _html5_sse_if_changed(last_fragments, "setup-summary", render_html5_setup_summary(project_id, setup)): - yield event_text - for event_text in _html5_sse_if_changed( - last_fragments, - "setup-import-job", - render_html5_import_job(project_id, _html5_latest_import_job(project_id)), - ): - yield event_text - if once: - break - await asyncio.sleep(2) - - return _html5_sse_response(stream_setup()) + return _html5_sse_response( + _html5_setup_event_stream( + project_id=project_id, + setup_response=_project_setup_response, + latest_import_job=_html5_latest_import_job, + once=once, + ) + ) @app.post("/html5/projects/{project_id}/setup/source") async def html5_project_setup_source(project_id: str, request: Request) -> Response: form = await _html5_form_data(request) - source = ImportSourceKind(_form_value(form, "source") or ImportSourceKind.XML_DUMP.value) - current = _project_setup_response(project_id) - settings = current.settings.model_copy(update={"structure_source": source}) - setup = await save_project_settings(project_id, settings) - return _html5_response(render_html5_setup_summary(project_id, setup)) + return _html5_response( + await _html5_setup_source( + project_id=project_id, + form=form, + import_source_kind=ImportSourceKind, + setup_response=_project_setup_response, + save_settings=save_project_settings, + ) + ) @app.post("/html5/projects/{project_id}/setup/settings") async def html5_project_setup_settings(project_id: str, request: Request) -> Response: form = await _html5_form_data(request) - current = _project_setup_response(project_id) - settings = current.settings.model_copy( - update={ - "name": _form_value(form, "name") or current.settings.name, - "platform_version": _form_value(form, "platform_version"), - "compatibility_mode": _form_value(form, "compatibility_mode"), - } + return _html5_response( + await _html5_setup_settings( + project_id=project_id, + form=form, + setup_response=_project_setup_response, + save_settings=save_project_settings, + ) ) - setup = await save_project_settings(project_id, settings) - return _html5_response(render_html5_settings_panel(project_id, setup, saved=True)) @app.post("/html5/projects/{project_id}/setup/check") async def html5_project_setup_check(project_id: str, request: Request) -> Response: 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 _html5_response(render_html5_import_check(project_id, check)) + return _html5_response( + _html5_setup_check( + project_id=project_id, + form=form, + import_source_kind=ImportSourceKind, + import_request=ImportRequest, + current_import_source=_current_import_source, + import_check=_import_check_response, + ) + ) @app.post("/html5/projects/{project_id}/setup/import") async def html5_project_setup_import(project_id: str, request: Request) -> Response: form = await _html5_form_data(request) - source = ImportSourceKind(_form_value(form, "source") or _current_import_source(project_id).value) - 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 _html5_response(render_html5_setup_summary(project_id, setup)) + return _html5_response( + _html5_setup_import( + project_id=project_id, + form=form, + import_source_kind=ImportSourceKind, + import_request=ImportRequest, + current_import_source=_current_import_source, + execute_import=_execute_import_project, + setup_response=_project_setup_response, + ) + ) @app.post("/html5/projects/{project_id}/setup/import-job") async def html5_project_setup_import_job(project_id: str, request: Request) -> Response: 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 _html5_response(render_html5_import_job(project_id, job)) + return _html5_response( + await _html5_setup_import_job( + project_id=project_id, + form=form, + import_source_kind=ImportSourceKind, + import_request=ImportRequest, + current_import_source=_current_import_source, + start_import_job=start_project_import_job, + ) + ) @app.get("/html5/projects/{project_id}/setup/jobs/{job_id}") 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 _html5_response(render_html5_import_job(project_id, job)) + return _html5_response(_html5_setup_job(project_id=project_id, job_id=job_id, jobs_by_id=_operations.jobs)) @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 _html5_response(render_html5_setup_summary(project_id, setup)) + return _html5_response( + await _html5_setup_reindex( + project_id=project_id, + reindex=reindex_project, + setup_response=_project_setup_response, + ) + ) @app.get("/version")