ArchiveBox/archivebox/crawls/schedule_util.py
Nick Sweeting 6ce2555dfd
fix: rename utils.py → util.py across modules, fix add --index-only, misc cleanups
Renames (no functional change, just consistency with the rest of the codebase):
- cli/cli_utils.py → cli/cli_util.py
- core/host_utils.py → core/host_util.py
- core/tag_utils.py → core/tag_util.py
- crawls/schedule_utils.py → crawls/schedule_util.py
- machine/env_utils.py → machine/env_util.py

Functional fixes:
- archivebox add --index-only now materializes Snapshot rows synchronously
  via crawl.create_snapshots_from_urls() instead of just queueing the Crawl
  and leaving the index empty. The previous behavior broke every test that
  expected --index-only to populate the index, since the runner is never
  started in index-only mode.
- config/collection.py: add _coerce_from_str_dict as the inverse of
  _coerce_to_str_dict so JSON-encoded INI values are decoded back to native
  dict/list types when mirrored into Machine.config (a JSONField). Without
  this, downstream consumers like MachineEvent / abx-dl get raw JSON
  strings where they expect dicts.

Plus matching admin / middleware / model touch-ups, the registration
password_change_form template, and assorted small cleanups the user
worked through while validating the deploy path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 14:30:33 -07:00

44 lines
1.1 KiB
Python

from __future__ import annotations
from datetime import datetime
from croniter import croniter
SCHEDULE_ALIASES: dict[str, str] = {
"minute": "* * * * *",
"minutely": "* * * * *",
"hour": "0 * * * *",
"hourly": "0 * * * *",
"day": "0 0 * * *",
"daily": "0 0 * * *",
"week": "0 0 * * 0",
"weekly": "0 0 * * 0",
"month": "0 0 1 * *",
"monthly": "0 0 1 * *",
"year": "0 0 1 1 *",
"yearly": "0 0 1 1 *",
}
def normalize_schedule(schedule: str) -> str:
normalized = (schedule or "").strip()
if not normalized:
raise ValueError("Schedule cannot be empty.")
return SCHEDULE_ALIASES.get(normalized.lower(), normalized)
def validate_schedule(schedule: str) -> str:
normalized = normalize_schedule(schedule)
if not croniter.is_valid(normalized):
raise ValueError(
"Invalid schedule. Use an alias like daily/weekly/monthly or a cron expression such as '0 */6 * * *'.",
)
return normalized
def next_run_for_schedule(schedule: str, after: datetime) -> datetime:
normalized = validate_schedule(schedule)
return croniter(normalized, after).get_next(datetime)