{"record":{"id":"75b69c234158b668","repo":"HKUDS/DeepTutor","slug":"at-schedules-need-a-time","errorCode":null,"errorMessage":"'at' schedules need a time","messagePattern":"'at' schedules need a time","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"deeptutor/services/cron/service.py","lineNumber":155,"sourceCode":"            base = datetime.fromtimestamp(now_ms / 1000, tz=tz)\n            next_dt = croniter(schedule.expr, base).get_next(datetime)\n            return int(next_dt.timestamp() * 1000)\n        except ImportError:\n            raise ValueError(\n                \"cron expressions need the 'croniter' package — \"\n                \"use an 'every' or 'at' schedule instead\"\n            ) from None\n        except Exception as exc:\n            raise ValueError(f\"invalid cron expression {schedule.expr!r}: {exc}\") from None\n\n    return None\n\n\ndef validate_schedule(schedule: CronSchedule) -> None:\n    \"\"\"Reject schedules that could never run (raises ValueError).\"\"\"\n    if schedule.kind == \"at\":\n        if not schedule.at_ms:\n            raise ValueError(\"'at' schedules need a time\")\n        if schedule.at_ms <= _now_ms():\n            raise ValueError(\"'at' time is in the past\")\n        return\n    if schedule.kind == \"every\":\n        if not schedule.every_seconds or schedule.every_seconds < 30:\n            raise ValueError(\"'every' interval must be at least 30 seconds\")\n        return\n    if schedule.kind == \"cron\":\n        if schedule.tz:\n            try:\n                from zoneinfo import ZoneInfo\n\n                ZoneInfo(schedule.tz)\n            except Exception:\n                raise ValueError(f\"unknown timezone {schedule.tz!r}\") from None\n        # Raises ValueError on bad/unsupported expressions.\n        if compute_next_run(schedule, _now_ms()) is None:\n            raise ValueError(f\"cron expression {schedule.expr!r} never fires\")","sourceCodeStart":137,"sourceCodeEnd":173,"githubUrl":"https://github.com/HKUDS/DeepTutor/blob/3e82f130422a813cdd73c10b21a44e9325f5821a/deeptutor/services/cron/service.py#L137-L173","documentation":"validate_schedule rejects 'at' (one-shot) schedules with no timestamp: at_ms is None, 0, or falsy. An 'at' job fires exactly once at a specific epoch-milliseconds time, so a missing time makes the schedule meaningless and fails at add_job time.","triggerScenarios":"CronSchedule(kind=\"at\") with at_ms omitted, None, or 0 passed to add_job/validate_schedule.","commonSituations":"Deserializing stored jobs where at_ms was dropped; schedule builders where the time field was optional and left blank; default-initialized dataclass without setting at_ms.","solutions":["Set at_ms to a future epoch-milliseconds timestamp (int(dt.timestamp() * 1000))","Require the time field in the UI/API before submitting","Skip/reject persisted records missing at_ms during migration instead of passing them through"],"exampleFix":"# before\nsched = CronSchedule(kind=\"at\")  # at_ms=None -> ValueError\n# after\nfrom datetime import datetime, timedelta\nsched = CronSchedule(\n    kind=\"at\",\n    at_ms=int((datetime.now() + timedelta(hours=1)).timestamp() * 1000),\n)","handlingStrategy":"validation","validationCode":"def valid_at_schedule(s: CronSchedule) -> bool:\n    return s.kind == \"at\" and bool(s.at_ms)","typeGuard":"def is_complete_at_schedule(s: CronSchedule) -> bool:\n    return s.kind == \"at\" and isinstance(s.at_ms, int) and s.at_ms > 0","tryCatchPattern":"try:\n    svc.add_job(schedule, ...)\nexcept ValueError as e:\n    if \"need a time\" in str(e):\n        return bad_request(\"at_ms is required for 'at' schedules\")\n    raise","preventionTips":["Make at_ms a required constructor arg in your own schedule builders","Reject persisted records missing at_ms during load"],"tags":["cron","validation","one-shot-schedule"],"backgroundTag":"required-field-missing","analyzedSha":"3e82f130422a813cdd73c10b21a44e9325f5821a","analyzedAt":"2026-08-27T06:57:25.364Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}