generated from john/python-template
refactor: route workflow sessions through unit of work
Co-authored-by: Copilot App <[email protected]>
This commit is contained in:
co-authored by
Copilot App
parent
3873810022
commit
efbae26f16
@@ -0,0 +1,24 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
from ..db.session import transaction_scope
|
||||||
|
from . import ServiceBundle
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def unit_of_work(
|
||||||
|
*,
|
||||||
|
services: ServiceBundle,
|
||||||
|
session: AsyncSession | None = None,
|
||||||
|
) -> AsyncIterator[AsyncSession]:
|
||||||
|
"""Yield one shared transactional session for orchestration paths."""
|
||||||
|
if session is not None:
|
||||||
|
yield session
|
||||||
|
return
|
||||||
|
|
||||||
|
async with transaction_scope(session_factory=services.jobs.session_factory) as local_session:
|
||||||
|
yield local_session
|
||||||
@@ -37,6 +37,7 @@ from .sources import build_prompt_execution
|
|||||||
from .sources import build_provider_input
|
from .sources import build_provider_input
|
||||||
from .sources import hash_prompt_text
|
from .sources import hash_prompt_text
|
||||||
from .sources import transcribe_document_image
|
from .sources import transcribe_document_image
|
||||||
|
from .unit_of_work import unit_of_work
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -627,7 +628,7 @@ async def _finalize_batch_outcome(
|
|||||||
status change (no sources, or the batch stopped before the last page).
|
status change (no sources, or the batch stopped before the last page).
|
||||||
"""
|
"""
|
||||||
if session is None:
|
if session is None:
|
||||||
async with services.jobs._session_scope() as local_session:
|
async with unit_of_work(services=services, session=session) as local_session:
|
||||||
if final_page is not None:
|
if final_page is not None:
|
||||||
await _write_page_outcome(job=job, services=services, page=final_page, session=local_session)
|
await _write_page_outcome(job=job, services=services, page=final_page, session=local_session)
|
||||||
updated_job = await services.jobs.mark_job_status(job.id, status, session=local_session)
|
updated_job = await services.jobs.mark_job_status(job.id, status, session=local_session)
|
||||||
@@ -665,7 +666,7 @@ async def _persist_page_outcome(
|
|||||||
session: AsyncSession | None,
|
session: AsyncSession | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if session is None:
|
if session is None:
|
||||||
async with services.sources._session_scope() as local_session:
|
async with unit_of_work(services=services, session=session) as local_session:
|
||||||
await _write_page_outcome(job=job, services=services, page=page, session=local_session)
|
await _write_page_outcome(job=job, services=services, page=page, session=local_session)
|
||||||
await local_session.commit()
|
await local_session.commit()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -64,3 +64,26 @@ def test_no_service_module_imports_another_service_module():
|
|||||||
name: sorted(_imported_sibling_modules(tree) & set(modules) - {name}) for name, tree in modules.items()
|
name: sorted(_imported_sibling_modules(tree) & set(modules) - {name}) for name, tree in modules.items()
|
||||||
}
|
}
|
||||||
assert {name: found for name, found in violations.items() if found} == {}
|
assert {name: found for name, found in violations.items() if found} == {}
|
||||||
|
|
||||||
|
|
||||||
|
def _foreign_session_scope_accesses(tree: ast.Module) -> list[int]:
|
||||||
|
lines: list[int] = []
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.Attribute) and node.attr == "_session_scope":
|
||||||
|
if isinstance(node.value, ast.Name) and node.value.id == "self":
|
||||||
|
continue
|
||||||
|
lines.append(node.lineno)
|
||||||
|
return sorted(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_scope_is_not_accessed_via_other_services():
|
||||||
|
"""P4-1: orchestration must use a shared unit-of-work entry point, not private service scopes."""
|
||||||
|
violations: dict[str, list[int]] = {}
|
||||||
|
for path in _module_paths():
|
||||||
|
if path.stem == "base":
|
||||||
|
continue
|
||||||
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||||
|
found = _foreign_session_scope_accesses(tree)
|
||||||
|
if found:
|
||||||
|
violations[path.name] = found
|
||||||
|
assert violations == {}
|
||||||
|
|||||||
Reference in New Issue
Block a user