Add schedule examples and cron testing utility
- Add SCHEDULE_EXAMPLES.md with ready-to-use schedule templates - Add test_cron.py utility for testing cron expressions - Include examples for daily, weekly, interval-based schedules - Add timezone conversion examples - Add testing recommendations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
181
SCHEDULE_EXAMPLES.md
Normal file
181
SCHEDULE_EXAMPLES.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# Примеры расписаний для быстрого старта
|
||||||
|
|
||||||
|
Скопируйте эти примеры и отправьте их через `/manage` → `⏰ Расписания` → `➕ Добавить расписание`
|
||||||
|
|
||||||
|
## Ежедневные публикации
|
||||||
|
|
||||||
|
### Утренняя публикация (9:00 UTC)
|
||||||
|
```
|
||||||
|
Утренний вопрос | 0 9 * * * | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Обеденная публикация (12:00 UTC)
|
||||||
|
```
|
||||||
|
Обеденный вопрос | 0 12 * * * | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вечерняя публикация (18:00 UTC)
|
||||||
|
```
|
||||||
|
Вечерний вопрос | 0 18 * * * | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Два раза в день (9:00 и 21:00 UTC)
|
||||||
|
```
|
||||||
|
Утро и вечер | 0 9,21 * * * | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Три раза в день (утро, обед, вечер)
|
||||||
|
```
|
||||||
|
Утро обед вечер | 0 8,13,19 * * * | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Рабочая неделя (Пн-Пт)
|
||||||
|
|
||||||
|
### Будни утром (9:00 UTC)
|
||||||
|
```
|
||||||
|
Рабочие дни утром | 0 9 * * 1-5 | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Будни в обед (12:30 UTC)
|
||||||
|
```
|
||||||
|
Рабочий обед | 30 12 * * 1-5 | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Будни вечером (17:00 UTC)
|
||||||
|
```
|
||||||
|
После работы | 0 17 * * 1-5 | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Выходные дни
|
||||||
|
|
||||||
|
### Выходные в полдень (12:00 UTC)
|
||||||
|
```
|
||||||
|
Выходные | 0 12 * * 6,0 | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Только суббота утром (10:00 UTC)
|
||||||
|
```
|
||||||
|
Суббота | 0 10 * * 6 | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Только воскресенье вечером (19:00 UTC)
|
||||||
|
```
|
||||||
|
Воскресенье вечер | 0 19 * * 0 | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Регулярные интервалы
|
||||||
|
|
||||||
|
### Каждые 2 часа
|
||||||
|
```
|
||||||
|
Каждые 2 часа | 0 */2 * * * | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Каждые 3 часа
|
||||||
|
```
|
||||||
|
Каждые 3 часа | 0 */3 * * * | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Каждые 4 часа
|
||||||
|
```
|
||||||
|
Каждые 4 часа | 0 */4 * * * | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Каждые 6 часов (4 раза в день)
|
||||||
|
```
|
||||||
|
4 раза в день | 0 */6 * * * | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Каждые 30 минут
|
||||||
|
```
|
||||||
|
Каждые полчаса | */30 * * * * | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Каждые 15 минут (для активных каналов)
|
||||||
|
```
|
||||||
|
Каждые 15 минут | */15 * * * * | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Специальные расписания
|
||||||
|
|
||||||
|
### Рабочие часы (9:00-17:00 каждый час по будням)
|
||||||
|
```
|
||||||
|
Рабочие часы | 0 9-17 * * 1-5 | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Первый день каждого месяца (10:00 UTC)
|
||||||
|
```
|
||||||
|
Первое число месяца | 0 10 1 * * | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Каждый понедельник утром (8:00 UTC)
|
||||||
|
```
|
||||||
|
Понедельник утром | 0 8 * * 1 | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Каждую пятницу вечером (20:00 UTC)
|
||||||
|
```
|
||||||
|
Пятница вечером | 0 20 * * 5 | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Адаптация к местному времени
|
||||||
|
|
||||||
|
Если ваш часовой пояс отличается от UTC, вычтите разницу из желаемого времени:
|
||||||
|
|
||||||
|
### Москва (UTC+3)
|
||||||
|
Для публикации в 12:00 по Москве используйте 9:00 UTC:
|
||||||
|
```
|
||||||
|
Обед по Москве | 0 9 * * * | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Киев/Минск (UTC+2)
|
||||||
|
Для публикации в 14:00 используйте 12:00 UTC:
|
||||||
|
```
|
||||||
|
14:00 Киев | 0 12 * * * | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Лондон (UTC+0/+1 летом)
|
||||||
|
Для публикации в 10:00 используйте 10:00 UTC (зима) или 9:00 UTC (лето):
|
||||||
|
```
|
||||||
|
Утро Лондон | 0 10 * * * | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Тестовые расписания
|
||||||
|
|
||||||
|
### Следующая минута (для теста)
|
||||||
|
Замените ХХ на текущий час+1 минута:
|
||||||
|
```
|
||||||
|
Тест | 18 10 * * * | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Каждую минуту (только для краткосрочных тестов!)
|
||||||
|
```
|
||||||
|
Тест каждую минуту | * * * * * | 1
|
||||||
|
```
|
||||||
|
⚠️ **Не забудьте удалить или деактивировать после теста!**
|
||||||
|
|
||||||
|
## Деактивированные расписания
|
||||||
|
|
||||||
|
Чтобы временно отключить расписание, установите последний параметр в `0`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Отключенное утро | 0 9 * * * | 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Позже можно активировать, изменив `0` на `1`.
|
||||||
|
|
||||||
|
## Полезные советы
|
||||||
|
|
||||||
|
1. **UTC vs местное время**: Бот работает в UTC. Используйте конвертер времени для расчета.
|
||||||
|
2. **Тестирование**: Создайте расписание на ближайшую минуту для проверки.
|
||||||
|
3. **Имена**: Используйте понятные имена для расписаний.
|
||||||
|
4. **Активность**: Используйте `0` вместо удаления для временного отключения.
|
||||||
|
5. **Проверка**: После создания проверьте список расписаний в меню.
|
||||||
|
|
||||||
|
## Инструменты
|
||||||
|
|
||||||
|
Проверить cron выражение перед добавлением:
|
||||||
|
```bash
|
||||||
|
python test_cron.py "0 9 * * 1-5" 168
|
||||||
|
```
|
||||||
|
|
||||||
|
Это покажет, когда сработает ваше расписание в ближайшие 168 часов (7 дней).
|
||||||
183
test_cron.py
Normal file
183
test_cron.py
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Утилита для тестирования cron выражений.
|
||||||
|
|
||||||
|
Использование:
|
||||||
|
python test_cron.py "0 9 * * 1-5"
|
||||||
|
python test_cron.py "*/15 * * * *"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
# Прямой импорт только нужных классов, избегая полный импорт bot
|
||||||
|
sys.path.insert(0, ".")
|
||||||
|
|
||||||
|
# Копируем необходимые классы прямо сюда для независимости
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class _CronField:
|
||||||
|
allowed: set[int]
|
||||||
|
|
||||||
|
def matches(self, value: int) -> bool:
|
||||||
|
return value in self.allowed
|
||||||
|
|
||||||
|
|
||||||
|
class CronExpression:
|
||||||
|
def __init__(self, expression: str) -> None:
|
||||||
|
parts = expression.strip().split()
|
||||||
|
if len(parts) != 5:
|
||||||
|
raise ValueError("Cron must have 5 fields: minute hour day month weekday.")
|
||||||
|
|
||||||
|
self.expression = expression.strip()
|
||||||
|
self._minute = self._parse_field(parts[0], 0, 59)
|
||||||
|
self._hour = self._parse_field(parts[1], 0, 23)
|
||||||
|
self._day = self._parse_field(parts[2], 1, 31)
|
||||||
|
self._month = self._parse_field(parts[3], 1, 12)
|
||||||
|
self._weekday = self._parse_field(parts[4], 0, 7)
|
||||||
|
|
||||||
|
def matches(self, dt: datetime) -> bool:
|
||||||
|
weekday = (dt.weekday() + 1) % 7
|
||||||
|
return (
|
||||||
|
self._minute.matches(dt.minute)
|
||||||
|
and self._hour.matches(dt.hour)
|
||||||
|
and self._day.matches(dt.day)
|
||||||
|
and self._month.matches(dt.month)
|
||||||
|
and (weekday in self._weekday.allowed or (7 in self._weekday.allowed and weekday == 0))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_field(self, token: str, minimum: int, maximum: int) -> _CronField:
|
||||||
|
allowed: set[int] = set()
|
||||||
|
for part in token.split(","):
|
||||||
|
part = part.strip()
|
||||||
|
if not part:
|
||||||
|
raise ValueError("Invalid empty cron token.")
|
||||||
|
|
||||||
|
step = 1
|
||||||
|
base = part
|
||||||
|
if "/" in part:
|
||||||
|
base, step_part = part.split("/", 1)
|
||||||
|
if not step_part.isdigit() or int(step_part) < 1:
|
||||||
|
raise ValueError(f"Invalid step value in cron token: {part}")
|
||||||
|
step = int(step_part)
|
||||||
|
|
||||||
|
if base == "*":
|
||||||
|
start, end = minimum, maximum
|
||||||
|
elif "-" in base:
|
||||||
|
start_str, end_str = base.split("-", 1)
|
||||||
|
start = int(start_str)
|
||||||
|
end = int(end_str)
|
||||||
|
else:
|
||||||
|
start = int(base)
|
||||||
|
end = int(base)
|
||||||
|
|
||||||
|
if start < minimum or end > maximum or start > end:
|
||||||
|
raise ValueError(f"Cron value out of range: {part}")
|
||||||
|
|
||||||
|
for value in range(start, end + 1, step):
|
||||||
|
allowed.add(value)
|
||||||
|
|
||||||
|
if not allowed:
|
||||||
|
raise ValueError("Cron field has no values.")
|
||||||
|
return _CronField(allowed=allowed)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cron(expression: str, hours: int = 24):
|
||||||
|
"""
|
||||||
|
Тестирует cron выражение на следующие N часов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expression: Cron выражение для теста
|
||||||
|
hours: Количество часов для проверки (по умолчанию 24)
|
||||||
|
"""
|
||||||
|
print(f"Тестирование cron выражения: '{expression}'")
|
||||||
|
print(f"Проверка следующих {hours} часов...")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cron = CronExpression(expression)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"❌ Ошибка в cron выражении: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc).replace(second=0, microsecond=0)
|
||||||
|
matches = []
|
||||||
|
|
||||||
|
# Проверяем каждую минуту на протяжении указанного времени
|
||||||
|
total_minutes = hours * 60
|
||||||
|
for i in range(total_minutes):
|
||||||
|
check_time = now + timedelta(minutes=i)
|
||||||
|
if cron.matches(check_time):
|
||||||
|
matches.append(check_time)
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
print(f"⚠️ Нет совпадений в ближайшие {hours} часов")
|
||||||
|
else:
|
||||||
|
print(f"✅ Найдено {len(matches)} совпадений:\n")
|
||||||
|
for i, match_time in enumerate(matches[:20], 1): # Показываем первые 20
|
||||||
|
# Форматируем в читаемый вид
|
||||||
|
day_name = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"][match_time.weekday()]
|
||||||
|
formatted = match_time.strftime("%Y-%m-%d %H:%M UTC")
|
||||||
|
print(f" {i:2d}. {formatted} ({day_name})")
|
||||||
|
|
||||||
|
if len(matches) > 20:
|
||||||
|
print(f"\n ... и ещё {len(matches) - 20} совпадений")
|
||||||
|
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
# Статистика
|
||||||
|
if matches:
|
||||||
|
first = matches[0]
|
||||||
|
time_until = (first - now).total_seconds() / 60
|
||||||
|
print(f"\nСледующее срабатывание через {int(time_until)} минут:")
|
||||||
|
day_name = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"][first.weekday()]
|
||||||
|
print(f" {first.strftime('%Y-%m-%d %H:%M UTC')} ({day_name})")
|
||||||
|
|
||||||
|
if len(matches) > 1:
|
||||||
|
# Вычисляем среднюю частоту
|
||||||
|
total_time = (matches[-1] - matches[0]).total_seconds() / 60
|
||||||
|
avg_interval = total_time / (len(matches) - 1) if len(matches) > 1 else 0
|
||||||
|
print(f"\nСредний интервал: {avg_interval:.1f} минут")
|
||||||
|
|
||||||
|
|
||||||
|
def show_examples():
|
||||||
|
"""Показывает примеры использования."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("Примеры cron выражений:")
|
||||||
|
print("=" * 60)
|
||||||
|
examples = [
|
||||||
|
("0 9 * * *", "Каждый день в 9:00 UTC"),
|
||||||
|
("0 9 * * 1-5", "Каждый будний день в 9:00 UTC"),
|
||||||
|
("*/15 * * * *", "Каждые 15 минут"),
|
||||||
|
("0 */4 * * *", "Каждые 4 часа"),
|
||||||
|
("0 8,12,18 * * *", "В 8:00, 12:00 и 18:00"),
|
||||||
|
("0 12 * * 6,0", "Суббота и воскресенье в 12:00"),
|
||||||
|
("30 14 1 * *", "1-го числа каждого месяца в 14:30"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for cron, description in examples:
|
||||||
|
print(f"\n{cron:20s} - {description}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Использование: python test_cron.py \"<cron выражение>\" [часов]")
|
||||||
|
print("Пример: python test_cron.py \"0 9 * * 1-5\" 48")
|
||||||
|
print()
|
||||||
|
show_examples()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
expression = sys.argv[1]
|
||||||
|
hours = int(sys.argv[2]) if len(sys.argv) > 2 else 24
|
||||||
|
|
||||||
|
print()
|
||||||
|
test_cron(expression, hours)
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user