paperless-export 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- paperless_export-0.0.2/.env.example +9 -0
- paperless_export-0.0.2/.github/CODEOWNERS +1 -0
- paperless_export-0.0.2/.github/workflows/ci.yml +24 -0
- paperless_export-0.0.2/.github/workflows/release.yml +48 -0
- paperless_export-0.0.2/.gitignore +10 -0
- paperless_export-0.0.2/CHANGELOG.md +66 -0
- paperless_export-0.0.2/LICENSE +21 -0
- paperless_export-0.0.2/PKG-INFO +135 -0
- paperless_export-0.0.2/README.md +110 -0
- paperless_export-0.0.2/pyproject.toml +86 -0
- paperless_export-0.0.2/renovate.json +37 -0
- paperless_export-0.0.2/src/paperless_export/__init__.py +3 -0
- paperless_export-0.0.2/src/paperless_export/__main__.py +5 -0
- paperless_export-0.0.2/src/paperless_export/cli.py +219 -0
- paperless_export-0.0.2/src/paperless_export/embed.py +50 -0
- paperless_export-0.0.2/src/paperless_export/errors.py +46 -0
- paperless_export-0.0.2/src/paperless_export/exporter.py +120 -0
- paperless_export-0.0.2/src/paperless_export/manifest.py +90 -0
- paperless_export-0.0.2/src/paperless_export/preflight.py +39 -0
- paperless_export-0.0.2/src/paperless_export/py.typed +0 -0
- paperless_export-0.0.2/src/paperless_export/taxview.py +106 -0
- paperless_export-0.0.2/tests/__init__.py +0 -0
- paperless_export-0.0.2/tests/conftest.py +98 -0
- paperless_export-0.0.2/tests/test_cli.py +86 -0
- paperless_export-0.0.2/tests/test_embed.py +52 -0
- paperless_export-0.0.2/tests/test_exporter.py +87 -0
- paperless_export-0.0.2/tests/test_manifest.py +64 -0
- paperless_export-0.0.2/tests/test_preflight.py +37 -0
- paperless_export-0.0.2/tests/test_taxview.py +87 -0
- paperless_export-0.0.2/uv.lock +759 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Copy to .env (never commit the real one) or export in your shell.
|
|
2
|
+
# Both are optional: they enable the API preflight check (fail fast on a bad
|
|
3
|
+
# token before running the exporter). Token: Paperless → Settings → API Tokens.
|
|
4
|
+
PAPERLESS_URL=https://paperless.local:8000
|
|
5
|
+
PAPERLESS_TOKEN=your-token-here
|
|
6
|
+
|
|
7
|
+
# Override how document_exporter is invoked if Paperless doesn't run via
|
|
8
|
+
# `docker compose exec -T webserver ...` (e.g. bare-metal or a named container).
|
|
9
|
+
#PAPERLESS_EXPORTER_CMD=docker exec -i paperless-webserver document_exporter
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* @gykonik
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on: [push, pull_request, workflow_dispatch]
|
|
3
|
+
|
|
4
|
+
jobs:
|
|
5
|
+
quality:
|
|
6
|
+
runs-on: ubuntu-latest
|
|
7
|
+
steps:
|
|
8
|
+
- uses: actions/checkout@v4
|
|
9
|
+
- uses: astral-sh/setup-uv@v5
|
|
10
|
+
- run: uv sync --all-extras --dev
|
|
11
|
+
|
|
12
|
+
- name: lint
|
|
13
|
+
run: |
|
|
14
|
+
uv run ruff check .
|
|
15
|
+
uv run ruff format --check .
|
|
16
|
+
|
|
17
|
+
- name: typecheck
|
|
18
|
+
run: uv run mypy
|
|
19
|
+
|
|
20
|
+
- name: test
|
|
21
|
+
run: uv run pytest -q
|
|
22
|
+
|
|
23
|
+
- name: build
|
|
24
|
+
run: uv build
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
contents: write # tag + GitHub Release
|
|
8
|
+
id-token: write # PyPI Trusted Publishing (OIDC, no token)
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
concurrency: release
|
|
14
|
+
steps:
|
|
15
|
+
# `git push` authenticates with the credentials checkout persists into the
|
|
16
|
+
# remote's extraheader — NOT with the token handed to the semantic-release
|
|
17
|
+
# action below (that one only signs GitHub API calls). The version commit
|
|
18
|
+
# lands on protected `main`, so the pushing identity needs a ruleset bypass:
|
|
19
|
+
# SEMANTIC_RELEASE_TOKEN is a PAT acting as an org owner, who has one.
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
with:
|
|
22
|
+
fetch-depth: 0
|
|
23
|
+
persist-credentials: true
|
|
24
|
+
token: ${{ secrets.SEMANTIC_RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
|
25
|
+
|
|
26
|
+
- name: Semantic release (version, tag, changelog, GitHub Release)
|
|
27
|
+
id: semrel
|
|
28
|
+
uses: python-semantic-release/python-semantic-release@v9
|
|
29
|
+
with:
|
|
30
|
+
github_token: ${{ secrets.SEMANTIC_RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
|
31
|
+
|
|
32
|
+
# No build step here on purpose. python-semantic-release already builds the
|
|
33
|
+
# sdist + wheel into dist/ via its `build_command` in pyproject.toml. It is
|
|
34
|
+
# a Docker action running as root, so those files are root-owned; a second
|
|
35
|
+
# `uv build` as the unprivileged runner user cannot overwrite them and dies
|
|
36
|
+
# with PermissionError, taking publish and the brew bump down with it.
|
|
37
|
+
- name: Publish to PyPI (OIDC)
|
|
38
|
+
if: steps.semrel.outputs.released == 'true'
|
|
39
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
40
|
+
|
|
41
|
+
- name: Bump Homebrew formula
|
|
42
|
+
if: steps.semrel.outputs.released == 'true'
|
|
43
|
+
run: |
|
|
44
|
+
gh workflow run bump.yml -R fileworks/homebrew-tap \
|
|
45
|
+
-f formula=paperless-export \
|
|
46
|
+
-f version=${{ steps.semrel.outputs.version }}
|
|
47
|
+
env:
|
|
48
|
+
GH_TOKEN: ${{ secrets.TAP_DISPATCH_TOKEN }}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## v0.0.2 (2026-07-13)
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- **release**: Drop the duplicate build that broke publishing
|
|
9
|
+
([#3](https://github.com/fileworks/paperless-export/pull/3),
|
|
10
|
+
[`a51f396`](https://github.com/fileworks/paperless-export/commit/a51f3963604e0777966a317f01d27cc9f1eec1bc))
|
|
11
|
+
|
|
12
|
+
The release job died at 'Build sdist + wheel' with
|
|
13
|
+
|
|
14
|
+
PermissionError: [Errno 13] Permission denied: dist/<pkg>-0.0.1.tar.gz
|
|
15
|
+
|
|
16
|
+
python-semantic-release already builds the sdist and wheel into dist/ via the build_command in
|
|
17
|
+
pyproject.toml. It is a Docker action running as root, so those artefacts are root-owned. The
|
|
18
|
+
workflow then ran a second, redundant 'uv build' as the unprivileged runner user, which cannot
|
|
19
|
+
overwrite them.
|
|
20
|
+
|
|
21
|
+
Publish to PyPI and the Homebrew bump are both gated on that step, so a permission error on a build
|
|
22
|
+
that never needed to happen silently took out the entire release.
|
|
23
|
+
|
|
24
|
+
Remove the duplicate build and the setup-uv step that only served it, and let the publish action
|
|
25
|
+
consume the dist/ that semantic-release produced.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## v0.0.1 (2026-07-12)
|
|
29
|
+
|
|
30
|
+
### Bug Fixes
|
|
31
|
+
|
|
32
|
+
- **release**: Give checkout the release token so the version push is allowed
|
|
33
|
+
([#2](https://github.com/fileworks/paperless-export/pull/2),
|
|
34
|
+
[`e201478`](https://github.com/fileworks/paperless-export/commit/e201478af0f822dbe19163edcfb1431f4427926d))
|
|
35
|
+
|
|
36
|
+
The release job fails with GH013 — the chore(release) version commit cannot be pushed to protected
|
|
37
|
+
main.
|
|
38
|
+
|
|
39
|
+
Handing SEMANTIC_RELEASE_TOKEN to the python-semantic-release action is not enough: that input only
|
|
40
|
+
authenticates GitHub API calls. The actual git push uses the credentials actions/checkout persists
|
|
41
|
+
into the remote's extraheader, which were still the default GITHUB_TOKEN — an identity with no
|
|
42
|
+
ruleset bypass.
|
|
43
|
+
|
|
44
|
+
Set the token on checkout as well, mirroring media-sorter's semantic-release workflow, so the push
|
|
45
|
+
authenticates as an org owner and the always-bypass applies.
|
|
46
|
+
|
|
47
|
+
### Continuous Integration
|
|
48
|
+
|
|
49
|
+
- Allow CI to be triggered manually ([#1](https://github.com/fileworks/paperless-export/pull/1),
|
|
50
|
+
[`ff5c192`](https://github.com/fileworks/paperless-export/commit/ff5c192cfcca77171f324f6f200c136a0a9f9879))
|
|
51
|
+
|
|
52
|
+
* ci: allow CI to be triggered manually
|
|
53
|
+
|
|
54
|
+
workflow_dispatch makes it possible to re-run checks without pushing a commit — needed while
|
|
55
|
+
verifying the org's Actions allow-list, and useful any time a run fails for reasons outside the
|
|
56
|
+
code.
|
|
57
|
+
|
|
58
|
+
* fix(release): push the version commit with a token that can bypass the ruleset
|
|
59
|
+
|
|
60
|
+
semantic-release commits the version bump to main. The branch ruleset requires a PR, and the default
|
|
61
|
+
GITHUB_TOKEN has no bypass — GitHub rejects the Actions app as a bypass actor ("must be part of
|
|
62
|
+
the ruleset source or owner organization"), so the release job could never land its commit.
|
|
63
|
+
|
|
64
|
+
SEMANTIC_RELEASE_TOKEN is a fine-grained PAT that acts as an org owner, who already holds an
|
|
65
|
+
always-bypass on the ruleset. Falls back to GITHUB_TOKEN so nothing breaks where the secret is
|
|
66
|
+
absent.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Niklas Büchel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: paperless-export
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Scheduled wrapper around Paperless-ngx's document_exporter plus a _Steuer/YYYY tax-view post-processor.
|
|
5
|
+
Project-URL: Repository, https://github.com/fileworks/paperless-export
|
|
6
|
+
Project-URL: Issues, https://github.com/fileworks/paperless-export/issues
|
|
7
|
+
Author: gykonik
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: backup,documents,export,paperless-ngx
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Requires-Dist: httpx>=0.28
|
|
20
|
+
Requires-Dist: pydantic>=2.9
|
|
21
|
+
Requires-Dist: typer>=0.15
|
|
22
|
+
Provides-Extra: pdf
|
|
23
|
+
Requires-Dist: pikepdf>=9.0; extra == 'pdf'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# paperless-export
|
|
27
|
+
|
|
28
|
+
A thin scheduled wrapper around [Paperless-ngx](https://docs.paperless-ngx.com)'s
|
|
29
|
+
built-in `document_exporter`, plus the one thing it doesn't do: a materialized
|
|
30
|
+
**`_Steuer/YYYY/` tax view** built from your `Steuer-YYYY` tags.
|
|
31
|
+
|
|
32
|
+
Paperless's exporter already produces the full no-lock-in export — every
|
|
33
|
+
document laid out by your storage-path template, originals *and* PDF/A archive
|
|
34
|
+
versions, and a complete `manifest.json` (tags, correspondents, types, custom
|
|
35
|
+
fields). This tool deliberately does **not** rebuild any of that. It:
|
|
36
|
+
|
|
37
|
+
1. runs `document_exporter <target> --use-filename-format --compare-checksums --delete`
|
|
38
|
+
(each flag toggleable), surfacing failures honestly and **falling back to a
|
|
39
|
+
flat export with a clear warning when a path exceeds the OS limit**,
|
|
40
|
+
2. reads `manifest.json` and builds `_Steuer/<YYYY>/` — one symlink (or copy)
|
|
41
|
+
per document tagged `Steuer-YYYY` — plus a greppable `_Steuer/INDEX.csv`,
|
|
42
|
+
3. optionally embeds tags/correspondent/type into the exported PDFs' XMP
|
|
43
|
+
(`--embed-tags`, needs the `[pdf]` extra).
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
export/
|
|
47
|
+
Bescheid/Finanzamt/2024-05-01 Steuerbescheid.pdf # ← document_exporter
|
|
48
|
+
manifest.json # ← document_exporter
|
|
49
|
+
_Steuer/
|
|
50
|
+
2024/2024-05-01 Steuerbescheid.pdf → ../../Bescheid/Finanzamt/…
|
|
51
|
+
INDEX.csv # year,title,correspondent,created,original_path
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
pipx install paperless-export # + 'paperless-export[pdf]' for --embed-tags
|
|
58
|
+
# or
|
|
59
|
+
brew install fileworks/tap/paperless-export
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
*(Not yet published — first release pending; until then: `uv run paperless-export` from a checkout.)*
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
# the nightly job (run from the directory containing your compose file):
|
|
68
|
+
paperless-export run --export-dir /volume1/paperless/export
|
|
69
|
+
|
|
70
|
+
# rebuild only the tax view from an existing export:
|
|
71
|
+
paperless-export tax-view --export-dir /volume1/paperless/export
|
|
72
|
+
|
|
73
|
+
# FAT/exFAT or cloud targets that don't preserve symlinks:
|
|
74
|
+
paperless-export run --export-dir ./export --copy
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Notes:
|
|
78
|
+
|
|
79
|
+
- `--exporter-target` (default `../export`) is the path **as the exporter
|
|
80
|
+
process sees it** inside the container; `--export-dir` is the same directory
|
|
81
|
+
**on this host**. With the standard compose setup they're the same bind mount.
|
|
82
|
+
- `PAPERLESS_URL` + `PAPERLESS_TOKEN` (env or flags) enable a preflight check
|
|
83
|
+
so a bad token fails fast with a clear message — they're optional because the
|
|
84
|
+
exporter itself runs inside the container and needs no API access.
|
|
85
|
+
- `--embed-tags` rewrites the exported PDFs, which changes their checksums, so
|
|
86
|
+
those files are re-exported on the next `--compare-checksums` run. The
|
|
87
|
+
manifest already preserves all metadata — only embed if you want tags
|
|
88
|
+
*inside* the files.
|
|
89
|
+
|
|
90
|
+
## Behavior guarantees
|
|
91
|
+
|
|
92
|
+
- **Read-only against Paperless** — writes only into the export directory.
|
|
93
|
+
- **Idempotent** — the `_Steuer/` view is rebuilt from scratch each run; safe nightly.
|
|
94
|
+
- **Verifiable** — after a run, `_Steuer/2025/` contains exactly the documents
|
|
95
|
+
tagged `Steuer-2025`; `INDEX.csv` matches a manifest query.
|
|
96
|
+
- **Honest failures** — a non-zero `document_exporter` exit surfaces its stderr
|
|
97
|
+
and exit code; symlink-unsupported filesystems auto-switch to copies with a notice.
|
|
98
|
+
|
|
99
|
+
## Exit codes
|
|
100
|
+
|
|
101
|
+
| Code | Meaning |
|
|
102
|
+
|---|---|
|
|
103
|
+
| 0 | success |
|
|
104
|
+
| 2 | bad configuration / authentication failure |
|
|
105
|
+
| 3 | Paperless (or Docker) not reachable |
|
|
106
|
+
| 4 | export dir missing / manifest unreadable |
|
|
107
|
+
| *n* | `document_exporter` failed with exit code *n* |
|
|
108
|
+
|
|
109
|
+
## Scheduling on a Synology (DSM Task Scheduler)
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
cd /volume1/docker/paperless && \
|
|
113
|
+
/usr/local/bin/paperless-export run --export-dir /volume1/paperless/export
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Nightly, after the Paperless backup window; the export target should live on a
|
|
117
|
+
share covered by your backup chain.
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
uv sync --all-extras --dev
|
|
123
|
+
uv run ruff check . && uv run ruff format --check . # lint
|
|
124
|
+
uv run mypy # strict types
|
|
125
|
+
uv run pytest # tests
|
|
126
|
+
uv build
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Conventional Commits drive releases (`python-semantic-release`): merge to
|
|
130
|
+
`main` → version bump + changelog + GitHub Release + PyPI publish (OIDC) +
|
|
131
|
+
Homebrew formula bump.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# paperless-export
|
|
2
|
+
|
|
3
|
+
A thin scheduled wrapper around [Paperless-ngx](https://docs.paperless-ngx.com)'s
|
|
4
|
+
built-in `document_exporter`, plus the one thing it doesn't do: a materialized
|
|
5
|
+
**`_Steuer/YYYY/` tax view** built from your `Steuer-YYYY` tags.
|
|
6
|
+
|
|
7
|
+
Paperless's exporter already produces the full no-lock-in export — every
|
|
8
|
+
document laid out by your storage-path template, originals *and* PDF/A archive
|
|
9
|
+
versions, and a complete `manifest.json` (tags, correspondents, types, custom
|
|
10
|
+
fields). This tool deliberately does **not** rebuild any of that. It:
|
|
11
|
+
|
|
12
|
+
1. runs `document_exporter <target> --use-filename-format --compare-checksums --delete`
|
|
13
|
+
(each flag toggleable), surfacing failures honestly and **falling back to a
|
|
14
|
+
flat export with a clear warning when a path exceeds the OS limit**,
|
|
15
|
+
2. reads `manifest.json` and builds `_Steuer/<YYYY>/` — one symlink (or copy)
|
|
16
|
+
per document tagged `Steuer-YYYY` — plus a greppable `_Steuer/INDEX.csv`,
|
|
17
|
+
3. optionally embeds tags/correspondent/type into the exported PDFs' XMP
|
|
18
|
+
(`--embed-tags`, needs the `[pdf]` extra).
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
export/
|
|
22
|
+
Bescheid/Finanzamt/2024-05-01 Steuerbescheid.pdf # ← document_exporter
|
|
23
|
+
manifest.json # ← document_exporter
|
|
24
|
+
_Steuer/
|
|
25
|
+
2024/2024-05-01 Steuerbescheid.pdf → ../../Bescheid/Finanzamt/…
|
|
26
|
+
INDEX.csv # year,title,correspondent,created,original_path
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pipx install paperless-export # + 'paperless-export[pdf]' for --embed-tags
|
|
33
|
+
# or
|
|
34
|
+
brew install fileworks/tap/paperless-export
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
*(Not yet published — first release pending; until then: `uv run paperless-export` from a checkout.)*
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
# the nightly job (run from the directory containing your compose file):
|
|
43
|
+
paperless-export run --export-dir /volume1/paperless/export
|
|
44
|
+
|
|
45
|
+
# rebuild only the tax view from an existing export:
|
|
46
|
+
paperless-export tax-view --export-dir /volume1/paperless/export
|
|
47
|
+
|
|
48
|
+
# FAT/exFAT or cloud targets that don't preserve symlinks:
|
|
49
|
+
paperless-export run --export-dir ./export --copy
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Notes:
|
|
53
|
+
|
|
54
|
+
- `--exporter-target` (default `../export`) is the path **as the exporter
|
|
55
|
+
process sees it** inside the container; `--export-dir` is the same directory
|
|
56
|
+
**on this host**. With the standard compose setup they're the same bind mount.
|
|
57
|
+
- `PAPERLESS_URL` + `PAPERLESS_TOKEN` (env or flags) enable a preflight check
|
|
58
|
+
so a bad token fails fast with a clear message — they're optional because the
|
|
59
|
+
exporter itself runs inside the container and needs no API access.
|
|
60
|
+
- `--embed-tags` rewrites the exported PDFs, which changes their checksums, so
|
|
61
|
+
those files are re-exported on the next `--compare-checksums` run. The
|
|
62
|
+
manifest already preserves all metadata — only embed if you want tags
|
|
63
|
+
*inside* the files.
|
|
64
|
+
|
|
65
|
+
## Behavior guarantees
|
|
66
|
+
|
|
67
|
+
- **Read-only against Paperless** — writes only into the export directory.
|
|
68
|
+
- **Idempotent** — the `_Steuer/` view is rebuilt from scratch each run; safe nightly.
|
|
69
|
+
- **Verifiable** — after a run, `_Steuer/2025/` contains exactly the documents
|
|
70
|
+
tagged `Steuer-2025`; `INDEX.csv` matches a manifest query.
|
|
71
|
+
- **Honest failures** — a non-zero `document_exporter` exit surfaces its stderr
|
|
72
|
+
and exit code; symlink-unsupported filesystems auto-switch to copies with a notice.
|
|
73
|
+
|
|
74
|
+
## Exit codes
|
|
75
|
+
|
|
76
|
+
| Code | Meaning |
|
|
77
|
+
|---|---|
|
|
78
|
+
| 0 | success |
|
|
79
|
+
| 2 | bad configuration / authentication failure |
|
|
80
|
+
| 3 | Paperless (or Docker) not reachable |
|
|
81
|
+
| 4 | export dir missing / manifest unreadable |
|
|
82
|
+
| *n* | `document_exporter` failed with exit code *n* |
|
|
83
|
+
|
|
84
|
+
## Scheduling on a Synology (DSM Task Scheduler)
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
cd /volume1/docker/paperless && \
|
|
88
|
+
/usr/local/bin/paperless-export run --export-dir /volume1/paperless/export
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Nightly, after the Paperless backup window; the export target should live on a
|
|
92
|
+
share covered by your backup chain.
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
uv sync --all-extras --dev
|
|
98
|
+
uv run ruff check . && uv run ruff format --check . # lint
|
|
99
|
+
uv run mypy # strict types
|
|
100
|
+
uv run pytest # tests
|
|
101
|
+
uv build
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Conventional Commits drive releases (`python-semantic-release`): merge to
|
|
105
|
+
`main` → version bump + changelog + GitHub Release + PyPI publish (OIDC) +
|
|
106
|
+
Homebrew formula bump.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "paperless-export"
|
|
3
|
+
version = "0.0.2"
|
|
4
|
+
description = "Scheduled wrapper around Paperless-ngx's document_exporter plus a _Steuer/YYYY tax-view post-processor."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "gykonik" }]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
keywords = ["paperless-ngx", "export", "backup", "documents"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: End Users/Desktop",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: System :: Archiving :: Backup",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"httpx>=0.28",
|
|
22
|
+
"pydantic>=2.9",
|
|
23
|
+
"typer>=0.15",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
pdf = ["pikepdf>=9.0"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Repository = "https://github.com/fileworks/paperless-export"
|
|
31
|
+
Issues = "https://github.com/fileworks/paperless-export/issues"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
paperless-export = "paperless_export.cli:app"
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"mypy>=1.13",
|
|
39
|
+
"pikepdf>=9.0",
|
|
40
|
+
"pytest>=8.3",
|
|
41
|
+
"respx>=0.22",
|
|
42
|
+
"ruff>=0.8",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[build-system]
|
|
46
|
+
requires = ["hatchling"]
|
|
47
|
+
build-backend = "hatchling.build"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.wheel]
|
|
50
|
+
packages = ["src/paperless_export"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 100
|
|
54
|
+
target-version = "py312"
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = ["E", "F", "I", "N", "UP", "B", "A", "C4", "SIM", "RUF", "PTH", "RET", "ARG"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint.per-file-ignores]
|
|
60
|
+
# pytest fixtures are frequently used only for their side effect
|
|
61
|
+
"tests/**" = ["ARG001"]
|
|
62
|
+
|
|
63
|
+
[tool.mypy]
|
|
64
|
+
python_version = "3.12"
|
|
65
|
+
strict = true
|
|
66
|
+
files = ["src", "tests"]
|
|
67
|
+
|
|
68
|
+
[[tool.mypy.overrides]]
|
|
69
|
+
module = "pikepdf.*"
|
|
70
|
+
ignore_missing_imports = true
|
|
71
|
+
|
|
72
|
+
[tool.pytest.ini_options]
|
|
73
|
+
testpaths = ["tests"]
|
|
74
|
+
|
|
75
|
+
[tool.semantic_release]
|
|
76
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
77
|
+
version_variables = ["src/paperless_export/__init__.py:__version__"]
|
|
78
|
+
commit_message = "chore(release): {version} [skip ci]"
|
|
79
|
+
build_command = "python -m pip install uv && uv build"
|
|
80
|
+
allow_zero_version = true
|
|
81
|
+
|
|
82
|
+
[tool.semantic_release.branches.main]
|
|
83
|
+
match = "main"
|
|
84
|
+
|
|
85
|
+
[tool.semantic_release.changelog.default_templates]
|
|
86
|
+
changelog_file = "CHANGELOG.md"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
|
+
"extends": ["config:recommended", ":semanticCommits", ":dependencyDashboard"],
|
|
4
|
+
"timezone": "Europe/Berlin",
|
|
5
|
+
"schedule": ["before 6am on monday"],
|
|
6
|
+
"prConcurrentLimit": 5,
|
|
7
|
+
"lockFileMaintenance": {
|
|
8
|
+
"enabled": true,
|
|
9
|
+
"schedule": ["before 6am on the first day of the month"]
|
|
10
|
+
},
|
|
11
|
+
"vulnerabilityAlerts": {
|
|
12
|
+
"labels": ["security"],
|
|
13
|
+
"schedule": ["at any time"],
|
|
14
|
+
"automerge": true
|
|
15
|
+
},
|
|
16
|
+
"packageRules": [
|
|
17
|
+
{
|
|
18
|
+
"description": "Auto-merge patch/minor once CI (lint/typecheck/test/build) is green — low risk, keeps the tree current without manual review.",
|
|
19
|
+
"matchUpdateTypes": ["patch", "minor", "digest"],
|
|
20
|
+
"automerge": true,
|
|
21
|
+
"automergeType": "pr",
|
|
22
|
+
"platformAutomerge": true
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"description": "Major bumps always get a PR, never auto-merged — API/behaviour changes need a look.",
|
|
26
|
+
"matchUpdateTypes": ["major"],
|
|
27
|
+
"automerge": false,
|
|
28
|
+
"labels": ["major-update"]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"description": "Group all GitHub Actions bumps into one PR.",
|
|
32
|
+
"matchManagers": ["github-actions"],
|
|
33
|
+
"groupName": "GitHub Actions",
|
|
34
|
+
"automerge": true
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|