Making long running operations asynchronous
This commit is contained in:
1 parent
373a2c6acf
commit
d108cb7149
7 files changed
+533
-76
No files matched your search
+284
-75
@@ -16,7 +16,7 @@ from fastapi import (
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.database import AsyncSessionLocal, get_db
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import augmented_part as augmented_part_crud
|
||||
from app.crud import augmented_part_note as augmented_part_note_crud
|
||||
@@ -44,6 +44,7 @@ from app.models.unc_thread import UncSeries
|
||||
from app.models.user import User
|
||||
from app.services import augmented_part_sync as augmented_part_sync_service
|
||||
from app.services import fit_matching as fit_matching_service
|
||||
from app.services import jobs
|
||||
from app.services import odata as odata_service
|
||||
from app.services import scope_import as scope_import_service
|
||||
from app.web.templates import templates
|
||||
@@ -1469,11 +1470,21 @@ async def scope_import_page(
|
||||
)
|
||||
|
||||
|
||||
_SCOPE_IMPORT_STATUS_ID = "scope-import-status"
|
||||
|
||||
|
||||
def _scope_import_progress_ctx(job: jobs.Job) -> dict:
|
||||
return {
|
||||
"container_id": _SCOPE_IMPORT_STATUS_ID,
|
||||
"poll_url": f"/admin/scope-import/jobs/{job.id}",
|
||||
"job": job,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/scope-import", response_class=HTMLResponse)
|
||||
async def upload_scope_import(
|
||||
request: Request,
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(web_require("augmented_parts:write")),
|
||||
):
|
||||
filename = file.filename or ""
|
||||
@@ -1481,33 +1492,74 @@ async def upload_scope_import(
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_scope_import_result.html",
|
||||
{"error": "Expected an .xlsx spreadsheet."},
|
||||
{
|
||||
"error": "Expected an .xlsx spreadsheet.",
|
||||
"container_id": _SCOPE_IMPORT_STATUS_ID,
|
||||
},
|
||||
)
|
||||
data = await file.read()
|
||||
try:
|
||||
result = await scope_import_service.import_from_bytes(db, data)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
actor_id = actor.id
|
||||
|
||||
async def _run(job: jobs.Job) -> None:
|
||||
async with AsyncSessionLocal() as db:
|
||||
try:
|
||||
result = await scope_import_service.import_from_bytes(db, data)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
job.status = "error"
|
||||
job.message = f"Could not read spreadsheet: {exc}"
|
||||
return
|
||||
actor_row = await user_crud.get(db, actor_id)
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor_row,
|
||||
action="update",
|
||||
resource_type="AugmentedPart",
|
||||
resource_id=None,
|
||||
description=(
|
||||
f"Scope import ({filename}): {result.created} created, "
|
||||
f"{result.updated} updated, {result.skipped} skipped, "
|
||||
f"{result.errors} errors"
|
||||
),
|
||||
)
|
||||
job.status = "done"
|
||||
job.result = result
|
||||
|
||||
job = jobs.start("scope_import", _run)
|
||||
return templates.TemplateResponse(
|
||||
request, "admin/_job_progress.html", _scope_import_progress_ctx(job)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/scope-import/jobs/{job_id}", response_class=HTMLResponse)
|
||||
async def scope_import_job_status(
|
||||
request: Request,
|
||||
job_id: str,
|
||||
_: User = Depends(web_require("augmented_parts:write")),
|
||||
):
|
||||
job = jobs.get(job_id)
|
||||
if job is None:
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_scope_import_result.html",
|
||||
{"error": f"Could not read spreadsheet: {exc}"},
|
||||
{
|
||||
"error": "Import job not found — it may have expired.",
|
||||
"container_id": _SCOPE_IMPORT_STATUS_ID,
|
||||
},
|
||||
)
|
||||
if job.status == "running":
|
||||
return templates.TemplateResponse(
|
||||
request, "admin/_job_progress.html", _scope_import_progress_ctx(job)
|
||||
)
|
||||
if job.status == "error":
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_scope_import_result.html",
|
||||
{"error": job.message, "container_id": _SCOPE_IMPORT_STATUS_ID},
|
||||
)
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor,
|
||||
action="update",
|
||||
resource_type="AugmentedPart",
|
||||
resource_id=None,
|
||||
description=(
|
||||
f"Scope import ({filename}): {result.created} created, "
|
||||
f"{result.updated} updated, {result.skipped} skipped, "
|
||||
f"{result.errors} errors"
|
||||
),
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_scope_import_result.html",
|
||||
{"result": result},
|
||||
{"result": job.result, "container_id": _SCOPE_IMPORT_STATUS_ID},
|
||||
)
|
||||
|
||||
|
||||
@@ -1645,38 +1697,89 @@ async def create_augmented_part(
|
||||
)
|
||||
|
||||
|
||||
_AUGMENTED_PARTS_SYNC_TARGET_ID = "augmented-part-list"
|
||||
|
||||
|
||||
@router.post("/augmented-parts/sync", response_class=HTMLResponse)
|
||||
async def sync_augmented_parts(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(web_require("augmented_parts:write")),
|
||||
):
|
||||
try:
|
||||
result = await augmented_part_sync_service.sync(db)
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor,
|
||||
action="update",
|
||||
resource_type="AugmentedPart",
|
||||
resource_id=None,
|
||||
description=(
|
||||
f"Sync: {result.created} created, {result.deleted} deleted, "
|
||||
f"{result.skipped} skipped (no type match)"
|
||||
),
|
||||
)
|
||||
sync_result = (
|
||||
f"Sync complete: {result.created} created, {result.deleted} deleted"
|
||||
+ (
|
||||
f", {result.skipped} skipped (type not recognised)"
|
||||
if result.skipped
|
||||
else ""
|
||||
actor_id = actor.id
|
||||
|
||||
async def _run(job: jobs.Job) -> None:
|
||||
async with AsyncSessionLocal() as db:
|
||||
try:
|
||||
result = await augmented_part_sync_service.sync(db)
|
||||
except Exception as exc:
|
||||
job.status = "error"
|
||||
job.message = f"Sync failed: {exc}"
|
||||
return
|
||||
actor_row = await user_crud.get(db, actor_id)
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor_row,
|
||||
action="update",
|
||||
resource_type="AugmentedPart",
|
||||
resource_id=None,
|
||||
description=(
|
||||
f"Sync: {result.created} created, {result.deleted} deleted, "
|
||||
f"{result.skipped} skipped (no type match)"
|
||||
),
|
||||
)
|
||||
+ "."
|
||||
job.status = "done"
|
||||
job.message = (
|
||||
f"Sync complete: {result.created} created, {result.deleted} deleted"
|
||||
+ (
|
||||
f", {result.skipped} skipped (type not recognised)"
|
||||
if result.skipped
|
||||
else ""
|
||||
)
|
||||
+ "."
|
||||
)
|
||||
|
||||
job = jobs.start("augmented_part_sync", _run)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_job_progress.html",
|
||||
{
|
||||
"container_id": _AUGMENTED_PARTS_SYNC_TARGET_ID,
|
||||
"poll_url": f"/admin/augmented-parts/sync/jobs/{job.id}",
|
||||
"job": job,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/augmented-parts/sync/jobs/{job_id}", response_class=HTMLResponse)
|
||||
async def augmented_parts_sync_job_status(
|
||||
request: Request,
|
||||
job_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("augmented_parts:write")),
|
||||
):
|
||||
job = jobs.get(job_id)
|
||||
if job is None:
|
||||
form_ctx = await _augmented_part_form_ctx(db)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_augmented_parts_table.html",
|
||||
{
|
||||
**form_ctx,
|
||||
**await _default_augmented_parts_table_ctx(db),
|
||||
"sync_result": None,
|
||||
"sync_error": "Sync job not found — it may have expired.",
|
||||
},
|
||||
)
|
||||
if job.status == "running":
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_job_progress.html",
|
||||
{
|
||||
"container_id": _AUGMENTED_PARTS_SYNC_TARGET_ID,
|
||||
"poll_url": f"/admin/augmented-parts/sync/jobs/{job_id}",
|
||||
"job": job,
|
||||
},
|
||||
)
|
||||
sync_error = None
|
||||
except Exception as exc:
|
||||
sync_result = None
|
||||
sync_error = f"Sync failed: {exc}"
|
||||
form_ctx = await _augmented_part_form_ctx(db)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
@@ -1684,8 +1787,8 @@ async def sync_augmented_parts(
|
||||
{
|
||||
**form_ctx,
|
||||
**await _default_augmented_parts_table_ctx(db),
|
||||
"sync_result": sync_result,
|
||||
"sync_error": sync_error,
|
||||
"sync_result": job.message if job.status == "done" else None,
|
||||
"sync_error": job.message if job.status == "error" else None,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2355,32 +2458,94 @@ async def augmented_part_fits(
|
||||
return templates.TemplateResponse(request, "admin/_augmented_part_fits.html", ctx)
|
||||
|
||||
|
||||
def _augmented_part_fits_target_id(augmented_part_id: int) -> str:
|
||||
return f"ap-{augmented_part_id}-fits"
|
||||
|
||||
|
||||
@router.post(
|
||||
"/augmented-parts/{augmented_part_id}/fits/recompute", response_class=HTMLResponse
|
||||
)
|
||||
async def recompute_augmented_part_fits(
|
||||
request: Request,
|
||||
augmented_part_id: int,
|
||||
actor: User = Depends(web_require("fits:write")),
|
||||
):
|
||||
actor_id = actor.id
|
||||
|
||||
async def _run(job: jobs.Job) -> None:
|
||||
async with AsyncSessionLocal() as db:
|
||||
result = await fit_matching_service.recompute_for_part(
|
||||
db, augmented_part_id
|
||||
)
|
||||
actor_row = await user_crud.get(db, actor_id)
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor_row,
|
||||
action="update",
|
||||
resource_type="Fit",
|
||||
resource_id=augmented_part_id,
|
||||
description=(
|
||||
f"Recomputed fits for AugmentedPart #{augmented_part_id}: "
|
||||
f"{result.created} created, {result.removed} removed"
|
||||
),
|
||||
)
|
||||
job.status = "done"
|
||||
job.message = f"{result.created} created, {result.removed} removed."
|
||||
|
||||
job = jobs.start("fit_recompute_part", _run)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_job_progress.html",
|
||||
{
|
||||
"container_id": _augmented_part_fits_target_id(augmented_part_id),
|
||||
"poll_url": f"/admin/augmented-parts/{augmented_part_id}/fits/recompute/jobs/{job.id}",
|
||||
"job": job,
|
||||
"extra_class": "ap-inline-section",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/augmented-parts/{augmented_part_id}/fits/recompute/jobs/{job_id}",
|
||||
response_class=HTMLResponse,
|
||||
)
|
||||
async def augmented_part_fits_recompute_job_status(
|
||||
request: Request,
|
||||
augmented_part_id: int,
|
||||
job_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(web_require("fits:write")),
|
||||
):
|
||||
result = await fit_matching_service.recompute_for_part(db, augmented_part_id)
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor,
|
||||
action="update",
|
||||
resource_type="Fit",
|
||||
resource_id=augmented_part_id,
|
||||
description=(
|
||||
f"Recomputed fits for AugmentedPart #{augmented_part_id}: "
|
||||
f"{result.created} created, {result.removed} removed"
|
||||
),
|
||||
)
|
||||
job = jobs.get(job_id)
|
||||
if job is None:
|
||||
ctx = await _augmented_part_fits_ctx(
|
||||
db,
|
||||
augmented_part_id,
|
||||
can_write=_can_write_fits(actor),
|
||||
error="Recompute job not found — it may have expired.",
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request, "admin/_augmented_part_fits.html", ctx
|
||||
)
|
||||
if job.status == "running":
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_job_progress.html",
|
||||
{
|
||||
"container_id": _augmented_part_fits_target_id(augmented_part_id),
|
||||
"poll_url": (
|
||||
f"/admin/augmented-parts/{augmented_part_id}/fits/recompute/jobs/{job_id}"
|
||||
),
|
||||
"job": job,
|
||||
"extra_class": "ap-inline-section",
|
||||
},
|
||||
)
|
||||
ctx = await _augmented_part_fits_ctx(
|
||||
db,
|
||||
augmented_part_id,
|
||||
can_write=_can_write_fits(actor),
|
||||
message=f"{result.created} created, {result.removed} removed.",
|
||||
message=job.message if job.status == "done" else None,
|
||||
error=job.message if job.status == "error" else None,
|
||||
)
|
||||
return templates.TemplateResponse(request, "admin/_augmented_part_fits.html", ctx)
|
||||
|
||||
@@ -2588,29 +2753,73 @@ async def delete_augmented_part_fit(
|
||||
return templates.TemplateResponse(request, "admin/_augmented_part_fits.html", ctx)
|
||||
|
||||
|
||||
_ALL_FITS_RECOMPUTE_STATUS_ID = "fits-recompute-status"
|
||||
|
||||
|
||||
@router.post("/augmented-parts/fits/recompute", response_class=HTMLResponse)
|
||||
async def recompute_all_fits(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(web_require("fits:write")),
|
||||
):
|
||||
result = await fit_matching_service.recompute_all(db)
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor,
|
||||
action="update",
|
||||
resource_type="Fit",
|
||||
resource_id=None,
|
||||
description=(
|
||||
f"Recomputed fits for all parts: {result.created} created, "
|
||||
f"{result.removed} removed"
|
||||
),
|
||||
)
|
||||
return HTMLResponse(
|
||||
f"Recompute complete: {result.created} created, {result.removed} removed."
|
||||
actor_id = actor.id
|
||||
|
||||
async def _run(job: jobs.Job) -> None:
|
||||
async with AsyncSessionLocal() as db:
|
||||
result = await fit_matching_service.recompute_all(db)
|
||||
actor_row = await user_crud.get(db, actor_id)
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor_row,
|
||||
action="update",
|
||||
resource_type="Fit",
|
||||
resource_id=None,
|
||||
description=(
|
||||
f"Recomputed fits for all parts: {result.created} created, "
|
||||
f"{result.removed} removed"
|
||||
),
|
||||
)
|
||||
job.status = "done"
|
||||
job.message = (
|
||||
f"Recompute complete: {result.created} created, "
|
||||
f"{result.removed} removed."
|
||||
)
|
||||
|
||||
job = jobs.start("fit_recompute_all", _run)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_job_progress_inline.html",
|
||||
{
|
||||
"container_id": _ALL_FITS_RECOMPUTE_STATUS_ID,
|
||||
"poll_url": f"/admin/augmented-parts/fits/recompute/jobs/{job.id}",
|
||||
"job": job,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/augmented-parts/fits/recompute/jobs/{job_id}", response_class=HTMLResponse
|
||||
)
|
||||
async def recompute_all_fits_job_status(
|
||||
request: Request,
|
||||
job_id: str,
|
||||
_: User = Depends(web_require("fits:write")),
|
||||
):
|
||||
job = jobs.get(job_id)
|
||||
if job is None:
|
||||
return HTMLResponse("Recompute job not found — it may have expired.")
|
||||
if job.status == "running":
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_job_progress_inline.html",
|
||||
{
|
||||
"container_id": _ALL_FITS_RECOMPUTE_STATUS_ID,
|
||||
"poll_url": f"/admin/augmented-parts/fits/recompute/jobs/{job_id}",
|
||||
"job": job,
|
||||
},
|
||||
)
|
||||
return HTMLResponse(job.message)
|
||||
|
||||
|
||||
# --- Augmented Part Notes -------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user