Refactor: migrated whatleft of sync code to async

This commit is contained in:
arabcoders 2025-12-18 18:21:34 +03:00
parent 699e21f54d
commit b36d9fd641
8 changed files with 283 additions and 77 deletions

View file

@ -17,6 +17,7 @@
"ahash",
"aiocron",
"aiosqlite",
"alnum",
"anyio",
"Archiver",
"arrowdown",

View file

@ -61,17 +61,20 @@ class DataStore:
async def saved_items(self) -> list[tuple[str, ItemDTO]]:
return await self._connection.fetch_saved(str(self._type))
def exists(self, key: str | None = None, url: str | None = None) -> bool:
async def exists(self, key: str | None = None, url: str | None = None) -> bool:
if not key and not url:
msg = "key or url must be provided."
raise KeyError(msg)
if key and key in self._dict:
return True
return any(
(key and self._dict[i].info._id == key) or (url and self._dict[i].info.url == url) for i in self._dict
)
def get(self, key: str | None = None, url: str | None = None) -> Download:
if any((key and self._dict[i].info._id == key) or (url and self._dict[i].info.url == url) for i in self._dict):
return True
return StoreType.HISTORY == self._type and await self._connection.exists(str(self._type), key=key, url=url)
async def get(self, key: str | None = None, url: str | None = None) -> Download:
if not key and not url:
msg = "key or url must be provided."
raise KeyError(msg)
@ -80,10 +83,14 @@ class DataStore:
if (key and self._dict[i].info._id == key) or (url and self._dict[i].info.url == url):
return self._dict[i]
if StoreType.HISTORY == self._type and (item := await self._connection.get(str(self._type), key=key, url=url)):
self._dict[item._id] = Download(info=item)
return self._dict[item._id]
msg: str = f"{key=} or {url=} not found."
raise KeyError(msg)
def get_item(self, **kwargs) -> Download | None:
async def get_item(self, **kwargs) -> Download | None:
if not kwargs:
return None
@ -96,6 +103,10 @@ class DataStore:
if any(matches_condition(key, value, info) for key, value in kwargs.items()):
return self._dict[i]
if StoreType.HISTORY == self._type and (item := await self._connection.get_item(str(self._type), **kwargs)):
self._dict[item._id] = Download(info=item)
return self._dict[item._id]
return None
async def get_by_id(self, id: str) -> Download | None:
@ -104,8 +115,7 @@ class DataStore:
return val
if item := await self._connection.get_by_id(str(self._type), id):
download = Download(info=item)
self._dict[id] = download
self._dict[item._id] = Download(info=item)
return self._dict[id]
return None

View file

@ -71,7 +71,6 @@ class DownloadQueue(metaclass=Singleton):
self._active: dict[str, Download] = {}
"""Dictionary of active downloads."""
# Loading happens in async initialize to avoid blocking loop creation.
self.paused.set()
@staticmethod
@ -170,7 +169,6 @@ class DownloadQueue(metaclass=Singleton):
f"Using '{self.config.max_workers}' workers for downloading and '{self.config.max_workers_per_extractor}' per extractor."
)
asyncio.create_task(self._download_pool(), name="download_pool")
await self.done.load()
async def start_items(self, ids: list[str]) -> dict[str, str]:
"""
@ -188,7 +186,7 @@ class DownloadQueue(metaclass=Singleton):
for item_id in ids:
try:
item: Download = self.queue.get(key=item_id)
item: Download = await self.queue.get(key=item_id)
except KeyError as e:
status[item_id] = f"not found: {e!s}"
status["status"] = "error"
@ -231,7 +229,7 @@ class DownloadQueue(metaclass=Singleton):
for item_id in ids:
try:
item: Download = self.queue.get(key=item_id)
item: Download = await self.queue.get(key=item_id)
except KeyError as e:
status[item_id] = f"not found: {e!s}"
status["status"] = "error"
@ -444,7 +442,7 @@ class DownloadQueue(metaclass=Singleton):
LOG.debug(f"Entry id '{entry.get('id')}' url '{entry.get('webpage_url')} - {entry.get('url')}'.")
try:
_item: Download = self.done.get(key=entry.get("id"), url=entry.get("webpage_url") or entry.get("url"))
_item: Download = await self.done.get(key=entry.get("id"), url=entry.get("webpage_url") or entry.get("url"))
err_msg: str = f"Removing {_item.info.name()} from history list."
LOG.warning(err_msg)
await self.clear([_item.info._id], remove_file=False)
@ -452,7 +450,7 @@ class DownloadQueue(metaclass=Singleton):
pass
try:
_item: Download = self.queue.get(
_item: Download = await self.queue.get(
key=str(entry.get("id")), url=str(entry.get("webpage_url") or entry.get("url"))
)
err_msg: str = f"Item {_item.info.name()} is already in download queue."
@ -713,7 +711,7 @@ class DownloadQueue(metaclass=Singleton):
if item.is_archived():
if archive_id:
store_type, _ = self.get_item(archive_id=archive_id)
store_type, _ = await self.get_item(archive_id=archive_id)
if not store_type:
dlInfo = Download(
info=ItemDTO(
@ -801,7 +799,7 @@ class DownloadQueue(metaclass=Singleton):
log_message = f"Ignoring download of '{item_title}' as per condition '{condition.name}'{extra_msg}."
store_type, _ = self.get_item(archive_id=archive_id)
store_type, _ = await self.get_item(archive_id=archive_id)
if not store_type:
dlInfo = Download(
info=ItemDTO(
@ -886,7 +884,7 @@ class DownloadQueue(metaclass=Singleton):
for id in ids:
try:
item = self.queue.get(key=id)
item = await self.queue.get(key=id)
except KeyError as e:
status[id] = str(e)
status["status"] = "error"
@ -940,7 +938,7 @@ class DownloadQueue(metaclass=Singleton):
for id in ids:
try:
item: Download = self.done.get(key=id)
item: Download = await self.done.get(key=id)
except KeyError as e:
status[id] = str(e)
status["status"] = "error"
@ -1041,7 +1039,7 @@ class DownloadQueue(metaclass=Singleton):
return items
def get_item(self, **kwargs) -> tuple[StoreType, Download] | tuple[None, None]:
async def get_item(self, **kwargs) -> tuple[StoreType, Download] | tuple[None, None]:
"""
Get a specific item from the download queue or history.
@ -1052,10 +1050,10 @@ class DownloadQueue(metaclass=Singleton):
(StoreType, Download) | None: The requested item if found, otherwise None.
"""
if item := self.queue.get_item(**kwargs):
if item := await self.queue.get_item(**kwargs):
return (StoreType.QUEUE, item)
if item := self.done.get_item(**kwargs):
if item := await self.done.get_item(**kwargs):
return (StoreType.HISTORY, item)
return (None, None)
@ -1166,7 +1164,7 @@ class DownloadQueue(metaclass=Singleton):
await entry.close()
if self.queue.exists(key=id):
if await self.queue.exists(key=id):
LOG.debug(f"Download Task '{id}' is completed. Removing from queue.")
await self.queue.delete(key=id)

View file

@ -828,11 +828,11 @@ class HandleTask:
if archive_file and archive_id in downloaded:
continue
if download_queue.queue.exists(url=url):
if await download_queue.queue.exists(url=url):
continue
try:
done = download_queue.done.get(url=url)
done = await download_queue.done.get(url=url)
if "error" != done.info.status:
continue
except KeyError:

View file

@ -12,6 +12,7 @@ import aiosqlite
from aiohttp import web
from .ItemDTO import ItemDTO
from .operations import Operation, matches_condition
from .Singleton import ThreadSafe
from .Utils import init_class
@ -70,7 +71,6 @@ class SqliteStore(metaclass=ThreadSafe):
async def __aexit__(self, exc_type, exc, tb) -> None:
await self.close()
# ---------- public API ----------
async def fetch_saved(self, type_value: str) -> list[tuple[str, ItemDTO]]:
await self._ensure_conn()
cursor = await self._conn.execute(
@ -89,6 +89,42 @@ class SqliteStore(metaclass=ThreadSafe):
items.append((row["id"], item))
return items
async def exists(self, type_value: str, key: str | None = None, url: str | None = None) -> bool:
return await self.get(type_value, key=key, url=url) is not None
async def get(self, type_value: str, key: str | None = None, url: str | None = None) -> bool:
if not key and not url:
msg = "key or url must be provided."
raise KeyError(msg)
await self._ensure_conn()
clauses: list[str] = []
params: list[str] = []
if key:
clauses.append('"id" = ?')
params.append(key)
if url:
clauses.append("json_extract(data, '$.url') = ?")
params.append(url)
where_clause = " OR ".join(clauses)
query = f'SELECT "id", "data", "created_at" FROM "history" WHERE "type" = ? AND ({where_clause}) LIMIT 1' # noqa: S608
cursor = await self._conn.execute(query, (type_value, *params))
row = await cursor.fetchone()
if not row:
return None
row_date = datetime.strptime(row["created_at"], "%Y-%m-%d %H:%M:%S") # noqa: DTZ007
data = json.loads(row["data"])
data.pop("_id", None)
item = init_class(ItemDTO, data, ITEM_DTO_FIELDS)
item._id = row["id"]
item.datetime = formatdate(row_date.replace(tzinfo=UTC).timestamp())
return item
async def get_by_id(self, type_value: str, id: str) -> ItemDTO | None:
await self._ensure_conn()
cursor = await self._conn.execute(
@ -107,6 +143,94 @@ class SqliteStore(metaclass=ThreadSafe):
item.datetime = formatdate(row_date.replace(tzinfo=UTC).timestamp())
return item
async def get_item(self, type_value: str, **kwargs) -> ItemDTO | None:
"""
Return first item of type matching *any* condition.
Mirrors :meth:`DataStore.get_item` semantics: if any provided condition
matches (OR logic) return the first row by creation time. Returns None
when kwargs is empty or no row matches.
"""
if not kwargs:
return None
await self._ensure_conn()
clauses: list[str] = []
params: list[str | float | int] = []
def _safe_key(key: str) -> str | None:
return key if key.replace("_", "").isalnum() else None
for key, raw_value in kwargs.items():
safe_key = _safe_key(key)
if not safe_key:
continue
if isinstance(raw_value, tuple) and len(raw_value) == 2:
operation, value = raw_value
else:
operation, value = Operation.EQUAL, raw_value
if isinstance(operation, str):
try:
operation = Operation(operation)
except ValueError:
operation = Operation.EQUAL
path = f"$.{safe_key}"
json_extract = f"json_extract(data, '{path}')"
if Operation.EQUAL == operation:
clauses.append(f"{json_extract} = ?")
params.append(value)
elif Operation.NOT_EQUAL == operation:
clauses.append(f"{json_extract} != ?")
params.append(value)
elif Operation.CONTAIN == operation:
clauses.append(f"{json_extract} LIKE ? ESCAPE '\\'")
params.append(f"%{value}%")
elif Operation.NOT_CONTAIN == operation:
clauses.append(f"({json_extract} IS NULL OR {json_extract} NOT LIKE ? ESCAPE '\\')")
params.append(f"%{value}%")
elif Operation.STARTS_WITH == operation:
clauses.append(f"{json_extract} LIKE ? ESCAPE '\\'")
params.append(f"{value}%")
elif Operation.ENDS_WITH == operation:
clauses.append(f"{json_extract} LIKE ? ESCAPE '\\'")
params.append(f"%{value}")
elif Operation.GREATER_THAN == operation:
clauses.append(f"{json_extract} > ?")
params.append(value)
elif Operation.LESS_THAN == operation:
clauses.append(f"{json_extract} < ?")
params.append(value)
elif Operation.GREATER_EQUAL == operation:
clauses.append(f"{json_extract} >= ?")
params.append(value)
elif Operation.LESS_EQUAL == operation:
clauses.append(f"{json_extract} <= ?")
params.append(value)
if not clauses:
return None
where_clause = " OR ".join(f"({clause})" for clause in clauses)
query = f'SELECT "id", "data", "created_at" FROM "history" WHERE "type" = ? AND ({where_clause}) ORDER BY "created_at" ASC LIMIT 1' # noqa: S608
cursor = await self._conn.execute(query, (type_value, *params))
row = await cursor.fetchone()
if not row:
return None
row_date = datetime.strptime(row["created_at"], "%Y-%m-%d %H:%M:%S") # noqa: DTZ007
data = json.loads(row["data"])
data.pop("_id", None)
item = init_class(ItemDTO, data, ITEM_DTO_FIELDS)
item._id = row["id"]
item.datetime = formatdate(row_date.replace(tzinfo=UTC).timestamp())
return item if any(matches_condition(k, v, item.__dict__) for k, v in kwargs.items()) else None
async def count(self, type_value: str, status_filter: str | None = None) -> int:
await self._ensure_conn()
where_clauses = ['"type" = ?']

View file

@ -397,7 +397,7 @@ async def path_actions(request: Request, config: Config, queue: DownloadQueue, n
for old_sidecar, new_sidecar in sidecar_renamed:
record(old_sidecar, ok=True, action=action, extra={"new_path": new_sidecar})
if item := queue.done.get_item(filename=str(path.relative_to(config.download_path))):
if item := await queue.done.get_item(filename=str(path.relative_to(config.download_path))):
item.info.filename = str(renamed.relative_to(config.download_path))
if sidecar_renamed:
item.info.get_file_sidecar()
@ -503,7 +503,7 @@ async def path_actions(request: Request, config: Config, queue: DownloadQueue, n
for old_sidecar, new_sidecar in sidecar_moved:
record(old_sidecar, ok=True, action=action, extra={"new_path": new_sidecar})
if item := queue.done.get_item(filename=str(path.relative_to(config.download_path))):
if item := await queue.done.get_item(filename=str(path.relative_to(config.download_path))):
item.info.filename = str(moved.relative_to(config.download_path))
if sidecar_moved:
item.info.get_file_sidecar()

View file

@ -145,19 +145,19 @@ class TestDataStore:
await store.put(d)
await store._connection.flush()
assert store.exists(key=item._id) is True
assert store.exists(url=item.url) is True
assert await store.exists(key=item._id) is True
assert await store.exists(url=item.url) is True
with pytest.raises(KeyError):
store.exists()
await store.exists()
got = store.get(key=item._id)
got = await store.get(key=item._id)
assert got.info._id == item._id
got2 = store.get(url=item.url)
got2 = await store.get(url=item.url)
assert got2.info.url == item.url
with pytest.raises(KeyError):
store.get()
await store.get()
with pytest.raises(KeyError):
store.get(key="missing")
await store.get(key="missing")
await store._connection.close()
@pytest.mark.asyncio
@ -212,7 +212,7 @@ class TestDataStore:
db = await make_db()
store = DataStore(StoreType.QUEUE, db)
result = store.get_item()
result = await store.get_item()
assert result is None
await db.close()
@ -236,19 +236,19 @@ class TestDataStore:
await store._connection.flush()
# Test finding by title
result = store.get_item(title="Video 1")
result = await store.get_item(title="Video 1")
assert result is not None
assert result.info._id == "id1"
assert result.info.title == "Video 1"
# Test finding by folder
result = store.get_item(folder="folder2")
result = await store.get_item(folder="folder2")
assert result is not None
assert result.info._id == "id2"
assert result.info.folder == "folder2"
# Test finding by url
result = store.get_item(url="http://example.com/1")
result = await store.get_item(url="http://example.com/1")
assert result is not None
assert result.info._id == "id1"
await db.close()
@ -272,12 +272,12 @@ class TestDataStore:
await store._connection.flush()
# Test finding by multiple attributes where one matches
result = store.get_item(title="Video 1", folder="wrong_folder")
result = await store.get_item(title="Video 1", folder="wrong_folder")
assert result is not None
assert result.info._id == "id1"
# Test finding where second attribute matches
result = store.get_item(title="Wrong Title", folder="folder2")
result = await store.get_item(title="Wrong Title", folder="folder2")
assert result is not None
assert result.info._id == "id2"
await db.close()
@ -295,11 +295,11 @@ class TestDataStore:
await store._connection.flush()
# Test with non-matching attribute
result = store.get_item(title="Nonexistent Video")
result = await store.get_item(title="Nonexistent Video")
assert result is None
# Test with non-existent attribute key
result = store.get_item(nonexistent_field="value")
result = await store.get_item(nonexistent_field="value")
assert result is None
await db.close()
@ -324,7 +324,7 @@ class TestDataStore:
store._dict["broken"] = BrokenDownload()
# Should still find the valid item
result = store.get_item(title="Video 1")
result = await store.get_item(title="Video 1")
assert result is not None
assert result.info._id == "id1"
await db.close()
@ -349,7 +349,7 @@ class TestDataStore:
await store._connection.flush()
# Should return first match (note: OrderedDict maintains insertion order)
result = store.get_item(title="Same Title")
result = await store.get_item(title="Same Title")
assert result is not None
assert result.info._id == "id1"
await db.close()
@ -788,17 +788,17 @@ class TestDataStoreOperations:
await store.put(StubDownload(info=item1))
# Test with explicit EQUAL operation
result = store.get_item(title=(Operation.EQUAL, "Exact Match"))
result = await store.get_item(title=(Operation.EQUAL, "Exact Match"))
assert result is not None
assert result.info._id == "id1"
# Test default behavior (no operation specified)
result = store.get_item(title="Exact Match")
result = await store.get_item(title="Exact Match")
assert result is not None
assert result.info._id == "id1"
# Test no match
result = store.get_item(title=(Operation.EQUAL, "No Match"))
result = await store.get_item(title=(Operation.EQUAL, "No Match"))
assert result is None
await db.close()
@ -816,7 +816,7 @@ class TestDataStoreOperations:
await store.put(StubDownload(info=item2))
# Find item where title is not "Video 1"
result = store.get_item(title=(Operation.NOT_EQUAL, "Video 1"))
result = await store.get_item(title=(Operation.NOT_EQUAL, "Video 1"))
assert result is not None
assert result.info._id == "id2"
await db.close()
@ -835,17 +835,17 @@ class TestDataStoreOperations:
await store.put(StubDownload(info=item2))
# Find item with "Python" in title
result = store.get_item(title=(Operation.CONTAIN, "Python"))
result = await store.get_item(title=(Operation.CONTAIN, "Python"))
assert result is not None
assert result.info._id == "id1"
# Find item with "Tutorial" in title
result = store.get_item(title=(Operation.CONTAIN, "Tutorial"))
result = await store.get_item(title=(Operation.CONTAIN, "Tutorial"))
assert result is not None
assert result.info._id == "id1"
# No match
result = store.get_item(title=(Operation.CONTAIN, "Rust"))
result = await store.get_item(title=(Operation.CONTAIN, "Rust"))
assert result is None
await db.close()
@ -863,7 +863,7 @@ class TestDataStoreOperations:
await store.put(StubDownload(info=item2))
# Find item that doesn't contain "Python"
result = store.get_item(title=(Operation.NOT_CONTAIN, "Python"))
result = await store.get_item(title=(Operation.NOT_CONTAIN, "Python"))
assert result is not None
assert result.info._id == "id2"
await db.close()
@ -882,17 +882,17 @@ class TestDataStoreOperations:
await store.put(StubDownload(info=item2))
# Find item starting with "Tutorial"
result = store.get_item(title=(Operation.STARTS_WITH, "Tutorial"))
result = await store.get_item(title=(Operation.STARTS_WITH, "Tutorial"))
assert result is not None
assert result.info._id == "id1"
# Find item starting with "Course"
result = store.get_item(title=(Operation.STARTS_WITH, "Course"))
result = await store.get_item(title=(Operation.STARTS_WITH, "Course"))
assert result is not None
assert result.info._id == "id2"
# No match
result = store.get_item(title=(Operation.STARTS_WITH, "Video"))
result = await store.get_item(title=(Operation.STARTS_WITH, "Video"))
assert result is None
await db.close()
@ -910,17 +910,17 @@ class TestDataStoreOperations:
await store.put(StubDownload(info=item2))
# Find item ending with "Python"
result = store.get_item(title=(Operation.ENDS_WITH, "Python"))
result = await store.get_item(title=(Operation.ENDS_WITH, "Python"))
assert result is not None
assert result.info._id == "id1"
# Find item ending with "JavaScript"
result = store.get_item(title=(Operation.ENDS_WITH, "JavaScript"))
result = await store.get_item(title=(Operation.ENDS_WITH, "JavaScript"))
assert result is not None
assert result.info._id == "id2"
# No match
result = store.get_item(title=(Operation.ENDS_WITH, "Course"))
result = await store.get_item(title=(Operation.ENDS_WITH, "Course"))
assert result is None
await db.close()
@ -940,12 +940,12 @@ class TestDataStoreOperations:
await store.put(StubDownload(info=item2))
# Find item with filesize > 1500
result = store.get_item(filesize=(Operation.GREATER_THAN, 1500))
result = await store.get_item(filesize=(Operation.GREATER_THAN, 1500))
assert result is not None
assert result.info._id == "id2"
# Find item with filesize > 500 (should return first match)
result = store.get_item(filesize=(Operation.GREATER_THAN, 500))
result = await store.get_item(filesize=(Operation.GREATER_THAN, 500))
assert result is not None
assert result.info._id == "id1"
await db.close()
@ -966,7 +966,7 @@ class TestDataStoreOperations:
await store.put(StubDownload(info=item2))
# Find item with filesize < 1500
result = store.get_item(filesize=(Operation.LESS_THAN, 1500))
result = await store.get_item(filesize=(Operation.LESS_THAN, 1500))
assert result is not None
assert result.info._id == "id1"
await db.close()
@ -983,17 +983,17 @@ class TestDataStoreOperations:
await store.put(StubDownload(info=item1))
# Test >= with exact match
result = store.get_item(filesize=(Operation.GREATER_EQUAL, 1000))
result = await store.get_item(filesize=(Operation.GREATER_EQUAL, 1000))
assert result is not None
assert result.info._id == "id1"
# Test >= with less than
result = store.get_item(filesize=(Operation.GREATER_EQUAL, 500))
result = await store.get_item(filesize=(Operation.GREATER_EQUAL, 500))
assert result is not None
assert result.info._id == "id1"
# Test >= with greater than
result = store.get_item(filesize=(Operation.GREATER_EQUAL, 1500))
result = await store.get_item(filesize=(Operation.GREATER_EQUAL, 1500))
assert result is None
await db.close()
@ -1009,15 +1009,15 @@ class TestDataStoreOperations:
await store.put(StubDownload(info=item1))
# Test <= with exact match
result = store.get_item(filesize=(Operation.LESS_EQUAL, 1000))
result = await store.get_item(filesize=(Operation.LESS_EQUAL, 1000))
assert result is not None
# Test <= with greater than
result = store.get_item(filesize=(Operation.LESS_EQUAL, 1500))
result = await store.get_item(filesize=(Operation.LESS_EQUAL, 1500))
assert result is not None
# Test <= with less than
result = store.get_item(filesize=(Operation.LESS_EQUAL, 500))
result = await store.get_item(filesize=(Operation.LESS_EQUAL, 500))
assert result is None
await db.close()
@ -1038,12 +1038,12 @@ class TestDataStoreOperations:
await store.put(StubDownload(info=item3))
# Mix of operation and default (any match returns true)
result = store.get_item(title=(Operation.CONTAIN, "Python"), folder="tutorials")
result = await store.get_item(title=(Operation.CONTAIN, "Python"), folder="tutorials")
assert result is not None
assert result.info._id == "id1"
# Mix where first condition matches
result = store.get_item(title=(Operation.CONTAIN, "JavaScript"), folder="nonexistent")
result = await store.get_item(title=(Operation.CONTAIN, "JavaScript"), folder="nonexistent")
assert result is not None
assert result.info._id == "id3"
await db.close()
@ -1060,15 +1060,15 @@ class TestDataStoreOperations:
await store.put(StubDownload(info=item1))
# CONTAIN with None field should return False
result = store.get_item(description=(Operation.CONTAIN, "test"))
result = await store.get_item(description=(Operation.CONTAIN, "test"))
assert result is None
# NOT_CONTAIN with None field should return True
result = store.get_item(description=(Operation.NOT_CONTAIN, "test"))
result = await store.get_item(description=(Operation.NOT_CONTAIN, "test"))
assert result is not None
# GREATER_THAN with None should return False
result = store.get_item(description=(Operation.GREATER_THAN, 100))
result = await store.get_item(description=(Operation.GREATER_THAN, 100))
assert result is None
await db.close()
@ -1083,7 +1083,7 @@ class TestDataStoreOperations:
await store.put(StubDownload(info=item1))
# Try to compare string with number using > (should return False/None)
result = store.get_item(title=(Operation.GREATER_THAN, 100))
result = await store.get_item(title=(Operation.GREATER_THAN, 100))
assert result is None
await db.close()
@ -1098,12 +1098,12 @@ class TestDataStoreOperations:
await store.put(StubDownload(info=item1))
# Using string operation value
result = store.get_item(title=("in", "Python"))
result = await store.get_item(title=("in", "Python"))
assert result is not None
assert result.info._id == "id1"
# Using string for EQUAL
result = store.get_item(title=("==", "Python Tutorial"))
result = await store.get_item(title=("==", "Python Tutorial"))
assert result is not None
await db.close()
@ -1118,9 +1118,9 @@ class TestDataStoreOperations:
await store.put(StubDownload(info=item1))
# Try to match on non-existent field
result = store.get_item(nonexistent_field=(Operation.EQUAL, "value"))
result = await store.get_item(nonexistent_field=(Operation.EQUAL, "value"))
assert result is None
result = store.get_item(nonexistent_field=(Operation.CONTAIN, "value"))
result = await store.get_item(nonexistent_field=(Operation.CONTAIN, "value"))
assert result is None
await db.close()

View file

@ -3,6 +3,7 @@ from datetime import UTC, datetime, timedelta
import pytest
from app.library.ItemDTO import ItemDTO
from app.library.operations import Operation
from app.library.sqlite_store import SqliteStore
@ -171,6 +172,47 @@ async def test_paginate_out_of_range_returns_last_page():
await store.close()
@pytest.mark.asyncio
async def test_get_item_returns_none_without_kwargs():
store = await make_store()
await store.enqueue_upsert("queue", make_item(1))
await store.flush()
result = await store.get_item("queue")
assert result is None
await store.close()
@pytest.mark.asyncio
async def test_get_item_matches_conditions():
store = await make_store()
first = make_item(1, status="finished")
second = make_item(2, status="pending")
await store.enqueue_upsert("queue", first)
await store.enqueue_upsert("queue", second)
await store.flush()
# equality match
match_equal = await store.get_item("queue", title=first.title)
assert match_equal is not None
assert match_equal._id == first._id
# NOT_EQUAL should find the second item
match_not_equal = await store.get_item("queue", status=(Operation.NOT_EQUAL, "finished"))
assert match_not_equal is not None
assert match_not_equal._id == second._id
# CONTAIN should find first created (first)
match_contain = await store.get_item("queue", title=(Operation.CONTAIN, "Video"))
assert match_contain is not None
assert match_contain._id == first._id
# No match returns None
no_match = await store.get_item("queue", title=(Operation.CONTAIN, "does-not-exist"))
assert no_match is None
await store.close()
@pytest.mark.asyncio
async def test_on_shutdown_closes_connection():
store = await make_store()
@ -181,3 +223,34 @@ async def test_on_shutdown_closes_connection():
assert store._conn is None
@pytest.mark.asyncio
async def test_exists_and_get_by_key_and_url():
store = await make_store()
item = make_item(42)
await store.enqueue_upsert("queue", item)
await store.flush()
assert await store.exists("queue", key=item._id) is True
assert await store.exists("queue", url=item.url) is True
fetched_by_key = await store.get("queue", key=item._id)
assert fetched_by_key is not None
assert fetched_by_key._id == item._id
fetched_by_url = await store.get("queue", url=item.url)
assert fetched_by_url is not None
assert fetched_by_url._id == item._id
assert await store.exists("queue", key="missing") is False
assert await store.get("queue", key="missing") is None
await store.close()
@pytest.mark.asyncio
async def test_exists_and_get_raise_without_key_or_url():
store = await make_store()
with pytest.raises(KeyError):
await store.exists("queue")
with pytest.raises(KeyError):
await store.get("queue")
await store.close()