fix(translate,automation): fix schedule import/param errors, add translation schedules to Automation view

- Fix ModuleNotFoundError: 4 lazy imports in _schedule_routes.py changed
  from 3-dot to 4-dot relative imports (src.api.dependencies -> src.dependencies)
- Fix TypeError: update_schedule() param name mismatch (timezone -> timezone_str)
- Add add_translation_job/remove_translation_job to SchedulerService
  to register translation schedules with APScheduler via execute_scheduled_translation
- Add GET /settings/automation/translation-schedules endpoint
  returning all translation schedules with joined job names
- Update Automation settings page to display translation schedules
  (cron, timezone, active badge, last run) below validation policies
This commit is contained in:
2026-05-13 21:08:10 +03:00
parent 1f8b8c9f47
commit d1695fe536
6 changed files with 172 additions and 8 deletions

View File

@@ -25,7 +25,9 @@ from ...schemas.settings import (
ValidationPolicyCreate,
ValidationPolicyUpdate,
ValidationPolicyResponse,
TranslationScheduleItem,
)
from ...models.translate import TranslationSchedule, TranslationJob
from ...core.database import get_db
from sqlalchemy.orm import Session
@@ -598,4 +600,40 @@ async def delete_validation_policy(
# #endregion delete_validation_policy
# #region get_translation_schedules [C:2] [TYPE Function]
# @BRIEF Lists all translation schedules joined with translation job names.
@router.get("/automation/translation-schedules", response_model=List[TranslationScheduleItem])
async def get_translation_schedules(
db: Session = Depends(get_db),
_=Depends(has_permission("admin:settings", "READ")),
):
with belief_scope("get_translation_schedules"):
results = (
db.query(
TranslationSchedule,
TranslationJob.name.label("job_name"),
)
.outerjoin(TranslationJob, TranslationSchedule.job_id == TranslationJob.id)
.all()
)
return [
TranslationScheduleItem(
schedule_id=ts.id,
job_id=ts.job_id,
job_name=job_name,
cron_expression=ts.cron_expression,
timezone=ts.timezone,
is_active=ts.is_active,
last_run_at=ts.last_run_at,
next_run_at=ts.next_run_at,
created_by=ts.created_by,
created_at=ts.created_at,
)
for ts, job_name in results
]
# #endregion get_translation_schedules
# #endregion SettingsRouter

View File

@@ -81,7 +81,7 @@ async def set_schedule(
schedule = scheduler.update_schedule(
job_id,
cron_expression=payload.cron_expression,
timezone=payload.timezone,
timezone_str=payload.timezone,
is_active=payload.is_active,
)
else:
@@ -92,7 +92,7 @@ async def set_schedule(
is_active=payload.is_active,
)
# Register with APScheduler via SchedulerService
from ...dependencies import get_scheduler_service
from ....dependencies import get_scheduler_service
sched_svc = get_scheduler_service()
sched_svc.add_translation_job(
schedule_id=schedule.id,
@@ -133,7 +133,7 @@ async def enable_schedule(
try:
scheduler = TranslationScheduler(db, config_manager, current_user.username)
schedule = scheduler.set_schedule_active(job_id, is_active=True)
from ...dependencies import get_scheduler_service
from ....dependencies import get_scheduler_service
sched_svc = get_scheduler_service()
sched_svc.add_translation_job(
schedule_id=schedule.id,
@@ -164,7 +164,7 @@ async def disable_schedule(
try:
scheduler = TranslationScheduler(db, config_manager, current_user.username)
scheduler.set_schedule_active(job_id, is_active=False)
from ...dependencies import get_scheduler_service
from ....dependencies import get_scheduler_service
sched_svc = get_scheduler_service()
sched_svc.remove_translation_job(schedule_id=job_id)
return {"status": "disabled", "job_id": job_id}
@@ -190,7 +190,7 @@ async def delete_schedule(
try:
scheduler = TranslationScheduler(db, config_manager, current_user.username)
scheduler.delete_schedule(job_id)
from ...dependencies import get_scheduler_service
from ....dependencies import get_scheduler_service
sched_svc = get_scheduler_service()
sched_svc.remove_translation_job(schedule_id=job_id)
return None

View File

@@ -6,6 +6,7 @@ from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from .logger import logger, belief_scope
from .config_manager import ConfigManager
from .database import SessionLocal
import asyncio
from datetime import datetime, time, timedelta, date
# #region SchedulerService [C:3] [TYPE Class] [SEMANTICS scheduler, service, apscheduler]
@@ -83,6 +84,57 @@ class SchedulerService:
except Exception as e:
logger.error(f"Failed to add backup job for environment {env_id}: {e}")
# #endregion add_backup_job
# #region add_translation_job [C:4] [TYPE Function] [SEMANTICS scheduler,translation,cron,apscheduler]
# @BRIEF Register a translation schedule with APScheduler.
# @PRE schedule_id, job_id, and cron_expression are valid strings.
# @POST A new APScheduler job is registered or replaced if it already exists.
# @SIDE_EFFECT Mutates APScheduler state; calls execute_scheduled_translation on trigger.
# @RELATION DEPENDS_ON -> [execute_scheduled_translation]
# @RELATION DEPENDS_ON -> [SessionLocal]
def add_translation_job(self, schedule_id: str, job_id: str, cron_expression: str, timezone: str = "UTC"):
with belief_scope(
"SchedulerService.add_translation_job",
f"schedule_id={schedule_id}, job_id={job_id}, cron={cron_expression}",
):
from zoneinfo import ZoneInfo
from ..plugins.translate.scheduler import execute_scheduled_translation
job_id_aps = f"translate_{schedule_id}"
try:
tz = ZoneInfo(timezone)
self.scheduler.add_job(
execute_scheduled_translation,
CronTrigger.from_crontab(cron_expression, timezone=tz),
id=job_id_aps,
args=[schedule_id, job_id, SessionLocal, self.config_manager],
replace_existing=True,
)
logger.reason(
f"Translation schedule registered: {job_id_aps} ({cron_expression})"
)
except Exception as e:
logger.explore(
f"Failed to register translation schedule: {job_id_aps}",
extra={"error": str(e)},
)
# #endregion add_translation_job
# #region remove_translation_job [C:4] [TYPE Function] [SEMANTICS scheduler,translation,remove,apscheduler]
# @BRIEF Remove a translation schedule from APScheduler.
# @PRE schedule_id is a valid string.
# @POST The APScheduler job is removed if it exists; silently ignored otherwise.
# @SIDE_EFFECT Mutates APScheduler state.
def remove_translation_job(self, schedule_id: str):
with belief_scope(
"SchedulerService.remove_translation_job",
f"schedule_id={schedule_id}",
):
job_id_aps = f"translate_{schedule_id}"
try:
self.scheduler.remove_job(job_id_aps)
logger.info(f"Translation schedule removed: {job_id_aps}")
except Exception:
logger.reason(f"Translation schedule not found (already removed): {job_id_aps}")
# #endregion remove_translation_job
# #region _trigger_backup [TYPE Function]
# @PURPOSE: Triggered by the scheduler to start a backup task.
# @PRE: env_id must be a valid environment ID.

View File

@@ -92,4 +92,25 @@ class ValidationPolicyResponse(ValidationPolicyBase):
# #endregion ValidationPolicyResponse
# #region TranslationScheduleItem [C:1] [TYPE Class]
# @BRIEF Response schema for a translation schedule item with joined job name.
class TranslationScheduleItem(BaseModel):
schedule_id: str
job_id: str
job_name: Optional[str] = None
cron_expression: str
timezone: str
is_active: bool
last_run_at: Optional[datetime] = None
next_run_at: Optional[datetime] = None
created_by: Optional[str] = None
created_at: Optional[datetime] = None
class Config:
from_attributes = True
# #endregion TranslationScheduleItem
# #endregion SettingsSchemas