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(
136                f"Миграция завершена. tasks.json → tasks.db, бэкап: {TASKS_FILE}.bak"
137            )
138        except Exception as e:
139            logger.error(f"Ошибка миграции из JSON: {e}")
140
141    @observe(name="Get user tasks")
142    def get_user_tasks(self, user_id: str) -> List[Dict]:
143        """
144        Возвращает все задачи указанного пользователя.
145
146        Args:
147            user_id: Идентификатор пользователя
148
149        Returns:
150            List[Dict]: Список задач пользователя, отсортированных по ID
151        """
152        with self._get_conn() as conn:
153            rows = conn.execute(
154                "SELECT * FROM tasks WHERE user_id = ? ORDER BY id", (user_id,)
155            ).fetchall()
156            return [_convert_task(row) for row in rows]
157
158    @observe(name="Get next task ID")
159    def get_next_task_id(self) -> int:
160        """
161        Возвращает следующий доступный ID для новой задачи.
162
163        Returns:
164            int: Следующий ID (MAX(id) + 1) или 1 если задач нет
165        """
166        with self._get_conn() as conn:
167            row = conn.execute("SELECT COALESCE(MAX(id), 0) + 1 FROM tasks").fetchone()
168            return row[0]
169
170    @observe(name="Add task")
171    def add_task(self, user_id: str, text: str, date: str | None = None) -> int:
172        """
173        Добавляет новую задачу для пользователя.
174
175        Args:
176            user_id: Идентификатор пользователя
177            text: Текст задачи
178            date: Дата задачи в формате YYYY-MM-DD (по умолчанию — 01.01.2100)
179
180        Returns:
181            int: ID созданной задачи
182        """
183        if date is None:
184            date = "2100-01-01"
185
186        with self._get_conn() as conn:
187            cur = conn.execute(
188                "INSERT INTO tasks (user_id, text, date, completed, created_at) VALUES (?, ?, ?, 0, ?)",
189                (user_id, text, date, datetime.now().isoformat()),
190            )
191            from typing import cast
192
193            task_id = cast(int, cur.lastrowid)
194            logger.debug(f"Добавлена задача {task_id} для пользователя {user_id}")
195            return task_id
196
197    @observe(name="Get tasks by date")
198    def get_tasks_by_date(
199        self, user_id: str, date: Optional[str] = None, completed: Optional[bool] = None
200    ) -> List[Dict]:
201        """
202        Возвращает задачи пользователя на указанную дату.
203
204        Args:
205            user_id: Идентификатор пользователя
206            date: Дата в формате YYYY-MM-DD (по умолчанию — сегодня)
207            completed: Фильтр по статусу выполнения (None — все задачи)
208
209        Returns:
210            List[Dict]: Список задач на указанную дату
211        """
212        if date is None:
213            date = self.get_today_date()
214
215        with self._get_conn() as conn:
216            if completed is None:
217                rows = conn.execute(
218                    "SELECT * FROM tasks WHERE user_id = ? AND date = ? ORDER BY id",
219                    (user_id, date),
220                ).fetchall()
221            else:
222                rows = conn.execute(
223                    "SELECT * FROM tasks WHERE user_id = ? AND date = ? AND completed = ? ORDER BY id",
224                    (user_id, date, 1 if completed else 0),
225                ).fetchall()
226            return [_convert_task(row) for row in rows]
227
228    def get_all_tasks(self, user_id: str) -> List[Dict]:
229        """Возвращает все невыполненные задачи на сегодня и позже."""
230        today = self.get_today_date()
231        with self._get_conn() as conn:
232            rows = conn.execute(
233                "SELECT * FROM tasks WHERE user_id = ? AND completed = 0 AND date >= ? ORDER BY date, id",
234                (user_id, today),
235            ).fetchall()
236            return [_convert_task(row) for row in rows]
237
238    @observe(name="Get task by ID")
239    def get_task(self, user_id: str, task_id: int) -> Optional[Dict]:
240        """
241        Возвращает задачу по её ID и идентификатору пользователя.
242
243        Args:
244            user_id: Идентификатор пользователя
245            task_id: Идентификатор задачи
246
247        Returns:
248            Optional[Dict]: Данные задачи или None, если задача не найдена
249        """
250        with self._get_conn() as conn:
251            row = conn.execute(
252                "SELECT * FROM tasks WHERE user_id = ? AND id = ?", (user_id, task_id)
253            ).fetchone()
254            return _convert_task(row) if row else None
255
256    @observe(name="Complete task")
257    def complete_task(self, user_id: str, task_id: int) -> bool:
258        """
259        Отмечает задачу как выполненную.
260
261        Устанавливает completed=1 и записывает временную метку завершения.
262
263        Args:
264            user_id: Идентификатор пользователя
265            task_id: Идентификатор задачи
266
267        Returns:
268            bool: True если задача найдена и обновлена, False в противном случае
269        """
270        with self._get_conn() as conn:
271            cur = conn.execute(
272                "UPDATE tasks SET completed = 1, completed_at = ? WHERE user_id = ? AND id = ?",
273                (datetime.now().isoformat(), user_id, task_id),
274            )
275            if cur.rowcount > 0:
276                logger.debug(f"Задача {task_id} отмечена как выполненная")
277                return True
278        return False
279
280    @observe(name="Move task")
281    def move_task(self, user_id: str, task_id: int, date: str) -> bool:
282        """
283        Переносит одну задачу на указанную дату.
284
285        Args:
286            user_id: Идентификатор пользователя
287            task_id: Идентификатор задачи
288            date: Новая дата в формате YYYY-MM-DD
289
290        Returns:
291            bool: True если задача найдена и перенесена, False в противном случае
292        """
293        with self._get_conn() as conn:
294            cur = conn.execute(
295                "UPDATE tasks SET date = ? WHERE user_id = ? AND id = ? AND completed = 0",
296                (date, user_id, task_id),
297            )
298            if cur.rowcount > 0:
299                logger.debug(f"Задача {task_id} перенесена на {date}")
300                return True
301        return False
302
303    @observe(name="Move tasks")
304    def move_tasks(self, user_id: str, date: str) -> bool:
305        """
306        Переносит все невыполненные задачи пользователя на указанную дату.
307
308        Args:
309            user_id: Идентификатор пользователя
310            date: Целевая дата в формате YYYY-MM-DD
311
312        Returns:
313            bool: True после завершения переноса
314        """
315        today = self.get_today_date()
316        with self._get_conn() as conn:
317            conn.execute(
318                "UPDATE tasks SET date = ? WHERE user_id = ? AND date = ? AND completed = 0",
319                (date, user_id, today),
320            )
321            logger.debug(f"Задачи пользователя {user_id} перенесены на {date}")
322        return True
323
324    @observe(name="Move incomplete tasks to today")
325    def move_incomplete_tasks_to_today(self) -> int:
326        """
327        Переносит все просроченные невыполненные задачи на сегодня.
328
329        Используется фоновым ассистентом OTM для ежедневного
330        автоматического переноса задач.
331
332        Returns:
333            int: Количество перенесённых задач
334        """
335        today = self.get_today_date()
336        with self._get_conn() as conn:
337            cur = conn.execute(
338                "UPDATE tasks SET date = ? WHERE completed = 0 AND date < ?",
339                (today, today),
340            )
341            moved = cur.rowcount
342            if moved > 0:
343                logger.debug(f"Перенесено {moved} задач на сегодня")
344        return moved
345
346    @observe(name="Delete task")
347    def delete_task(self, user_id: str, task_id: int) -> bool:
348        """
349        Удаляет задачу по её ID.
350
351        Args:
352            user_id: Идентификатор пользователя
353            task_id: Идентификатор задачи
354
355        Returns:
356            bool: True если задача найдена и удалена, False в противном случае
357        """
358        with self._get_conn() as conn:
359            cur = conn.execute(
360                "DELETE FROM tasks WHERE user_id = ? AND id = ?", (user_id, task_id)
361            )
362            if cur.rowcount > 0:
363                logger.debug(f"Задача {task_id} удалена")
364                return True
365        return False
366
367    @observe(name="Delete user tasks")
368    def delete_user_tasks(self, user_id: str) -> int:
369        """
370        Удаляет все задачи указанного пользователя.
371
372        Args:
373            user_id: Идентификатор пользователя
374
375        Returns:
376            int: Количество удалённых задач
377        """
378        with self._get_conn() as conn:
379            cur = conn.execute("DELETE FROM tasks WHERE user_id = ?", (user_id,))
380            logger.debug(f"Удалено {cur.rowcount} задач пользователя {user_id}")
381            return cur.rowcount
382
383    @observe(name="Get incomplete tasks count")
384    def get_incomplete_tasks_count(
385        self, user_id: str, date: Optional[str] = None
386    ) -> int:
387        """
388        Возвращает количество невыполненных задач пользователя на дату.
389
390        Args:
391            user_id: Идентификатор пользователя
392            date: Дата в формате YYYY-MM-DD (по умолчанию — сегодня)
393
394        Returns:
395            int: Количество невыполненных задач
396        """
397        if date is None:
398            date = self.get_today_date()
399        with self._get_conn() as conn:
400            row = conn.execute(
401                "SELECT COUNT(*) FROM tasks WHERE user_id = ? AND date = ? AND completed = 0",
402                (user_id, date),
403            ).fetchone()
404            return row[0]
405
406    def get_user_count(self) -> int:
407        """
408        Возвращает количество уникальных пользователей в системе.
409
410        Returns:
411            int: Количество пользователей, имеющих хотя бы одну задачу
412        """
413        with self._get_conn() as conn:
414            row = conn.execute("SELECT COUNT(DISTINCT user_id) FROM tasks").fetchone()
415            return row[0]
416
417    @observe(name="Get all user IDs")
418    def get_all_user_ids(self) -> List[str]:
419        """
420        Возвращает список всех идентификаторов пользователей.
421
422        Returns:
423            List[str]: Список user_id всех пользователей, имеющих задачи
424        """
425        with self._get_conn() as conn:
426            rows = conn.execute("SELECT DISTINCT user_id FROM tasks").fetchall()
427            return [row["user_id"] for row in rows]
428
429    @observe(name="Get today date")
430    def get_today_date(self) -> str:
431        """
432        Возвращает текущую дату в формате YYYY-MM-DD.
433
434        Returns:
435            str: Сегодняшняя дата
436        """
437        return datetime.now().strftime("%Y-%m-%d")
438
439    @observe(name="Format task list")
440    def format_task_list_truncated(
441        self, tasks: List[Dict], show_completed: bool = False, max_chars: int = 1024
442    ) -> str:
443        """
444        Форматирует список задач в строку с учётом ограничения длины.
445
446        Задачи выводятся в обратном порядке (новые сверху).
447        Если список не помещается в лимит, добавляется строка
448        "и еще N задач" или "Не показано ни одной задачи".
449
450        Args:
451            tasks: Список задач (словарей)
452            show_completed: Показывать ли выполненные задачи
453            max_chars: Максимальная длина строки (по умолчанию 1024)
454
455        Returns:
456            str: Отформатированный список задач
457        """
458        if not tasks:
459            return "Задач нет."
460
461        if show_completed:
462            raw_lines = [f"{task['id']}. {task['text']}" for task in tasks]
463        else:
464            raw_lines = [
465                f"{task['id']}. {task['text']}"
466                for task in tasks
467                if not task["completed"]
468            ]
469
470        if not raw_lines:
471            return "Невыполненных задач нет."
472
473        raw_lines.reverse()
474        total = len(raw_lines)
475        tail_template = "\nи еще {} задач"
476        best_lines = []
477
478        for line in raw_lines:
479            tail = (
480                tail_template.format(total - len(best_lines) - 1)
481                if len(best_lines) < total
482                else ""
483            )
484            candidate = "\n".join(best_lines + [line]) + tail
485            if len(candidate) > max_chars:
486                break
487            best_lines.append(line)
488
489        shown = len(best_lines)
490        if shown == total:
491            if shown == 1:
492                return f"Задача: {best_lines[0]}"
493            return "Вот ваши задачи: \n" + "\n".join(best_lines)
494
495        tail = tail_template.format(total - shown)
496        if shown == 0:
497            return f"Не показано ни одной задачи. {tail.strip()}"
498
499        return "\n".join(best_lines) + tail
500
501    @observe(name="Get incomplete tasks summary")
502    def get_incomplete_tasks_summary(self, user_id: str) -> Dict[str, int]:
503        """
504        Возвращает сводку по невыполненным задачам пользователя.
505
506        Подсчитывает общее количество невыполненных задач, а также
507        количество на сегодня и завтра.
508
509        Args:
510            user_id: Идентификатор пользователя
511
512        Returns:
513            Dict[str, int]: Словарь с ключами total, today, tomorrow
514        """
515        today = self.get_today_date()
516        tomorrow = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
517
518        rows = (
519            self._get_conn()
520            .execute(
521                "SELECT date, COUNT(*) as cnt FROM tasks WHERE user_id = ? AND completed = 0 GROUP BY date",
522                (user_id,),
523            )
524            .fetchall()
525        )
526
527        total = sum(r["cnt"] for r in rows)
528        today_count = 0
529        tomorrow_count = 0
530
531        for r in rows:
532            if r["date"] == today:
533                today_count = r["cnt"]
534            if r["date"] == tomorrow:
535                tomorrow_count = r["cnt"]
536
537        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(
137                f"Миграция завершена. tasks.json → tasks.db, бэкап: {TASKS_FILE}.bak"
138            )
139        except Exception as e:
140            logger.error(f"Ошибка миграции из JSON: {e}")
141
142    @observe(name="Get user tasks")
143    def get_user_tasks(self, user_id: str) -> List[Dict]:
144        """
145        Возвращает все задачи указанного пользователя.
146
147        Args:
148            user_id: Идентификатор пользователя
149
150        Returns:
151            List[Dict]: Список задач пользователя, отсортированных по ID
152        """
153        with self._get_conn() as conn:
154            rows = conn.execute(
155                "SELECT * FROM tasks WHERE user_id = ? ORDER BY id", (user_id,)
156            ).fetchall()
157            return [_convert_task(row) for row in rows]
158
159    @observe(name="Get next task ID")
160    def get_next_task_id(self) -> int:
161        """
162        Возвращает следующий доступный ID для новой задачи.
163
164        Returns:
165            int: Следующий ID (MAX(id) + 1) или 1 если задач нет
166        """
167        with self._get_conn() as conn:
168            row = conn.execute("SELECT COALESCE(MAX(id), 0) + 1 FROM tasks").fetchone()
169            return row[0]
170
171    @observe(name="Add task")
172    def add_task(self, user_id: str, text: str, date: str | None = None) -> int:
173        """
174        Добавляет новую задачу для пользователя.
175
176        Args:
177            user_id: Идентификатор пользователя
178            text: Текст задачи
179            date: Дата задачи в формате YYYY-MM-DD (по умолчанию — 01.01.2100)
180
181        Returns:
182            int: ID созданной задачи
183        """
184        if date is None:
185            date = "2100-01-01"
186
187        with self._get_conn() as conn:
188            cur = conn.execute(
189                "INSERT INTO tasks (user_id, text, date, completed, created_at) VALUES (?, ?, ?, 0, ?)",
190                (user_id, text, date, datetime.now().isoformat()),
191            )
192            from typing import cast
193
194            task_id = cast(int, cur.lastrowid)
195            logger.debug(f"Добавлена задача {task_id} для пользователя {user_id}")
196            return task_id
197
198    @observe(name="Get tasks by date")
199    def get_tasks_by_date(
200        self, user_id: str, date: Optional[str] = None, completed: Optional[bool] = None
201    ) -> List[Dict]:
202        """
203        Возвращает задачи пользователя на указанную дату.
204
205        Args:
206            user_id: Идентификатор пользователя
207            date: Дата в формате YYYY-MM-DD (по умолчанию — сегодня)
208            completed: Фильтр по статусу выполнения (None — все задачи)
209
210        Returns:
211            List[Dict]: Список задач на указанную дату
212        """
213        if date is None:
214            date = self.get_today_date()
215
216        with self._get_conn() as conn:
217            if completed is None:
218                rows = conn.execute(
219                    "SELECT * FROM tasks WHERE user_id = ? AND date = ? ORDER BY id",
220                    (user_id, date),
221                ).fetchall()
222            else:
223                rows = conn.execute(
224                    "SELECT * FROM tasks WHERE user_id = ? AND date = ? AND completed = ? ORDER BY id",
225                    (user_id, date, 1 if completed else 0),
226                ).fetchall()
227            return [_convert_task(row) for row in rows]
228
229    def get_all_tasks(self, user_id: str) -> List[Dict]:
230        """Возвращает все невыполненные задачи на сегодня и позже."""
231        today = self.get_today_date()
232        with self._get_conn() as conn:
233            rows = conn.execute(
234                "SELECT * FROM tasks WHERE user_id = ? AND completed = 0 AND date >= ? ORDER BY date, id",
235                (user_id, today),
236            ).fetchall()
237            return [_convert_task(row) for row in rows]
238
239    @observe(name="Get task by ID")
240    def get_task(self, user_id: str, task_id: int) -> Optional[Dict]:
241        """
242        Возвращает задачу по её ID и идентификатору пользователя.
243
244        Args:
245            user_id: Идентификатор пользователя
246            task_id: Идентификатор задачи
247
248        Returns:
249            Optional[Dict]: Данные задачи или None, если задача не найдена
250        """
251        with self._get_conn() as conn:
252            row = conn.execute(
253                "SELECT * FROM tasks WHERE user_id = ? AND id = ?", (user_id, task_id)
254            ).fetchone()
255            return _convert_task(row) if row else None
256
257    @observe(name="Complete task")
258    def complete_task(self, user_id: str, task_id: int) -> bool:
259        """
260        Отмечает задачу как выполненную.
261
262        Устанавливает completed=1 и записывает временную метку завершения.
263
264        Args:
265            user_id: Идентификатор пользователя
266            task_id: Идентификатор задачи
267
268        Returns:
269            bool: True если задача найдена и обновлена, False в противном случае
270        """
271        with self._get_conn() as conn:
272            cur = conn.execute(
273                "UPDATE tasks SET completed = 1, completed_at = ? WHERE user_id = ? AND id = ?",
274                (datetime.now().isoformat(), user_id, task_id),
275            )
276            if cur.rowcount > 0:
277                logger.debug(f"Задача {task_id} отмечена как выполненная")
278                return True
279        return False
280
281    @observe(name="Move task")
282    def move_task(self, user_id: str, task_id: int, date: str) -> bool:
283        """
284        Переносит одну задачу на указанную дату.
285
286        Args:
287            user_id: Идентификатор пользователя
288            task_id: Идентификатор задачи
289            date: Новая дата в формате YYYY-MM-DD
290
291        Returns:
292            bool: True если задача найдена и перенесена, False в противном случае
293        """
294        with self._get_conn() as conn:
295            cur = conn.execute(
296                "UPDATE tasks SET date = ? WHERE user_id = ? AND id = ? AND completed = 0",
297                (date, user_id, task_id),
298            )
299            if cur.rowcount > 0:
300                logger.debug(f"Задача {task_id} перенесена на {date}")
301                return True
302        return False
303
304    @observe(name="Move tasks")
305    def move_tasks(self, user_id: str, date: str) -> bool:
306        """
307        Переносит все невыполненные задачи пользователя на указанную дату.
308
309        Args:
310            user_id: Идентификатор пользователя
311            date: Целевая дата в формате YYYY-MM-DD
312
313        Returns:
314            bool: True после завершения переноса
315        """
316        today = self.get_today_date()
317        with self._get_conn() as conn:
318            conn.execute(
319                "UPDATE tasks SET date = ? WHERE user_id = ? AND date = ? AND completed = 0",
320                (date, user_id, today),
321            )
322            logger.debug(f"Задачи пользователя {user_id} перенесены на {date}")
323        return True
324
325    @observe(name="Move incomplete tasks to today")
326    def move_incomplete_tasks_to_today(self) -> int:
327        """
328        Переносит все просроченные невыполненные задачи на сегодня.
329
330        Используется фоновым ассистентом OTM для ежедневного
331        автоматического переноса задач.
332
333        Returns:
334            int: Количество перенесённых задач
335        """
336        today = self.get_today_date()
337        with self._get_conn() as conn:
338            cur = conn.execute(
339                "UPDATE tasks SET date = ? WHERE completed = 0 AND date < ?",
340                (today, today),
341            )
342            moved = cur.rowcount
343            if moved > 0:
344                logger.debug(f"Перенесено {moved} задач на сегодня")
345        return moved
346
347    @observe(name="Delete task")
348    def delete_task(self, user_id: str, task_id: int) -> bool:
349        """
350        Удаляет задачу по её ID.
351
352        Args:
353            user_id: Идентификатор пользователя
354            task_id: Идентификатор задачи
355
356        Returns:
357            bool: True если задача найдена и удалена, False в противном случае
358        """
359        with self._get_conn() as conn:
360            cur = conn.execute(
361                "DELETE FROM tasks WHERE user_id = ? AND id = ?", (user_id, task_id)
362            )
363            if cur.rowcount > 0:
364                logger.debug(f"Задача {task_id} удалена")
365                return True
366        return False
367
368    @observe(name="Delete user tasks")
369    def delete_user_tasks(self, user_id: str) -> int:
370        """
371        Удаляет все задачи указанного пользователя.
372
373        Args:
374            user_id: Идентификатор пользователя
375
376        Returns:
377            int: Количество удалённых задач
378        """
379        with self._get_conn() as conn:
380            cur = conn.execute("DELETE FROM tasks WHERE user_id = ?", (user_id,))
381            logger.debug(f"Удалено {cur.rowcount} задач пользователя {user_id}")
382            return cur.rowcount
383
384    @observe(name="Get incomplete tasks count")
385    def get_incomplete_tasks_count(
386        self, user_id: str, date: Optional[str] = None
387    ) -> int:
388        """
389        Возвращает количество невыполненных задач пользователя на дату.
390
391        Args:
392            user_id: Идентификатор пользователя
393            date: Дата в формате YYYY-MM-DD (по умолчанию — сегодня)
394
395        Returns:
396            int: Количество невыполненных задач
397        """
398        if date is None:
399            date = self.get_today_date()
400        with self._get_conn() as conn:
401            row = conn.execute(
402                "SELECT COUNT(*) FROM tasks WHERE user_id = ? AND date = ? AND completed = 0",
403                (user_id, date),
404            ).fetchone()
405            return row[0]
406
407    def get_user_count(self) -> int:
408        """
409        Возвращает количество уникальных пользователей в системе.
410
411        Returns:
412            int: Количество пользователей, имеющих хотя бы одну задачу
413        """
414        with self._get_conn() as conn:
415            row = conn.execute("SELECT COUNT(DISTINCT user_id) FROM tasks").fetchone()
416            return row[0]
417
418    @observe(name="Get all user IDs")
419    def get_all_user_ids(self) -> List[str]:
420        """
421        Возвращает список всех идентификаторов пользователей.
422
423        Returns:
424            List[str]: Список user_id всех пользователей, имеющих задачи
425        """
426        with self._get_conn() as conn:
427            rows = conn.execute("SELECT DISTINCT user_id FROM tasks").fetchall()
428            return [row["user_id"] for row in rows]
429
430    @observe(name="Get today date")
431    def get_today_date(self) -> str:
432        """
433        Возвращает текущую дату в формате YYYY-MM-DD.
434
435        Returns:
436            str: Сегодняшняя дата
437        """
438        return datetime.now().strftime("%Y-%m-%d")
439
440    @observe(name="Format task list")
441    def format_task_list_truncated(
442        self, tasks: List[Dict], show_completed: bool = False, max_chars: int = 1024
443    ) -> str:
444        """
445        Форматирует список задач в строку с учётом ограничения длины.
446
447        Задачи выводятся в обратном порядке (новые сверху).
448        Если список не помещается в лимит, добавляется строка
449        "и еще N задач" или "Не показано ни одной задачи".
450
451        Args:
452            tasks: Список задач (словарей)
453            show_completed: Показывать ли выполненные задачи
454            max_chars: Максимальная длина строки (по умолчанию 1024)
455
456        Returns:
457            str: Отформатированный список задач
458        """
459        if not tasks:
460            return "Задач нет."
461
462        if show_completed:
463            raw_lines = [f"{task['id']}. {task['text']}" for task in tasks]
464        else:
465            raw_lines = [
466                f"{task['id']}. {task['text']}"
467                for task in tasks
468                if not task["completed"]
469            ]
470
471        if not raw_lines:
472            return "Невыполненных задач нет."
473
474        raw_lines.reverse()
475        total = len(raw_lines)
476        tail_template = "\nи еще {} задач"
477        best_lines = []
478
479        for line in raw_lines:
480            tail = (
481                tail_template.format(total - len(best_lines) - 1)
482                if len(best_lines) < total
483                else ""
484            )
485            candidate = "\n".join(best_lines + [line]) + tail
486            if len(candidate) > max_chars:
487                break
488            best_lines.append(line)
489
490        shown = len(best_lines)
491        if shown == total:
492            if shown == 1:
493                return f"Задача: {best_lines[0]}"
494            return "Вот ваши задачи: \n" + "\n".join(best_lines)
495
496        tail = tail_template.format(total - shown)
497        if shown == 0:
498            return f"Не показано ни одной задачи. {tail.strip()}"
499
500        return "\n".join(best_lines) + tail
501
502    @observe(name="Get incomplete tasks summary")
503    def get_incomplete_tasks_summary(self, user_id: str) -> Dict[str, int]:
504        """
505        Возвращает сводку по невыполненным задачам пользователя.
506
507        Подсчитывает общее количество невыполненных задач, а также
508        количество на сегодня и завтра.
509
510        Args:
511            user_id: Идентификатор пользователя
512
513        Returns:
514            Dict[str, int]: Словарь с ключами total, today, tomorrow
515        """
516        today = self.get_today_date()
517        tomorrow = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
518
519        rows = (
520            self._get_conn()
521            .execute(
522                "SELECT date, COUNT(*) as cnt FROM tasks WHERE user_id = ? AND completed = 0 GROUP BY date",
523                (user_id,),
524            )
525            .fetchall()
526        )
527
528        total = sum(r["cnt"] for r in rows)
529        today_count = 0
530        tomorrow_count = 0
531
532        for r in rows:
533            if r["date"] == today:
534                today_count = r["cnt"]
535            if r["date"] == tomorrow:
536                tomorrow_count = r["cnt"]
537
538        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 если такой файл существует.

Args: db_path: Путь к файлу базы данных SQLite

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

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

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

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

@observe(name='Get next task ID')
def get_next_task_id(self) -> int:
159    @observe(name="Get next task ID")
160    def get_next_task_id(self) -> int:
161        """
162        Возвращает следующий доступный ID для новой задачи.
163
164        Returns:
165            int: Следующий ID (MAX(id) + 1) или 1 если задач нет
166        """
167        with self._get_conn() as conn:
168            row = conn.execute("SELECT COALESCE(MAX(id), 0) + 1 FROM tasks").fetchone()
169            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:
171    @observe(name="Add task")
172    def add_task(self, user_id: str, text: str, date: str | None = None) -> int:
173        """
174        Добавляет новую задачу для пользователя.
175
176        Args:
177            user_id: Идентификатор пользователя
178            text: Текст задачи
179            date: Дата задачи в формате YYYY-MM-DD (по умолчанию — 01.01.2100)
180
181        Returns:
182            int: ID созданной задачи
183        """
184        if date is None:
185            date = "2100-01-01"
186
187        with self._get_conn() as conn:
188            cur = conn.execute(
189                "INSERT INTO tasks (user_id, text, date, completed, created_at) VALUES (?, ?, ?, 0, ?)",
190                (user_id, text, date, datetime.now().isoformat()),
191            )
192            from typing import cast
193
194            task_id = cast(int, cur.lastrowid)
195            logger.debug(f"Добавлена задача {task_id} для пользователя {user_id}")
196            return task_id

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

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

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

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

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

def get_all_tasks(self, user_id: str) -> List[Dict]:
229    def get_all_tasks(self, user_id: str) -> List[Dict]:
230        """Возвращает все невыполненные задачи на сегодня и позже."""
231        today = self.get_today_date()
232        with self._get_conn() as conn:
233            rows = conn.execute(
234                "SELECT * FROM tasks WHERE user_id = ? AND completed = 0 AND date >= ? ORDER BY date, id",
235                (user_id, today),
236            ).fetchall()
237            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]:
239    @observe(name="Get task by ID")
240    def get_task(self, user_id: str, task_id: int) -> Optional[Dict]:
241        """
242        Возвращает задачу по её ID и идентификатору пользователя.
243
244        Args:
245            user_id: Идентификатор пользователя
246            task_id: Идентификатор задачи
247
248        Returns:
249            Optional[Dict]: Данные задачи или None, если задача не найдена
250        """
251        with self._get_conn() as conn:
252            row = conn.execute(
253                "SELECT * FROM tasks WHERE user_id = ? AND id = ?", (user_id, task_id)
254            ).fetchone()
255            return _convert_task(row) if row else None

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@observe(name='Format task list')
def format_task_list_truncated( self, tasks: List[Dict], show_completed: bool = False, max_chars: int = 1024) -> str:
440    @observe(name="Format task list")
441    def format_task_list_truncated(
442        self, tasks: List[Dict], show_completed: bool = False, max_chars: int = 1024
443    ) -> str:
444        """
445        Форматирует список задач в строку с учётом ограничения длины.
446
447        Задачи выводятся в обратном порядке (новые сверху).
448        Если список не помещается в лимит, добавляется строка
449        "и еще N задач" или "Не показано ни одной задачи".
450
451        Args:
452            tasks: Список задач (словарей)
453            show_completed: Показывать ли выполненные задачи
454            max_chars: Максимальная длина строки (по умолчанию 1024)
455
456        Returns:
457            str: Отформатированный список задач
458        """
459        if not tasks:
460            return "Задач нет."
461
462        if show_completed:
463            raw_lines = [f"{task['id']}. {task['text']}" for task in tasks]
464        else:
465            raw_lines = [
466                f"{task['id']}. {task['text']}"
467                for task in tasks
468                if not task["completed"]
469            ]
470
471        if not raw_lines:
472            return "Невыполненных задач нет."
473
474        raw_lines.reverse()
475        total = len(raw_lines)
476        tail_template = "\nи еще {} задач"
477        best_lines = []
478
479        for line in raw_lines:
480            tail = (
481                tail_template.format(total - len(best_lines) - 1)
482                if len(best_lines) < total
483                else ""
484            )
485            candidate = "\n".join(best_lines + [line]) + tail
486            if len(candidate) > max_chars:
487                break
488            best_lines.append(line)
489
490        shown = len(best_lines)
491        if shown == total:
492            if shown == 1:
493                return f"Задача: {best_lines[0]}"
494            return "Вот ваши задачи: \n" + "\n".join(best_lines)
495
496        tail = tail_template.format(total - shown)
497        if shown == 0:
498            return f"Не показано ни одной задачи. {tail.strip()}"
499
500        return "\n".join(best_lines) + tail

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

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

Args: 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]:
502    @observe(name="Get incomplete tasks summary")
503    def get_incomplete_tasks_summary(self, user_id: str) -> Dict[str, int]:
504        """
505        Возвращает сводку по невыполненным задачам пользователя.
506
507        Подсчитывает общее количество невыполненных задач, а также
508        количество на сегодня и завтра.
509
510        Args:
511            user_id: Идентификатор пользователя
512
513        Returns:
514            Dict[str, int]: Словарь с ключами total, today, tomorrow
515        """
516        today = self.get_today_date()
517        tomorrow = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
518
519        rows = (
520            self._get_conn()
521            .execute(
522                "SELECT date, COUNT(*) as cnt FROM tasks WHERE user_id = ? AND completed = 0 GROUP BY date",
523                (user_id,),
524            )
525            .fetchall()
526        )
527
528        total = sum(r["cnt"] for r in rows)
529        today_count = 0
530        tomorrow_count = 0
531
532        for r in rows:
533            if r["date"] == today:
534                today_count = r["cnt"]
535            if r["date"] == tomorrow:
536                tomorrow_count = r["cnt"]
537
538        return {"total": total, "today": today_count, "tomorrow": tomorrow_count}

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

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

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

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