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 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.runtime import ConflictError

View File

@ -19,14 +19,8 @@ from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from deerflow.domain.schedule.model import (
ACTIVE_RUN_STATUSES,
TERMINAL_RUN_STATUSES,
ActiveRunConflictError,
RunStatus,
ScheduledRun,
TriggerKind,
)
from deerflow.domain.schedule.exceptions import ActiveRunConflictError
from deerflow.domain.schedule.model import ACTIVE_RUN_STATUSES, TERMINAL_RUN_STATUSES, RunStatus, ScheduledRun, TriggerKind
from deerflow.domain.schedule.ports import ScheduledRunRepository
# 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.ext.asyncio import AsyncSession, async_sessionmaker
from deerflow.domain.schedule.model import (
TERMINAL_TASK_STATUSES,
ContextMode,
InvalidScheduleError,
ScheduledTask,
ScheduleSpec,
ScheduleType,
TaskStatus,
)
from deerflow.domain.schedule.exceptions import InvalidScheduleError
from deerflow.domain.schedule.model import TERMINAL_TASK_STATUSES, ContextMode, ScheduledTask, ScheduleSpec, ScheduleType, TaskStatus
from deerflow.domain.schedule.ports import ScheduledTaskRepository
# 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,
TriggerResponse,
)
from deerflow.domain.schedule.model import (
DispatchOutcome,
from deerflow.domain.schedule.exceptions import (
InvalidContextModeError,
InvalidScheduleError,
ScheduleError,
@ -39,6 +38,7 @@ from deerflow.domain.schedule.model import (
TaskNotMutableError,
ThreadNotFoundError,
)
from deerflow.domain.schedule.model import DispatchOutcome
from deerflow.domain.schedule.service import ContextChange
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.
"""
from deerflow.domain.schedule.exceptions import (
ActiveRunConflictError,
InvalidContextModeError,
InvalidScheduleError,
LaunchFailedError,
ScheduleError,
TaskNotFoundError,
TaskNotMutableError,
ThreadBusyError,
ThreadNotFoundError,
)
from deerflow.domain.schedule.model import (
ACTIVE_RUN_STATUSES,
TERMINAL_RUN_STATUSES,
TERMINAL_TASK_STATUSES,
ActiveRunConflictError,
ContextMode,
DispatchOutcome,
InvalidContextModeError,
InvalidScheduleError,
LaunchFailedError,
RunStatus,
ScheduledRun,
ScheduledTask,
ScheduleError,
SchedulePolicy,
ScheduleSpec,
ScheduleType,
TaskNotFoundError,
TaskNotMutableError,
TaskStatus,
ThreadBusyError,
ThreadNotFoundError,
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

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
owns that block; alphabetical happens to satisfy the real dependency order
too (enums and errors depend on nothing, run depends on enums, spec depends
on enums and errors, task depends on all three), so it needs no override.
too (enums depend on nothing, run depends on enums, spec depends on enums,
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:
**a submodule imports its siblings directly, never this package.** Reaching
@ -27,17 +30,6 @@ from deerflow.domain.schedule.model.enums import (
TaskStatus,
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.spec import SchedulePolicy, ScheduleSpec
from deerflow.domain.schedule.model.task import TERMINAL_TASK_STATUSES, ScheduledTask
@ -46,23 +38,14 @@ __all__ = [
"ACTIVE_RUN_STATUSES",
"TERMINAL_RUN_STATUSES",
"TERMINAL_TASK_STATUSES",
"ActiveRunConflictError",
"ContextMode",
"DispatchOutcome",
"InvalidContextModeError",
"InvalidScheduleError",
"LaunchFailedError",
"RunStatus",
"ScheduleError",
"SchedulePolicy",
"ScheduleSpec",
"ScheduleType",
"ScheduledRun",
"ScheduledTask",
"TaskNotFoundError",
"TaskNotMutableError",
"TaskStatus",
"ThreadBusyError",
"ThreadNotFoundError",
"TriggerKind",
]

View File

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

View File

@ -4,8 +4,8 @@ import uuid
from dataclasses import dataclass, field, replace
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.errors import InvalidContextModeError, TaskNotMutableError
from deerflow.domain.schedule.model.spec import SchedulePolicy, ScheduleSpec
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 datetime import datetime
from deerflow.domain.schedule.model import (
from deerflow.domain.schedule.exceptions import (
ActiveRunConflictError,
LaunchFailedError,
TaskNotFoundError,
ThreadBusyError,
ThreadNotFoundError,
)
from deerflow.domain.schedule.model import (
ContextMode,
DispatchOutcome,
LaunchFailedError,
RunStatus,
ScheduledRun,
ScheduledTask,
SchedulePolicy,
ScheduleSpec,
ScheduleType,
TaskNotFoundError,
ThreadBusyError,
ThreadNotFoundError,
TriggerKind,
)
from deerflow.domain.schedule.ports import (

View File

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

View File

@ -19,17 +19,15 @@ from datetime import UTC, datetime, timedelta
import pytest
from deerflow.domain.schedule.exceptions import InvalidContextModeError, InvalidScheduleError, TaskNotMutableError
from deerflow.domain.schedule.model import (
ContextMode,
InvalidContextModeError,
InvalidScheduleError,
RunStatus,
ScheduledRun,
ScheduledTask,
SchedulePolicy,
ScheduleSpec,
ScheduleType,
TaskNotMutableError,
TaskStatus,
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_task_repository import SqlScheduledTaskRepository
from deerflow.config.database_config import DatabaseConfig
from deerflow.domain.schedule.model import (
ActiveRunConflictError,
ContextMode,
RunStatus,
ScheduledRun,
ScheduledTask,
ScheduleSpec,
TaskStatus,
TriggerKind,
)
from deerflow.domain.schedule.exceptions import ActiveRunConflictError
from deerflow.domain.schedule.model import ContextMode, RunStatus, ScheduledRun, ScheduledTask, ScheduleSpec, TaskStatus, TriggerKind
from deerflow.domain.schedule.ports import ScheduledRunRepository, ScheduledTaskRepository
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
from app.gateway.routers.schedule.models import ScheduledRunResponse, ScheduledTaskResponse
from deerflow.domain.schedule.model import (
ContextMode,
RunStatus,
ScheduledRun,
ScheduledTask,
ScheduleSpec,
TaskStatus,
TriggerKind,
)
from deerflow.domain.schedule.model import ContextMode, RunStatus, ScheduledRun, ScheduledTask, ScheduleSpec, TaskStatus, TriggerKind
# Exactly the frontend's `ScheduledTask` type (frontend/src/core/scheduled-tasks/types.ts).
FRONTEND_TASK_FIELDS = {

View File

@ -30,15 +30,8 @@ from schedule_fakes import (
)
from app.gateway.routers.schedule import router as router_module
from deerflow.domain.schedule.model import (
LaunchFailedError,
RunStatus,
ScheduledRun,
SchedulePolicy,
TaskStatus,
ThreadBusyError,
TriggerKind,
)
from deerflow.domain.schedule.exceptions import LaunchFailedError, ThreadBusyError
from deerflow.domain.schedule.model import RunStatus, ScheduledRun, SchedulePolicy, TaskStatus, TriggerKind
from deerflow.domain.schedule.service import ScheduleService
USER = "user-1"
@ -528,7 +521,7 @@ class TestRunHistory:
class TestErrorMapping:
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)
@ -537,7 +530,7 @@ class TestErrorMapping:
"""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
whatever 4xx happened to be nearest."""
from deerflow.domain.schedule.model import ScheduleError
from deerflow.domain.schedule.exceptions import ScheduleError
class NewDomainError(ScheduleError):
pass
@ -551,7 +544,7 @@ class TestErrorMapping:
@pytest.mark.asyncio
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
async def handler():

View File

@ -26,7 +26,7 @@ import pytest
from fastapi import HTTPException
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.runtime import ConflictError

View File

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