taskman

Управление задачами: CRUD-операции с SQLite, форматирование и парсинг дат.

Предоставляет класс TaskManager для работы с задачами пользователей, включая добавление, получение, выполнение, удаление и перенос задач. Хранение осуществляется в SQLite с автоматической миграцией из JSON.

  1"""
  2Управление задачами: CRUD-операции с SQLite, форматирование и парсинг дат.
  3
  4Предоставляет класс TaskManager для работы с задачами пользователей,
  5включая добавление, получение, выполнение, удаление и перенос задач.
  6Хранение осуществляется в SQLite с автоматической миграцией из JSON.
  7"""
  8
  9import json
 10import os
 11import sqlite3
 12
 13from typing import Dict, List, Optional
 14from datetime import datetime, timedelta
 15from langfuse import observe
 16from loguru import logger
 17
 18DATA_DIR = os.getenv("DATA_DIR", "./data")
 19DB_FILE = os.path.join(DATA_DIR, "tasks.db")
 20TASKS_FILE = os.path.join(DATA_DIR, "tasks.json")
 21
 22
 23def _convert_task(row: sqlite3.Row) -> Dict:
 24    """
 25    Преобразует строку из SQLite (sqlite3.Row) в словарь с корректным булевым типом.
 26
 27    Args:
 28        row: Строка результата запроса SQLite
 29
 30    Returns:
 31        Dict: Словарь с данными задачи, где поле completed — bool
 32    """
 33    task = dict(row)
 34    task["completed"] = bool(task["completed"])
 35    return task
 36
 37
 38class TaskManager:
 39    """
 40    Менеджер задач с хранением в SQLite.
 41
 42    Предоставляет полный набор CRUD-операций для управления задачами
 43    пользователей. Поддерживает автоматическую миграцию из JSON-формата,
 44    парсинг дат на русском языке и форматирование списков задач
 45    с учётом ограничения на длину ответа Яндекс Алисы (1024 символа).
 46    """
 47
 48    def __init__(self, db_path: str = DB_FILE):
 49        """
 50        Инициализирует TaskManager, создавая/подключая БД SQLite.
 51
 52        При первом запуске создаёт необходимые таблицы и индексы.
 53        Выполняет миграцию из tasks.json если такой файл существует.
 54
 55        Args:
 56            db_path: Путь к файлу базы данных SQLite
 57        """
 58        self.db_path = db_path
 59        os.makedirs(DATA_DIR, exist_ok=True)
 60        self._init_db()
 61        self._migrate_from_json_if_needed()
 62
 63    def _get_conn(self) -> sqlite3.Connection:
 64        """
 65        Создаёт и возвращает подключение к SQLite.
 66
 67        Настраивает row_factory для доступа по имени колонки,
 68        включает WAL-режим журнала и таймаут ожидания блокировки.
 69
 70        Returns:
 71            sqlite3.Connection: Подключение к БД
 72        """
 73        conn = sqlite3.connect(self.db_path)
 74        conn.row_factory = sqlite3.Row
 75        conn.execute("PRAGMA journal_mode=WAL")
 76        conn.execute("PRAGMA busy_timeout=5000")
 77        return conn
 78
 79    def _init_db(self):
 80        """
 81        Инициализирует структуру базы данных.
 82
 83        Создаёт таблицу tasks (если не существует) с колонками:
 84        id, user_id, text, date, completed, created_at, completed_at.
 85        Также создаёт индексы для ускорения запросов.
 86        """
 87        with self._get_conn() as conn:
 88            conn.executescript("""
 89                CREATE TABLE IF NOT EXISTS tasks (
 90                    id INTEGER PRIMARY KEY AUTOINCREMENT,
 91                    user_id TEXT NOT NULL,
 92                    text TEXT NOT NULL,
 93                    date TEXT NOT NULL,
 94                    completed INTEGER NOT NULL DEFAULT 0,
 95                    created_at TEXT NOT NULL,
 96                    completed_at TEXT
 97                );
 98                CREATE INDEX IF NOT EXISTS idx_tasks_user_date ON tasks(user_id, date);
 99                CREATE INDEX IF NOT EXISTS idx_tasks_user_comp ON tasks(user_id, completed, date);
100            """)
101
102    def _migrate_from_json_if_needed(self):
103        """
104        Выполняет миграцию данных из tasks.json в SQLite.
105
106        Если файл tasks.json существует, данные переносятся в таблицу tasks,
107        после чего JSON-файл переименовывается в tasks.json.bak.
108        """
109        if not os.path.exists(TASKS_FILE):
110            return
111
112        logger.info("Миграция данных из tasks.json в SQLite...")
113        try:
114            with open(TASKS_FILE, "r", encoding="utf-8") as f:
115                data = json.load(f)
116
117            users = data.get("users", {})
118            with self._get_conn() as conn:
119                for user_id, user_data in users.items():
120                    for task in user_data.get("tasks", []):
121                        conn.execute(
122                            """INSERT INTO tasks (id, user_id, text, date, completed, created_at, completed_at)
123                               VALUES (?, ?, ?, ?, ?, ?, ?)""",
124                            (
125                                task["id"],
126                                user_id,
127                                task["text"],
128                                task["date"],
129                                1 if task.get("completed") else 0,
130                                task.get("created_at", ""),
131                                task.get("completed_at"),
132                            ),
133                        )
134            os.rename(TASKS_FILE, TASKS_FILE + ".bak")
135            logger.success(f"Миграция завершена. tasks.json → tasks.db, бэкап: {TASKS_FILE}.bak")
136        except Exception as e:
137            logger.error(f"Ошибка миграции из JSON: {e}")
138
139    @observe(name="Get user tasks")
140    def get_user_tasks(self, user_id: str) -> List[Dict]:
141        """
142        Возвращает все задачи указанного пользователя.
143
144        Args:
145            user_id: Идентификатор пользователя
146
147        Returns:
148            List[Dict]: Список задач пользователя, отсортированных по ID
149        """
150        with self._get_conn() as conn:
151            rows = conn.execute(
152                "SELECT * FROM tasks WHERE user_id = ? ORDER BY id", (user_id,)
153            ).fetchall()
154            return [_convert_task(row) for row in rows]
155
156    @observe(name="Get next task ID")
157    def get_next_task_id(self) -> int:
158        """
159        Возвращает следующий доступный ID для новой задачи.
160
161        Returns:
162            int: Следующий ID (MAX(id) + 1) или 1 если задач нет
163        """
164        with self._get_conn() as conn:
165            row = conn.execute("SELECT COALESCE(MAX(id), 0) + 1 FROM tasks").fetchone()
166            return row[0]
167
168    @observe(name="Add task")
169    def add_task(self, user_id: str, text: str, date: str | None = None) -> int:
170        """
171        Добавляет новую задачу для пользователя.
172
173        Args:
174            user_id: Идентификатор пользователя
175            text: Текст задачи
176            date: Дата задачи в формате YYYY-MM-DD (по умолчанию — 01.01.2100)
177
178        Returns:
179            int: ID созданной задачи
180        """
181        if date is None:
182            date = "2100-01-01"
183
184        with self._get_conn() as conn:
185            cur = conn.execute(
186                "INSERT INTO tasks (user_id, text, date, completed, created_at) VALUES (?, ?, ?, 0, ?)",
187                (user_id, text, date, datetime.now().isoformat()),
188            )
189            from typing import cast
190
191            task_id = cast(int, cur.lastrowid)
192            logger.debug(f"Добавлена задача {task_id} для пользователя {user_id}")
193            return task_id
194
195    @observe(name="Get tasks by date")
196    def get_tasks_by_date(
197        self, user_id: str, date: Optional[str] = None, completed: Optional[bool] = None
198    ) -> List[Dict]:
199        """
200        Возвращает задачи пользователя на указанную дату.
201
202        Args:
203            user_id: Идентификатор пользователя
204            date: Дата в формате YYYY-MM-DD (по умолчанию — сегодня)
205            completed: Фильтр по статусу выполнения (None — все задачи)
206
207        Returns:
208            List[Dict]: Список задач на указанную дату
209        """
210        if date is None:
211            date = self.get_today_date()
212
213        with self._get_conn() as conn:
214            if completed is None:
215                rows = conn.execute(
216                    "SELECT * FROM tasks WHERE user_id = ? AND date = ? ORDER BY id",
217                    (user_id, date),
218                ).fetchall()
219            else:
220                rows = conn.execute(
221                    "SELECT * FROM tasks WHERE user_id = ? AND date = ? AND completed = ? ORDER BY id",
222                    (user_id, date, 1 if completed else 0),
223                ).fetchall()
224            return [_convert_task(row) for row in rows]
225
226    def get_all_tasks(self, user_id: str) -> List[Dict]:
227        """Возвращает все невыполненные задачи на сегодня и позже."""
228        today = self.get_today_date()
229        with self._get_conn() as conn:
230            rows = conn.execute(
231                "SELECT * FROM tasks WHERE user_id = ? AND completed = 0 AND date >= ? ORDER BY date, id",
232                (user_id, today),
233            ).fetchall()
234            return [_convert_task(row) for row in rows]
235
236    @observe(name="Get task by ID")
237    def get_task(self, user_id: str, task_id: int) -> Optional[Dict]:
238        """
239        Возвращает задачу по её ID и идентификатору пользователя.
240
241        Args:
242            user_id: Идентификатор пользователя
243            task_id: Идентификатор задачи
244
245        Returns:
246            Optional[Dict]: Данные задачи или None, если задача не найдена
247        """
248        with self._get_conn() as conn:
249            row = conn.execute(
250                "SELECT * FROM tasks WHERE user_id = ? AND id = ?", (user_id, task_id)
251            ).fetchone()
252            return _convert_task(row) if row else None
253
254    @observe(name="Complete task")
255    def complete_task(self, user_id: str, task_id: int) -> bool:
256        """
257        Отмечает задачу как выполненную.
258
259        Устанавливает completed=1 и записывает временную метку завершения.
260
261        Args:
262            user_id: Идентификатор пользователя
263            task_id: Идентификатор задачи
264
265        Returns:
266            bool: True если задача найдена и обновлена, False в противном случае
267        """
268        with self._get_conn() as conn:
269            cur = conn.execute(
270                "UPDATE tasks SET completed = 1, completed_at = ? WHERE user_id = ? AND id = ?",
271                (datetime.now().isoformat(), user_id, task_id),
272            )
273            if cur.rowcount > 0:
274                logger.debug(f"Задача {task_id} отмечена как выполненная")
275                return True
276        return False
277
278    @observe(name="Move task")
279    def move_task(self, user_id: str, task_id: int, date: str) -> bool:
280        """
281        Переносит одну задачу на указанную дату.
282
283        Args:
284            user_id: Идентификатор пользователя
285            task_id: Идентификатор задачи
286            date: Новая дата в формате YYYY-MM-DD
287
288        Returns:
289            bool: True если задача найдена и перенесена, False в противном случае
290        """
291        with self._get_conn() as conn:
292            cur = conn.execute(
293                "UPDATE tasks SET date = ? WHERE user_id = ? AND id = ? AND completed = 0",
294                (date, user_id, task_id),
295            )
296            if cur.rowcount > 0:
297                logger.debug(f"Задача {task_id} перенесена на {date}")
298                return True
299        return False
300
301    @observe(name="Move tasks")
302    def move_tasks(self, user_id: str, date: str) -> bool:
303        """
304        Переносит все невыполненные задачи пользователя на указанную дату.
305
306        Args:
307            user_id: Идентификатор пользователя
308            date: Целевая дата в формате YYYY-MM-DD
309
310        Returns:
311            bool: True после завершения переноса
312        """
313        today = self.get_today_date()
314        with self._get_conn() as conn:
315            conn.execute(
316                "UPDATE tasks SET date = ? WHERE user_id = ? AND date = ? AND completed = 0",
317                (date, user_id, today),
318            )
319            logger.debug(f"Задачи пользователя {user_id} перенесены на {date}")
320        return True
321
322    @observe(name="Move incomplete tasks to today")
323    def move_incomplete_tasks_to_today(self) -> int:
324        """
325        Переносит все просроченные невыполненные задачи на сегодня.
326
327        Используется фоновым ассистентом OTM для ежедневного
328        автоматического переноса задач.
329
330        Returns:
331            int: Количество перенесённых задач
332        """
333        today = self.get_today_date()
334        with self._get_conn() as conn:
335            cur = conn.execute(
336                "UPDATE tasks SET date = ? WHERE completed = 0 AND date < ?",
337                (today, today),
338            )
339            moved = cur.rowcount
340            if moved > 0:
341                logger.debug(f"Перенесено {moved} задач на сегодня")
342        return moved
343
344    @observe(name="Delete task")
345    def delete_task(self, user_id: str, task_id: int) -> bool:
346        """
347        Удаляет задачу по её ID.
348
349        Args:
350            user_id: Идентификатор пользователя
351            task_id: Идентификатор задачи
352
353        Returns:
354            bool: True если задача найдена и удалена, False в противном случае
355        """
356        with self._get_conn() as conn:
357            cur = conn.execute("DELETE FROM tasks WHERE user_id = ? AND id = ?", (user_id, task_id))
358            if cur.rowcount > 0:
359                logger.debug(f"Задача {task_id} удалена")
360                return True
361        return False
362
363    @observe(name="Delete user tasks")
364    def delete_user_tasks(self, user_id: str) -> int:
365        """
366        Удаляет все задачи указанного пользователя.
367
368        Args:
369            user_id: Идентификатор пользователя
370
371        Returns:
372            int: Количество удалённых задач
373        """
374        with self._get_conn() as conn:
375            cur = conn.execute("DELETE FROM tasks WHERE user_id = ?", (user_id,))
376            logger.debug(f"Удалено {cur.rowcount} задач пользователя {user_id}")
377            return cur.rowcount
378
379    @observe(name="Get incomplete tasks count")
380    def get_incomplete_tasks_count(self, user_id: str, date: Optional[str] = None) -> int:
381        """
382        Возвращает количество невыполненных задач пользователя на дату.
383
384        Args:
385            user_id: Идентификатор пользователя
386            date: Дата в формате YYYY-MM-DD (по умолчанию — сегодня)
387
388        Returns:
389            int: Количество невыполненных задач
390        """
391        if date is None:
392            date = self.get_today_date()
393        with self._get_conn() as conn:
394            row = conn.execute(
395                "SELECT COUNT(*) FROM tasks WHERE user_id = ? AND date = ? AND completed = 0",
396                (user_id, date),
397            ).fetchone()
398            return row[0]
399
400    def get_user_count(self) -> int:
401        """
402        Возвращает количество уникальных пользователей в системе.
403
404        Returns:
405            int: Количество пользователей, имеющих хотя бы одну задачу
406        """
407        with self._get_conn() as conn:
408            row = conn.execute("SELECT COUNT(DISTINCT user_id) FROM tasks").fetchone()
409            return row[0]
410
411    @observe(name="Get all user IDs")
412    def get_all_user_ids(self) -> List[str]:
413        """
414        Возвращает список всех идентификаторов пользователей.
415
416        Returns:
417            List[str]: Список user_id всех пользователей, имеющих задачи
418        """
419        with self._get_conn() as conn:
420            rows = conn.execute("SELECT DISTINCT user_id FROM tasks").fetchall()
421            return [row["user_id"] for row in rows]
422
423    @observe(name="Get today date")
424    def get_today_date(self) -> str:
425        """
426        Возвращает текущую дату в формате YYYY-MM-DD.
427
428        Returns:
429            str: Сегодняшняя дата
430        """
431        return datetime.now().strftime("%Y-%m-%d")
432
433    @observe(name="Parse date from text")
434    def parse_date(self, date_str: str) -> str:
435        """
436        Преобразует текстовое представление даты в формат YYYY-MM-DD.
437
438        Поддерживает: "сегодня", "завтра", "послезавтра", "ДД.ММ", "ДД.ММ.ГГГГ".
439
440        Args:
441            date_str: Текстовое представление даты
442
443        Returns:
444            str: Дата в формате YYYY-MM-DD
445        """
446        date_str = date_str.lower().strip()
447        today = datetime.now()
448
449        date_map = {
450            "сегодня": 0,
451            "на сегодня": 0,
452            "завтра": 1,
453            "на завтра": 1,
454            "послезавтра": 2,
455            "на послезавтра": 2,
456        }
457
458        if date_str in date_map:
459            target_date = today + timedelta(days=date_map[date_str])
460            return target_date.strftime("%Y-%m-%d")
461
462        try:
463            if "." in date_str:
464                parts = date_str.split(".")
465                if len(parts) == 2:
466                    day, month = map(int, parts)
467                    year = today.year
468                    datetime(year, month, day)
469                    return f"{year:04d}-{month:02d}-{day:02d}"
470                elif len(parts) == 3:
471                    day, month, year = map(int, parts)
472                    datetime(year, month, day)
473                    return f"{year:04d}-{month:02d}-{day:02d}"
474        except (ValueError, TypeError):
475            pass
476
477        return self.get_today_date()
478
479    @observe(name="Format task list")
480    def format_task_list_truncated(
481        self, tasks: List[Dict], show_completed: bool = False, max_chars: int = 1024
482    ) -> str:
483        """
484        Форматирует список задач в строку с учётом ограничения длины.
485
486        Задачи выводятся в обратном порядке (новые сверху).
487        Если список не помещается в лимит, добавляется строка
488        "и еще N задач" или "Не показано ни одной задачи".
489
490        Args:
491            tasks: Список задач (словарей)
492            show_completed: Показывать ли выполненные задачи
493            max_chars: Максимальная длина строки (по умолчанию 1024)
494
495        Returns:
496            str: Отформатированный список задач
497        """
498        if not tasks:
499            return "Задач нет."
500
501        if show_completed:
502            raw_lines = [f"{task['id']}. {task['text']}" for task in tasks]
503        else:
504            raw_lines = [f"{task['id']}. {task['text']}" for task in tasks if not task["completed"]]
505
506        if not raw_lines:
507            return "Невыполненных задач нет."
508
509        raw_lines.reverse()
510        total = len(raw_lines)
511        tail_template = "\nи еще {} задач"
512        best_lines = []
513        used_chars = 0
514
515        for line in raw_lines:
516            tail = (
517                tail_template.format(total - len(best_lines) - 1) if len(best_lines) < total else ""
518            )
519            candidate = "\n".join(best_lines + [line]) + tail
520            if len(candidate) > max_chars:
521                break
522            best_lines.append(line)
523            used_chars = len("\n".join(best_lines))
524
525        shown = len(best_lines)
526        if shown == total:
527            if shown == 1:
528                return f"Задача: {best_lines[0]}"
529            return "Вот ваши задачи: \n" + "\n".join(best_lines)
530
531        tail = tail_template.format(total - shown)
532        if shown == 0:
533            return f"Не показано ни одной задачи. {tail.strip()}"
534
535        return "\n".join(best_lines) + tail
536
537    @observe(name="Get incomplete tasks summary")
538    def get_incomplete_tasks_summary(self, user_id: str) -> Dict[str, int]:
539        """
540        Возвращает сводку по невыполненным задачам пользователя.
541
542        Подсчитывает общее количество невыполненных задач, а также
543        количество на сегодня и завтра.
544
545        Args:
546            user_id: Идентификатор пользователя
547
548        Returns:
549            Dict[str, int]: Словарь с ключами total, today, tomorrow
550        """
551        today = self.get_today_date()
552        tomorrow = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
553
554        rows = (
555            self
556            ._get_conn()
557            .execute(
558                "SELECT date, COUNT(*) as cnt FROM tasks WHERE user_id = ? AND completed = 0 GROUP BY date",
559                (user_id,),
560            )
561            .fetchall()
562        )
563
564        total = sum(r["cnt"] for r in rows)
565        today_count = 0
566        tomorrow_count = 0
567
568        for r in rows:
569            if r["date"] == today:
570                today_count = r["cnt"]
571            if r["date"] == tomorrow:
572                tomorrow_count = r["cnt"]
573
574        return {"total": total, "today": today_count, "tomorrow": tomorrow_count}
DATA_DIR = './data'
DB_FILE = './data/tasks.db'
TASKS_FILE = './data/tasks.json'
class TaskManager:
 39class TaskManager:
 40    """
 41    Менеджер задач с хранением в SQLite.
 42
 43    Предоставляет полный набор CRUD-операций для управления задачами
 44    пользователей. Поддерживает автоматическую миграцию из JSON-формата,
 45    парсинг дат на русском языке и форматирование списков задач
 46    с учётом ограничения на длину ответа Яндекс Алисы (1024 символа).
 47    """
 48
 49    def __init__(self, db_path: str = DB_FILE):
 50        """
 51        Инициализирует TaskManager, создавая/подключая БД SQLite.
 52
 53        При первом запуске создаёт необходимые таблицы и индексы.
 54        Выполняет миграцию из tasks.json если такой файл существует.
 55
 56        Args:
 57            db_path: Путь к файлу базы данных SQLite
 58        """
 59        self.db_path = db_path
 60        os.makedirs(DATA_DIR, exist_ok=True)
 61        self._init_db()
 62        self._migrate_from_json_if_needed()
 63
 64    def _get_conn(self) -> sqlite3.Connection:
 65        """
 66        Создаёт и возвращает подключение к SQLite.
 67
 68        Настраивает row_factory для доступа по имени колонки,
 69        включает WAL-режим журнала и таймаут ожидания блокировки.
 70
 71        Returns:
 72            sqlite3.Connection: Подключение к БД
 73        """
 74        conn = sqlite3.connect(self.db_path)
 75        conn.row_factory = sqlite3.Row
 76        conn.execute("PRAGMA journal_mode=WAL")
 77        conn.execute("PRAGMA busy_timeout=5000")
 78        return conn
 79
 80    def _init_db(self):
 81        """
 82        Инициализирует структуру базы данных.
 83
 84        Создаёт таблицу tasks (если не существует) с колонками:
 85        id, user_id, text, date, completed, created_at, completed_at.
 86        Также создаёт индексы для ускорения запросов.
 87        """
 88        with self._get_conn() as conn:
 89            conn.executescript("""
 90                CREATE TABLE IF NOT EXISTS tasks (
 91                    id INTEGER PRIMARY KEY AUTOINCREMENT,
 92                    user_id TEXT NOT NULL,
 93                    text TEXT NOT NULL,
 94                    date TEXT NOT NULL,
 95                    completed INTEGER NOT NULL DEFAULT 0,
 96                    created_at TEXT NOT NULL,
 97                    completed_at TEXT
 98                );
 99                CREATE INDEX IF NOT EXISTS idx_tasks_user_date ON tasks(user_id, date);
100                CREATE INDEX IF NOT EXISTS idx_tasks_user_comp ON tasks(user_id, completed, date);
101            """)
102
103    def _migrate_from_json_if_needed(self):
104        """
105        Выполняет миграцию данных из tasks.json в SQLite.
106
107        Если файл tasks.json существует, данные переносятся в таблицу tasks,
108        после чего JSON-файл переименовывается в tasks.json.bak.
109        """
110        if not os.path.exists(TASKS_FILE):
111            return
112
113        logger.info("Миграция данных из tasks.json в SQLite...")
114        try:
115            with open(TASKS_FILE, "r", encoding="utf-8") as f:
116                data = json.load(f)
117
118            users = data.get("users", {})
119            with self._get_conn() as conn:
120                for user_id, user_data in users.items():
121                    for task in user_data.get("tasks", []):
122                        conn.execute(
123                            """INSERT INTO tasks (id, user_id, text, date, completed, created_at, completed_at)
124                               VALUES (?, ?, ?, ?, ?, ?, ?)""",
125                            (
126                                task["id"],
127                                user_id,
128                                task["text"],
129                                task["date"],
130                                1 if task.get("completed") else 0,
131                                task.get("created_at", ""),
132                                task.get("completed_at"),
133                            ),
134                        )
135            os.rename(TASKS_FILE, TASKS_FILE + ".bak")
136            logger.success(f"Миграция завершена. tasks.json → tasks.db, бэкап: {TASKS_FILE}.bak")
137        except Exception as e:
138            logger.error(f"Ошибка миграции из JSON: {e}")
139
140    @observe(name="Get user tasks")
141    def get_user_tasks(self, user_id: str) -> List[Dict]:
142        """
143        Возвращает все задачи указанного пользователя.
144
145        Args:
146            user_id: Идентификатор пользователя
147
148        Returns:
149            List[Dict]: Список задач пользователя, отсортированных по ID
150        """
151        with self._get_conn() as conn:
152            rows = conn.execute(
153                "SELECT * FROM tasks WHERE user_id = ? ORDER BY id", (user_id,)
154            ).fetchall()
155            return [_convert_task(row) for row in rows]
156
157    @observe(name="Get next task ID")
158    def get_next_task_id(self) -> int:
159        """
160        Возвращает следующий доступный ID для новой задачи.
161
162        Returns:
163            int: Следующий ID (MAX(id) + 1) или 1 если задач нет
164        """
165        with self._get_conn() as conn:
166            row = conn.execute("SELECT COALESCE(MAX(id), 0) + 1 FROM tasks").fetchone()
167            return row[0]
168
169    @observe(name="Add task")
170    def add_task(self, user_id: str, text: str, date: str | None = None) -> int:
171        """
172        Добавляет новую задачу для пользователя.
173
174        Args:
175            user_id: Идентификатор пользователя
176            text: Текст задачи
177            date: Дата задачи в формате YYYY-MM-DD (по умолчанию — 01.01.2100)
178
179        Returns:
180            int: ID созданной задачи
181        """
182        if date is None:
183            date = "2100-01-01"
184
185        with self._get_conn() as conn:
186            cur = conn.execute(
187                "INSERT INTO tasks (user_id, text, date, completed, created_at) VALUES (?, ?, ?, 0, ?)",
188                (user_id, text, date, datetime.now().isoformat()),
189            )
190            from typing import cast
191
192            task_id = cast(int, cur.lastrowid)
193            logger.debug(f"Добавлена задача {task_id} для пользователя {user_id}")
194            return task_id
195
196    @observe(name="Get tasks by date")
197    def get_tasks_by_date(
198        self, user_id: str, date: Optional[str] = None, completed: Optional[bool] = None
199    ) -> List[Dict]:
200        """
201        Возвращает задачи пользователя на указанную дату.
202
203        Args:
204            user_id: Идентификатор пользователя
205            date: Дата в формате YYYY-MM-DD (по умолчанию — сегодня)
206            completed: Фильтр по статусу выполнения (None — все задачи)
207
208        Returns:
209            List[Dict]: Список задач на указанную дату
210        """
211        if date is None:
212            date = self.get_today_date()
213
214        with self._get_conn() as conn:
215            if completed is None:
216                rows = conn.execute(
217                    "SELECT * FROM tasks WHERE user_id = ? AND date = ? ORDER BY id",
218                    (user_id, date),
219                ).fetchall()
220            else:
221                rows = conn.execute(
222                    "SELECT * FROM tasks WHERE user_id = ? AND date = ? AND completed = ? ORDER BY id",
223                    (user_id, date, 1 if completed else 0),
224                ).fetchall()
225            return [_convert_task(row) for row in rows]
226
227    def get_all_tasks(self, user_id: str) -> List[Dict]:
228        """Возвращает все невыполненные задачи на сегодня и позже."""
229        today = self.get_today_date()
230        with self._get_conn() as conn:
231            rows = conn.execute(
232                "SELECT * FROM tasks WHERE user_id = ? AND completed = 0 AND date >= ? ORDER BY date, id",
233                (user_id, today),
234            ).fetchall()
235            return [_convert_task(row) for row in rows]
236
237    @observe(name="Get task by ID")
238    def get_task(self, user_id: str, task_id: int) -> Optional[Dict]:
239        """
240        Возвращает задачу по её ID и идентификатору пользователя.
241
242        Args:
243            user_id: Идентификатор пользователя
244            task_id: Идентификатор задачи
245
246        Returns:
247            Optional[Dict]: Данные задачи или None, если задача не найдена
248        """
249        with self._get_conn() as conn:
250            row = conn.execute(
251                "SELECT * FROM tasks WHERE user_id = ? AND id = ?", (user_id, task_id)
252            ).fetchone()
253            return _convert_task(row) if row else None
254
255    @observe(name="Complete task")
256    def complete_task(self, user_id: str, task_id: int) -> bool:
257        """
258        Отмечает задачу как выполненную.
259
260        Устанавливает completed=1 и записывает временную метку завершения.
261
262        Args:
263            user_id: Идентификатор пользователя
264            task_id: Идентификатор задачи
265
266        Returns:
267            bool: True если задача найдена и обновлена, False в противном случае
268        """
269        with self._get_conn() as conn:
270            cur = conn.execute(
271                "UPDATE tasks SET completed = 1, completed_at = ? WHERE user_id = ? AND id = ?",
272                (datetime.now().isoformat(), user_id, task_id),
273            )
274            if cur.rowcount > 0:
275                logger.debug(f"Задача {task_id} отмечена как выполненная")
276                return True
277        return False
278
279    @observe(name="Move task")
280    def move_task(self, user_id: str, task_id: int, date: str) -> bool:
281        """
282        Переносит одну задачу на указанную дату.
283
284        Args:
285            user_id: Идентификатор пользователя
286            task_id: Идентификатор задачи
287            date: Новая дата в формате YYYY-MM-DD
288
289        Returns:
290            bool: True если задача найдена и перенесена, False в противном случае
291        """
292        with self._get_conn() as conn:
293            cur = conn.execute(
294                "UPDATE tasks SET date = ? WHERE user_id = ? AND id = ? AND completed = 0",
295                (date, user_id, task_id),
296            )
297            if cur.rowcount > 0:
298                logger.debug(f"Задача {task_id} перенесена на {date}")
299                return True
300        return False
301
302    @observe(name="Move tasks")
303    def move_tasks(self, user_id: str, date: str) -> bool:
304        """
305        Переносит все невыполненные задачи пользователя на указанную дату.
306
307        Args:
308            user_id: Идентификатор пользователя
309            date: Целевая дата в формате YYYY-MM-DD
310
311        Returns:
312            bool: True после завершения переноса
313        """
314        today = self.get_today_date()
315        with self._get_conn() as conn:
316            conn.execute(
317                "UPDATE tasks SET date = ? WHERE user_id = ? AND date = ? AND completed = 0",
318                (date, user_id, today),
319            )
320            logger.debug(f"Задачи пользователя {user_id} перенесены на {date}")
321        return True
322
323    @observe(name="Move incomplete tasks to today")
324    def move_incomplete_tasks_to_today(self) -> int:
325        """
326        Переносит все просроченные невыполненные задачи на сегодня.
327
328        Используется фоновым ассистентом OTM для ежедневного
329        автоматического переноса задач.
330
331        Returns:
332            int: Количество перенесённых задач
333        """
334        today = self.get_today_date()
335        with self._get_conn() as conn:
336            cur = conn.execute(
337                "UPDATE tasks SET date = ? WHERE completed = 0 AND date < ?",
338                (today, today),
339            )
340            moved = cur.rowcount
341            if moved > 0:
342                logger.debug(f"Перенесено {moved} задач на сегодня")
343        return moved
344
345    @observe(name="Delete task")
346    def delete_task(self, user_id: str, task_id: int) -> bool:
347        """
348        Удаляет задачу по её ID.
349
350        Args:
351            user_id: Идентификатор пользователя
352            task_id: Идентификатор задачи
353
354        Returns:
355            bool: True если задача найдена и удалена, False в противном случае
356        """
357        with self._get_conn() as conn:
358            cur = conn.execute("DELETE FROM tasks WHERE user_id = ? AND id = ?", (user_id, task_id))
359            if cur.rowcount > 0:
360                logger.debug(f"Задача {task_id} удалена")
361                return True
362        return False
363
364    @observe(name="Delete user tasks")
365    def delete_user_tasks(self, user_id: str) -> int:
366        """
367        Удаляет все задачи указанного пользователя.
368
369        Args:
370            user_id: Идентификатор пользователя
371
372        Returns:
373            int: Количество удалённых задач
374        """
375        with self._get_conn() as conn:
376            cur = conn.execute("DELETE FROM tasks WHERE user_id = ?", (user_id,))
377            logger.debug(f"Удалено {cur.rowcount} задач пользователя {user_id}")
378            return cur.rowcount
379
380    @observe(name="Get incomplete tasks count")
381    def get_incomplete_tasks_count(self, user_id: str, date: Optional[str] = None) -> int:
382        """
383        Возвращает количество невыполненных задач пользователя на дату.
384
385        Args:
386            user_id: Идентификатор пользователя
387            date: Дата в формате YYYY-MM-DD (по умолчанию — сегодня)
388
389        Returns:
390            int: Количество невыполненных задач
391        """
392        if date is None:
393            date = self.get_today_date()
394        with self._get_conn() as conn:
395            row = conn.execute(
396                "SELECT COUNT(*) FROM tasks WHERE user_id = ? AND date = ? AND completed = 0",
397                (user_id, date),
398            ).fetchone()
399            return row[0]
400
401    def get_user_count(self) -> int:
402        """
403        Возвращает количество уникальных пользователей в системе.
404
405        Returns:
406            int: Количество пользователей, имеющих хотя бы одну задачу
407        """
408        with self._get_conn() as conn:
409            row = conn.execute("SELECT COUNT(DISTINCT user_id) FROM tasks").fetchone()
410            return row[0]
411
412    @observe(name="Get all user IDs")
413    def get_all_user_ids(self) -> List[str]:
414        """
415        Возвращает список всех идентификаторов пользователей.
416
417        Returns:
418            List[str]: Список user_id всех пользователей, имеющих задачи
419        """
420        with self._get_conn() as conn:
421            rows = conn.execute("SELECT DISTINCT user_id FROM tasks").fetchall()
422            return [row["user_id"] for row in rows]
423
424    @observe(name="Get today date")
425    def get_today_date(self) -> str:
426        """
427        Возвращает текущую дату в формате YYYY-MM-DD.
428
429        Returns:
430            str: Сегодняшняя дата
431        """
432        return datetime.now().strftime("%Y-%m-%d")
433
434    @observe(name="Parse date from text")
435    def parse_date(self, date_str: str) -> str:
436        """
437        Преобразует текстовое представление даты в формат YYYY-MM-DD.
438
439        Поддерживает: "сегодня", "завтра", "послезавтра", "ДД.ММ", "ДД.ММ.ГГГГ".
440
441        Args:
442            date_str: Текстовое представление даты
443
444        Returns:
445            str: Дата в формате YYYY-MM-DD
446        """
447        date_str = date_str.lower().strip()
448        today = datetime.now()
449
450        date_map = {
451            "сегодня": 0,
452            "на сегодня": 0,
453            "завтра": 1,
454            "на завтра": 1,
455            "послезавтра": 2,
456            "на послезавтра": 2,
457        }
458
459        if date_str in date_map:
460            target_date = today + timedelta(days=date_map[date_str])
461            return target_date.strftime("%Y-%m-%d")
462
463        try:
464            if "." in date_str:
465                parts = date_str.split(".")
466                if len(parts) == 2:
467                    day, month = map(int, parts)
468                    year = today.year
469                    datetime(year, month, day)
470                    return f"{year:04d}-{month:02d}-{day:02d}"
471                elif len(parts) == 3:
472                    day, month, year = map(int, parts)
473                    datetime(year, month, day)
474                    return f"{year:04d}-{month:02d}-{day:02d}"
475        except (ValueError, TypeError):
476            pass
477
478        return self.get_today_date()
479
480    @observe(name="Format task list")
481    def format_task_list_truncated(
482        self, tasks: List[Dict], show_completed: bool = False, max_chars: int = 1024
483    ) -> str:
484        """
485        Форматирует список задач в строку с учётом ограничения длины.
486
487        Задачи выводятся в обратном порядке (новые сверху).
488        Если список не помещается в лимит, добавляется строка
489        "и еще N задач" или "Не показано ни одной задачи".
490
491        Args:
492            tasks: Список задач (словарей)
493            show_completed: Показывать ли выполненные задачи
494            max_chars: Максимальная длина строки (по умолчанию 1024)
495
496        Returns:
497            str: Отформатированный список задач
498        """
499        if not tasks:
500            return "Задач нет."
501
502        if show_completed:
503            raw_lines = [f"{task['id']}. {task['text']}" for task in tasks]
504        else:
505            raw_lines = [f"{task['id']}. {task['text']}" for task in tasks if not task["completed"]]
506
507        if not raw_lines:
508            return "Невыполненных задач нет."
509
510        raw_lines.reverse()
511        total = len(raw_lines)
512        tail_template = "\nи еще {} задач"
513        best_lines = []
514        used_chars = 0
515
516        for line in raw_lines:
517            tail = (
518                tail_template.format(total - len(best_lines) - 1) if len(best_lines) < total else ""
519            )
520            candidate = "\n".join(best_lines + [line]) + tail
521            if len(candidate) > max_chars:
522                break
523            best_lines.append(line)
524            used_chars = len("\n".join(best_lines))
525
526        shown = len(best_lines)
527        if shown == total:
528            if shown == 1:
529                return f"Задача: {best_lines[0]}"
530            return "Вот ваши задачи: \n" + "\n".join(best_lines)
531
532        tail = tail_template.format(total - shown)
533        if shown == 0:
534            return f"Не показано ни одной задачи. {tail.strip()}"
535
536        return "\n".join(best_lines) + tail
537
538    @observe(name="Get incomplete tasks summary")
539    def get_incomplete_tasks_summary(self, user_id: str) -> Dict[str, int]:
540        """
541        Возвращает сводку по невыполненным задачам пользователя.
542
543        Подсчитывает общее количество невыполненных задач, а также
544        количество на сегодня и завтра.
545
546        Args:
547            user_id: Идентификатор пользователя
548
549        Returns:
550            Dict[str, int]: Словарь с ключами total, today, tomorrow
551        """
552        today = self.get_today_date()
553        tomorrow = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
554
555        rows = (
556            self
557            ._get_conn()
558            .execute(
559                "SELECT date, COUNT(*) as cnt FROM tasks WHERE user_id = ? AND completed = 0 GROUP BY date",
560                (user_id,),
561            )
562            .fetchall()
563        )
564
565        total = sum(r["cnt"] for r in rows)
566        today_count = 0
567        tomorrow_count = 0
568
569        for r in rows:
570            if r["date"] == today:
571                today_count = r["cnt"]
572            if r["date"] == tomorrow:
573                tomorrow_count = r["cnt"]
574
575        return {"total": total, "today": today_count, "tomorrow": tomorrow_count}

Менеджер задач с хранением в SQLite.

Предоставляет полный набор CRUD-операций для управления задачами пользователей. Поддерживает автоматическую миграцию из JSON-формата, парсинг дат на русском языке и форматирование списков задач с учётом ограничения на длину ответа Яндекс Алисы (1024 символа).

TaskManager(db_path: str = './data/tasks.db')
49    def __init__(self, db_path: str = DB_FILE):
50        """
51        Инициализирует TaskManager, создавая/подключая БД SQLite.
52
53        При первом запуске создаёт необходимые таблицы и индексы.
54        Выполняет миграцию из tasks.json если такой файл существует.
55
56        Args:
57            db_path: Путь к файлу базы данных SQLite
58        """
59        self.db_path = db_path
60        os.makedirs(DATA_DIR, exist_ok=True)
61        self._init_db()
62        self._migrate_from_json_if_needed()

Инициализирует TaskManager, создавая/подключая БД SQLite.

При первом запуске создаёт необходимые таблицы и индексы. Выполняет миграцию из tasks.json если такой файл существует.

Arguments:
  • db_path: Путь к файлу базы данных SQLite
db_path
@observe(name='Get user tasks')
def get_user_tasks(self, user_id: str) -> List[Dict]:
140    @observe(name="Get user tasks")
141    def get_user_tasks(self, user_id: str) -> List[Dict]:
142        """
143        Возвращает все задачи указанного пользователя.
144
145        Args:
146            user_id: Идентификатор пользователя
147
148        Returns:
149            List[Dict]: Список задач пользователя, отсортированных по ID
150        """
151        with self._get_conn() as conn:
152            rows = conn.execute(
153                "SELECT * FROM tasks WHERE user_id = ? ORDER BY id", (user_id,)
154            ).fetchall()
155            return [_convert_task(row) for row in rows]

Возвращает все задачи указанного пользователя.

Arguments:
  • user_id: Идентификатор пользователя
Returns:

List[Dict]: Список задач пользователя, отсортированных по ID

@observe(name='Get next task ID')
def get_next_task_id(self) -> int:
157    @observe(name="Get next task ID")
158    def get_next_task_id(self) -> int:
159        """
160        Возвращает следующий доступный ID для новой задачи.
161
162        Returns:
163            int: Следующий ID (MAX(id) + 1) или 1 если задач нет
164        """
165        with self._get_conn() as conn:
166            row = conn.execute("SELECT COALESCE(MAX(id), 0) + 1 FROM tasks").fetchone()
167            return row[0]

Возвращает следующий доступный ID для новой задачи.

Returns:

int: Следующий ID (MAX(id) + 1) или 1 если задач нет

@observe(name='Add task')
def add_task(self, user_id: str, text: str, date: str | None = None) -> int:
169    @observe(name="Add task")
170    def add_task(self, user_id: str, text: str, date: str | None = None) -> int:
171        """
172        Добавляет новую задачу для пользователя.
173
174        Args:
175            user_id: Идентификатор пользователя
176            text: Текст задачи
177            date: Дата задачи в формате YYYY-MM-DD (по умолчанию — 01.01.2100)
178
179        Returns:
180            int: ID созданной задачи
181        """
182        if date is None:
183            date = "2100-01-01"
184
185        with self._get_conn() as conn:
186            cur = conn.execute(
187                "INSERT INTO tasks (user_id, text, date, completed, created_at) VALUES (?, ?, ?, 0, ?)",
188                (user_id, text, date, datetime.now().isoformat()),
189            )
190            from typing import cast
191
192            task_id = cast(int, cur.lastrowid)
193            logger.debug(f"Добавлена задача {task_id} для пользователя {user_id}")
194            return task_id

Добавляет новую задачу для пользователя.

Arguments:
  • user_id: Идентификатор пользователя
  • text: Текст задачи
  • date: Дата задачи в формате YYYY-MM-DD (по умолчанию — 01.01.2100)
Returns:

int: ID созданной задачи

@observe(name='Get tasks by date')
def get_tasks_by_date( self, user_id: str, date: Optional[str] = None, completed: Optional[bool] = None) -> List[Dict]:
196    @observe(name="Get tasks by date")
197    def get_tasks_by_date(
198        self, user_id: str, date: Optional[str] = None, completed: Optional[bool] = None
199    ) -> List[Dict]:
200        """
201        Возвращает задачи пользователя на указанную дату.
202
203        Args:
204            user_id: Идентификатор пользователя
205            date: Дата в формате YYYY-MM-DD (по умолчанию — сегодня)
206            completed: Фильтр по статусу выполнения (None — все задачи)
207
208        Returns:
209            List[Dict]: Список задач на указанную дату
210        """
211        if date is None:
212            date = self.get_today_date()
213
214        with self._get_conn() as conn:
215            if completed is None:
216                rows = conn.execute(
217                    "SELECT * FROM tasks WHERE user_id = ? AND date = ? ORDER BY id",
218                    (user_id, date),
219                ).fetchall()
220            else:
221                rows = conn.execute(
222                    "SELECT * FROM tasks WHERE user_id = ? AND date = ? AND completed = ? ORDER BY id",
223                    (user_id, date, 1 if completed else 0),
224                ).fetchall()
225            return [_convert_task(row) for row in rows]

Возвращает задачи пользователя на указанную дату.

Arguments:
  • user_id: Идентификатор пользователя
  • date: Дата в формате YYYY-MM-DD (по умолчанию — сегодня)
  • completed: Фильтр по статусу выполнения (None — все задачи)
Returns:

List[Dict]: Список задач на указанную дату

def get_all_tasks(self, user_id: str) -> List[Dict]:
227    def get_all_tasks(self, user_id: str) -> List[Dict]:
228        """Возвращает все невыполненные задачи на сегодня и позже."""
229        today = self.get_today_date()
230        with self._get_conn() as conn:
231            rows = conn.execute(
232                "SELECT * FROM tasks WHERE user_id = ? AND completed = 0 AND date >= ? ORDER BY date, id",
233                (user_id, today),
234            ).fetchall()
235            return [_convert_task(row) for row in rows]

Возвращает все невыполненные задачи на сегодня и позже.

@observe(name='Get task by ID')
def get_task(self, user_id: str, task_id: int) -> Optional[Dict]:
237    @observe(name="Get task by ID")
238    def get_task(self, user_id: str, task_id: int) -> Optional[Dict]:
239        """
240        Возвращает задачу по её ID и идентификатору пользователя.
241
242        Args:
243            user_id: Идентификатор пользователя
244            task_id: Идентификатор задачи
245
246        Returns:
247            Optional[Dict]: Данные задачи или None, если задача не найдена
248        """
249        with self._get_conn() as conn:
250            row = conn.execute(
251                "SELECT * FROM tasks WHERE user_id = ? AND id = ?", (user_id, task_id)
252            ).fetchone()
253            return _convert_task(row) if row else None

Возвращает задачу по её ID и идентификатору пользователя.

Arguments:
  • user_id: Идентификатор пользователя
  • task_id: Идентификатор задачи
Returns:

Optional[Dict]: Данные задачи или None, если задача не найдена

@observe(name='Complete task')
def complete_task(self, user_id: str, task_id: int) -> bool:
255    @observe(name="Complete task")
256    def complete_task(self, user_id: str, task_id: int) -> bool:
257        """
258        Отмечает задачу как выполненную.
259
260        Устанавливает completed=1 и записывает временную метку завершения.
261
262        Args:
263            user_id: Идентификатор пользователя
264            task_id: Идентификатор задачи
265
266        Returns:
267            bool: True если задача найдена и обновлена, False в противном случае
268        """
269        with self._get_conn() as conn:
270            cur = conn.execute(
271                "UPDATE tasks SET completed = 1, completed_at = ? WHERE user_id = ? AND id = ?",
272                (datetime.now().isoformat(), user_id, task_id),
273            )
274            if cur.rowcount > 0:
275                logger.debug(f"Задача {task_id} отмечена как выполненная")
276                return True
277        return False

Отмечает задачу как выполненную.

Устанавливает completed=1 и записывает временную метку завершения.

Arguments:
  • user_id: Идентификатор пользователя
  • task_id: Идентификатор задачи
Returns:

bool: True если задача найдена и обновлена, False в противном случае

@observe(name='Move task')
def move_task(self, user_id: str, task_id: int, date: str) -> bool:
279    @observe(name="Move task")
280    def move_task(self, user_id: str, task_id: int, date: str) -> bool:
281        """
282        Переносит одну задачу на указанную дату.
283
284        Args:
285            user_id: Идентификатор пользователя
286            task_id: Идентификатор задачи
287            date: Новая дата в формате YYYY-MM-DD
288
289        Returns:
290            bool: True если задача найдена и перенесена, False в противном случае
291        """
292        with self._get_conn() as conn:
293            cur = conn.execute(
294                "UPDATE tasks SET date = ? WHERE user_id = ? AND id = ? AND completed = 0",
295                (date, user_id, task_id),
296            )
297            if cur.rowcount > 0:
298                logger.debug(f"Задача {task_id} перенесена на {date}")
299                return True
300        return False

Переносит одну задачу на указанную дату.

Arguments:
  • user_id: Идентификатор пользователя
  • task_id: Идентификатор задачи
  • date: Новая дата в формате YYYY-MM-DD
Returns:

bool: True если задача найдена и перенесена, False в противном случае

@observe(name='Move tasks')
def move_tasks(self, user_id: str, date: str) -> bool:
302    @observe(name="Move tasks")
303    def move_tasks(self, user_id: str, date: str) -> bool:
304        """
305        Переносит все невыполненные задачи пользователя на указанную дату.
306
307        Args:
308            user_id: Идентификатор пользователя
309            date: Целевая дата в формате YYYY-MM-DD
310
311        Returns:
312            bool: True после завершения переноса
313        """
314        today = self.get_today_date()
315        with self._get_conn() as conn:
316            conn.execute(
317                "UPDATE tasks SET date = ? WHERE user_id = ? AND date = ? AND completed = 0",
318                (date, user_id, today),
319            )
320            logger.debug(f"Задачи пользователя {user_id} перенесены на {date}")
321        return True

Переносит все невыполненные задачи пользователя на указанную дату.

Arguments:
  • user_id: Идентификатор пользователя
  • date: Целевая дата в формате YYYY-MM-DD
Returns:

bool: True после завершения переноса

@observe(name='Move incomplete tasks to today')
def move_incomplete_tasks_to_today(self) -> int:
323    @observe(name="Move incomplete tasks to today")
324    def move_incomplete_tasks_to_today(self) -> int:
325        """
326        Переносит все просроченные невыполненные задачи на сегодня.
327
328        Используется фоновым ассистентом OTM для ежедневного
329        автоматического переноса задач.
330
331        Returns:
332            int: Количество перенесённых задач
333        """
334        today = self.get_today_date()
335        with self._get_conn() as conn:
336            cur = conn.execute(
337                "UPDATE tasks SET date = ? WHERE completed = 0 AND date < ?",
338                (today, today),
339            )
340            moved = cur.rowcount
341            if moved > 0:
342                logger.debug(f"Перенесено {moved} задач на сегодня")
343        return moved

Переносит все просроченные невыполненные задачи на сегодня.

Используется фоновым ассистентом OTM для ежедневного автоматического переноса задач.

Returns:

int: Количество перенесённых задач

@observe(name='Delete task')
def delete_task(self, user_id: str, task_id: int) -> bool:
345    @observe(name="Delete task")
346    def delete_task(self, user_id: str, task_id: int) -> bool:
347        """
348        Удаляет задачу по её ID.
349
350        Args:
351            user_id: Идентификатор пользователя
352            task_id: Идентификатор задачи
353
354        Returns:
355            bool: True если задача найдена и удалена, False в противном случае
356        """
357        with self._get_conn() as conn:
358            cur = conn.execute("DELETE FROM tasks WHERE user_id = ? AND id = ?", (user_id, task_id))
359            if cur.rowcount > 0:
360                logger.debug(f"Задача {task_id} удалена")
361                return True
362        return False

Удаляет задачу по её ID.

Arguments:
  • user_id: Идентификатор пользователя
  • task_id: Идентификатор задачи
Returns:

bool: True если задача найдена и удалена, False в противном случае

@observe(name='Delete user tasks')
def delete_user_tasks(self, user_id: str) -> int:
364    @observe(name="Delete user tasks")
365    def delete_user_tasks(self, user_id: str) -> int:
366        """
367        Удаляет все задачи указанного пользователя.
368
369        Args:
370            user_id: Идентификатор пользователя
371
372        Returns:
373            int: Количество удалённых задач
374        """
375        with self._get_conn() as conn:
376            cur = conn.execute("DELETE FROM tasks WHERE user_id = ?", (user_id,))
377            logger.debug(f"Удалено {cur.rowcount} задач пользователя {user_id}")
378            return cur.rowcount

Удаляет все задачи указанного пользователя.

Arguments:
  • user_id: Идентификатор пользователя
Returns:

int: Количество удалённых задач

@observe(name='Get incomplete tasks count')
def get_incomplete_tasks_count(self, user_id: str, date: Optional[str] = None) -> int:
380    @observe(name="Get incomplete tasks count")
381    def get_incomplete_tasks_count(self, user_id: str, date: Optional[str] = None) -> int:
382        """
383        Возвращает количество невыполненных задач пользователя на дату.
384
385        Args:
386            user_id: Идентификатор пользователя
387            date: Дата в формате YYYY-MM-DD (по умолчанию — сегодня)
388
389        Returns:
390            int: Количество невыполненных задач
391        """
392        if date is None:
393            date = self.get_today_date()
394        with self._get_conn() as conn:
395            row = conn.execute(
396                "SELECT COUNT(*) FROM tasks WHERE user_id = ? AND date = ? AND completed = 0",
397                (user_id, date),
398            ).fetchone()
399            return row[0]

Возвращает количество невыполненных задач пользователя на дату.

Arguments:
  • user_id: Идентификатор пользователя
  • date: Дата в формате YYYY-MM-DD (по умолчанию — сегодня)
Returns:

int: Количество невыполненных задач

def get_user_count(self) -> int:
401    def get_user_count(self) -> int:
402        """
403        Возвращает количество уникальных пользователей в системе.
404
405        Returns:
406            int: Количество пользователей, имеющих хотя бы одну задачу
407        """
408        with self._get_conn() as conn:
409            row = conn.execute("SELECT COUNT(DISTINCT user_id) FROM tasks").fetchone()
410            return row[0]

Возвращает количество уникальных пользователей в системе.

Returns:

int: Количество пользователей, имеющих хотя бы одну задачу

@observe(name='Get all user IDs')
def get_all_user_ids(self) -> List[str]:
412    @observe(name="Get all user IDs")
413    def get_all_user_ids(self) -> List[str]:
414        """
415        Возвращает список всех идентификаторов пользователей.
416
417        Returns:
418            List[str]: Список user_id всех пользователей, имеющих задачи
419        """
420        with self._get_conn() as conn:
421            rows = conn.execute("SELECT DISTINCT user_id FROM tasks").fetchall()
422            return [row["user_id"] for row in rows]

Возвращает список всех идентификаторов пользователей.

Returns:

List[str]: Список user_id всех пользователей, имеющих задачи

@observe(name='Get today date')
def get_today_date(self) -> str:
424    @observe(name="Get today date")
425    def get_today_date(self) -> str:
426        """
427        Возвращает текущую дату в формате YYYY-MM-DD.
428
429        Returns:
430            str: Сегодняшняя дата
431        """
432        return datetime.now().strftime("%Y-%m-%d")

Возвращает текущую дату в формате YYYY-MM-DD.

Returns:

str: Сегодняшняя дата

@observe(name='Parse date from text')
def parse_date(self, date_str: str) -> str:
434    @observe(name="Parse date from text")
435    def parse_date(self, date_str: str) -> str:
436        """
437        Преобразует текстовое представление даты в формат YYYY-MM-DD.
438
439        Поддерживает: "сегодня", "завтра", "послезавтра", "ДД.ММ", "ДД.ММ.ГГГГ".
440
441        Args:
442            date_str: Текстовое представление даты
443
444        Returns:
445            str: Дата в формате YYYY-MM-DD
446        """
447        date_str = date_str.lower().strip()
448        today = datetime.now()
449
450        date_map = {
451            "сегодня": 0,
452            "на сегодня": 0,
453            "завтра": 1,
454            "на завтра": 1,
455            "послезавтра": 2,
456            "на послезавтра": 2,
457        }
458
459        if date_str in date_map:
460            target_date = today + timedelta(days=date_map[date_str])
461            return target_date.strftime("%Y-%m-%d")
462
463        try:
464            if "." in date_str:
465                parts = date_str.split(".")
466                if len(parts) == 2:
467                    day, month = map(int, parts)
468                    year = today.year
469                    datetime(year, month, day)
470                    return f"{year:04d}-{month:02d}-{day:02d}"
471                elif len(parts) == 3:
472                    day, month, year = map(int, parts)
473                    datetime(year, month, day)
474                    return f"{year:04d}-{month:02d}-{day:02d}"
475        except (ValueError, TypeError):
476            pass
477
478        return self.get_today_date()

Преобразует текстовое представление даты в формат YYYY-MM-DD.

Поддерживает: "сегодня", "завтра", "послезавтра", "ДД.ММ", "ДД.ММ.ГГГГ".

Arguments:
  • date_str: Текстовое представление даты
Returns:

str: Дата в формате YYYY-MM-DD

@observe(name='Format task list')
def format_task_list_truncated( self, tasks: List[Dict], show_completed: bool = False, max_chars: int = 1024) -> str:
480    @observe(name="Format task list")
481    def format_task_list_truncated(
482        self, tasks: List[Dict], show_completed: bool = False, max_chars: int = 1024
483    ) -> str:
484        """
485        Форматирует список задач в строку с учётом ограничения длины.
486
487        Задачи выводятся в обратном порядке (новые сверху).
488        Если список не помещается в лимит, добавляется строка
489        "и еще N задач" или "Не показано ни одной задачи".
490
491        Args:
492            tasks: Список задач (словарей)
493            show_completed: Показывать ли выполненные задачи
494            max_chars: Максимальная длина строки (по умолчанию 1024)
495
496        Returns:
497            str: Отформатированный список задач
498        """
499        if not tasks:
500            return "Задач нет."
501
502        if show_completed:
503            raw_lines = [f"{task['id']}. {task['text']}" for task in tasks]
504        else:
505            raw_lines = [f"{task['id']}. {task['text']}" for task in tasks if not task["completed"]]
506
507        if not raw_lines:
508            return "Невыполненных задач нет."
509
510        raw_lines.reverse()
511        total = len(raw_lines)
512        tail_template = "\nи еще {} задач"
513        best_lines = []
514        used_chars = 0
515
516        for line in raw_lines:
517            tail = (
518                tail_template.format(total - len(best_lines) - 1) if len(best_lines) < total else ""
519            )
520            candidate = "\n".join(best_lines + [line]) + tail
521            if len(candidate) > max_chars:
522                break
523            best_lines.append(line)
524            used_chars = len("\n".join(best_lines))
525
526        shown = len(best_lines)
527        if shown == total:
528            if shown == 1:
529                return f"Задача: {best_lines[0]}"
530            return "Вот ваши задачи: \n" + "\n".join(best_lines)
531
532        tail = tail_template.format(total - shown)
533        if shown == 0:
534            return f"Не показано ни одной задачи. {tail.strip()}"
535
536        return "\n".join(best_lines) + tail

Форматирует список задач в строку с учётом ограничения длины.

Задачи выводятся в обратном порядке (новые сверху). Если список не помещается в лимит, добавляется строка "и еще N задач" или "Не показано ни одной задачи".

Arguments:
  • tasks: Список задач (словарей)
  • show_completed: Показывать ли выполненные задачи
  • max_chars: Максимальная длина строки (по умолчанию 1024)
Returns:

str: Отформатированный список задач

@observe(name='Get incomplete tasks summary')
def get_incomplete_tasks_summary(self, user_id: str) -> Dict[str, int]:
538    @observe(name="Get incomplete tasks summary")
539    def get_incomplete_tasks_summary(self, user_id: str) -> Dict[str, int]:
540        """
541        Возвращает сводку по невыполненным задачам пользователя.
542
543        Подсчитывает общее количество невыполненных задач, а также
544        количество на сегодня и завтра.
545
546        Args:
547            user_id: Идентификатор пользователя
548
549        Returns:
550            Dict[str, int]: Словарь с ключами total, today, tomorrow
551        """
552        today = self.get_today_date()
553        tomorrow = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
554
555        rows = (
556            self
557            ._get_conn()
558            .execute(
559                "SELECT date, COUNT(*) as cnt FROM tasks WHERE user_id = ? AND completed = 0 GROUP BY date",
560                (user_id,),
561            )
562            .fetchall()
563        )
564
565        total = sum(r["cnt"] for r in rows)
566        today_count = 0
567        tomorrow_count = 0
568
569        for r in rows:
570            if r["date"] == today:
571                today_count = r["cnt"]
572            if r["date"] == tomorrow:
573                tomorrow_count = r["cnt"]
574
575        return {"total": total, "today": today_count, "tomorrow": tomorrow_count}

Возвращает сводку по невыполненным задачам пользователя.

Подсчитывает общее количество невыполненных задач, а также количество на сегодня и завтра.

Arguments:
  • user_id: Идентификатор пользователя
Returns:

Dict[str, int]: Словарь с ключами total, today, tomorrow