Update Query Filters in pypaperless
When to Use
- paperless-ngx added or changed filter fields
- A new resource needs a typed
filter()override rufformypyreports issues inpypaperless/models/filters.py
Files Involved
| File | Purpose |
|---|---|
pypaperless/models/filters.py |
TypedDicts — one per filterable resource |
pypaperless/models/types.py |
Re-exports all filter TypedDicts |
pypaperless/services/<name>.py |
filter() override with Unpack[XxxFilters] |
pypaperless/services/trash.py |
Also uses DocumentFilters |
docs/resources/<name>.md |
Per-resource filter examples |
Step 1 — Read the filter fields
Primary: tests/data/schema.json — fetch first to ensure it's current:
python script/pngx_fetch_schema.py
For each list endpoint collect every "in": "query" parameter. Exclude: page, page_size, format, ordering, full_perms.
Fallback: upstream source — only for fields the schema omits (known case: more_like_id on DocumentFilters):
curl -sL https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/dev/src/documents/filters.py
Schema type → Python type
| Schema type | Python type |
|---|---|
integer |
int |
boolean |
bool |
string / string+date / string+date-time |
str |
number |
int (date/time components) |
array[integer] |
str — comma-separated PKs; do not change to list[int] |
array[string+enum] |
EnumType | str | list[EnumType | str] |
Schema bug: shared_by__id is boolean in the schema — keep int in the TypedDict.
Step 2 — Update pypaperless/models/filters.py
FilterSet → TypedDict
| paperless-ngx FilterSet | pypaperless TypedDict |
|---|---|
CorrespondentFilterSet |
CorrespondentFilters |
TagFilterSet |
TagFilters |
DocumentTypeFilterSet |
DocumentTypeFilters |
StoragePathFilterSet |
StoragePathFilters |
CustomFieldFilterSet |
CustomFieldFilters |
DocumentFilterSet |
DocumentFilters |
PaperlessTaskFilterSet |
TaskFilters |
ShareLinkFilterSet |
ShareLinkFilters |
Style rules
total=Falseon every class.- No
page/page_sizefields. - No section-header comments (ERA001).
- Fields sorted alphabetically. Classes sorted alphabetically; private bases (
_CreatedFilters,_IdFilters,_NameFilters,_ExpirationFilters) always first. - Use a pypaperless enum type instead of plain
strwhenever the schema documents a fixed value set for a field. Pattern:XxxEnum | strfor single-value fields;XxxEnum | str | list[XxxEnum | str]for array fields. Import the enum directly from its model module (not fromtypes.py). - Inline comments only for non-obvious semantics:
# comma-separated PKs,# JSON expression,# True → has at least one tag, etc. - No blank lines between fields.
Inheritance rules
Absolute rule: no field may appear in more than one public TypedDict. Whenever identical fields exist in two or more classes, extract them into a new private base _XxxFilters.
Always use the narrowest fitting base.
| Situation | Inherit from |
|---|---|
Resource has id + name__* |
_NameFilters |
Resource has id only |
_IdFilters |
Resource has created__* date range |
_CreatedFilters |
Resource has expiration__* date range |
_ExpirationFilters |
| Resource has both id and created | _IdFilters, _CreatedFilters |
| Resource has created + expiration | _CreatedFilters, _ExpirationFilters |
Current private bases and what they provide:
| Base | Fields provided |
|---|---|
_IdFilters |
id, id__in |
_NameFilters(_IdFilters) |
+ name__icontains/iendswith/iexact/istartswith |
_CreatedFilters |
created__date__gt/gte/lt/lte, created__day/month/year, created__gt/gte/lt/lte |
_ExpirationFilters |
same fields with expiration__ prefix |
A class body with only a docstring is correct when the base covers everything. When adding a new date-range field group that would appear in ≥ 2 classes, create a new private base first.
Step 3 — Update pypaperless/models/types.py
Import the new TypedDict and add it to __all__ in alphabetical order.
Step 4 — Update service filter() overrides
Pattern (identical for every service):
@asynccontextmanager
async def filter(self, **kwargs: Unpack[XxxFilters]) -> AsyncGenerator[Self, None]:
"""Iterate with server-side filters.
See :class:`~pypaperless.models.filters.XxxFilters` for available keys.
"""
async with self._store_filters(**kwargs) as ctx:
yield ctx
Always call self._store_filters(**kwargs) — never super().filter(**kwargs).
Service → Filters
| Service | TypedDict |
|---|---|
CorrespondentService |
CorrespondentFilters |
CustomFieldService |
CustomFieldFilters |
DocumentsService |
DocumentFilters |
DocumentTypeService |
DocumentTypeFilters |
GroupService |
GroupFilters |
ShareLinkService |
ShareLinkFilters |
StoragePathService |
StoragePathFilters |
TagService |
TagFilters |
TaskService |
TaskFilters |
TrashService |
DocumentFilters |
UserService |
UserFilters |
No override for: SavedViewService, WorkflowService, MailAccountService, MailRuleService, ProcessedMailService.
Step 5 — Update docs
Only update docs/resources/<name>.md files that already have a filter() example — do not create new doc files.
Step 6 — Add or Update Tests
tests/test_filters.py — add the new TypedDict to _ALL_FILTER_CLASSES and add a test verifying its resource-specific fields via __optional_keys__ | __required_keys__.
tests/test_service_mixins.py — add an HTTP-level test for the new service's filter() using a key specific to that TypedDict.
script/pngx_smoketest.py — add a live call inside test_filter_context using a safe, read-only filter key.
Step 7 — Validate
/usr/local/py-utils/bin/pytest -x -q
source /home/vscode/.local/dev-venv/bin/activate && python script/pngx_smoketest.py
python script/pngx_audit_coverage.py
Common Mistakes
- Repeating
id/id__inorname__*in a class that inherits_NameFilters. - Repeating
created__*in a class that inherits_CreatedFilters. - Adding
page/page_sizeto a TypedDict. - Adding section-header comments inside TypedDicts (ERA001).
- Sorting fields non-alphabetically.
- Using
TypeVarwithUnpack(not supported by PEP 692). - Calling
super().filter(**kwargs)instead ofself._store_filters(**kwargs). - Forgetting to add the new TypedDict to
_ALL_FILTER_CLASSESin tests.
Source: tb1337/paperless-api — distributed by TomeVault.