# Update Filters

> 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.

- Skill: `tomevault-io/update-filters` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/update-filters`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/update-filters/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/update-filters

---


# 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:

```bash
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`):

```bash
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):

```python
@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

```bash
/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](https://github.com/tb1337/paperless-api) — distributed by [TomeVault](https://tomevault.io).
<!-- tomevault:4.0:skill_md:2026-06-29 -->

