Refactor: migrated whatleft of sync code to async
This commit is contained in:
parent
699e21f54d
commit
b36d9fd641
8 changed files with 283 additions and 77 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
|
@ -17,6 +17,7 @@
|
|||
"ahash",
|
||||
"aiocron",
|
||||
"aiosqlite",
|
||||
"alnum",
|
||||
"anyio",
|
||||
"Archiver",
|
||||
"arrowdown",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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" = ?']
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue