refspy.indexers

Indexing functions for libraries, books, and book aliases.

 1"""Indexing functions for libraries, books, and book aliases."""
 2
 3from refspy.models.book import Book
 4from refspy.models.library import Library
 5
 6from refspy.types.number import Number
 7
 8from refspy.utils import strip_book_number
 9
10PARAM_CHAR_LIMIT = 20
11"""The maximum book name or alias length to convert to URL parameter names."""
12
13
14def index_libraries(libraries: list[Library]) -> dict[Number, Library]:
15    """Create a lookup dict for libraries by library.id"""
16    index: dict[Number, Library] = dict()
17    for library in libraries:
18        index[library.id] = library
19    return index
20
21
22def index_books(libraries: list[Library]) -> dict[tuple[Number, Number], Book]:
23    """Create a lookup dict for books by (library.id, book.id)"""
24    index: dict[tuple[Number, Number], Book] = dict()
25    for library in libraries:
26        for book in library.books:
27            index[library.id, book.id] = book
28    return index
29
30
31def add_unique_book_alias(
32    index: dict[str, tuple[Number, Number]],
33    alias: str,
34    library_id: Number,
35    book_id: Number,
36    strict: bool = True,
37):
38    """Add an index entry for a library and book if none exists
39
40    Modifies the passed index dict.
41
42    Args:
43        strict: raise an error in the case of a collision between
44            alias values.
45    """
46    if alias in index and strict:
47        raise ValueError(f"Book alias '{alias}' is not unique.")
48    if alias == "":
49        raise ValueError(f"Book alias is '' for ({library_id},{book_id})")
50    else:
51        index[alias] = (library_id, book_id)
52
53
54def index_book_aliases(
55    libraries: list[Library],
56    include_two_letter_aliases: bool = True,
57    strict: bool = True,
58) -> dict[str, tuple[Number, Number]]:
59    """Create a lookup table for library and book IDs by book aliases.
60
61    Args:
62        libraries: A list of `refspy.library.Library` objects to be indexed.
63        include_two_letter_aliases: Allow book aliases of only two letters,
64            i.e. 'Jn' and '1 Ti'.
65        strict: Throw a ValueError if any two aliases are the same, or any
66            alias is an empty string.
67
68    Note:
69        Because books like Ruth or Mark have book.abbrev == book.name, we only
70        index the abbrev if it differs from the name. These books will not raise
71        a ValueError when strict=True.
72    """
73    index: dict[str, tuple[Number, Number]] = dict()
74    for library in libraries:
75        for book in library.books:
76            aliases = [book.name]
77            if book.name != book.abbrev:
78                aliases.append(book.abbrev)
79            for alias in book.aliases:
80                len_alias = len(strip_book_number(alias))
81                if (len_alias > 2 and len_alias < PARAM_CHAR_LIMIT) or (
82                    include_two_letter_aliases and len_alias == 2
83                ):
84                    aliases.append(alias)
85            for alias in aliases:
86                add_unique_book_alias(index, alias, library.id, book.id, strict)
87    return index
PARAM_CHAR_LIMIT = 20

The maximum book name or alias length to convert to URL parameter names.

def index_libraries( libraries: list[refspy.models.library.Library]) -> dict[typing.Annotated[int, Ge(ge=1), Le(le=999)], refspy.models.library.Library]:
15def index_libraries(libraries: list[Library]) -> dict[Number, Library]:
16    """Create a lookup dict for libraries by library.id"""
17    index: dict[Number, Library] = dict()
18    for library in libraries:
19        index[library.id] = library
20    return index

Create a lookup dict for libraries by library.id

def index_books( libraries: list[refspy.models.library.Library]) -> dict[tuple[typing.Annotated[int, Ge(ge=1), Le(le=999)], typing.Annotated[int, Ge(ge=1), Le(le=999)]], refspy.models.book.Book]:
23def index_books(libraries: list[Library]) -> dict[tuple[Number, Number], Book]:
24    """Create a lookup dict for books by (library.id, book.id)"""
25    index: dict[tuple[Number, Number], Book] = dict()
26    for library in libraries:
27        for book in library.books:
28            index[library.id, book.id] = book
29    return index

Create a lookup dict for books by (library.id, book.id)

def add_unique_book_alias( index: dict[str, tuple[typing.Annotated[int, Ge(ge=1), Le(le=999)], typing.Annotated[int, Ge(ge=1), Le(le=999)]]], alias: str, library_id: Annotated[int, Ge(ge=1), Le(le=999)], book_id: Annotated[int, Ge(ge=1), Le(le=999)], strict: bool = True):
32def add_unique_book_alias(
33    index: dict[str, tuple[Number, Number]],
34    alias: str,
35    library_id: Number,
36    book_id: Number,
37    strict: bool = True,
38):
39    """Add an index entry for a library and book if none exists
40
41    Modifies the passed index dict.
42
43    Args:
44        strict: raise an error in the case of a collision between
45            alias values.
46    """
47    if alias in index and strict:
48        raise ValueError(f"Book alias '{alias}' is not unique.")
49    if alias == "":
50        raise ValueError(f"Book alias is '' for ({library_id},{book_id})")
51    else:
52        index[alias] = (library_id, book_id)

Add an index entry for a library and book if none exists

Modifies the passed index dict.

Arguments:
  • strict: raise an error in the case of a collision between alias values.
def index_book_aliases( libraries: list[refspy.models.library.Library], include_two_letter_aliases: bool = True, strict: bool = True) -> dict[str, tuple[typing.Annotated[int, Ge(ge=1), Le(le=999)], typing.Annotated[int, Ge(ge=1), Le(le=999)]]]:
55def index_book_aliases(
56    libraries: list[Library],
57    include_two_letter_aliases: bool = True,
58    strict: bool = True,
59) -> dict[str, tuple[Number, Number]]:
60    """Create a lookup table for library and book IDs by book aliases.
61
62    Args:
63        libraries: A list of `refspy.library.Library` objects to be indexed.
64        include_two_letter_aliases: Allow book aliases of only two letters,
65            i.e. 'Jn' and '1 Ti'.
66        strict: Throw a ValueError if any two aliases are the same, or any
67            alias is an empty string.
68
69    Note:
70        Because books like Ruth or Mark have book.abbrev == book.name, we only
71        index the abbrev if it differs from the name. These books will not raise
72        a ValueError when strict=True.
73    """
74    index: dict[str, tuple[Number, Number]] = dict()
75    for library in libraries:
76        for book in library.books:
77            aliases = [book.name]
78            if book.name != book.abbrev:
79                aliases.append(book.abbrev)
80            for alias in book.aliases:
81                len_alias = len(strip_book_number(alias))
82                if (len_alias > 2 and len_alias < PARAM_CHAR_LIMIT) or (
83                    include_two_letter_aliases and len_alias == 2
84                ):
85                    aliases.append(alias)
86            for alias in aliases:
87                add_unique_book_alias(index, alias, library.id, book.id, strict)
88    return index

Create a lookup table for library and book IDs by book aliases.

Arguments:
  • libraries: A list of refspy.library.Library objects to be indexed.
  • include_two_letter_aliases: Allow book aliases of only two letters, i.e. 'Jn' and '1 Ti'.
  • strict: Throw a ValueError if any two aliases are the same, or any alias is an empty string.
Note:

Because books like Ruth or Mark have book.abbrev == book.name, we only index the abbrev if it differs from the name. These books will not raise a ValueError when strict=True.