feat: add Celery task infrastructure for module system
Phase 4 of module migration plan: - Add ScheduledTask dataclass for declaring Celery Beat tasks - Add tasks_path and scheduled_tasks fields to ModuleDefinition - Create ModuleTask base class with database session management - Create task discovery utilities (discover_module_tasks, build_beat_schedule) - Update celery_config.py to discover and register module tasks - Maintain backward compatibility with legacy task modules Modules can now define tasks in their tasks/ directory and scheduled tasks in their definition. The infrastructure supports gradual migration of existing tasks from app/tasks/ to their respective modules. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,8 @@ Self-Contained Module Structure:
|
||||
├── exceptions.py # Module-specific exceptions (optional)
|
||||
├── routes/ # FastAPI routers
|
||||
├── services/ # Business logic
|
||||
├── tasks/ # Celery background tasks (optional)
|
||||
│ └── __init__.py # Task module discovery marker
|
||||
├── models/ # SQLAlchemy models
|
||||
├── schemas/ # Pydantic schemas
|
||||
├── migrations/ # Alembic migrations for this module
|
||||
@@ -45,6 +47,41 @@ if TYPE_CHECKING:
|
||||
from models.database.admin_menu_config import FrontendType
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScheduledTask:
|
||||
"""
|
||||
Definition of a Celery Beat scheduled task.
|
||||
|
||||
Used in ModuleDefinition to declare scheduled tasks that should be
|
||||
registered with Celery Beat when the module is loaded.
|
||||
|
||||
Attributes:
|
||||
name: Unique name for the schedule entry (e.g., "billing.reset_counters")
|
||||
task: Full Python path to the task (e.g., "app.modules.billing.tasks.subscription.reset_period_counters")
|
||||
schedule: Cron expression string or crontab dict
|
||||
- String format: "minute hour day_of_month month day_of_week" (e.g., "5 0 * * *")
|
||||
- Dict format: {"minute": 5, "hour": 0} for crontab kwargs
|
||||
args: Positional arguments to pass to the task
|
||||
kwargs: Keyword arguments to pass to the task
|
||||
options: Celery task options (e.g., {"queue": "scheduled"})
|
||||
|
||||
Example:
|
||||
ScheduledTask(
|
||||
name="billing.reset_period_counters",
|
||||
task="app.modules.billing.tasks.subscription.reset_period_counters",
|
||||
schedule="5 0 * * *", # Daily at 00:05
|
||||
options={"queue": "scheduled"},
|
||||
)
|
||||
"""
|
||||
|
||||
name: str
|
||||
task: str
|
||||
schedule: str | dict[str, Any]
|
||||
args: tuple = ()
|
||||
kwargs: dict[str, Any] = field(default_factory=dict)
|
||||
options: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModuleDefinition:
|
||||
"""
|
||||
@@ -191,6 +228,12 @@ class ModuleDefinition:
|
||||
locales_path: str | None = None # Relative to module directory
|
||||
migrations_path: str | None = None # Relative to module directory, e.g., "migrations"
|
||||
|
||||
# =========================================================================
|
||||
# Celery Tasks (optional)
|
||||
# =========================================================================
|
||||
tasks_path: str | None = None # Python import path, e.g., "app.modules.billing.tasks"
|
||||
scheduled_tasks: list[ScheduledTask] = field(default_factory=list)
|
||||
|
||||
# =========================================================================
|
||||
# Menu Item Methods
|
||||
# =========================================================================
|
||||
@@ -355,7 +398,7 @@ class ModuleDefinition:
|
||||
Get the Python import path for a module component.
|
||||
|
||||
Args:
|
||||
component: One of "services", "models", "schemas", "exceptions"
|
||||
component: One of "services", "models", "schemas", "exceptions", "tasks"
|
||||
|
||||
Returns:
|
||||
Import path string, or None if not configured
|
||||
@@ -365,9 +408,51 @@ class ModuleDefinition:
|
||||
"models": self.models_path,
|
||||
"schemas": self.schemas_path,
|
||||
"exceptions": self.exceptions_path,
|
||||
"tasks": self.tasks_path,
|
||||
}
|
||||
return paths.get(component)
|
||||
|
||||
def get_tasks_module(self) -> str | None:
|
||||
"""
|
||||
Get the Python import path for this module's tasks.
|
||||
|
||||
Returns the explicitly configured tasks_path, or infers it from
|
||||
the module code if the module is self-contained.
|
||||
|
||||
Returns:
|
||||
Import path string (e.g., "app.modules.billing.tasks"), or None
|
||||
"""
|
||||
if self.tasks_path:
|
||||
return self.tasks_path
|
||||
if self.is_self_contained:
|
||||
dir_name = self.code.replace("-", "_")
|
||||
return f"app.modules.{dir_name}.tasks"
|
||||
return None
|
||||
|
||||
def get_tasks_dir(self) -> Path | None:
|
||||
"""
|
||||
Get the filesystem path to this module's tasks directory.
|
||||
|
||||
Returns:
|
||||
Path to tasks directory, or None if not configured
|
||||
"""
|
||||
tasks_module = self.get_tasks_module()
|
||||
if not tasks_module:
|
||||
return None
|
||||
return self.get_module_dir() / "tasks"
|
||||
|
||||
def has_tasks(self) -> bool:
|
||||
"""
|
||||
Check if this module has a tasks directory.
|
||||
|
||||
Returns:
|
||||
True if tasks directory exists and contains __init__.py
|
||||
"""
|
||||
tasks_dir = self.get_tasks_dir()
|
||||
if not tasks_dir:
|
||||
return False
|
||||
return tasks_dir.exists() and (tasks_dir / "__init__.py").exists()
|
||||
|
||||
def validate_structure(self) -> list[str]:
|
||||
"""
|
||||
Validate that self-contained module has expected directory structure.
|
||||
|
||||
Reference in New Issue
Block a user