bookcli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bookcli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """BookCLI package."""
2
+
3
+ __version__ = "0.1.0"
bookcli/cache.py ADDED
@@ -0,0 +1,103 @@
1
+ """Metadata cache using SQLite for BookCLI."""
2
+
3
+ import json
4
+ import logging
5
+ from datetime import datetime, timezone
6
+ from typing import Optional
7
+ from bookcli.database import DatabaseManager
8
+ from bookcli.models import BookMetadata
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ async def get_book_from_cache(
14
+ db_manager: DatabaseManager, book_id: str, ttl_seconds: int
15
+ ) -> Optional[BookMetadata]:
16
+ """Retrieves a book from the cache if it exists and has not expired."""
17
+ query = "SELECT json_data, cached_at FROM cached_books WHERE id = ?"
18
+ async with db_manager.connection() as conn:
19
+ async with conn.execute(query, (book_id,)) as cursor:
20
+ row = await cursor.fetchone()
21
+ if not row:
22
+ return None
23
+
24
+ json_data, cached_at_str = row[0], row[1]
25
+ try:
26
+ # SQLite datetime strings are stored in ISO format
27
+ # Parse timestamp: 'YYYY-MM-DD HH:MM:SS' or 'YYYY-MM-DDTHH:MM:SS...'
28
+ # We can replace ' ' with 'T' if needed, or use fromisoformat.
29
+ # In standard SQLite datetime, it might not contain 'T', let's format it.
30
+ if "T" not in cached_at_str:
31
+ # e.g., '2026-07-11 04:53:00'
32
+ cached_at = datetime.strptime(cached_at_str.split(".")[0], "%Y-%m-%d %H:%M:%S")
33
+ else:
34
+ # e.g., ISO format
35
+ cached_at = datetime.fromisoformat(cached_at_str)
36
+
37
+ # Convert to naive or timezone-aware matching
38
+ # Since datetime.now(timezone.utc) is timezone aware, let's make cached_at UTC aware.
39
+ if cached_at.tzinfo is None:
40
+ cached_at = cached_at.replace(tzinfo=timezone.utc)
41
+
42
+ age = (datetime.now(timezone.utc) - cached_at).total_seconds()
43
+ if age > ttl_seconds:
44
+ logger.debug("Cache expired for book ID %s (age: %s seconds)", book_id, age)
45
+ # We could delete it, but simple overwrite on set is fine, or we can delete it now
46
+ await conn.execute("DELETE FROM cached_books WHERE id = ?", (book_id,))
47
+ await conn.commit()
48
+ return None
49
+
50
+ data = json.loads(json_data)
51
+ return BookMetadata(**data)
52
+ except Exception as e:
53
+ logger.error("Failed to parse cached book %s: %s", book_id, e)
54
+ return None
55
+
56
+
57
+ async def save_book_to_cache(db_manager: DatabaseManager, book: BookMetadata) -> None:
58
+ """Saves a book to the SQLite cache, overwriting if it already exists."""
59
+ query = """
60
+ INSERT OR REPLACE INTO cached_books (id, provider, json_data, cached_at)
61
+ VALUES (?, ?, ?, ?)
62
+ """
63
+ async with db_manager.connection() as conn:
64
+ try:
65
+ # Save date in standard ISO format with timezone (UTC)
66
+ now_str = datetime.now(timezone.utc).isoformat()
67
+ await conn.execute(
68
+ query,
69
+ (book.id, book.source, book.model_dump_json(), now_str)
70
+ )
71
+ await conn.commit()
72
+ logger.debug("Saved book %s to cache", book.id)
73
+ except Exception as e:
74
+ logger.error("Failed to write book %s to cache: %s", book.id, e)
75
+
76
+
77
+ async def clear_cache_db(db_manager: DatabaseManager) -> int:
78
+ """Clears all cached books. Returns the number of rows deleted."""
79
+ async with db_manager.connection() as conn:
80
+ async with conn.execute("SELECT COUNT(*) FROM cached_books") as cursor:
81
+ row = await cursor.fetchone()
82
+ count = row[0] if row else 0
83
+
84
+ await conn.execute("DELETE FROM cached_books")
85
+ await conn.commit()
86
+ logger.info("Cleared cached metadata database (%d items).", count)
87
+ return count
88
+
89
+
90
+ async def get_cache_db_stats(db_manager: DatabaseManager) -> dict:
91
+ """Gets metadata cache statistics."""
92
+ async with db_manager.connection() as conn:
93
+ async with conn.execute("SELECT COUNT(*), MIN(cached_at), MAX(cached_at) FROM cached_books") as cursor:
94
+ row = await cursor.fetchone()
95
+ count = row[0] if row else 0
96
+ oldest = row[1] if row and row[1] else "N/A"
97
+ newest = row[2] if row and row[2] else "N/A"
98
+
99
+ return {
100
+ "total_items": count,
101
+ "oldest_item": oldest,
102
+ "newest_item": newest
103
+ }