thingman
Управление местоположений вещей: CRUD-операции с SQLite.
Предоставляет класс ThingManager для работы с записями о вещях и их местах, включая добавление, обновление, поиск и удаление локаций. Хранение осуществляется в SQLite.
1""" 2Управление местоположений вещей: CRUD-операции с SQLite. 3 4Предоставляет класс ThingManager для работы с записями о вещях и их местах, 5включая добавление, обновление, поиск и удаление локаций. 6Хранение осуществляется в SQLite. 7""" 8 9import os 10import sqlite3 11from datetime import datetime 12from typing import Dict, List, Optional, cast 13 14from langfuse import observe 15from loguru import logger 16 17DATA_DIR = os.getenv("DATA_DIR", "./data") 18DB_FILE = os.path.join(DATA_DIR, "locations.db") 19 20 21def _convert_location(row: sqlite3.Row) -> Dict: 22 """ 23 Преобразует строку из SQLite (sqlite3.Row) в словарь. 24 25 Args: 26 row: Строка результата запроса SQLite 27 28 Returns: 29 Dict: Словарь с данными локации 30 """ 31 return dict(row) 32 33 34def _common_prefix_len(a: str, b: str) -> int: 35 """ 36 Длина общего префикса двух строк. 37 38 Считает количество совпадающих символов с начала строк (для учета 39 словоформ: 'болгарку' и 'болгарка' дают 7). 40 41 Args: 42 a: Первая строка 43 b: Вторая строка 44 45 Returns: 46 int: Длина общего префикса 47 """ 48 for i, (ca, cb) in enumerate(zip(a, b)): 49 if ca != cb: 50 return i 51 return min(len(a), len(b)) 52 53 54def _token_in_thing(token: str, thing: str) -> bool: 55 """ 56 Проверяет, входит ли токен запроса в название вещи. 57 58 Ищет вхождение подстроки, совпадение на уровне отдельного слова 59 и морфологическую близость (длинный общий префикс — например, 60 'болгарку' vs 'болгарка'). 61 62 Args: 63 token: Токен поискового запроса 64 thing: Название вещи 65 66 Returns: 67 bool: True, если токен релевантен для названия вещи 68 """ 69 if token in thing: 70 return True 71 for word in thing.split(): 72 if token in word or word in token: 73 return True 74 common = _common_prefix_len(token, word) 75 if common >= 3 and common >= 0.6 * min(len(token), len(word)): 76 return True 77 return False 78 79 80class ThingManager: 81 """ 82 Менеджер вещей с хранением в SQLite. 83 84 Предоставляет полный набор CRUD-операций для управления записями о вещях 85 пользователей. Поддерживает поиск по thing и возврат всех мест. 86 """ 87 88 def __init__(self, db_path: str = DB_FILE): 89 """ 90 Инициализирует ThingManager, создавая/подключая БД SQLite. 91 92 При первом запуске создаёт необходимые таблицы и индексы. 93 94 Args: 95 db_path: Путь к файлу базы данных SQLite 96 """ 97 self.db_path = db_path 98 os.makedirs(DATA_DIR, exist_ok=True) 99 self._init_db() 100 101 def _get_conn(self) -> sqlite3.Connection: 102 """ 103 Создаёт и возвращает подключение к SQLite. 104 105 Настраивает row_factory для доступа по имени колонки, 106 включает WAL-режим журнала и таймаут ожидания блокировки. 107 108 Returns: 109 sqlite3.Connection: Подключение к БД 110 """ 111 conn = sqlite3.connect(self.db_path) 112 conn.row_factory = sqlite3.Row 113 conn.execute("PRAGMA journal_mode=WAL") 114 conn.execute("PRAGMA busy_timeout=5000") 115 return conn 116 117 def _init_db(self): 118 """ 119 Инициализирует структуру базы данных. 120 121 Создаёт таблицу locations (если не существует) с колонками: 122 id, user_id, thing, place, created_at. 123 Также создаёт индексы для ускорения запросов. 124 """ 125 with self._get_conn() as conn: 126 conn.executescript(""" 127 CREATE TABLE IF NOT EXISTS locations ( 128 id INTEGER PRIMARY KEY AUTOINCREMENT, 129 user_id TEXT NOT NULL, 130 thing TEXT NOT NULL, 131 place TEXT NOT NULL, 132 created_at TEXT NOT NULL 133 ); 134 CREATE INDEX IF NOT EXISTS idx_loc_thing ON locations(thing); 135 CREATE INDEX IF NOT EXISTS idx_loc_user_thing ON locations(user_id, thing); 136 """) 137 138 @observe(name="Add location") 139 def add_location(self, user_id: str, thing: str, place: str) -> int: 140 """ 141 Добавляет новую запись о местоположении вещи. 142 143 Args: 144 user_id: Идентификатор пользователя 145 thing: Название вещи 146 place: Название места 147 148 Returns: 149 int: ID созданной записи 150 """ 151 with self._get_conn() as conn: 152 cur = conn.execute( 153 "INSERT INTO locations (user_id, thing, place, created_at) VALUES (?, ?, ?, ?)", 154 (user_id, thing.lower(), place.lower(), datetime.now().isoformat()), 155 ) 156 loc_id = cast(int, cur.lastrowid) 157 logger.debug(f"Добавлена запись {loc_id}: '{thing}' → '{place}'") 158 return loc_id 159 160 @observe(name="Update location") 161 def update_location(self, user_id: str, thing: str, place: str) -> bool: 162 """ 163 Обновляет место для существующей вещи. 164 165 Args: 166 user_id: Идентификатор пользователя 167 thing: Название вещи 168 place: Новое название места 169 170 Returns: 171 bool: True если запись найдена и обновлена, False в противном случае 172 """ 173 with self._get_conn() as conn: 174 cur = conn.execute( 175 "UPDATE locations SET place = ?, created_at = ? WHERE user_id = ? AND thing = ?", 176 (place.lower(), datetime.now().isoformat(), user_id, thing.lower()), 177 ) 178 if cur.rowcount > 0: 179 logger.debug(f"Обновлена место для '{thing}': '{place}'") 180 return True 181 return False 182 183 @observe(name="Find thing locations") 184 def find_thing(self, user_id: str, thing: str) -> List[Dict]: 185 """ 186 Возвращает все места где вещь была записена. 187 188 Args: 189 user_id: Идентификатор пользователя 190 thing: Название вещи для поиска 191 192 Returns: 193 List[Dict]: Список записей о местоположении вещи, отсортированных по ID (новые сверху) 194 """ 195 with self._get_conn() as conn: 196 rows = conn.execute( 197 "SELECT * FROM locations WHERE user_id = ? AND thing = ? ORDER BY id DESC", 198 (user_id, thing.lower()), 199 ).fetchall() 200 return [_convert_location(row) for row in rows] 201 202 @observe(name="Search things") 203 def search_thing(self, user_id: str, query: str, limit: int = 5) -> List[Dict]: 204 """ 205 Нечёткий поиск вещей по названию. 206 207 Разбивает запрос на токены и ищет записи, где токен входит в название 208 вещи как подстрока либо морфологически близок к слову (общий префикс — 209 учёт словоформ вроде 'болгарку' vs 'болгарка'). Ранжирует по количеству 210 совпавших токенов и возвращает уникальные вещи (по самому свежему месту). 211 212 Args: 213 user_id: Идентификатор пользователя 214 query: Поисковый запрос (произвольная фраза) 215 limit: Максимальное количество результатов (по умолчанию 5) 216 217 Returns: 218 List[Dict]: Найденные записи (thing, place, id), отсортированные 219 по релевантности — сначала больше совпавших токенов, затем свежие 220 """ 221 tokens = [t for t in query.strip().lower().split() if t] 222 if not tokens: 223 return [] 224 225 params = [user_id] 226 clauses = [] 227 for tok in tokens: 228 clauses.append("thing LIKE ?") 229 params.append(f"%{tok}%") 230 if len(tok) >= 5: 231 # префиксное вхождение ловит словоформы ('болгарку' → 'болгарка') 232 clauses.append("thing LIKE ?") 233 params.append(f"%{tok[:4]}%") 234 235 sql = ( 236 "SELECT * FROM locations WHERE user_id = ? AND (" 237 + " OR ".join(clauses) 238 + ") ORDER BY id DESC" 239 ) 240 241 with self._get_conn() as conn: 242 rows = conn.execute(sql, params).fetchall() 243 244 best: Dict[str, tuple] = {} 245 for row in rows: 246 loc = _convert_location(row) 247 thing = loc["thing"] 248 score = sum(1 for tok in tokens if _token_in_thing(tok, thing)) 249 if score == 0: 250 continue 251 if thing not in best or score > best[thing][0]: 252 best[thing] = (score, loc["place"], loc["id"]) 253 254 ranked = sorted(best.items(), key=lambda kv: (kv[1][0], kv[1][2]), reverse=True) 255 return [ 256 {"thing": thing, "place": place, "id": rec_id} 257 for thing, (score, place, rec_id) in ranked[:limit] 258 ] 259 260 @observe(name="Get all things") 261 def get_all_things(self, user_id: str) -> List[Dict]: 262 """ 263 Возвращает все вещи пользователя. 264 265 Args: 266 user_id: Идентификатор пользователя 267 268 Returns: 269 List[Dict]: Список всех записей, отсортированных по ID 270 """ 271 with self._get_conn() as conn: 272 rows = conn.execute( 273 "SELECT * FROM locations WHERE user_id = ? ORDER BY id", (user_id,) 274 ).fetchall() 275 return [_convert_location(row) for row in rows] 276 277 @observe(name="Delete location") 278 def delete_location(self, user_id: str, thing: str) -> bool: 279 """ 280 Удаляет все записи о вещи по её названию. 281 282 Args: 283 user_id: Идентификатор пользователя 284 thing: Название вещи для удаления 285 286 Returns: 287 bool: True если записи найдены и удалены, False в противном случае 288 """ 289 with self._get_conn() as conn: 290 cur = conn.execute( 291 "DELETE FROM locations WHERE user_id = ? AND thing = ?", 292 (user_id, thing.lower()), 293 ) 294 if cur.rowcount > 0: 295 logger.debug(f"Удалены записи для '{thing}'") 296 return True 297 return False 298 299 def get_location_count(self, user_id: Optional[str] = None) -> int: 300 """ 301 Возвращает количество записей пользователя. 302 303 Args: 304 user_id: Идентификатор пользователя. Если None — считает все записи. 305 306 Returns: 307 int: Количество записей 308 """ 309 if user_id is None: 310 query = "SELECT COUNT(*) FROM locations" 311 params: tuple = () 312 else: 313 query = "SELECT COUNT(*) FROM locations WHERE user_id = ?" 314 params = (user_id,) 315 with self._get_conn() as conn: 316 row = conn.execute(query, params).fetchone() 317 return row[0] 318 319 @observe(name="Format location list") 320 def format_location_list(self, locations: List[Dict], max_chars: int = 1024) -> str: 321 """ 322 Форматирует список местоположений в строку с учётом ограничения длины. 323 324 Args: 325 locations: Список записей (словарей) 326 max_chars: Максимальная длина строки (по умолчанию 1024) 327 328 Returns: 329 str: Отформатированный список местоположений 330 """ 331 if not locations: 332 return "Записей нет." 333 334 raw_lines = [f"{loc['thing']} → {loc['place']}" for loc in locations] 335 raw_lines.reverse() # новые сверху 336 337 total = len(raw_lines) 338 tail_template = "\nи еще {} записей" 339 best_lines = [] 340 341 for line in raw_lines: 342 tail = ( 343 tail_template.format(total - len(best_lines) - 1) 344 if len(best_lines) < total 345 else "" 346 ) 347 candidate = "\n".join(best_lines + [line]) + tail 348 if len(candidate) > max_chars: 349 break 350 best_lines.append(line) 351 352 shown = len(best_lines) 353 if shown == total: 354 if shown == 1: 355 return f"Запись: {best_lines[0]}" 356 return "Ваши записей: \n" + "\n".join(best_lines) 357 358 tail = tail_template.format(total - shown) 359 if shown == 0: 360 return f"Не показано ни одной записи. {tail.strip()}" 361 362 return "\n".join(best_lines) + tail 363 364 @observe(name="Get all thing names") 365 def get_all_thing_names(self, user_id: str) -> List[str]: 366 """ 367 Возвращает список всех названий вещей пользователя. 368 369 Args: 370 user_id: Идентификатор пользователя 371 372 Returns: 373 List[str]: Список названий вещей 374 """ 375 with self._get_conn() as conn: 376 rows = conn.execute( 377 "SELECT DISTINCT thing FROM locations WHERE user_id = ?", (user_id,) 378 ).fetchall() 379 return [row["thing"] for row in rows] 380 381 @observe(name="Get all records") 382 def get_all(self, user_id: Optional[str] = None) -> List[Dict]: 383 """ 384 Возвращает все записи о вещах (всех пользователей или одного). 385 386 Args: 387 user_id: Если задан — только записи этого пользователя, 388 иначе записи всех пользователей. 389 390 Returns: 391 List[Dict]: Список записей, отсортированных по ID 392 """ 393 with self._get_conn() as conn: 394 if user_id is None: 395 rows = conn.execute("SELECT * FROM locations ORDER BY id").fetchall() 396 else: 397 rows = conn.execute( 398 "SELECT * FROM locations WHERE user_id = ? ORDER BY id", 399 (user_id,), 400 ).fetchall() 401 return [_convert_location(row) for row in rows]
81class ThingManager: 82 """ 83 Менеджер вещей с хранением в SQLite. 84 85 Предоставляет полный набор CRUD-операций для управления записями о вещях 86 пользователей. Поддерживает поиск по thing и возврат всех мест. 87 """ 88 89 def __init__(self, db_path: str = DB_FILE): 90 """ 91 Инициализирует ThingManager, создавая/подключая БД SQLite. 92 93 При первом запуске создаёт необходимые таблицы и индексы. 94 95 Args: 96 db_path: Путь к файлу базы данных SQLite 97 """ 98 self.db_path = db_path 99 os.makedirs(DATA_DIR, exist_ok=True) 100 self._init_db() 101 102 def _get_conn(self) -> sqlite3.Connection: 103 """ 104 Создаёт и возвращает подключение к SQLite. 105 106 Настраивает row_factory для доступа по имени колонки, 107 включает WAL-режим журнала и таймаут ожидания блокировки. 108 109 Returns: 110 sqlite3.Connection: Подключение к БД 111 """ 112 conn = sqlite3.connect(self.db_path) 113 conn.row_factory = sqlite3.Row 114 conn.execute("PRAGMA journal_mode=WAL") 115 conn.execute("PRAGMA busy_timeout=5000") 116 return conn 117 118 def _init_db(self): 119 """ 120 Инициализирует структуру базы данных. 121 122 Создаёт таблицу locations (если не существует) с колонками: 123 id, user_id, thing, place, created_at. 124 Также создаёт индексы для ускорения запросов. 125 """ 126 with self._get_conn() as conn: 127 conn.executescript(""" 128 CREATE TABLE IF NOT EXISTS locations ( 129 id INTEGER PRIMARY KEY AUTOINCREMENT, 130 user_id TEXT NOT NULL, 131 thing TEXT NOT NULL, 132 place TEXT NOT NULL, 133 created_at TEXT NOT NULL 134 ); 135 CREATE INDEX IF NOT EXISTS idx_loc_thing ON locations(thing); 136 CREATE INDEX IF NOT EXISTS idx_loc_user_thing ON locations(user_id, thing); 137 """) 138 139 @observe(name="Add location") 140 def add_location(self, user_id: str, thing: str, place: str) -> int: 141 """ 142 Добавляет новую запись о местоположении вещи. 143 144 Args: 145 user_id: Идентификатор пользователя 146 thing: Название вещи 147 place: Название места 148 149 Returns: 150 int: ID созданной записи 151 """ 152 with self._get_conn() as conn: 153 cur = conn.execute( 154 "INSERT INTO locations (user_id, thing, place, created_at) VALUES (?, ?, ?, ?)", 155 (user_id, thing.lower(), place.lower(), datetime.now().isoformat()), 156 ) 157 loc_id = cast(int, cur.lastrowid) 158 logger.debug(f"Добавлена запись {loc_id}: '{thing}' → '{place}'") 159 return loc_id 160 161 @observe(name="Update location") 162 def update_location(self, user_id: str, thing: str, place: str) -> bool: 163 """ 164 Обновляет место для существующей вещи. 165 166 Args: 167 user_id: Идентификатор пользователя 168 thing: Название вещи 169 place: Новое название места 170 171 Returns: 172 bool: True если запись найдена и обновлена, False в противном случае 173 """ 174 with self._get_conn() as conn: 175 cur = conn.execute( 176 "UPDATE locations SET place = ?, created_at = ? WHERE user_id = ? AND thing = ?", 177 (place.lower(), datetime.now().isoformat(), user_id, thing.lower()), 178 ) 179 if cur.rowcount > 0: 180 logger.debug(f"Обновлена место для '{thing}': '{place}'") 181 return True 182 return False 183 184 @observe(name="Find thing locations") 185 def find_thing(self, user_id: str, thing: str) -> List[Dict]: 186 """ 187 Возвращает все места где вещь была записена. 188 189 Args: 190 user_id: Идентификатор пользователя 191 thing: Название вещи для поиска 192 193 Returns: 194 List[Dict]: Список записей о местоположении вещи, отсортированных по ID (новые сверху) 195 """ 196 with self._get_conn() as conn: 197 rows = conn.execute( 198 "SELECT * FROM locations WHERE user_id = ? AND thing = ? ORDER BY id DESC", 199 (user_id, thing.lower()), 200 ).fetchall() 201 return [_convert_location(row) for row in rows] 202 203 @observe(name="Search things") 204 def search_thing(self, user_id: str, query: str, limit: int = 5) -> List[Dict]: 205 """ 206 Нечёткий поиск вещей по названию. 207 208 Разбивает запрос на токены и ищет записи, где токен входит в название 209 вещи как подстрока либо морфологически близок к слову (общий префикс — 210 учёт словоформ вроде 'болгарку' vs 'болгарка'). Ранжирует по количеству 211 совпавших токенов и возвращает уникальные вещи (по самому свежему месту). 212 213 Args: 214 user_id: Идентификатор пользователя 215 query: Поисковый запрос (произвольная фраза) 216 limit: Максимальное количество результатов (по умолчанию 5) 217 218 Returns: 219 List[Dict]: Найденные записи (thing, place, id), отсортированные 220 по релевантности — сначала больше совпавших токенов, затем свежие 221 """ 222 tokens = [t for t in query.strip().lower().split() if t] 223 if not tokens: 224 return [] 225 226 params = [user_id] 227 clauses = [] 228 for tok in tokens: 229 clauses.append("thing LIKE ?") 230 params.append(f"%{tok}%") 231 if len(tok) >= 5: 232 # префиксное вхождение ловит словоформы ('болгарку' → 'болгарка') 233 clauses.append("thing LIKE ?") 234 params.append(f"%{tok[:4]}%") 235 236 sql = ( 237 "SELECT * FROM locations WHERE user_id = ? AND (" 238 + " OR ".join(clauses) 239 + ") ORDER BY id DESC" 240 ) 241 242 with self._get_conn() as conn: 243 rows = conn.execute(sql, params).fetchall() 244 245 best: Dict[str, tuple] = {} 246 for row in rows: 247 loc = _convert_location(row) 248 thing = loc["thing"] 249 score = sum(1 for tok in tokens if _token_in_thing(tok, thing)) 250 if score == 0: 251 continue 252 if thing not in best or score > best[thing][0]: 253 best[thing] = (score, loc["place"], loc["id"]) 254 255 ranked = sorted(best.items(), key=lambda kv: (kv[1][0], kv[1][2]), reverse=True) 256 return [ 257 {"thing": thing, "place": place, "id": rec_id} 258 for thing, (score, place, rec_id) in ranked[:limit] 259 ] 260 261 @observe(name="Get all things") 262 def get_all_things(self, user_id: str) -> List[Dict]: 263 """ 264 Возвращает все вещи пользователя. 265 266 Args: 267 user_id: Идентификатор пользователя 268 269 Returns: 270 List[Dict]: Список всех записей, отсортированных по ID 271 """ 272 with self._get_conn() as conn: 273 rows = conn.execute( 274 "SELECT * FROM locations WHERE user_id = ? ORDER BY id", (user_id,) 275 ).fetchall() 276 return [_convert_location(row) for row in rows] 277 278 @observe(name="Delete location") 279 def delete_location(self, user_id: str, thing: str) -> bool: 280 """ 281 Удаляет все записи о вещи по её названию. 282 283 Args: 284 user_id: Идентификатор пользователя 285 thing: Название вещи для удаления 286 287 Returns: 288 bool: True если записи найдены и удалены, False в противном случае 289 """ 290 with self._get_conn() as conn: 291 cur = conn.execute( 292 "DELETE FROM locations WHERE user_id = ? AND thing = ?", 293 (user_id, thing.lower()), 294 ) 295 if cur.rowcount > 0: 296 logger.debug(f"Удалены записи для '{thing}'") 297 return True 298 return False 299 300 def get_location_count(self, user_id: Optional[str] = None) -> int: 301 """ 302 Возвращает количество записей пользователя. 303 304 Args: 305 user_id: Идентификатор пользователя. Если None — считает все записи. 306 307 Returns: 308 int: Количество записей 309 """ 310 if user_id is None: 311 query = "SELECT COUNT(*) FROM locations" 312 params: tuple = () 313 else: 314 query = "SELECT COUNT(*) FROM locations WHERE user_id = ?" 315 params = (user_id,) 316 with self._get_conn() as conn: 317 row = conn.execute(query, params).fetchone() 318 return row[0] 319 320 @observe(name="Format location list") 321 def format_location_list(self, locations: List[Dict], max_chars: int = 1024) -> str: 322 """ 323 Форматирует список местоположений в строку с учётом ограничения длины. 324 325 Args: 326 locations: Список записей (словарей) 327 max_chars: Максимальная длина строки (по умолчанию 1024) 328 329 Returns: 330 str: Отформатированный список местоположений 331 """ 332 if not locations: 333 return "Записей нет." 334 335 raw_lines = [f"{loc['thing']} → {loc['place']}" for loc in locations] 336 raw_lines.reverse() # новые сверху 337 338 total = len(raw_lines) 339 tail_template = "\nи еще {} записей" 340 best_lines = [] 341 342 for line in raw_lines: 343 tail = ( 344 tail_template.format(total - len(best_lines) - 1) 345 if len(best_lines) < total 346 else "" 347 ) 348 candidate = "\n".join(best_lines + [line]) + tail 349 if len(candidate) > max_chars: 350 break 351 best_lines.append(line) 352 353 shown = len(best_lines) 354 if shown == total: 355 if shown == 1: 356 return f"Запись: {best_lines[0]}" 357 return "Ваши записей: \n" + "\n".join(best_lines) 358 359 tail = tail_template.format(total - shown) 360 if shown == 0: 361 return f"Не показано ни одной записи. {tail.strip()}" 362 363 return "\n".join(best_lines) + tail 364 365 @observe(name="Get all thing names") 366 def get_all_thing_names(self, user_id: str) -> List[str]: 367 """ 368 Возвращает список всех названий вещей пользователя. 369 370 Args: 371 user_id: Идентификатор пользователя 372 373 Returns: 374 List[str]: Список названий вещей 375 """ 376 with self._get_conn() as conn: 377 rows = conn.execute( 378 "SELECT DISTINCT thing FROM locations WHERE user_id = ?", (user_id,) 379 ).fetchall() 380 return [row["thing"] for row in rows] 381 382 @observe(name="Get all records") 383 def get_all(self, user_id: Optional[str] = None) -> List[Dict]: 384 """ 385 Возвращает все записи о вещах (всех пользователей или одного). 386 387 Args: 388 user_id: Если задан — только записи этого пользователя, 389 иначе записи всех пользователей. 390 391 Returns: 392 List[Dict]: Список записей, отсортированных по ID 393 """ 394 with self._get_conn() as conn: 395 if user_id is None: 396 rows = conn.execute("SELECT * FROM locations ORDER BY id").fetchall() 397 else: 398 rows = conn.execute( 399 "SELECT * FROM locations WHERE user_id = ? ORDER BY id", 400 (user_id,), 401 ).fetchall() 402 return [_convert_location(row) for row in rows]
Менеджер вещей с хранением в SQLite.
Предоставляет полный набор CRUD-операций для управления записями о вещях пользователей. Поддерживает поиск по thing и возврат всех мест.
89 def __init__(self, db_path: str = DB_FILE): 90 """ 91 Инициализирует ThingManager, создавая/подключая БД SQLite. 92 93 При первом запуске создаёт необходимые таблицы и индексы. 94 95 Args: 96 db_path: Путь к файлу базы данных SQLite 97 """ 98 self.db_path = db_path 99 os.makedirs(DATA_DIR, exist_ok=True) 100 self._init_db()
Инициализирует ThingManager, создавая/подключая БД SQLite.
При первом запуске создаёт необходимые таблицы и индексы.
Args: db_path: Путь к файлу базы данных SQLite
139 @observe(name="Add location") 140 def add_location(self, user_id: str, thing: str, place: str) -> int: 141 """ 142 Добавляет новую запись о местоположении вещи. 143 144 Args: 145 user_id: Идентификатор пользователя 146 thing: Название вещи 147 place: Название места 148 149 Returns: 150 int: ID созданной записи 151 """ 152 with self._get_conn() as conn: 153 cur = conn.execute( 154 "INSERT INTO locations (user_id, thing, place, created_at) VALUES (?, ?, ?, ?)", 155 (user_id, thing.lower(), place.lower(), datetime.now().isoformat()), 156 ) 157 loc_id = cast(int, cur.lastrowid) 158 logger.debug(f"Добавлена запись {loc_id}: '{thing}' → '{place}'") 159 return loc_id
Добавляет новую запись о местоположении вещи.
Args: user_id: Идентификатор пользователя thing: Название вещи place: Название места
Returns: int: ID созданной записи
161 @observe(name="Update location") 162 def update_location(self, user_id: str, thing: str, place: str) -> bool: 163 """ 164 Обновляет место для существующей вещи. 165 166 Args: 167 user_id: Идентификатор пользователя 168 thing: Название вещи 169 place: Новое название места 170 171 Returns: 172 bool: True если запись найдена и обновлена, False в противном случае 173 """ 174 with self._get_conn() as conn: 175 cur = conn.execute( 176 "UPDATE locations SET place = ?, created_at = ? WHERE user_id = ? AND thing = ?", 177 (place.lower(), datetime.now().isoformat(), user_id, thing.lower()), 178 ) 179 if cur.rowcount > 0: 180 logger.debug(f"Обновлена место для '{thing}': '{place}'") 181 return True 182 return False
Обновляет место для существующей вещи.
Args: user_id: Идентификатор пользователя thing: Название вещи place: Новое название места
Returns: bool: True если запись найдена и обновлена, False в противном случае
184 @observe(name="Find thing locations") 185 def find_thing(self, user_id: str, thing: str) -> List[Dict]: 186 """ 187 Возвращает все места где вещь была записена. 188 189 Args: 190 user_id: Идентификатор пользователя 191 thing: Название вещи для поиска 192 193 Returns: 194 List[Dict]: Список записей о местоположении вещи, отсортированных по ID (новые сверху) 195 """ 196 with self._get_conn() as conn: 197 rows = conn.execute( 198 "SELECT * FROM locations WHERE user_id = ? AND thing = ? ORDER BY id DESC", 199 (user_id, thing.lower()), 200 ).fetchall() 201 return [_convert_location(row) for row in rows]
Возвращает все места где вещь была записена.
Args: user_id: Идентификатор пользователя thing: Название вещи для поиска
Returns: List[Dict]: Список записей о местоположении вещи, отсортированных по ID (новые сверху)
203 @observe(name="Search things") 204 def search_thing(self, user_id: str, query: str, limit: int = 5) -> List[Dict]: 205 """ 206 Нечёткий поиск вещей по названию. 207 208 Разбивает запрос на токены и ищет записи, где токен входит в название 209 вещи как подстрока либо морфологически близок к слову (общий префикс — 210 учёт словоформ вроде 'болгарку' vs 'болгарка'). Ранжирует по количеству 211 совпавших токенов и возвращает уникальные вещи (по самому свежему месту). 212 213 Args: 214 user_id: Идентификатор пользователя 215 query: Поисковый запрос (произвольная фраза) 216 limit: Максимальное количество результатов (по умолчанию 5) 217 218 Returns: 219 List[Dict]: Найденные записи (thing, place, id), отсортированные 220 по релевантности — сначала больше совпавших токенов, затем свежие 221 """ 222 tokens = [t for t in query.strip().lower().split() if t] 223 if not tokens: 224 return [] 225 226 params = [user_id] 227 clauses = [] 228 for tok in tokens: 229 clauses.append("thing LIKE ?") 230 params.append(f"%{tok}%") 231 if len(tok) >= 5: 232 # префиксное вхождение ловит словоформы ('болгарку' → 'болгарка') 233 clauses.append("thing LIKE ?") 234 params.append(f"%{tok[:4]}%") 235 236 sql = ( 237 "SELECT * FROM locations WHERE user_id = ? AND (" 238 + " OR ".join(clauses) 239 + ") ORDER BY id DESC" 240 ) 241 242 with self._get_conn() as conn: 243 rows = conn.execute(sql, params).fetchall() 244 245 best: Dict[str, tuple] = {} 246 for row in rows: 247 loc = _convert_location(row) 248 thing = loc["thing"] 249 score = sum(1 for tok in tokens if _token_in_thing(tok, thing)) 250 if score == 0: 251 continue 252 if thing not in best or score > best[thing][0]: 253 best[thing] = (score, loc["place"], loc["id"]) 254 255 ranked = sorted(best.items(), key=lambda kv: (kv[1][0], kv[1][2]), reverse=True) 256 return [ 257 {"thing": thing, "place": place, "id": rec_id} 258 for thing, (score, place, rec_id) in ranked[:limit] 259 ]
Нечёткий поиск вещей по названию.
Разбивает запрос на токены и ищет записи, где токен входит в название вещи как подстрока либо морфологически близок к слову (общий префикс — учёт словоформ вроде 'болгарку' vs 'болгарка'). Ранжирует по количеству совпавших токенов и возвращает уникальные вещи (по самому свежему месту).
Args: user_id: Идентификатор пользователя query: Поисковый запрос (произвольная фраза) limit: Максимальное количество результатов (по умолчанию 5)
Returns: List[Dict]: Найденные записи (thing, place, id), отсортированные по релевантности — сначала больше совпавших токенов, затем свежие
261 @observe(name="Get all things") 262 def get_all_things(self, user_id: str) -> List[Dict]: 263 """ 264 Возвращает все вещи пользователя. 265 266 Args: 267 user_id: Идентификатор пользователя 268 269 Returns: 270 List[Dict]: Список всех записей, отсортированных по ID 271 """ 272 with self._get_conn() as conn: 273 rows = conn.execute( 274 "SELECT * FROM locations WHERE user_id = ? ORDER BY id", (user_id,) 275 ).fetchall() 276 return [_convert_location(row) for row in rows]
Возвращает все вещи пользователя.
Args: user_id: Идентификатор пользователя
Returns: List[Dict]: Список всех записей, отсортированных по ID
278 @observe(name="Delete location") 279 def delete_location(self, user_id: str, thing: str) -> bool: 280 """ 281 Удаляет все записи о вещи по её названию. 282 283 Args: 284 user_id: Идентификатор пользователя 285 thing: Название вещи для удаления 286 287 Returns: 288 bool: True если записи найдены и удалены, False в противном случае 289 """ 290 with self._get_conn() as conn: 291 cur = conn.execute( 292 "DELETE FROM locations WHERE user_id = ? AND thing = ?", 293 (user_id, thing.lower()), 294 ) 295 if cur.rowcount > 0: 296 logger.debug(f"Удалены записи для '{thing}'") 297 return True 298 return False
Удаляет все записи о вещи по её названию.
Args: user_id: Идентификатор пользователя thing: Название вещи для удаления
Returns: bool: True если записи найдены и удалены, False в противном случае
300 def get_location_count(self, user_id: Optional[str] = None) -> int: 301 """ 302 Возвращает количество записей пользователя. 303 304 Args: 305 user_id: Идентификатор пользователя. Если None — считает все записи. 306 307 Returns: 308 int: Количество записей 309 """ 310 if user_id is None: 311 query = "SELECT COUNT(*) FROM locations" 312 params: tuple = () 313 else: 314 query = "SELECT COUNT(*) FROM locations WHERE user_id = ?" 315 params = (user_id,) 316 with self._get_conn() as conn: 317 row = conn.execute(query, params).fetchone() 318 return row[0]
Возвращает количество записей пользователя.
Args: user_id: Идентификатор пользователя. Если None — считает все записи.
Returns: int: Количество записей
320 @observe(name="Format location list") 321 def format_location_list(self, locations: List[Dict], max_chars: int = 1024) -> str: 322 """ 323 Форматирует список местоположений в строку с учётом ограничения длины. 324 325 Args: 326 locations: Список записей (словарей) 327 max_chars: Максимальная длина строки (по умолчанию 1024) 328 329 Returns: 330 str: Отформатированный список местоположений 331 """ 332 if not locations: 333 return "Записей нет." 334 335 raw_lines = [f"{loc['thing']} → {loc['place']}" for loc in locations] 336 raw_lines.reverse() # новые сверху 337 338 total = len(raw_lines) 339 tail_template = "\nи еще {} записей" 340 best_lines = [] 341 342 for line in raw_lines: 343 tail = ( 344 tail_template.format(total - len(best_lines) - 1) 345 if len(best_lines) < total 346 else "" 347 ) 348 candidate = "\n".join(best_lines + [line]) + tail 349 if len(candidate) > max_chars: 350 break 351 best_lines.append(line) 352 353 shown = len(best_lines) 354 if shown == total: 355 if shown == 1: 356 return f"Запись: {best_lines[0]}" 357 return "Ваши записей: \n" + "\n".join(best_lines) 358 359 tail = tail_template.format(total - shown) 360 if shown == 0: 361 return f"Не показано ни одной записи. {tail.strip()}" 362 363 return "\n".join(best_lines) + tail
Форматирует список местоположений в строку с учётом ограничения длины.
Args: locations: Список записей (словарей) max_chars: Максимальная длина строки (по умолчанию 1024)
Returns: str: Отформатированный список местоположений
365 @observe(name="Get all thing names") 366 def get_all_thing_names(self, user_id: str) -> List[str]: 367 """ 368 Возвращает список всех названий вещей пользователя. 369 370 Args: 371 user_id: Идентификатор пользователя 372 373 Returns: 374 List[str]: Список названий вещей 375 """ 376 with self._get_conn() as conn: 377 rows = conn.execute( 378 "SELECT DISTINCT thing FROM locations WHERE user_id = ?", (user_id,) 379 ).fetchall() 380 return [row["thing"] for row in rows]
Возвращает список всех названий вещей пользователя.
Args: user_id: Идентификатор пользователя
Returns: List[str]: Список названий вещей
382 @observe(name="Get all records") 383 def get_all(self, user_id: Optional[str] = None) -> List[Dict]: 384 """ 385 Возвращает все записи о вещах (всех пользователей или одного). 386 387 Args: 388 user_id: Если задан — только записи этого пользователя, 389 иначе записи всех пользователей. 390 391 Returns: 392 List[Dict]: Список записей, отсортированных по ID 393 """ 394 with self._get_conn() as conn: 395 if user_id is None: 396 rows = conn.execute("SELECT * FROM locations ORDER BY id").fetchall() 397 else: 398 rows = conn.execute( 399 "SELECT * FROM locations WHERE user_id = ? ORDER BY id", 400 (user_id,), 401 ).fetchall() 402 return [_convert_location(row) for row in rows]
Возвращает все записи о вещах (всех пользователей или одного).
Args: user_id: Если задан — только записи этого пользователя, иначе записи всех пользователей.
Returns: List[Dict]: Список записей, отсортированных по ID