From f88c8e61bc8475e73a28f121ca3c019d77b8a3f9 Mon Sep 17 00:00:00 2001 From: rayhpeng Date: Wed, 29 Jul 2026 19:33:12 +0800 Subject: [PATCH] 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. --- backend/app/adapters/schedule/run_launcher.py | 2 +- .../schedule/scheduled_run_repository.py | 10 ++----- .../schedule/scheduled_task_repository.py | 11 ++------ .../app/gateway/routers/schedule/router.py | 4 +-- .../deerflow/domain/schedule/__init__.py | 20 +++++++------- .../{model/errors.py => exceptions.py} | 8 ++++++ .../domain/schedule/model/__init__.py | 27 ++++--------------- .../deerflow/domain/schedule/model/spec.py | 2 +- .../deerflow/domain/schedule/model/task.py | 2 +- .../deerflow/domain/schedule/service.py | 12 +++++---- backend/tests/schedule_fakes.py | 2 +- backend/tests/test_schedule_domain.py | 4 +-- backend/tests/test_schedule_fakes.py | 12 ++------- .../tests/test_schedule_response_models.py | 10 +------ backend/tests/test_schedule_router.py | 17 ++++-------- backend/tests/test_schedule_run_launcher.py | 2 +- backend/tests/test_schedule_service.py | 14 +++++----- 17 files changed, 59 insertions(+), 100 deletions(-) rename backend/packages/harness/deerflow/domain/schedule/{model/errors.py => exceptions.py} (81%) diff --git a/backend/app/adapters/schedule/run_launcher.py b/backend/app/adapters/schedule/run_launcher.py index 995ffaf64..d3c96b6af 100644 --- a/backend/app/adapters/schedule/run_launcher.py +++ b/backend/app/adapters/schedule/run_launcher.py @@ -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 diff --git a/backend/app/adapters/schedule/scheduled_run_repository.py b/backend/app/adapters/schedule/scheduled_run_repository.py index 834870c99..7c55aa427 100644 --- a/backend/app/adapters/schedule/scheduled_run_repository.py +++ b/backend/app/adapters/schedule/scheduled_run_repository.py @@ -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 diff --git a/backend/app/adapters/schedule/scheduled_task_repository.py b/backend/app/adapters/schedule/scheduled_task_repository.py index a7ff6a1fd..750cb4107 100644 --- a/backend/app/adapters/schedule/scheduled_task_repository.py +++ b/backend/app/adapters/schedule/scheduled_task_repository.py @@ -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 diff --git a/backend/app/gateway/routers/schedule/router.py b/backend/app/gateway/routers/schedule/router.py index 6476712df..9b0a05bfb 100644 --- a/backend/app/gateway/routers/schedule/router.py +++ b/backend/app/gateway/routers/schedule/router.py @@ -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"]) diff --git a/backend/packages/harness/deerflow/domain/schedule/__init__.py b/backend/packages/harness/deerflow/domain/schedule/__init__.py index 60e7bd1d9..fbeb8d7fc 100644 --- a/backend/packages/harness/deerflow/domain/schedule/__init__.py +++ b/backend/packages/harness/deerflow/domain/schedule/__init__.py @@ -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, ) diff --git a/backend/packages/harness/deerflow/domain/schedule/model/errors.py b/backend/packages/harness/deerflow/domain/schedule/exceptions.py similarity index 81% rename from backend/packages/harness/deerflow/domain/schedule/model/errors.py rename to backend/packages/harness/deerflow/domain/schedule/exceptions.py index d222e5bae..d34d4a815 100644 --- a/backend/packages/harness/deerflow/domain/schedule/model/errors.py +++ b/backend/packages/harness/deerflow/domain/schedule/exceptions.py @@ -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 diff --git a/backend/packages/harness/deerflow/domain/schedule/model/__init__.py b/backend/packages/harness/deerflow/domain/schedule/model/__init__.py index e983d912a..0b240dea7 100644 --- a/backend/packages/harness/deerflow/domain/schedule/model/__init__.py +++ b/backend/packages/harness/deerflow/domain/schedule/model/__init__.py @@ -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", ] diff --git a/backend/packages/harness/deerflow/domain/schedule/model/spec.py b/backend/packages/harness/deerflow/domain/schedule/model/spec.py index e3189abed..4fe54ff93 100644 --- a/backend/packages/harness/deerflow/domain/schedule/model/spec.py +++ b/backend/packages/harness/deerflow/domain/schedule/model/spec.py @@ -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 diff --git a/backend/packages/harness/deerflow/domain/schedule/model/task.py b/backend/packages/harness/deerflow/domain/schedule/model/task.py index 0f72bc919..678be725d 100644 --- a/backend/packages/harness/deerflow/domain/schedule/model/task.py +++ b/backend/packages/harness/deerflow/domain/schedule/model/task.py @@ -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}) diff --git a/backend/packages/harness/deerflow/domain/schedule/service.py b/backend/packages/harness/deerflow/domain/schedule/service.py index 9a7d78d4d..846dc6070 100644 --- a/backend/packages/harness/deerflow/domain/schedule/service.py +++ b/backend/packages/harness/deerflow/domain/schedule/service.py @@ -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 ( diff --git a/backend/tests/schedule_fakes.py b/backend/tests/schedule_fakes.py index 2e0b84744..3fe575b1a 100644 --- a/backend/tests/schedule_fakes.py +++ b/backend/tests/schedule_fakes.py @@ -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, diff --git a/backend/tests/test_schedule_domain.py b/backend/tests/test_schedule_domain.py index a20def22f..b8ec54f7d 100644 --- a/backend/tests/test_schedule_domain.py +++ b/backend/tests/test_schedule_domain.py @@ -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, ) diff --git a/backend/tests/test_schedule_fakes.py b/backend/tests/test_schedule_fakes.py index ec1b248e9..46d6a1387 100644 --- a/backend/tests/test_schedule_fakes.py +++ b/backend/tests/test_schedule_fakes.py @@ -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 diff --git a/backend/tests/test_schedule_response_models.py b/backend/tests/test_schedule_response_models.py index a900a5f33..1866c9f5d 100644 --- a/backend/tests/test_schedule_response_models.py +++ b/backend/tests/test_schedule_response_models.py @@ -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 = { diff --git a/backend/tests/test_schedule_router.py b/backend/tests/test_schedule_router.py index 7120a7c94..9449b9daa 100644 --- a/backend/tests/test_schedule_router.py +++ b/backend/tests/test_schedule_router.py @@ -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(): diff --git a/backend/tests/test_schedule_run_launcher.py b/backend/tests/test_schedule_run_launcher.py index 16cac930a..90768427e 100644 --- a/backend/tests/test_schedule_run_launcher.py +++ b/backend/tests/test_schedule_run_launcher.py @@ -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 diff --git a/backend/tests/test_schedule_service.py b/backend/tests/test_schedule_service.py index 7e6b8c098..60241d15b 100644 --- a/backend/tests/test_schedule_service.py +++ b/backend/tests/test_schedule_service.py @@ -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