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",
|
"ahash",
|
||||||
"aiocron",
|
"aiocron",
|
||||||
"aiosqlite",
|
"aiosqlite",
|
||||||
|
"alnum",
|
||||||
"anyio",
|
"anyio",
|
||||||
"Archiver",
|
"Archiver",
|
||||||
"arrowdown",
|
"arrowdown",
|
||||||
|
|
|
||||||
|
|
@ -61,17 +61,20 @@ class DataStore:
|
||||||
async def saved_items(self) -> list[tuple[str, ItemDTO]]:
|
async def saved_items(self) -> list[tuple[str, ItemDTO]]:
|
||||||
return await self._connection.fetch_saved(str(self._type))
|
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:
|
if not key and not url:
|
||||||
msg = "key or url must be provided."
|
msg = "key or url must be provided."
|
||||||
raise KeyError(msg)
|
raise KeyError(msg)
|
||||||
|
|
||||||
if key and key in self._dict:
|
if key and key in self._dict:
|
||||||
return True
|
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:
|
if not key and not url:
|
||||||
msg = "key or url must be provided."
|
msg = "key or url must be provided."
|
||||||
raise KeyError(msg)
|
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):
|
if (key and self._dict[i].info._id == key) or (url and self._dict[i].info.url == url):
|
||||||
return self._dict[i]
|
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."
|
msg: str = f"{key=} or {url=} not found."
|
||||||
raise KeyError(msg)
|
raise KeyError(msg)
|
||||||
|
|
||||||
def get_item(self, **kwargs) -> Download | None:
|
async def get_item(self, **kwargs) -> Download | None:
|
||||||
if not kwargs:
|
if not kwargs:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -96,6 +103,10 @@ class DataStore:
|
||||||
if any(matches_condition(key, value, info) for key, value in kwargs.items()):
|
if any(matches_condition(key, value, info) for key, value in kwargs.items()):
|
||||||
return self._dict[i]
|
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
|
return None
|
||||||
|
|
||||||
async def get_by_id(self, id: str) -> Download | None:
|
async def get_by_id(self, id: str) -> Download | None:
|
||||||
|
|
@ -104,8 +115,7 @@ class DataStore:
|
||||||
return val
|
return val
|
||||||
|
|
||||||
if item := await self._connection.get_by_id(str(self._type), id):
|
if item := await self._connection.get_by_id(str(self._type), id):
|
||||||
download = Download(info=item)
|
self._dict[item._id] = Download(info=item)
|
||||||
self._dict[id] = download
|
|
||||||
return self._dict[id]
|
return self._dict[id]
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,6 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
self._active: dict[str, Download] = {}
|
self._active: dict[str, Download] = {}
|
||||||
"""Dictionary of active downloads."""
|
"""Dictionary of active downloads."""
|
||||||
|
|
||||||
# Loading happens in async initialize to avoid blocking loop creation.
|
|
||||||
self.paused.set()
|
self.paused.set()
|
||||||
|
|
||||||
@staticmethod
|
@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."
|
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")
|
asyncio.create_task(self._download_pool(), name="download_pool")
|
||||||
await self.done.load()
|
|
||||||
|
|
||||||
async def start_items(self, ids: list[str]) -> dict[str, str]:
|
async def start_items(self, ids: list[str]) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -188,7 +186,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
|
|
||||||
for item_id in ids:
|
for item_id in ids:
|
||||||
try:
|
try:
|
||||||
item: Download = self.queue.get(key=item_id)
|
item: Download = await self.queue.get(key=item_id)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
status[item_id] = f"not found: {e!s}"
|
status[item_id] = f"not found: {e!s}"
|
||||||
status["status"] = "error"
|
status["status"] = "error"
|
||||||
|
|
@ -231,7 +229,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
|
|
||||||
for item_id in ids:
|
for item_id in ids:
|
||||||
try:
|
try:
|
||||||
item: Download = self.queue.get(key=item_id)
|
item: Download = await self.queue.get(key=item_id)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
status[item_id] = f"not found: {e!s}"
|
status[item_id] = f"not found: {e!s}"
|
||||||
status["status"] = "error"
|
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')}'.")
|
LOG.debug(f"Entry id '{entry.get('id')}' url '{entry.get('webpage_url')} - {entry.get('url')}'.")
|
||||||
|
|
||||||
try:
|
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."
|
err_msg: str = f"Removing {_item.info.name()} from history list."
|
||||||
LOG.warning(err_msg)
|
LOG.warning(err_msg)
|
||||||
await self.clear([_item.info._id], remove_file=False)
|
await self.clear([_item.info._id], remove_file=False)
|
||||||
|
|
@ -452,7 +450,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
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"))
|
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."
|
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 item.is_archived():
|
||||||
if archive_id:
|
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:
|
if not store_type:
|
||||||
dlInfo = Download(
|
dlInfo = Download(
|
||||||
info=ItemDTO(
|
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}."
|
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:
|
if not store_type:
|
||||||
dlInfo = Download(
|
dlInfo = Download(
|
||||||
info=ItemDTO(
|
info=ItemDTO(
|
||||||
|
|
@ -886,7 +884,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
|
|
||||||
for id in ids:
|
for id in ids:
|
||||||
try:
|
try:
|
||||||
item = self.queue.get(key=id)
|
item = await self.queue.get(key=id)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
status[id] = str(e)
|
status[id] = str(e)
|
||||||
status["status"] = "error"
|
status["status"] = "error"
|
||||||
|
|
@ -940,7 +938,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
|
|
||||||
for id in ids:
|
for id in ids:
|
||||||
try:
|
try:
|
||||||
item: Download = self.done.get(key=id)
|
item: Download = await self.done.get(key=id)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
status[id] = str(e)
|
status[id] = str(e)
|
||||||
status["status"] = "error"
|
status["status"] = "error"
|
||||||
|
|
@ -1041,7 +1039,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
|
|
||||||
return items
|
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.
|
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.
|
(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)
|
return (StoreType.QUEUE, item)
|
||||||
|
|
||||||
if item := self.done.get_item(**kwargs):
|
if item := await self.done.get_item(**kwargs):
|
||||||
return (StoreType.HISTORY, item)
|
return (StoreType.HISTORY, item)
|
||||||
|
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
@ -1166,7 +1164,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
|
|
||||||
await entry.close()
|
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.")
|
LOG.debug(f"Download Task '{id}' is completed. Removing from queue.")
|
||||||
await self.queue.delete(key=id)
|
await self.queue.delete(key=id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -828,11 +828,11 @@ class HandleTask:
|
||||||
if archive_file and archive_id in downloaded:
|
if archive_file and archive_id in downloaded:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if download_queue.queue.exists(url=url):
|
if await download_queue.queue.exists(url=url):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
done = download_queue.done.get(url=url)
|
done = await download_queue.done.get(url=url)
|
||||||
if "error" != done.info.status:
|
if "error" != done.info.status:
|
||||||
continue
|
continue
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import aiosqlite
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from .ItemDTO import ItemDTO
|
from .ItemDTO import ItemDTO
|
||||||
|
from .operations import Operation, matches_condition
|
||||||
from .Singleton import ThreadSafe
|
from .Singleton import ThreadSafe
|
||||||
from .Utils import init_class
|
from .Utils import init_class
|
||||||
|
|
||||||
|
|
@ -70,7 +71,6 @@ class SqliteStore(metaclass=ThreadSafe):
|
||||||
async def __aexit__(self, exc_type, exc, tb) -> None:
|
async def __aexit__(self, exc_type, exc, tb) -> None:
|
||||||
await self.close()
|
await self.close()
|
||||||
|
|
||||||
# ---------- public API ----------
|
|
||||||
async def fetch_saved(self, type_value: str) -> list[tuple[str, ItemDTO]]:
|
async def fetch_saved(self, type_value: str) -> list[tuple[str, ItemDTO]]:
|
||||||
await self._ensure_conn()
|
await self._ensure_conn()
|
||||||
cursor = await self._conn.execute(
|
cursor = await self._conn.execute(
|
||||||
|
|
@ -89,6 +89,42 @@ class SqliteStore(metaclass=ThreadSafe):
|
||||||
items.append((row["id"], item))
|
items.append((row["id"], item))
|
||||||
return items
|
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:
|
async def get_by_id(self, type_value: str, id: str) -> ItemDTO | None:
|
||||||
await self._ensure_conn()
|
await self._ensure_conn()
|
||||||
cursor = await self._conn.execute(
|
cursor = await self._conn.execute(
|
||||||
|
|
@ -107,6 +143,94 @@ class SqliteStore(metaclass=ThreadSafe):
|
||||||
item.datetime = formatdate(row_date.replace(tzinfo=UTC).timestamp())
|
item.datetime = formatdate(row_date.replace(tzinfo=UTC).timestamp())
|
||||||
return item
|
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:
|
async def count(self, type_value: str, status_filter: str | None = None) -> int:
|
||||||
await self._ensure_conn()
|
await self._ensure_conn()
|
||||||
where_clauses = ['"type" = ?']
|
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:
|
for old_sidecar, new_sidecar in sidecar_renamed:
|
||||||
record(old_sidecar, ok=True, action=action, extra={"new_path": new_sidecar})
|
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))
|
item.info.filename = str(renamed.relative_to(config.download_path))
|
||||||
if sidecar_renamed:
|
if sidecar_renamed:
|
||||||
item.info.get_file_sidecar()
|
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:
|
for old_sidecar, new_sidecar in sidecar_moved:
|
||||||
record(old_sidecar, ok=True, action=action, extra={"new_path": new_sidecar})
|
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))
|
item.info.filename = str(moved.relative_to(config.download_path))
|
||||||
if sidecar_moved:
|
if sidecar_moved:
|
||||||
item.info.get_file_sidecar()
|
item.info.get_file_sidecar()
|
||||||
|
|
|
||||||
|
|
@ -145,19 +145,19 @@ class TestDataStore:
|
||||||
await store.put(d)
|
await store.put(d)
|
||||||
await store._connection.flush()
|
await store._connection.flush()
|
||||||
|
|
||||||
assert store.exists(key=item._id) is True
|
assert await store.exists(key=item._id) is True
|
||||||
assert store.exists(url=item.url) is True
|
assert await store.exists(url=item.url) is True
|
||||||
with pytest.raises(KeyError):
|
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
|
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
|
assert got2.info.url == item.url
|
||||||
with pytest.raises(KeyError):
|
with pytest.raises(KeyError):
|
||||||
store.get()
|
await store.get()
|
||||||
with pytest.raises(KeyError):
|
with pytest.raises(KeyError):
|
||||||
store.get(key="missing")
|
await store.get(key="missing")
|
||||||
await store._connection.close()
|
await store._connection.close()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
@ -212,7 +212,7 @@ class TestDataStore:
|
||||||
db = await make_db()
|
db = await make_db()
|
||||||
store = DataStore(StoreType.QUEUE, db)
|
store = DataStore(StoreType.QUEUE, db)
|
||||||
|
|
||||||
result = store.get_item()
|
result = await store.get_item()
|
||||||
assert result is None
|
assert result is None
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
||||||
|
|
@ -236,19 +236,19 @@ class TestDataStore:
|
||||||
await store._connection.flush()
|
await store._connection.flush()
|
||||||
|
|
||||||
# Test finding by title
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
assert result.info.title == "Video 1"
|
assert result.info.title == "Video 1"
|
||||||
|
|
||||||
# Test finding by folder
|
# Test finding by folder
|
||||||
result = store.get_item(folder="folder2")
|
result = await store.get_item(folder="folder2")
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result.info._id == "id2"
|
assert result.info._id == "id2"
|
||||||
assert result.info.folder == "folder2"
|
assert result.info.folder == "folder2"
|
||||||
|
|
||||||
# Test finding by url
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
@ -272,12 +272,12 @@ class TestDataStore:
|
||||||
await store._connection.flush()
|
await store._connection.flush()
|
||||||
|
|
||||||
# Test finding by multiple attributes where one matches
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
|
|
||||||
# Test finding where second attribute matches
|
# 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 is not None
|
||||||
assert result.info._id == "id2"
|
assert result.info._id == "id2"
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
@ -295,11 +295,11 @@ class TestDataStore:
|
||||||
await store._connection.flush()
|
await store._connection.flush()
|
||||||
|
|
||||||
# Test with non-matching attribute
|
# Test with non-matching attribute
|
||||||
result = store.get_item(title="Nonexistent Video")
|
result = await store.get_item(title="Nonexistent Video")
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
# Test with non-existent attribute key
|
# 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
|
assert result is None
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
||||||
|
|
@ -324,7 +324,7 @@ class TestDataStore:
|
||||||
store._dict["broken"] = BrokenDownload()
|
store._dict["broken"] = BrokenDownload()
|
||||||
|
|
||||||
# Should still find the valid item
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
@ -349,7 +349,7 @@ class TestDataStore:
|
||||||
await store._connection.flush()
|
await store._connection.flush()
|
||||||
|
|
||||||
# Should return first match (note: OrderedDict maintains insertion order)
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
@ -788,17 +788,17 @@ class TestDataStoreOperations:
|
||||||
await store.put(StubDownload(info=item1))
|
await store.put(StubDownload(info=item1))
|
||||||
|
|
||||||
# Test with explicit EQUAL operation
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
|
|
||||||
# Test default behavior (no operation specified)
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
|
|
||||||
# Test no match
|
# 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
|
assert result is None
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
||||||
|
|
@ -816,7 +816,7 @@ class TestDataStoreOperations:
|
||||||
await store.put(StubDownload(info=item2))
|
await store.put(StubDownload(info=item2))
|
||||||
|
|
||||||
# Find item where title is not "Video 1"
|
# 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 is not None
|
||||||
assert result.info._id == "id2"
|
assert result.info._id == "id2"
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
@ -835,17 +835,17 @@ class TestDataStoreOperations:
|
||||||
await store.put(StubDownload(info=item2))
|
await store.put(StubDownload(info=item2))
|
||||||
|
|
||||||
# Find item with "Python" in title
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
|
|
||||||
# Find item with "Tutorial" in title
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
|
|
||||||
# No match
|
# No match
|
||||||
result = store.get_item(title=(Operation.CONTAIN, "Rust"))
|
result = await store.get_item(title=(Operation.CONTAIN, "Rust"))
|
||||||
assert result is None
|
assert result is None
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
||||||
|
|
@ -863,7 +863,7 @@ class TestDataStoreOperations:
|
||||||
await store.put(StubDownload(info=item2))
|
await store.put(StubDownload(info=item2))
|
||||||
|
|
||||||
# Find item that doesn't contain "Python"
|
# 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 is not None
|
||||||
assert result.info._id == "id2"
|
assert result.info._id == "id2"
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
@ -882,17 +882,17 @@ class TestDataStoreOperations:
|
||||||
await store.put(StubDownload(info=item2))
|
await store.put(StubDownload(info=item2))
|
||||||
|
|
||||||
# Find item starting with "Tutorial"
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
|
|
||||||
# Find item starting with "Course"
|
# 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 is not None
|
||||||
assert result.info._id == "id2"
|
assert result.info._id == "id2"
|
||||||
|
|
||||||
# No match
|
# 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
|
assert result is None
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
||||||
|
|
@ -910,17 +910,17 @@ class TestDataStoreOperations:
|
||||||
await store.put(StubDownload(info=item2))
|
await store.put(StubDownload(info=item2))
|
||||||
|
|
||||||
# Find item ending with "Python"
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
|
|
||||||
# Find item ending with "JavaScript"
|
# 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 is not None
|
||||||
assert result.info._id == "id2"
|
assert result.info._id == "id2"
|
||||||
|
|
||||||
# No match
|
# 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
|
assert result is None
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
||||||
|
|
@ -940,12 +940,12 @@ class TestDataStoreOperations:
|
||||||
await store.put(StubDownload(info=item2))
|
await store.put(StubDownload(info=item2))
|
||||||
|
|
||||||
# Find item with filesize > 1500
|
# 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 is not None
|
||||||
assert result.info._id == "id2"
|
assert result.info._id == "id2"
|
||||||
|
|
||||||
# Find item with filesize > 500 (should return first match)
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
@ -966,7 +966,7 @@ class TestDataStoreOperations:
|
||||||
await store.put(StubDownload(info=item2))
|
await store.put(StubDownload(info=item2))
|
||||||
|
|
||||||
# Find item with filesize < 1500
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
@ -983,17 +983,17 @@ class TestDataStoreOperations:
|
||||||
await store.put(StubDownload(info=item1))
|
await store.put(StubDownload(info=item1))
|
||||||
|
|
||||||
# Test >= with exact match
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
|
|
||||||
# Test >= with less than
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
|
|
||||||
# Test >= with greater than
|
# 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
|
assert result is None
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
||||||
|
|
@ -1009,15 +1009,15 @@ class TestDataStoreOperations:
|
||||||
await store.put(StubDownload(info=item1))
|
await store.put(StubDownload(info=item1))
|
||||||
|
|
||||||
# Test <= with exact match
|
# 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
|
assert result is not None
|
||||||
|
|
||||||
# Test <= with greater than
|
# 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
|
assert result is not None
|
||||||
|
|
||||||
# Test <= with less than
|
# 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
|
assert result is None
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
||||||
|
|
@ -1038,12 +1038,12 @@ class TestDataStoreOperations:
|
||||||
await store.put(StubDownload(info=item3))
|
await store.put(StubDownload(info=item3))
|
||||||
|
|
||||||
# Mix of operation and default (any match returns true)
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
|
|
||||||
# Mix where first condition matches
|
# 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 is not None
|
||||||
assert result.info._id == "id3"
|
assert result.info._id == "id3"
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
@ -1060,15 +1060,15 @@ class TestDataStoreOperations:
|
||||||
await store.put(StubDownload(info=item1))
|
await store.put(StubDownload(info=item1))
|
||||||
|
|
||||||
# CONTAIN with None field should return False
|
# 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
|
assert result is None
|
||||||
|
|
||||||
# NOT_CONTAIN with None field should return True
|
# 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
|
assert result is not None
|
||||||
|
|
||||||
# GREATER_THAN with None should return False
|
# 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
|
assert result is None
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
||||||
|
|
@ -1083,7 +1083,7 @@ class TestDataStoreOperations:
|
||||||
await store.put(StubDownload(info=item1))
|
await store.put(StubDownload(info=item1))
|
||||||
|
|
||||||
# Try to compare string with number using > (should return False/None)
|
# 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
|
assert result is None
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
||||||
|
|
@ -1098,12 +1098,12 @@ class TestDataStoreOperations:
|
||||||
await store.put(StubDownload(info=item1))
|
await store.put(StubDownload(info=item1))
|
||||||
|
|
||||||
# Using string operation value
|
# 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 is not None
|
||||||
assert result.info._id == "id1"
|
assert result.info._id == "id1"
|
||||||
|
|
||||||
# Using string for EQUAL
|
# Using string for EQUAL
|
||||||
result = store.get_item(title=("==", "Python Tutorial"))
|
result = await store.get_item(title=("==", "Python Tutorial"))
|
||||||
assert result is not None
|
assert result is not None
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
||||||
|
|
@ -1118,9 +1118,9 @@ class TestDataStoreOperations:
|
||||||
await store.put(StubDownload(info=item1))
|
await store.put(StubDownload(info=item1))
|
||||||
|
|
||||||
# Try to match on non-existent field
|
# 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
|
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
|
assert result is None
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from datetime import UTC, datetime, timedelta
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.library.ItemDTO import ItemDTO
|
from app.library.ItemDTO import ItemDTO
|
||||||
|
from app.library.operations import Operation
|
||||||
from app.library.sqlite_store import SqliteStore
|
from app.library.sqlite_store import SqliteStore
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -171,6 +172,47 @@ async def test_paginate_out_of_range_returns_last_page():
|
||||||
await store.close()
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_on_shutdown_closes_connection():
|
async def test_on_shutdown_closes_connection():
|
||||||
store = await make_store()
|
store = await make_store()
|
||||||
|
|
@ -181,3 +223,34 @@ async def test_on_shutdown_closes_connection():
|
||||||
assert store._conn is None
|
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