Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ Dev venv: `/home/vscode/.local/dev-venv/bin/activate`

**After any code change, always run the full validation sequence before considering the task complete:**

**Exception:** for CLI-only changes limited to `pypaperless/cli.py` and/or CLI-focused tests/docs, the live smoketest is not required on every change.
In this case, run unit tests + coverage and report that the smoketest was intentionally skipped due to CLI-only scope.

1. **Unit tests + coverage**

```
Expand All @@ -38,18 +41,14 @@ Dev venv: `/home/vscode/.local/dev-venv/bin/activate`

Required: 0 failures.

3. **API field coverage audit**
```
python script/pngx_audit_coverage.py
```
Required: no new gaps compared to before the change.

Report all three results explicitly before closing the task.
Report both results explicitly before closing the task.

---

## Code Conventions

- **Ruff compliance is mandatory for all newly generated code**: always follow the currently active Ruff rules/configuration in this repository when writing or modifying code.
- **Mypy compliance is mandatory for all newly generated code**: all new and modified code must type-check cleanly under the repository's active mypy configuration.
- Models use **Pydantic v2** (`BaseModel`, `model_validator`, `Field`).
- Services use **httpx** for async HTTP.
- Context managers (`@asynccontextmanager`) always use `try/finally` to guarantee cleanup.
Expand Down
2 changes: 1 addition & 1 deletion .github/skills/add-resource/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class DocumentHistoryService(ServiceBase):
async def __call__(self, pk=None):
doc_pk = self._get_document_pk(pk)
res = await self._client.request_json("get", self._api_path.format(pk=doc_pk))
return [self._resource_cls.create_with_data(self._client, {**item, "document": doc_pk})
return [self._resource_cls.from_data(self._client, {**item, "document": doc_pk})
for item in res]

def _get_document_pk(self, pk=None):
Expand Down
4 changes: 2 additions & 2 deletions .github/skills/add-resource/references/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class DocumentHistoryService(ServiceBase):
doc_pk = self._get_document_pk(pk)
res = await self._client.request_json("get", self._api_path.format(pk=doc_pk))
return [
self._resource_cls.create_with_data(self._client, {**item, "document": doc_pk})
self._resource_cls.from_data(self._client, {**item, "document": doc_pk})
for item in res
]

Expand Down Expand Up @@ -255,7 +255,7 @@ KNOWN_SCHEMA_EXTRAS["WidgetMeta"] = {
| ------------------- | --------------------------------------------------------------------- |
| `CallableMixin[T]` | `await service(pk)` — fetch single item |
| `IterableMixin[T]` | `async for item in service` + `.pages()` + `.as_list()` + `.filter()` |
| `DraftableMixin[D]` | `service.draft(...)` + `await service.save(draft)` |
| `DraftableMixin[D]` | `service.create(...)` + `await service.save(draft)` |
| `UpdatableMixin[T]` | `await service.update(item)` |
| `DeletableMixin[T]` | `await service.delete(item)` |
| `SecurableMixin` | `permissions` field + `?full_perms=true` on requests |
Expand Down
6 changes: 3 additions & 3 deletions docs/concepts/custom_fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,12 @@ await paperless.documents.update(document)

## Creating a custom field

Use `draft()` and `save()` on `paperless.custom_fields`:
Use `create()` and `save()` on `paperless.custom_fields`:

```python
from pypaperless.models.custom_fields import CustomFieldType

draft = paperless.custom_fields.draft(
draft = paperless.custom_fields.create(
name="Invoice amount",
data_type=CustomFieldType.MONETARY,
)
Expand All @@ -221,7 +221,7 @@ from pypaperless.models.custom_fields import (
CustomFieldType,
)

draft = paperless.custom_fields.draft(
draft = paperless.custom_fields.create(
name="Status",
data_type=CustomFieldType.SELECT,
extra_data=CustomFieldExtraData(
Expand Down
42 changes: 31 additions & 11 deletions docs/concepts/documents.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ Or using an already-fetched document:
```python
doc = await paperless.documents(42)

download = await doc.get_download()
preview = await doc.get_preview()
thumbnail = await doc.get_thumbnail()
download = await doc.download()
preview = await doc.preview()
thumbnail = await doc.thumbnail()
```

`DownloadedDocument` gives you the raw bytes plus everything from the response headers you'd need to save or serve the file:
Expand Down Expand Up @@ -110,6 +110,11 @@ Find documents similar to a given document:
```python
async for document in paperless.documents.more_like(42):
print(document.title)

# or via a fetched document
doc = await paperless.documents(42)
async for document in doc.more_like():
print(document.title)
```

---
Expand All @@ -121,7 +126,7 @@ meta = await paperless.documents.metadata(42)

# or via a fetched document
doc = await paperless.documents(42)
meta = await doc.get_metadata()
meta = await doc.metadata()
```

The returned `DocumentMeta` object includes embedded metadata from the file (e.g. EXIF or PDF metadata):
Expand All @@ -142,7 +147,7 @@ suggestions = await paperless.documents.suggestions(42)

# or via a fetched document
doc = await paperless.documents(42)
suggestions = await doc.get_suggestions()
suggestions = await doc.suggestions()

print(suggestions.correspondents)
print(suggestions.document_types)
Expand Down Expand Up @@ -173,23 +178,29 @@ for note in notes:

```python
# Pass the document pk as the first positional argument
draft = paperless.documents.notes.draft(42, note="This needs review")
draft = paperless.documents.notes.create(42, note="This needs review")
note_id, doc_id = await paperless.documents.notes.save(draft)
```

Or via a fetched document (the document pk is bound automatically):

```python
doc = await paperless.documents(42)
draft = doc.notes.draft(note="This needs review")
draft = doc.notes.create(note="This needs review")
note_id, doc_id = await doc.notes.save(draft)

# or use the shortcut directly on the draft
note_id, doc_id = await draft.save()
```

### Deleting a note

```python
note = notes[0]
await paperless.documents.notes.delete(note)

# or via the note instance directly
await note.delete()
```

---
Expand All @@ -207,13 +218,13 @@ print(f"Next ASN: {next_asn}")

## Uploading a document

Use `draft()` to construct a document upload and `save()` to submit it. The document content must be provided as `bytes`. All fields except `document` are optional.
Use `create()` to construct a document upload and `save()` to submit it. The document content must be provided as `bytes`. All fields except `document` are optional.

```python
with open("invoice.pdf", "rb") as f:
content = f.read()

draft = paperless.documents.draft(
draft = paperless.documents.create(
document=content, # required — raw file bytes
filename="invoice.pdf", # original filename
title="Invoice 2024-01",
Expand Down Expand Up @@ -245,7 +256,7 @@ cf_list = DocumentCustomFieldList(paperless, data=[])
cf_list += CustomFieldValue(field=3, value="ACME Corp")
cf_list += CustomFieldValue(field=8, value=42)

draft = paperless.documents.draft(document=content, custom_fields=cf_list)
draft = paperless.documents.create(document=content, custom_fields=cf_list)
```

See [Custom fields](custom_fields.md) for the full custom field API.
Expand Down Expand Up @@ -299,7 +310,7 @@ await paperless.documents.email(
)
```

A single document can also be passed as an integer:
A single document can also be passed as an integer, or use the shortcut on a fetched `Document` instance:

```python
await paperless.documents.email(
Expand All @@ -309,6 +320,15 @@ await paperless.documents.email(
message="See attachment.",
use_archive_version=False, # send original instead of archived version
)

# shortcut — document pk is bound automatically
doc = await paperless.documents(42)
await doc.email(
addresses="alice@example.com",
subject="Invoice",
message="See attachment.",
use_archive_version=False,
)
```

| Parameter | Default | Description |
Expand Down
2 changes: 1 addition & 1 deletion docs/concepts/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ perms = Permissions(
change_groups=[1],
)

draft = paperless.tags.draft(
draft = paperless.tags.create(
name="confidential",
owner=1,
set_permissions=perms,
Expand Down
2 changes: 1 addition & 1 deletion docs/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Base class for exceptions raised during draft lifecycle operations. Catch this t
Raised by `draft.validate_draft()` (called automatically inside `save()`) when one or more required fields are missing.

```python
draft = paperless.documents.draft() # missing `document` field
draft = paperless.documents.create() # missing `document` field

try:
await paperless.documents.save(draft)
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ asyncio.run(main())
## Documentation

- [Getting started](quickstart.md) — installation, first session, token generation
- [Session management](session.md) — lifecycle, context manager, custom client
- [Session management](session.md) — lifecycle, configuration modes, custom client
- [Resources](resources.md) — all available resources and their capabilities
- [Documents](concepts/documents.md) — uploads, search, download, notes, metadata
- [Custom fields](concepts/custom_fields.md) — typed field values, cache, read & write
Expand Down
Loading
Loading