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