"""
Provide queryset mixin classes for extending queryset functionality.
This module contains Protocol classes that define interfaces and mixins that
implement common filtering patterns for different types of querysets. These
mixins can be combined with concrete queryset classes to add specialized
filtering methods for different resource types.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Protocol, Self
if TYPE_CHECKING:
from paperap.models.abstract.queryset import BaseQuerySet, StandardQuerySet
[docs]
class QuerySetProtocol(Protocol):
"""
Define the basic interface for querysets.
This protocol establishes the minimum interface that all queryset mixins
can depend on. It's used primarily for type hinting and to ensure
consistent behavior across different queryset implementations.
All queryset classes should implement these methods to be compatible
with the mixin classes in this module.
"""
[docs]
def all(self) -> Self:
"""
Return a new queryset with all objects.
Returns:
Self: A new queryset containing all objects.
"""
...
[docs]
def filter(self, **kwargs: Any) -> Self:
"""
Filter the queryset by the given keyword arguments.
Args:
**kwargs: Field lookups to filter by.
Returns:
Self: A new filtered queryset.
Example:
>>> queryset.filter(name="example", created__gt="2023-01-01")
"""
...
[docs]
def filter_field_by_str(self, field: str, value: str, *, exact: bool = True, case_insensitive: bool = True) -> Self:
"""
Filter the queryset by comparing a field to a string value.
Args:
field: The name of the field to filter on.
value: The string value to compare against.
exact: Whether to perform an exact match (True) or a contains match (False).
case_insensitive: Whether to perform case-insensitive comparison.
Returns:
Self: A new filtered queryset.
Example:
>>> queryset.filter_field_by_str("name", "invoice", exact=False)
"""
...
[docs]
class HasDocumentCount(QuerySetProtocol, Protocol):
"""
Provide filtering methods for querysets with a document_count field.
This mixin provides convenience methods for filtering models based on their
document count. It's intended for resources like tags, correspondents, and
document types that have an associated count of documents.
Examples:
>>> # Find tags with exactly 5 documents
>>> client.tags().document_count(5)
>>>
>>> # Find document types with more than 10 documents
>>> client.document_types().document_count_over(10)
>>>
>>> # Find correspondents with between 5 and 20 documents
>>> client.correspondents().document_count_between(5, 20)
"""
[docs]
def document_count(self, count: int) -> Self:
"""
Filter models by exact document count.
Args:
count: The exact document count to filter by.
Returns:
Self: A new filtered queryset containing only models with exactly
the specified document count.
Example:
>>> client.tags().document_count(5) # Tags with exactly 5 documents
"""
return self.filter(document_count=count)
[docs]
def document_count_over(self, count: int) -> Self:
"""
Filter models by document count greater than a value.
Args:
count: The minimum document count (exclusive).
Returns:
Self: A new filtered queryset containing only models with more
documents than the specified count.
Example:
>>> client.tags().document_count_over(10) # Tags with more than 10 documents
"""
return self.filter(document_count__gt=count)
[docs]
def document_count_under(self, count: int) -> Self:
"""
Filter models by document count less than a value.
Args:
count: The maximum document count (exclusive).
Returns:
Self: A new filtered queryset containing only models with fewer
documents than the specified count.
Example:
>>> client.tags().document_count_under(3) # Tags with fewer than 3 documents
"""
return self.filter(document_count__lt=count)
[docs]
def document_count_between(self, lower: int, upper: int) -> Self:
"""
Filter models by document count between two values (inclusive).
Args:
lower: The lower document count bound (inclusive).
upper: The upper document count bound (inclusive).
Returns:
Self: A new filtered queryset containing only models with document
count within the specified range.
Example:
>>> client.tags().document_count_between(5, 20) # Tags with 5 to 20 documents
"""
return self.filter(document_count__range=(lower, upper))
[docs]
class HasOwner(QuerySetProtocol, Protocol):
"""
Provide filtering methods for querysets with an owner field.
This mixin provides convenience methods for filtering models based on their
owner. It's intended for resources like documents, saved views, and other
user-owned resources in Paperless-NgX.
Examples:
>>> # Find documents owned by user with ID 1
>>> client.documents().owner(1)
>>>
>>> # Find documents owned by any of several users
>>> client.documents().owner([1, 2, 3])
>>>
>>> # Find documents with no owner
>>> client.documents().owner(None)
"""
[docs]
def owner(self, owner: int | list[int] | None) -> Self:
"""
Filter models by owner.
Args:
owner: The owner ID, list of owner IDs, or None to filter for
unowned items. When a list is provided, models owned by any
of the specified owners will be included.
Returns:
Self: A new filtered queryset containing only models with the
specified owner(s).
Examples:
>>> client.documents().owner(1) # Documents owned by user 1
>>> client.documents().owner([1, 2, 3]) # Documents owned by users 1, 2, or 3
>>> client.documents().owner(None) # Documents with no owner
"""
if isinstance(owner, list):
return self.filter(owner__in=owner)
return self.filter(owner=owner)
[docs]
class HasStandard(HasOwner, HasDocumentCount, Protocol):
"""
Combine standard filtering methods for common Paperless-NgX resources.
This mixin combines the HasOwner and HasDocumentCount mixins and adds
additional filtering methods for the name and slug fields. It's intended
for resources like tags, correspondents, and document types that have
these standard fields.
The standard fields are:
- owner: The user who owns the resource
- document_count: The number of documents associated with the resource
- name: The display name of the resource
- slug: The URL-friendly identifier for the resource
Examples:
>>> # Find tags with a specific name
>>> client.tags().name("Invoices")
>>>
>>> # Find document types with names containing "tax" (case insensitive)
>>> client.document_types().name("tax", exact=False)
>>>
>>> # Find correspondents with a specific slug
>>> client.correspondents().slug("acme-corp")
"""
[docs]
def name(self, value: str, *, exact: bool = True, case_insensitive: bool = True) -> Self:
"""
Filter models by name field.
Args:
value: The name value to filter by.
exact: If True, performs exact matching. If False, performs contains matching.
case_insensitive: If True, performs case-insensitive comparison.
Returns:
Self: A new filtered queryset containing only models with matching names.
Examples:
>>> client.tags().name("Invoices") # Tags named exactly "Invoices"
>>> client.tags().name("tax", exact=False) # Tags with "tax" in their name
>>> client.tags().name("TAX", case_insensitive=True) # Case-insensitive match
"""
return self.filter_field_by_str("name", value, exact=exact, case_insensitive=case_insensitive)
[docs]
def slug(self, value: str, *, exact: bool = True, case_insensitive: bool = True) -> Self:
"""
Filter models by slug field.
Args:
value: The slug value to filter by.
exact: If True, performs exact matching. If False, performs contains matching.
case_insensitive: If True, performs case-insensitive comparison.
Returns:
Self: A new filtered queryset containing only models with matching slugs.
Examples:
>>> client.tags().slug("invoices") # Tags with slug exactly "invoices"
>>> client.tags().slug("tax", exact=False) # Tags with "tax" in their slug
"""
return self.filter_field_by_str("slug", value, exact=exact, case_insensitive=case_insensitive)