Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 2 additions & 13 deletions autobot-backend/llc/api/approvals.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
from sqlalchemy.ext.asyncio import AsyncSession

from autobot_shared.logging_manager import get_logger
from autobot_shared.singleton_factory import lazy_singleton
from user_management.database import get_async_session_factory
from llc.deps import get_session, service_dep

from ..models.enums import ApprovalStatus, ApprovalType
from ..services.approval import (
Expand All @@ -31,17 +30,7 @@

logger = get_logger(__name__)
router = APIRouter(prefix="/approvals", tags=["llc-approvals"])
_get_service = lazy_singleton(ApprovalService)


def _service() -> ApprovalService:
return _get_service()


async def get_session() -> AsyncSession:
factory = get_async_session_factory()
async with factory() as session:
yield session
_service = service_dep(ApprovalService)


# ------------------------------------------------------------------
Expand Down
15 changes: 2 additions & 13 deletions autobot-backend/llc/api/backlog.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,14 @@
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession

from autobot_shared.singleton_factory import lazy_singleton
from user_management.database import get_async_session_factory
from llc.deps import get_session, service_dep

from ..models.enums import WorkItemStatus, WorkItemType
from ..models.work_item import LLCWorkItem
from ..services.backlog import BacklogService

router = APIRouter(prefix="/backlog", tags=["llc-backlog"])
_get_service = lazy_singleton(BacklogService)


def _service() -> BacklogService:
return _get_service()


async def get_session() -> AsyncSession:
factory = get_async_session_factory()
async with factory() as session:
yield session
_service = service_dep(BacklogService)


def _item_to_dict(item: LLCWorkItem) -> Dict[str, Any]:
Expand Down
15 changes: 2 additions & 13 deletions autobot-backend/llc/api/boards.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,15 @@
from sqlalchemy.ext.asyncio import AsyncSession

from autobot_shared.logging_manager import get_logger
from autobot_shared.singleton_factory import lazy_singleton
from user_management.database import get_async_session_factory
from llc.deps import get_session, service_dep

from ..exceptions import WipLimitExceeded
from ..services.board import BoardService
from ..services.work_item_service import InvalidTransition

logger = get_logger(__name__)
router = APIRouter(prefix="/boards", tags=["llc-boards"])
_get_service = lazy_singleton(BoardService)


def _service() -> BoardService:
return _get_service()


async def get_session() -> AsyncSession:
factory = get_async_session_factory()
async with factory() as session:
yield session
_service = service_dep(BoardService)


# ------------------------------------------------------------------
Expand Down
8 changes: 1 addition & 7 deletions autobot-backend/llc/api/controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

from api.user_management.dependencies import get_current_user
from autobot_shared.singleton_factory import lazy_singleton
from user_management.database import get_async_session_factory
from llc.deps import get_session

from ..models.enums import MembershipRole
from ..services.controls_service import (
Expand All @@ -45,12 +45,6 @@
_ALLOWED_ROLES = {MembershipRole.OWNER, MembershipRole.ADMIN}


async def get_session() -> AsyncSession:
factory = get_async_session_factory()
async with factory() as session:
yield session


def _controls_svc() -> ControlsService:
return _get_controls()

Expand Down
15 changes: 2 additions & 13 deletions autobot-backend/llc/api/labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession

from autobot_shared.singleton_factory import lazy_singleton
from user_management.database import get_async_session_factory
from llc.deps import get_session, service_dep

from ..services.label_service import (
LabelNameConflict,
Expand All @@ -31,17 +30,7 @@
)

router = APIRouter(prefix="/companies/{company_id}/labels", tags=["llc-labels"])
_get_service = lazy_singleton(LLCLabelService)


def _service() -> LLCLabelService:
return _get_service()


async def get_session() -> AsyncSession:
factory = get_async_session_factory()
async with factory() as session:
yield session
_service = service_dep(LLCLabelService)


# ------------------------------------------------------------------
Expand Down
15 changes: 2 additions & 13 deletions autobot-backend/llc/api/review_gate_policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@
from pydantic import BaseModel, ConfigDict
from sqlalchemy.ext.asyncio import AsyncSession

from autobot_shared.singleton_factory import lazy_singleton
from user_management.database import get_async_session_factory
from llc.deps import get_session, service_dep

from ..models.enums import WorkItemType
from ..services.review_gate import (
Expand All @@ -30,17 +29,7 @@
)

router = APIRouter(tags=["llc-review-gate-policies"])
_get_service = lazy_singleton(ReviewGatePolicyService)


def _service() -> ReviewGatePolicyService:
return _get_service()


async def get_session() -> AsyncSession:
factory = get_async_session_factory()
async with factory() as session:
yield session
_service = service_dep(ReviewGatePolicyService)


# ------------------------------------------------------------------
Expand Down
15 changes: 2 additions & 13 deletions autobot-backend/llc/api/routines.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,14 @@

from api.user_management.dependencies import get_current_user
from autobot_shared.redis_client import get_async_redis_client
from autobot_shared.singleton_factory import lazy_singleton
from user_management.database import get_async_session_factory
from llc.deps import get_session, service_dep

from ..models.enums import RoutineProduces, RoutineStatus
from ..scheduler.routine_scheduler import _SCHEDULE_KEY
from ..services.routine_service import RoutineService

router = APIRouter(tags=["llc-routines"])
_get_service = lazy_singleton(RoutineService)


def _service() -> RoutineService:
return _get_service()


async def get_session() -> AsyncSession:
factory = get_async_session_factory()
async with factory() as session:
yield session
_service = service_dep(RoutineService)


# ------------------------------------------------------------------
Expand Down
13 changes: 2 additions & 11 deletions autobot-backend/llc/api/work_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
from autobot_shared.logging_manager import get_logger
from autobot_shared.redis_client import get_async_redis_client
from autobot_shared.singleton_factory import lazy_singleton
from llc.deps import get_session, service_dep
from models.agent_org import AgentOrgNode
from user_management.database import get_async_session_factory
from user_management.models.user import User
from user_management.services import TenantContext

Expand Down Expand Up @@ -103,7 +103,6 @@ class CoworkerRequest(BaseModel):

logger = get_logger(__name__)
router = APIRouter(prefix="/work-items", tags=["llc-work-items"])
_get_service = lazy_singleton(WorkItemService)
_get_product_service = lazy_singleton(WorkProductService)
_get_handoff_service = lazy_singleton(HandoffService)
_kb_manager = KbCollectionManager()
Expand All @@ -113,9 +112,7 @@ class CoworkerRequest(BaseModel):
_get_heartbeat_scheduler = lazy_singleton(HeartbeatScheduler)
_get_activity_service = lazy_singleton(LLCActivityLogService)


def _service() -> WorkItemService:
return _get_service()
_service = service_dep(WorkItemService)


def _handoff_service() -> HandoffService:
Expand All @@ -130,12 +127,6 @@ def _attachment_service() -> AttachmentService:
return _get_attachment_service()


async def get_session() -> AsyncSession:
factory = get_async_session_factory()
async with factory() as session:
yield session


# ------------------------------------------------------------------
# Request / Response schemas
# ------------------------------------------------------------------
Expand Down
91 changes: 91 additions & 0 deletions autobot-backend/llc/deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright 2025-2026 mrveiss
# SPDX-License-Identifier: Apache-2.0
# AutoBot - AI-Powered Automation Platform
# Author: mrveiss
"""Shared FastAPI dependency helpers for LLC API routers (GH#9843).

Eliminates per-router copy-paste of:
- ``get_session`` — bare async session (no auto-commit; callers manage
transactions explicitly via ``session.begin()`` / ``session.commit()``)
- ``_service()`` / ``lazy_singleton`` boilerplate — replaced by
``service_dep(SvcClass)`` which returns a cacheable Depends-compatible
factory per service class.

Placed at ``llc.deps`` (not ``llc.api.deps``) so individual router modules can
import it without triggering ``llc.api.__init__`` — which would cause a
circular import when a router is loaded in isolation via importlib (e.g. tests).

Usage::

from llc.deps import get_session, service_dep
from llc.services.my_service import MyService

router = APIRouter(...)
_my_svc_dep = service_dep(MyService)

@router.get("/")
async def handler(
session: AsyncSession = Depends(get_session),
svc: MyService = Depends(_my_svc_dep),
) -> ...:
...
"""

from __future__ import annotations

import functools
from typing import AsyncGenerator, Callable, Type, TypeVar

from sqlalchemy.ext.asyncio import AsyncSession

from autobot_shared.singleton_factory import lazy_singleton
from user_management.database import get_async_session_factory

_T = TypeVar("_T")


async def get_session() -> AsyncGenerator[AsyncSession, None]:
"""Bare async DB session — no auto-commit or auto-rollback.

Callers are responsible for explicit transaction management
(``session.begin()`` / ``session.commit()``). This mirrors the
pre-refactor per-router ``get_session`` implementations exactly so that
endpoint behaviour is unchanged.

Because every LLC router shares this single function object, a test's
``app.dependency_overrides[get_session]`` overrides the session for ALL
migrated llc routers mounted on that app, not just one module.
"""
factory = get_async_session_factory()
async with factory() as session:
yield session


@functools.lru_cache(maxsize=None)
def service_dep(service_cls: Type[_T]) -> Callable[[], _T]:
"""Return a zero-argument FastAPI dependency that yields the singleton
instance of *service_cls*.

The returned callable is cached per class (``lru_cache``), so repeated
calls with the same class return the same dependency function — avoiding
redundant closure creation per request.

Example::

_svc_dep = service_dep(ApprovalService)

@router.post("/")
async def handler(svc: ApprovalService = Depends(_svc_dep)):
...
"""
_get = lazy_singleton(service_cls)

def _dep() -> _T:
return _get()

# Give the function a readable name for FastAPI dependency introspection.
_dep.__name__ = f"_dep_{service_cls.__name__}"
return _dep


__all__ = ["get_session", "service_dep"]
5 changes: 1 addition & 4 deletions autobot-backend/llc/tests/test_backlog.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,7 @@ def test_get_backlog_calls_service(self, app_client):
company_id = str(uuid.uuid4())
items = [_make_item(priority=WorkItemPriority.CRITICAL)]

with (
patch("llc.api.backlog._service") as mock_svc_factory,
patch("llc.api.backlog.get_async_session_factory"),
):
with patch("llc.api.backlog._service") as mock_svc_factory:
svc = AsyncMock()
svc.list_backlog = AsyncMock(return_value=(items, 1))
mock_svc_factory.return_value = svc
Expand Down
Loading