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}
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 символа).
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
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
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 если задач нет
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 созданной задачи
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]: Список задач на указанную дату
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]
Возвращает все невыполненные задачи на сегодня и позже.
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, если задача не найдена
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 в противном случае
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 в противном случае
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 после завершения переноса
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: Количество перенесённых задач
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 в противном случае
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: Количество удалённых задач
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: Количество невыполненных задач
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: Количество пользователей, имеющих хотя бы одну задачу
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 всех пользователей, имеющих задачи
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: Сегодняшняя дата
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: Отформатированный список задач
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