The application
I'm making a small app (currently ~500 lines) to manage and download books and other media items. Each item is uniquely identify by its (downloader, code)
tuple and the index (save in a pickled file) contains the title, authors, and tags (e.g. "scfi-fi", "comedy") of each item. The "downloader" is a module in a collection of user-made modules to scrape certain pages for metadata (title, authors and tags). For example, the module example_dl
, given either the url example.com/book/123
or code 123
, handles scraping example.com
for the book with the code/ID of 123
, namely its metadata and possibly download the book to a directory.
The code below contains the dictionary config
for global configuration variables (stored in a config file shelf.yaml
), a short description of the classes Item
and Shelf
and the function I'd like to be reviewed, search_item()
.
Code
import sys
import os
import re
import argparse
import importlib.util
import yaml
import pickle
# Default configurations
PATH = os.path.expanduser("~/.config/shelf")
config = {
"config_file": PATH + "/shelf.yaml",
"index_file": PATH + "/data/index",
"favorites_file": PATH + "/data/favorites",
"downloaders_dir": PATH + "/downloaders",
"data_dir": PATH + "/data",
"threads": 10,
}
class Item:
"""Items stored in the Shelf
Attributes:
title: title of the media item
authors: set of authors
tags: set of tags
media: media type the item is stored as (e.g. png, md, pdf)
"""
title = None
authors = None
tags = None
media = None
class Shelf:
"""Shelf containing indexed data of items
Attributes:
downloaders: a dict of available downloaders
index: Shelf indexing containing data about all items saved. Data is read from index file
and is as follows:
(
(downloader1, code1) : item_1,
(downloader2, code2) : item_2
)
"""
global config
downloaders = dict()
index = dict()
favorites = set()
verbosity = 0
def search_item(
self,
title_regex: str,
authors: set[str],
tags: set[str],
blacklist: set[str],
broad_search=False,
favorites=False,
) -> list[tuple[str, str]]:
"""Search item in index
Args:
title_regex: regex to search title by
authors: set of authors OR comma-separated string
tags: set of tags OR comma-separated string
broad_search: If False (Default), returns only when *all* tags match. If True, returns when at least 1 tag matches.
favorites: only search items in favorties
Returns:
A 2-tuple containing the downloader and code. For example: ("test_dl", "test_code")
"""
if self.verbosity >= 2:
print(
"Searching with:\n"
f"\tTitle regex: {title_regex}\n"
f"\tAuthors: {authors}\n"
f"\tTags: {tags}\n"
f"\tBroad search: {broad_search}\n"
f"\tFavorites: {favorites}"
)
result = None
if favorites: # Search in favorites
result = self.favorites
if self.verbosity >= 2:
print(f"Filtered by favorites: {result}")
if title_regex: # Search with title regex
result = {
(k, v)
for (k, v) in self.index
if re.match(title_regex, self.index[(k, v)].title)
}
if self.verbosity >= 2:
print(f"Filtered by title regex: {result}")
if authors: # Search by authors (always broad search)
if type(authors) == str:
authors = set(authors.split(","))
if result:
result = {
(k, v) for (k, v) in result if authors & self.index[(k, v)].authors
}
else:
result = {
(k, v)
for (k, v) in self.index
if authors & self.index[(k, v)].authors
}
if self.verbosity >= 2:
print(f"Filtered by authors: {result}")
if tags: # Search by tags
if type(tags) == str:
tags = set(tags.split(","))
if broad_search: # Broad search
if result:
result = {
(k, v) for (k, v) in result if tags & self.index[(k, v)].tags
}
else:
result = {
(k, v)
for (k, v) in self.index
if tags & self.index[(k, v)].tags
}
if self.verbosity >= 2:
print(f"Filtered by broad_search: {result}")
else: # Normal search
if result:
result = {
(k, v)
for (k, v) in result
if not (tags - self.index[(k, v)].tags)
}
else:
result = {
(k, v)
for (k, v) in self.index
if not (tags - self.index[(k, v)].tags)
}
if self.verbosity >= 2:
print(f"Filtered by broad_search: {result}")
if blacklist:
if type(blacklist) == str:
blacklist = set(blacklist.split(","))
if result:
result = {
(k, v) for (k, v) in result if not blacklist & self.index[(k, v)].tags
}
else:
result = {
(k, v)
for (k, v) in self.index
if not blacklist & self.index[(k, v)].tags
}
return result
Description of search_item
The function search_item
goes runs the index
dictionary through 5 filters (kinda): favorites
, title_regex
, author
, tags
, and blacklist
. The switches available to affect the search are -t TITLE_REGEX
, -a AUTHOR
, -T TAGS
, --broad-search
, --favorites
, --blacklist
.
if result
I have if result
and else
in almost every filter to hopefully improve speed and memory usage, since not every filter is required to be used in a single search.
favorites
The Shelf
object has a set
of 2-tuples, each tuple being (downloader, code)
. Since favorites
is a subset of Shelf.index.keys()
, I simply copy the entire set
of favorites
to be further filtered.
title_regex
and author
These 2 are similar in implementation. The only major difference is that title_regex
is matched using re.match(user_defined_regex, titles_in_index)
, while author is filtered with `author in set_of_authors.
tags
The --broad-search
switch is only used with the search_book
function when tags
is used, and is ignored otherwise.
Normal Search
In normal search, all of the provided tags (comma-separated) must match the book's tags to show up in the result.
Broad search
In broad search, books show up in the result if at least 1 of the provided tags is found in the book's set
of tags.
blacklist
Basically the inverse of broad search.
os
andre
but I don't want to assume. \$\endgroup\$author
is to be matched againstself.index[(k, v)].tags
? \$\endgroup\$not blacklist & tags
\$\endgroup\$