refactor(schedule): promote the domain errors to exceptions.py

Move model/errors.py up one level to domain/schedule/exceptions.py, a
sibling of the model, matching the AWS domain layout the spec mandates
(exceptions/ is its own member of the domain folder, not part of the
model) and the feedback reference implementation. Class names keep the
PEP 8 Error suffix. Pure move -- the nine classes are AST-identical to
the originals; imports across the domain, adapters, router, and tests
now take errors from deerflow.domain.schedule.exceptions.
This commit is contained in:
rayhpeng 2026-07-29 19:33:12 +08:00
parent a7c1a34b28
commit f88c8e61bc
17 changed files with 59 additions and 100 deletions

View File

@ -25,7 +25,7 @@ from typing import Any
from fastapi import HTTPException from fastapi import HTTPException
from deerflow.domain.schedule.model import LaunchFailedError, ThreadBusyError from deerflow.domain.schedule.exceptions import LaunchFailedError, ThreadBusyError
from deerflow.domain.schedule.ports import LaunchedRun, RunLauncher from deerflow.domain.schedule.ports import LaunchedRun, RunLauncher
from deerflow.runtime import ConflictError from deerflow.runtime import ConflictError

View File

@ -19,14 +19,8 @@ from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from deerflow.domain.schedule.model import ( from deerflow.domain.schedule.exceptions import ActiveRunConflictError
ACTIVE_RUN_STATUSES, from deerflow.domain.schedule.model import ACTIVE_RUN_STATUSES, TERMINAL_RUN_STATUSES, RunStatus, ScheduledRun, TriggerKind
TERMINAL_RUN_STATUSES,
ActiveRunConflictError,
RunStatus,
ScheduledRun,
TriggerKind,
)
from deerflow.domain.schedule.ports import ScheduledRunRepository from deerflow.domain.schedule.ports import ScheduledRunRepository
# Transitional: the ORM row stays in the harness until engine, models, and # Transitional: the ORM row stays in the harness until engine, models, and

View File

@ -26,15 +26,8 @@ from datetime import UTC, datetime, timedelta
from sqlalchemy import and_, or_, select from sqlalchemy import and_, or_, select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from deerflow.domain.schedule.model import ( from deerflow.domain.schedule.exceptions import InvalidScheduleError
TERMINAL_TASK_STATUSES, from deerflow.domain.schedule.model import TERMINAL_TASK_STATUSES, ContextMode, ScheduledTask, ScheduleSpec, ScheduleType, TaskStatus
ContextMode,
InvalidScheduleError,
ScheduledTask,
ScheduleSpec,
ScheduleType,
TaskStatus,
)
from deerflow.domain.schedule.ports import ScheduledTaskRepository from deerflow.domain.schedule.ports import ScheduledTaskRepository
# Transitional: the ORM row stays in the harness until engine, models, and # Transitional: the ORM row stays in the harness until engine, models, and

View File

@ -30,8 +30,7 @@ from app.gateway.routers.schedule.models import (
ScheduledTaskUpdateRequest, ScheduledTaskUpdateRequest,
TriggerResponse, TriggerResponse,
) )
from deerflow.domain.schedule.model import ( from deerflow.domain.schedule.exceptions import (
DispatchOutcome,
InvalidContextModeError, InvalidContextModeError,
InvalidScheduleError, InvalidScheduleError,
ScheduleError, ScheduleError,
@ -39,6 +38,7 @@ from deerflow.domain.schedule.model import (
TaskNotMutableError, TaskNotMutableError,
ThreadNotFoundError, ThreadNotFoundError,
) )
from deerflow.domain.schedule.model import DispatchOutcome
from deerflow.domain.schedule.service import ContextChange from deerflow.domain.schedule.service import ContextChange
router = APIRouter(prefix="/api", tags=["scheduled-tasks"]) router = APIRouter(prefix="/api", tags=["scheduled-tasks"])

View File

@ -7,28 +7,30 @@ tests, not everyday call-site symbols.
`ScheduleService` is not exported yet: the application service has not landed. `ScheduleService` is not exported yet: the application service has not landed.
""" """
from deerflow.domain.schedule.exceptions import (
ActiveRunConflictError,
InvalidContextModeError,
InvalidScheduleError,
LaunchFailedError,
ScheduleError,
TaskNotFoundError,
TaskNotMutableError,
ThreadBusyError,
ThreadNotFoundError,
)
from deerflow.domain.schedule.model import ( from deerflow.domain.schedule.model import (
ACTIVE_RUN_STATUSES, ACTIVE_RUN_STATUSES,
TERMINAL_RUN_STATUSES, TERMINAL_RUN_STATUSES,
TERMINAL_TASK_STATUSES, TERMINAL_TASK_STATUSES,
ActiveRunConflictError,
ContextMode, ContextMode,
DispatchOutcome, DispatchOutcome,
InvalidContextModeError,
InvalidScheduleError,
LaunchFailedError,
RunStatus, RunStatus,
ScheduledRun, ScheduledRun,
ScheduledTask, ScheduledTask,
ScheduleError,
SchedulePolicy, SchedulePolicy,
ScheduleSpec, ScheduleSpec,
ScheduleType, ScheduleType,
TaskNotFoundError,
TaskNotMutableError,
TaskStatus, TaskStatus,
ThreadBusyError,
ThreadNotFoundError,
TriggerKind, TriggerKind,
) )

View File

@ -1,3 +1,11 @@
"""The known errors of the schedule context.
One family under one base class, so the primary adapter can map the whole
family onto protocol codes in a single table. Class names keep the PEP 8
``Error`` suffix; the module is named ``exceptions`` after the AWS
hexagonal guidance's domain folder of the same name.
"""
from __future__ import annotations from __future__ import annotations

View File

@ -8,8 +8,11 @@ what a single `model.py` would have.
The re-exports below are ordered alphabetically because ruff's isort rule The re-exports below are ordered alphabetically because ruff's isort rule
owns that block; alphabetical happens to satisfy the real dependency order owns that block; alphabetical happens to satisfy the real dependency order
too (enums and errors depend on nothing, run depends on enums, spec depends too (enums depend on nothing, run depends on enums, spec depends on enums,
on enums and errors, task depends on all three), so it needs no override. task depends on both), so it needs no override. The domain errors are not
re-exported here: they live one level up in
`deerflow.domain.schedule.exceptions`, a sibling of the model per the AWS
domain layout, and are imported from there.
What actually keeps the package acyclic is a rule isort cannot express: What actually keeps the package acyclic is a rule isort cannot express:
**a submodule imports its siblings directly, never this package.** Reaching **a submodule imports its siblings directly, never this package.** Reaching
@ -27,17 +30,6 @@ from deerflow.domain.schedule.model.enums import (
TaskStatus, TaskStatus,
TriggerKind, TriggerKind,
) )
from deerflow.domain.schedule.model.errors import (
ActiveRunConflictError,
InvalidContextModeError,
InvalidScheduleError,
LaunchFailedError,
ScheduleError,
TaskNotFoundError,
TaskNotMutableError,
ThreadBusyError,
ThreadNotFoundError,
)
from deerflow.domain.schedule.model.run import ACTIVE_RUN_STATUSES, TERMINAL_RUN_STATUSES, ScheduledRun from deerflow.domain.schedule.model.run import ACTIVE_RUN_STATUSES, TERMINAL_RUN_STATUSES, ScheduledRun
from deerflow.domain.schedule.model.spec import SchedulePolicy, ScheduleSpec from deerflow.domain.schedule.model.spec import SchedulePolicy, ScheduleSpec
from deerflow.domain.schedule.model.task import TERMINAL_TASK_STATUSES, ScheduledTask from deerflow.domain.schedule.model.task import TERMINAL_TASK_STATUSES, ScheduledTask
@ -46,23 +38,14 @@ __all__ = [
"ACTIVE_RUN_STATUSES", "ACTIVE_RUN_STATUSES",
"TERMINAL_RUN_STATUSES", "TERMINAL_RUN_STATUSES",
"TERMINAL_TASK_STATUSES", "TERMINAL_TASK_STATUSES",
"ActiveRunConflictError",
"ContextMode", "ContextMode",
"DispatchOutcome", "DispatchOutcome",
"InvalidContextModeError",
"InvalidScheduleError",
"LaunchFailedError",
"RunStatus", "RunStatus",
"ScheduleError",
"SchedulePolicy", "SchedulePolicy",
"ScheduleSpec", "ScheduleSpec",
"ScheduleType", "ScheduleType",
"ScheduledRun", "ScheduledRun",
"ScheduledTask", "ScheduledTask",
"TaskNotFoundError",
"TaskNotMutableError",
"TaskStatus", "TaskStatus",
"ThreadBusyError",
"ThreadNotFoundError",
"TriggerKind", "TriggerKind",
] ]

View File

@ -6,8 +6,8 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from croniter import croniter from croniter import croniter
from deerflow.domain.schedule.exceptions import InvalidScheduleError
from deerflow.domain.schedule.model.enums import ScheduleType from deerflow.domain.schedule.model.enums import ScheduleType
from deerflow.domain.schedule.model.errors import InvalidScheduleError
CRON_FIELD_COUNT = 5 CRON_FIELD_COUNT = 5

View File

@ -4,8 +4,8 @@ import uuid
from dataclasses import dataclass, field, replace from dataclasses import dataclass, field, replace
from datetime import UTC, datetime from datetime import UTC, datetime
from deerflow.domain.schedule.exceptions import InvalidContextModeError, TaskNotMutableError
from deerflow.domain.schedule.model.enums import ContextMode, RunStatus, ScheduleType, TaskStatus, TriggerKind from deerflow.domain.schedule.model.enums import ContextMode, RunStatus, ScheduleType, TaskStatus, TriggerKind
from deerflow.domain.schedule.model.errors import InvalidContextModeError, TaskNotMutableError
from deerflow.domain.schedule.model.spec import SchedulePolicy, ScheduleSpec from deerflow.domain.schedule.model.spec import SchedulePolicy, ScheduleSpec
TERMINAL_TASK_STATUSES = frozenset({TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED}) TERMINAL_TASK_STATUSES = frozenset({TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED})

View File

@ -17,20 +17,22 @@ from __future__ import annotations
from dataclasses import dataclass, replace from dataclasses import dataclass, replace
from datetime import datetime from datetime import datetime
from deerflow.domain.schedule.model import ( from deerflow.domain.schedule.exceptions import (
ActiveRunConflictError, ActiveRunConflictError,
LaunchFailedError,
TaskNotFoundError,
ThreadBusyError,
ThreadNotFoundError,
)
from deerflow.domain.schedule.model import (
ContextMode, ContextMode,
DispatchOutcome, DispatchOutcome,
LaunchFailedError,
RunStatus, RunStatus,
ScheduledRun, ScheduledRun,
ScheduledTask, ScheduledTask,
SchedulePolicy, SchedulePolicy,
ScheduleSpec, ScheduleSpec,
ScheduleType, ScheduleType,
TaskNotFoundError,
ThreadBusyError,
ThreadNotFoundError,
TriggerKind, TriggerKind,
) )
from deerflow.domain.schedule.ports import ( from deerflow.domain.schedule.ports import (

View File

@ -17,11 +17,11 @@ from __future__ import annotations
from dataclasses import dataclass, field, replace from dataclasses import dataclass, field, replace
from datetime import datetime, timedelta from datetime import datetime, timedelta
from deerflow.domain.schedule.exceptions import ActiveRunConflictError
from deerflow.domain.schedule.model import ( from deerflow.domain.schedule.model import (
ACTIVE_RUN_STATUSES, ACTIVE_RUN_STATUSES,
TERMINAL_RUN_STATUSES, TERMINAL_RUN_STATUSES,
TERMINAL_TASK_STATUSES, TERMINAL_TASK_STATUSES,
ActiveRunConflictError,
RunStatus, RunStatus,
ScheduledRun, ScheduledRun,
ScheduledTask, ScheduledTask,

View File

@ -19,17 +19,15 @@ from datetime import UTC, datetime, timedelta
import pytest import pytest
from deerflow.domain.schedule.exceptions import InvalidContextModeError, InvalidScheduleError, TaskNotMutableError
from deerflow.domain.schedule.model import ( from deerflow.domain.schedule.model import (
ContextMode, ContextMode,
InvalidContextModeError,
InvalidScheduleError,
RunStatus, RunStatus,
ScheduledRun, ScheduledRun,
ScheduledTask, ScheduledTask,
SchedulePolicy, SchedulePolicy,
ScheduleSpec, ScheduleSpec,
ScheduleType, ScheduleType,
TaskNotMutableError,
TaskStatus, TaskStatus,
TriggerKind, TriggerKind,
) )

View File

@ -31,16 +31,8 @@ from schedule_fakes import (
from app.adapters.schedule.scheduled_run_repository import SqlScheduledRunRepository from app.adapters.schedule.scheduled_run_repository import SqlScheduledRunRepository
from app.adapters.schedule.scheduled_task_repository import SqlScheduledTaskRepository from app.adapters.schedule.scheduled_task_repository import SqlScheduledTaskRepository
from deerflow.config.database_config import DatabaseConfig from deerflow.config.database_config import DatabaseConfig
from deerflow.domain.schedule.model import ( from deerflow.domain.schedule.exceptions import ActiveRunConflictError
ActiveRunConflictError, from deerflow.domain.schedule.model import ContextMode, RunStatus, ScheduledRun, ScheduledTask, ScheduleSpec, TaskStatus, TriggerKind
ContextMode,
RunStatus,
ScheduledRun,
ScheduledTask,
ScheduleSpec,
TaskStatus,
TriggerKind,
)
from deerflow.domain.schedule.ports import ScheduledRunRepository, ScheduledTaskRepository from deerflow.domain.schedule.ports import ScheduledRunRepository, ScheduledTaskRepository
from deerflow.persistence.engine import close_engine, get_session_factory, init_engine_from_config from deerflow.persistence.engine import close_engine, get_session_factory, init_engine_from_config

View File

@ -17,15 +17,7 @@ from datetime import UTC, datetime, timedelta, timezone
import pytest import pytest
from app.gateway.routers.schedule.models import ScheduledRunResponse, ScheduledTaskResponse from app.gateway.routers.schedule.models import ScheduledRunResponse, ScheduledTaskResponse
from deerflow.domain.schedule.model import ( from deerflow.domain.schedule.model import ContextMode, RunStatus, ScheduledRun, ScheduledTask, ScheduleSpec, TaskStatus, TriggerKind
ContextMode,
RunStatus,
ScheduledRun,
ScheduledTask,
ScheduleSpec,
TaskStatus,
TriggerKind,
)
# Exactly the frontend's `ScheduledTask` type (frontend/src/core/scheduled-tasks/types.ts). # Exactly the frontend's `ScheduledTask` type (frontend/src/core/scheduled-tasks/types.ts).
FRONTEND_TASK_FIELDS = { FRONTEND_TASK_FIELDS = {

View File

@ -30,15 +30,8 @@ from schedule_fakes import (
) )
from app.gateway.routers.schedule import router as router_module from app.gateway.routers.schedule import router as router_module
from deerflow.domain.schedule.model import ( from deerflow.domain.schedule.exceptions import LaunchFailedError, ThreadBusyError
LaunchFailedError, from deerflow.domain.schedule.model import RunStatus, ScheduledRun, SchedulePolicy, TaskStatus, TriggerKind
RunStatus,
ScheduledRun,
SchedulePolicy,
TaskStatus,
ThreadBusyError,
TriggerKind,
)
from deerflow.domain.schedule.service import ScheduleService from deerflow.domain.schedule.service import ScheduleService
USER = "user-1" USER = "user-1"
@ -528,7 +521,7 @@ class TestRunHistory:
class TestErrorMapping: class TestErrorMapping:
def test_every_mapped_error_is_a_schedule_error(self): def test_every_mapped_error_is_a_schedule_error(self):
from deerflow.domain.schedule.model import ScheduleError from deerflow.domain.schedule.exceptions import ScheduleError
assert all(issubclass(error, ScheduleError) for error in router_module._STATUS_BY_ERROR) assert all(issubclass(error, ScheduleError) for error in router_module._STATUS_BY_ERROR)
@ -537,7 +530,7 @@ class TestErrorMapping:
"""A new domain error is a new protocol decision. Letting it through """A new domain error is a new protocol decision. Letting it through
surfaces as a 500 that has to be classified, rather than shipping as surfaces as a 500 that has to be classified, rather than shipping as
whatever 4xx happened to be nearest.""" whatever 4xx happened to be nearest."""
from deerflow.domain.schedule.model import ScheduleError from deerflow.domain.schedule.exceptions import ScheduleError
class NewDomainError(ScheduleError): class NewDomainError(ScheduleError):
pass pass
@ -551,7 +544,7 @@ class TestErrorMapping:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_a_mapped_error_keeps_its_message_as_the_detail(self): async def test_a_mapped_error_keeps_its_message_as_the_detail(self):
from deerflow.domain.schedule.model import TaskNotFoundError from deerflow.domain.schedule.exceptions import TaskNotFoundError
@router_module._map_domain_errors @router_module._map_domain_errors
async def handler(): async def handler():

View File

@ -26,7 +26,7 @@ import pytest
from fastapi import HTTPException from fastapi import HTTPException
from app.adapters.schedule.run_launcher import GatewayRunLauncher from app.adapters.schedule.run_launcher import GatewayRunLauncher
from deerflow.domain.schedule.model import LaunchFailedError, ThreadBusyError from deerflow.domain.schedule.exceptions import LaunchFailedError, ThreadBusyError
from deerflow.domain.schedule.ports import LaunchedRun from deerflow.domain.schedule.ports import LaunchedRun
from deerflow.runtime import ConflictError from deerflow.runtime import ConflictError

View File

@ -23,19 +23,21 @@ from schedule_fakes import (
InMemoryScheduledTaskRepository, InMemoryScheduledTaskRepository,
) )
from deerflow.domain.schedule.exceptions import (
InvalidScheduleError,
LaunchFailedError,
TaskNotFoundError,
TaskNotMutableError,
ThreadBusyError,
ThreadNotFoundError,
)
from deerflow.domain.schedule.model import ( from deerflow.domain.schedule.model import (
ContextMode, ContextMode,
DispatchOutcome, DispatchOutcome,
InvalidScheduleError,
LaunchFailedError,
RunStatus, RunStatus,
SchedulePolicy, SchedulePolicy,
ScheduleSpec, ScheduleSpec,
TaskNotFoundError,
TaskNotMutableError,
TaskStatus, TaskStatus,
ThreadBusyError,
ThreadNotFoundError,
TriggerKind, TriggerKind,
) )
from deerflow.domain.schedule.ports import RunOutcome from deerflow.domain.schedule.ports import RunOutcome