billwright 0.2.0__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.
Files changed (83) hide show
  1. billwright-0.2.0/.github/workflows/ci.yml +135 -0
  2. billwright-0.2.0/.github/workflows/release.yml +92 -0
  3. billwright-0.2.0/.gitignore +259 -0
  4. billwright-0.2.0/.pre-commit-config.yaml +41 -0
  5. billwright-0.2.0/AGENTS.md +344 -0
  6. billwright-0.2.0/CHANGELOG.md +197 -0
  7. billwright-0.2.0/CLAUDE.md +5 -0
  8. billwright-0.2.0/GEMINI.md +5 -0
  9. billwright-0.2.0/LICENSE +201 -0
  10. billwright-0.2.0/Makefile +100 -0
  11. billwright-0.2.0/NOTICE +31 -0
  12. billwright-0.2.0/PKG-INFO +514 -0
  13. billwright-0.2.0/README.md +492 -0
  14. billwright-0.2.0/docs/assets/banner-dark.svg +102 -0
  15. billwright-0.2.0/docs/assets/banner-light.svg +99 -0
  16. billwright-0.2.0/docs/example-invoice.png +0 -0
  17. billwright-0.2.0/docs/example-statement.png +0 -0
  18. billwright-0.2.0/example/bills/2026/RE-26001.toml +35 -0
  19. billwright-0.2.0/example/bills/2026/RE-26002.toml +30 -0
  20. billwright-0.2.0/example/brand.toml +30 -0
  21. billwright-0.2.0/example/clients/example-institute.toml +22 -0
  22. billwright-0.2.0/example/company.toml +41 -0
  23. billwright-0.2.0/example/rates.toml +9 -0
  24. billwright-0.2.0/example/years/2025.toml +11 -0
  25. billwright-0.2.0/example/years/2026.toml +47 -0
  26. billwright-0.2.0/pyproject.toml +103 -0
  27. billwright-0.2.0/skills/README.md +60 -0
  28. billwright-0.2.0/skills/brand-profile/SKILL.md +125 -0
  29. billwright-0.2.0/skills/brand-profile/references/deriving-tokens.md +77 -0
  30. billwright-0.2.0/skills/swiss-billing-generator/SKILL.md +152 -0
  31. billwright-0.2.0/skills/swiss-billing-generator/references/architecture.md +141 -0
  32. billwright-0.2.0/skills/swiss-billing-generator/references/swiss-rules.md +81 -0
  33. billwright-0.2.0/skills/swiss-billing-generator/references/weasyprint.md +118 -0
  34. billwright-0.2.0/src/billwright/__init__.py +12 -0
  35. billwright-0.2.0/src/billwright/__main__.py +4 -0
  36. billwright-0.2.0/src/billwright/assets/fonts/Inter-Medium.otf +0 -0
  37. billwright-0.2.0/src/billwright/assets/fonts/Inter-Regular.otf +0 -0
  38. billwright-0.2.0/src/billwright/assets/fonts/Inter-SemiBold.otf +0 -0
  39. billwright-0.2.0/src/billwright/assets/fonts/OFL.txt +92 -0
  40. billwright-0.2.0/src/billwright/audit.py +101 -0
  41. billwright-0.2.0/src/billwright/doctor.py +248 -0
  42. billwright-0.2.0/src/billwright/fonts.py +51 -0
  43. billwright-0.2.0/src/billwright/i18n.py +187 -0
  44. billwright-0.2.0/src/billwright/load.py +385 -0
  45. billwright-0.2.0/src/billwright/main.py +341 -0
  46. billwright-0.2.0/src/billwright/mcp_server.py +441 -0
  47. billwright-0.2.0/src/billwright/model.py +262 -0
  48. billwright-0.2.0/src/billwright/money.py +64 -0
  49. billwright-0.2.0/src/billwright/native.py +90 -0
  50. billwright-0.2.0/src/billwright/numbering.py +62 -0
  51. billwright-0.2.0/src/billwright/paths.py +164 -0
  52. billwright-0.2.0/src/billwright/qr.py +64 -0
  53. billwright-0.2.0/src/billwright/render.py +270 -0
  54. billwright-0.2.0/src/billwright/scaffold.py +206 -0
  55. billwright-0.2.0/src/billwright/scan.py +334 -0
  56. billwright-0.2.0/src/billwright/statement.py +122 -0
  57. billwright-0.2.0/src/billwright/styles/bill.css +260 -0
  58. billwright-0.2.0/src/billwright/styles/print.css +67 -0
  59. billwright-0.2.0/src/billwright/styles/statement.css +167 -0
  60. billwright-0.2.0/src/billwright/styles/tokens.css +103 -0
  61. billwright-0.2.0/src/billwright/templates/_wordmark.html.j2 +16 -0
  62. billwright-0.2.0/src/billwright/templates/base.html.j2 +17 -0
  63. billwright-0.2.0/src/billwright/templates/bill.html.j2 +156 -0
  64. billwright-0.2.0/src/billwright/templates/figures.html.j2 +81 -0
  65. billwright-0.2.0/src/billwright/templates/statement.html.j2 +110 -0
  66. billwright-0.2.0/tests/conftest.py +36 -0
  67. billwright-0.2.0/tests/test_audit.py +84 -0
  68. billwright-0.2.0/tests/test_doctor.py +135 -0
  69. billwright-0.2.0/tests/test_fonts.py +53 -0
  70. billwright-0.2.0/tests/test_load.py +150 -0
  71. billwright-0.2.0/tests/test_mcp.py +146 -0
  72. billwright-0.2.0/tests/test_money.py +54 -0
  73. billwright-0.2.0/tests/test_numbering.py +38 -0
  74. billwright-0.2.0/tests/test_overrides.py +120 -0
  75. billwright-0.2.0/tests/test_paths.py +105 -0
  76. billwright-0.2.0/tests/test_profile.py +180 -0
  77. billwright-0.2.0/tests/test_qr.py +51 -0
  78. billwright-0.2.0/tests/test_render.py +165 -0
  79. billwright-0.2.0/tests/test_reproducible.py +31 -0
  80. billwright-0.2.0/tests/test_scaffold.py +137 -0
  81. billwright-0.2.0/tests/test_scan.py +336 -0
  82. billwright-0.2.0/tests/test_statement.py +138 -0
  83. billwright-0.2.0/uv.lock +2671 -0
@@ -0,0 +1,135 @@
1
+ name: CI
2
+
3
+ # Runs against `example/` only. The private profile does not exist on a runner,
4
+ # and a workflow that needed it would be a workflow that one day leaked it.
5
+
6
+ on:
7
+ push:
8
+ branches: [main]
9
+ pull_request:
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ concurrency:
16
+ group: ${{ github.workflow }}-${{ github.ref }}
17
+ cancel-in-progress: true
18
+
19
+ env:
20
+ FORCE_COLOR: "1"
21
+
22
+ jobs:
23
+ linux:
24
+ name: ubuntu / python ${{ matrix.python-version }}
25
+ runs-on: ubuntu-latest
26
+ strategy:
27
+ fail-fast: false
28
+ matrix:
29
+ # Every version `requires-python` claims to support. A claim nothing
30
+ # exercises is a claim that is wrong sooner or later.
31
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
32
+
33
+ steps:
34
+ - uses: actions/checkout@v5
35
+
36
+ # WeasyPrint binds Pango and Cairo through cffi. Without these every
37
+ # rendering test fails on `cannot load library 'libgobject-2.0-0'`.
38
+ - name: Install native libraries
39
+ run: |
40
+ sudo apt-get update
41
+ sudo apt-get install -y --no-install-recommends \
42
+ libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b libcairo2 poppler-utils
43
+
44
+ - uses: astral-sh/setup-uv@v7
45
+ with:
46
+ enable-cache: true
47
+ python-version: ${{ matrix.python-version }}
48
+
49
+ # --locked, so a stale uv.lock fails the build rather than being
50
+ # silently resolved around.
51
+ - name: Install
52
+ run: uv sync --locked --all-extras --dev
53
+
54
+ - name: Lint
55
+ run: |
56
+ uv run ruff check --output-format=github .
57
+ uv run ruff format --check .
58
+
59
+ - name: Types
60
+ run: uv run mypy
61
+
62
+ # The leak guard. On a clone there is no private profile, so this is the
63
+ # structural layer: anything shaped like an IBAN, a Swiss UID, an email
64
+ # address or a phone number in a tracked file fails the build.
65
+ - name: Scan for private values
66
+ run: uv run billwright scan
67
+
68
+ - name: Test
69
+ run: uv run pytest -q
70
+
71
+ - name: Numbering and sanity checks
72
+ run: uv run billwright check
73
+
74
+ - name: Render the example documents
75
+ run: |
76
+ uv run billwright bill RE-26001
77
+ uv run billwright statement 2026
78
+
79
+ # So a human can look at a render without cloning anything. Rule 6:
80
+ # structural checks pass happily while the layout is broken.
81
+ - name: Upload rendered PDFs
82
+ if: matrix.python-version == '3.13'
83
+ uses: actions/upload-artifact@v4
84
+ with:
85
+ name: example-documents
86
+ path: out/*.pdf
87
+ if-no-files-found: error
88
+
89
+ macos:
90
+ name: macos / python 3.13
91
+ runs-on: macos-latest
92
+ steps:
93
+ - uses: actions/checkout@v5
94
+
95
+ # Worth a separate job: macOS is the platform this is developed on, and
96
+ # the only one that exercises native.py's dyld repair.
97
+ - name: Install native libraries
98
+ run: brew install pango
99
+
100
+ - uses: astral-sh/setup-uv@v7
101
+ with:
102
+ enable-cache: true
103
+ python-version: "3.13"
104
+
105
+ - name: Install
106
+ run: uv sync --locked --all-extras --dev
107
+
108
+ - name: Test
109
+ run: make test
110
+
111
+ - name: Render the example bill through the CLI
112
+ run: uv run billwright bill RE-26001
113
+
114
+ hooks:
115
+ name: pre-commit
116
+ runs-on: ubuntu-latest
117
+ steps:
118
+ - uses: actions/checkout@v5
119
+
120
+ - name: Install native libraries
121
+ run: |
122
+ sudo apt-get update
123
+ sudo apt-get install -y --no-install-recommends \
124
+ libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b libcairo2
125
+
126
+ - uses: astral-sh/setup-uv@v7
127
+ with:
128
+ enable-cache: true
129
+ python-version: "3.13"
130
+
131
+ - name: Install
132
+ run: uv sync --locked --all-extras --dev
133
+
134
+ - name: Run every hook over every file
135
+ run: uv run pre-commit run --all-files --show-diff-on-failure
@@ -0,0 +1,92 @@
1
+ name: Release
2
+
3
+ # Publishes to PyPI when a `v*` tag is pushed.
4
+ #
5
+ # There is no token anywhere — not in the repository, not in a secret, not on
6
+ # anyone's laptop. PyPI's trusted publishing exchanges this workflow's OIDC
7
+ # identity for a credential that lasts minutes. That makes three things in here
8
+ # load-bearing rather than decorative, and PyPI matches on all of them: the
9
+ # filename `release.yml`, the `pypi` environment, and `id-token: write`. Rename
10
+ # any of them and the upload fails with a 403 that does not explain itself.
11
+
12
+ on:
13
+ push:
14
+ tags: ["v*"]
15
+ workflow_dispatch:
16
+
17
+ permissions:
18
+ contents: read
19
+
20
+ jobs:
21
+ # Build and publish are separate jobs on purpose. Only `publish` can mint a
22
+ # PyPI credential, and all it does is upload files it did not produce; the
23
+ # build — which runs hatchling and the dependency tree — never holds one.
24
+ build:
25
+ name: build and check the artifacts
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v5
29
+
30
+ - uses: astral-sh/setup-uv@v7
31
+ with:
32
+ enable-cache: true
33
+ python-version: "3.13"
34
+
35
+ - name: The tag must be the version being published
36
+ run: |
37
+ tag="${GITHUB_REF_NAME#v}"
38
+ version=$(uv run --no-project python -c \
39
+ 'import tomllib,pathlib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')
40
+ if [ "$tag" != "$version" ]; then
41
+ echo "tag $GITHUB_REF_NAME does not match pyproject version $version" >&2
42
+ exit 1
43
+ fi
44
+ echo "publishing $version"
45
+
46
+ - name: Build
47
+ run: uv build
48
+
49
+ # The regression that 0.2.0 exists to fix: the typeface was not in the
50
+ # wheel, so an installed copy typeset client invoices in a substituted
51
+ # font and said nothing. A release that ships that again is worse than no
52
+ # release, and this is the last point at which it is cheap to notice.
53
+ - name: The wheel must carry the fonts and the manual
54
+ run: |
55
+ wheel=$(ls dist/*.whl)
56
+ for entry in \
57
+ billwright/assets/fonts/Inter-Regular.otf \
58
+ billwright/assets/fonts/Inter-Medium.otf \
59
+ billwright/assets/fonts/Inter-SemiBold.otf \
60
+ billwright/assets/fonts/OFL.txt \
61
+ billwright/AGENTS.md
62
+ do
63
+ unzip -l "$wheel" | grep -q "$entry" || { echo "missing from wheel: $entry" >&2; exit 1; }
64
+ done
65
+ echo "wheel contents verified"
66
+
67
+ - uses: actions/upload-artifact@v4
68
+ with:
69
+ name: dist
70
+ path: dist/
71
+ if-no-files-found: error
72
+
73
+ publish:
74
+ name: publish to PyPI
75
+ needs: build
76
+ runs-on: ubuntu-latest
77
+ environment: pypi
78
+ permissions:
79
+ id-token: write # the whole mechanism; without it there is no credential
80
+ steps:
81
+ - uses: astral-sh/setup-uv@v7
82
+
83
+ - uses: actions/download-artifact@v4
84
+ with:
85
+ name: dist
86
+ path: dist/
87
+
88
+ # `always`, not `automatic`: if trusted publishing is unavailable for any
89
+ # reason, fail rather than quietly looking for a token that should not
90
+ # exist.
91
+ - name: Publish
92
+ run: uv publish --trusted-publishing always
@@ -0,0 +1,259 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
219
+
220
+ # Agent working notes — local only, never version controlled.
221
+ # Build plans, handovers and scratch thinking. Company facts may appear here.
222
+ notes/
223
+
224
+ # Rendered drafts. The archive/ copies are the record; out/ is scratch.
225
+ out/
226
+
227
+ # ---------------------------------------------------------------------------
228
+ # The private surface. Everything company-specific lives here and nowhere else:
229
+ # the engine in src/ holds no company values, so these are the only paths that
230
+ # must never reach a public remote.
231
+ #
232
+ # data/ the active company profile — issuer, IBAN, clients,
233
+ # rates, issued bills, brand tokens
234
+ # archive/ the PDFs actually sent, retained 10 years (OR 958f)
235
+ # assets/reference/ source documents from clients
236
+ #
237
+ # No .gitkeep placeholders here — an empty but present data/ would be selected
238
+ # by resolve_profile() and then fail, instead of falling through to example/.
239
+ # Directories are created on demand by the code that writes into them.
240
+ # ---------------------------------------------------------------------------
241
+ /data/
242
+ /archive/
243
+ assets/reference/
244
+
245
+ # Company-specific skill: client names, VAT status, rate, business structure.
246
+ # Ignored so it cannot be swept into a commit while its fate is undecided —
247
+ # renaming or splitting it into a generic skill plus a private profile is a
248
+ # decision, not a chore. Un-ignore once that call is made.
249
+ /skills/private-company-profile/
250
+
251
+ # uv
252
+ .venv/
253
+
254
+ # Finder and editor clutter. Finder drops .DS_Store into any directory it has
255
+ # been opened in, and the file records that directory's contents — so it is
256
+ # both noise and, in a repository organised around keeping company values out
257
+ # of git, a small thing that lists filenames nobody chose to publish.
258
+ .DS_Store
259
+ ._*
@@ -0,0 +1,41 @@
1
+ # Runs before a commit is created, rather than telling you about it afterwards.
2
+ # Install once with `make hooks`; `make check` runs the same set over every file.
3
+ #
4
+ # mypy and the leak scan are `local` hooks on purpose: both need the project's
5
+ # own environment (its dependencies, its profile), and a hook running in an
6
+ # isolated venv would either fail to import weasyprint or scan with no profile
7
+ # at all — passing for the wrong reason, which is worse than failing.
8
+ repos:
9
+ - repo: https://github.com/astral-sh/ruff-pre-commit
10
+ rev: v0.14.5
11
+ hooks:
12
+ - id: ruff-check
13
+ args: [--fix]
14
+ - id: ruff-format
15
+
16
+ - repo: https://github.com/pre-commit/pre-commit-hooks
17
+ rev: v6.0.0
18
+ hooks:
19
+ - id: end-of-file-fixer
20
+ - id: trailing-whitespace
21
+ - id: check-yaml
22
+ - id: check-toml
23
+ - id: check-added-large-files
24
+ - id: check-merge-conflict
25
+ - id: detect-private-key
26
+
27
+ - repo: local
28
+ hooks:
29
+ - id: mypy
30
+ name: mypy
31
+ entry: uv run mypy
32
+ language: system
33
+ types: [python]
34
+ pass_filenames: false
35
+
36
+ - id: scan
37
+ name: scan for private values in tracked files
38
+ entry: uv run billwright scan
39
+ language: system
40
+ pass_filenames: false
41
+ always_run: true