| name | update-filters |
| description | Update the query filter TypedDicts in pypaperless/models/filters.py by reading the paperless-ngx source FilterSets, then sync the service filter() overrides and the docs. Use whenever paperless-ngx adds, renames, or removes filter fields. Use when this capability is needed. |
| metadata | {"author":"tb1337"} |
Update Query Filters in pypaperless
When to Use
- paperless-ngx added or changed filter fields
- A new resource needs a typed
filter() override
ruff or mypy reports issues in pypaperless/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=False on every class.
- No
page / page_size fields.
- 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
str whenever the schema documents a fixed value set for a field. Pattern: XxxEnum | str for single-value fields; XxxEnum | str | list[XxxEnum | str] for array fields. Import the enum directly from its model module (not from types.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__in or name__* in a class that inherits _NameFilters.
- Repeating
created__* in a class that inherits _CreatedFilters.
- Adding
page / page_size to a TypedDict.
- Adding section-header comments inside TypedDicts (ERA001).
- Sorting fields non-alphabetically.
- Using
TypeVar with Unpack (not supported by PEP 692).
- Calling
super().filter(**kwargs) instead of self._store_filters(**kwargs).
- Forgetting to add the new TypedDict to
_ALL_FILTER_CLASSES in tests.
Source: tb1337/paperless-api — distributed by TomeVault.