mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
chore(ci): migrate Python tooling to uv and standardize workflow execution (#7386)
# Description of Changes This PR modernizes the project's Python tooling across GitHub Actions by migrating CI workflows from pip-based dependency management to `uv` and aligning Python execution with the engine project's managed environment. ### What was changed - Replaced `actions/setup-python` and ad-hoc `pip install` steps with `astral-sh/setup-uv` across CI workflows. - Configured shared `uv` dependency caching using `engine/pyproject.toml` and `engine/uv.lock`. - Updated Python script execution to use `uv run --project engine --locked` for a consistent runtime environment. - Replaced package installation steps with `uv sync` for the required dependency groups (e.g. `tools` and `cucumber`). - Added Docker image build validation for both production and development AI engine images. - Updated workflow cache configuration and Docker build context where required. - Removed obsolete Python requirements files that are no longer needed after the migration. - Applied minor Python code modernizations, including import cleanup, modern built-in generic type annotations (`list[...]`, `tuple[...]`, `float | None`), and small style improvements. - Removed unnecessary Python formatter/linter extensions from the development container configuration. ### Why the change was made - Standardize Python dependency management across the repository. - Reduce duplicated dependency installation logic in CI. - Improve workflow performance through shared dependency caching. - Ensure all Python utilities execute against the same locked dependency set managed by the engine project. - Simplify long-term maintenance by eliminating legacy requirements files and pip-specific workflow steps. --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --------- Signed-off-by: Carsten Drewes <c.drewes@stud.uni-hannover.de> Co-authored-by: albanobattistella <34811668+albanobattistella@users.noreply.github.com> Co-authored-by: kastenherri <116314318+kastenherri@users.noreply.github.com> Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: James Brunton <jbrunton96@gmail.com>
This commit is contained in:
co-authored by
albanobattistella
kastenherri
Anthony Stirling
Copilot
James Brunton
parent
bbaff8d6c4
commit
05eb74022a
@@ -119,8 +119,6 @@
|
||||
"extensions": [
|
||||
"elagil.pre-commit-helper", // Support for pre-commit hooks to enforce code quality
|
||||
"josevseb.google-java-format-for-vs-code", // Google Java code formatter to follow the Google Java Style Guide
|
||||
"ms-python.black-formatter", // Python code formatter using Black
|
||||
"ms-python.flake8", // Flake8 linter for Python to enforce code quality
|
||||
"ms-python.python", // Official Microsoft Python extension with IntelliSense, debugging, and Jupyter support
|
||||
"ms-vscode-remote.vscode-remote-extensionpack", // Remote Development Pack for SSH, WSL, and Containers
|
||||
// "Oracle.oracle-java", // Oracle Java extension with additional features for Java development
|
||||
|
||||
@@ -19,9 +19,9 @@ import argparse
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import tomllib # Python 3.11+ (stdlib)
|
||||
from pathlib import Path
|
||||
|
||||
import tomllib # Python 3.11+ (stdlib)
|
||||
import tomli_w # For writing TOML files
|
||||
|
||||
|
||||
@@ -133,11 +133,7 @@ def update_missing_keys(reference_file, file_list, branch=""):
|
||||
file_path = Path(file_path)
|
||||
language_dir = file_path.parent.name
|
||||
reference_lang_dir = reference_file.parent.name
|
||||
if (
|
||||
language_dir == reference_lang_dir
|
||||
or file_path.suffix != ".toml"
|
||||
or file_path.parents[1].name != "locales"
|
||||
):
|
||||
if language_dir == reference_lang_dir or file_path.suffix != ".toml" or file_path.parents[1].name != "locales":
|
||||
print(f"Skipping file: {file_path}")
|
||||
continue
|
||||
|
||||
@@ -198,9 +194,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
# Verify that file is within the expected directory
|
||||
if not absolute_path.is_relative_to(base_dir):
|
||||
has_differences = True
|
||||
report.append(
|
||||
f"\n⚠️ Unsafe file found: `{locale_dir}/{basename_current_file}`\n\n---\n"
|
||||
)
|
||||
report.append(f"\n⚠️ Unsafe file found: `{locale_dir}/{basename_current_file}`\n\n---\n")
|
||||
continue
|
||||
|
||||
# Verify file size before processing
|
||||
@@ -214,10 +208,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
if basename_current_file == basename_reference_file and locale_dir == "en-US":
|
||||
continue
|
||||
|
||||
if (
|
||||
file_normpath.suffix != ".toml"
|
||||
or basename_current_file != "translation.toml"
|
||||
):
|
||||
if file_normpath.suffix != ".toml" or basename_current_file != "translation.toml":
|
||||
continue
|
||||
|
||||
only_reference_file = False
|
||||
@@ -261,9 +252,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
)
|
||||
report.append("")
|
||||
report.append(" Use the following command to remove them:")
|
||||
report.append(
|
||||
f" `python scripts/translations/translation_merger.py {locale_dir} remove-unused`"
|
||||
)
|
||||
report.append(f" `python scripts/translations/translation_merger.py {locale_dir} remove-unused`")
|
||||
report.append("")
|
||||
if extra_keys_list:
|
||||
report.append(
|
||||
@@ -271,9 +260,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
)
|
||||
report.append("")
|
||||
report.append(" Use the following command to add them:")
|
||||
report.append(
|
||||
f" `python scripts/translations/translation_merger.py {locale_dir} add-missing`"
|
||||
)
|
||||
report.append(f" `python scripts/translations/translation_merger.py {locale_dir} add-missing`")
|
||||
report.append("")
|
||||
|
||||
if missing_keys_list or extra_keys_list:
|
||||
@@ -288,9 +275,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
output = "\n".join(
|
||||
[
|
||||
f" - `{key}`: first at {first}, duplicate at `{duplicate}`"
|
||||
for key, first, duplicate in find_duplicate_keys(
|
||||
branch_path / file_normpath
|
||||
)
|
||||
for key, first, duplicate in find_duplicate_keys(branch_path / file_normpath)
|
||||
]
|
||||
)
|
||||
report.append("3. **Test Status:** ❌ **_Failed_**")
|
||||
@@ -313,18 +298,14 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
else:
|
||||
report.append("## ✅ Overall Check Status: **_Success_**")
|
||||
report.append("")
|
||||
report.append(
|
||||
f"Thanks @{actor} for your help in keeping the translations up to date."
|
||||
)
|
||||
report.append(f"Thanks @{actor} for your help in keeping the translations up to date.")
|
||||
|
||||
if not only_reference_file:
|
||||
print("\n".join(report))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Find missing keys in TOML translation files"
|
||||
)
|
||||
parser = argparse.ArgumentParser(description="Find missing keys in TOML translation files")
|
||||
parser.add_argument(
|
||||
"--actor",
|
||||
required=False,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
pip
|
||||
setuptools
|
||||
WeasyPrint
|
||||
pdf2image
|
||||
pillow
|
||||
unoserver
|
||||
opencv-python-headless
|
||||
pre-commit
|
||||
brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29
|
||||
@@ -1,528 +0,0 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.13
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --allow-unsafe --generate-hashes --output-file='.github\scripts\requirements_dev.txt' --strip-extras '.github\scripts\requirements_dev.in'
|
||||
#
|
||||
# WARNING: pip install will require the following package to be hashed.
|
||||
# Consider using a hashable URL like https://github.com/jazzband/pip-tools/archive/SOMECOMMIT.zip
|
||||
brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29
|
||||
# via
|
||||
# -r .github/scripts/requirements_dev.in
|
||||
# fonttools
|
||||
cffi==2.1.0 \
|
||||
--hash=sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc \
|
||||
--hash=sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd \
|
||||
--hash=sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d \
|
||||
--hash=sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5 \
|
||||
--hash=sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f \
|
||||
--hash=sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6 \
|
||||
--hash=sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c \
|
||||
--hash=sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda \
|
||||
--hash=sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd \
|
||||
--hash=sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a \
|
||||
--hash=sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd \
|
||||
--hash=sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd \
|
||||
--hash=sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3 \
|
||||
--hash=sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb \
|
||||
--hash=sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66 \
|
||||
--hash=sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d \
|
||||
--hash=sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f \
|
||||
--hash=sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6 \
|
||||
--hash=sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0 \
|
||||
--hash=sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c \
|
||||
--hash=sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93 \
|
||||
--hash=sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d \
|
||||
--hash=sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d \
|
||||
--hash=sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8 \
|
||||
--hash=sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b \
|
||||
--hash=sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001 \
|
||||
--hash=sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d \
|
||||
--hash=sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43 \
|
||||
--hash=sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b \
|
||||
--hash=sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0 \
|
||||
--hash=sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0 \
|
||||
--hash=sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458 \
|
||||
--hash=sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8 \
|
||||
--hash=sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d \
|
||||
--hash=sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94 \
|
||||
--hash=sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022 \
|
||||
--hash=sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db \
|
||||
--hash=sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479 \
|
||||
--hash=sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376 \
|
||||
--hash=sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d \
|
||||
--hash=sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6 \
|
||||
--hash=sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3 \
|
||||
--hash=sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea \
|
||||
--hash=sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd \
|
||||
--hash=sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02 \
|
||||
--hash=sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde \
|
||||
--hash=sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224 \
|
||||
--hash=sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76 \
|
||||
--hash=sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804 \
|
||||
--hash=sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1 \
|
||||
--hash=sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913 \
|
||||
--hash=sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714 \
|
||||
--hash=sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc \
|
||||
--hash=sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2 \
|
||||
--hash=sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e \
|
||||
--hash=sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda \
|
||||
--hash=sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512 \
|
||||
--hash=sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28 \
|
||||
--hash=sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699 \
|
||||
--hash=sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3 \
|
||||
--hash=sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c \
|
||||
--hash=sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe \
|
||||
--hash=sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a \
|
||||
--hash=sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f \
|
||||
--hash=sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c \
|
||||
--hash=sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2 \
|
||||
--hash=sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f \
|
||||
--hash=sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b \
|
||||
--hash=sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565 \
|
||||
--hash=sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056 \
|
||||
--hash=sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629 \
|
||||
--hash=sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7 \
|
||||
--hash=sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0 \
|
||||
--hash=sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9 \
|
||||
--hash=sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853 \
|
||||
--hash=sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13 \
|
||||
--hash=sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a \
|
||||
--hash=sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4 \
|
||||
--hash=sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce \
|
||||
--hash=sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac \
|
||||
--hash=sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c \
|
||||
--hash=sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46 \
|
||||
--hash=sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384 \
|
||||
--hash=sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b \
|
||||
--hash=sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210 \
|
||||
--hash=sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc \
|
||||
--hash=sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a \
|
||||
--hash=sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5 \
|
||||
--hash=sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7 \
|
||||
--hash=sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2 \
|
||||
--hash=sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326 \
|
||||
--hash=sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f \
|
||||
--hash=sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca \
|
||||
--hash=sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98 \
|
||||
--hash=sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9 \
|
||||
--hash=sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5 \
|
||||
--hash=sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7 \
|
||||
--hash=sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc \
|
||||
--hash=sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da \
|
||||
--hash=sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f
|
||||
# via weasyprint
|
||||
cfgv==3.5.0 \
|
||||
--hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \
|
||||
--hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132
|
||||
# via pre-commit
|
||||
cssselect2==0.9.0 \
|
||||
--hash=sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563 \
|
||||
--hash=sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb
|
||||
# via weasyprint
|
||||
distlib==0.4.3 \
|
||||
--hash=sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b \
|
||||
--hash=sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed
|
||||
# via virtualenv
|
||||
filelock==3.30.0 \
|
||||
--hash=sha256:1774e682dbe443bd60f9609162fc596e2c80dc84ffc2957068953406d0520090 \
|
||||
--hash=sha256:40632998f0772e64183bb819f086a1b9def6be1090cf1dcb9d45f46806ef279b
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
fonttools==4.63.0 \
|
||||
--hash=sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69 \
|
||||
--hash=sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c \
|
||||
--hash=sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac \
|
||||
--hash=sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096 \
|
||||
--hash=sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d \
|
||||
--hash=sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68 \
|
||||
--hash=sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616 \
|
||||
--hash=sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78 \
|
||||
--hash=sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f \
|
||||
--hash=sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b \
|
||||
--hash=sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b \
|
||||
--hash=sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02 \
|
||||
--hash=sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d \
|
||||
--hash=sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f \
|
||||
--hash=sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8 \
|
||||
--hash=sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272 \
|
||||
--hash=sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49 \
|
||||
--hash=sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419 \
|
||||
--hash=sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001 \
|
||||
--hash=sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03 \
|
||||
--hash=sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196 \
|
||||
--hash=sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9 \
|
||||
--hash=sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e \
|
||||
--hash=sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5 \
|
||||
--hash=sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007 \
|
||||
--hash=sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380 \
|
||||
--hash=sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8 \
|
||||
--hash=sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27 \
|
||||
--hash=sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40 \
|
||||
--hash=sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e \
|
||||
--hash=sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0 \
|
||||
--hash=sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263 \
|
||||
--hash=sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb \
|
||||
--hash=sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94 \
|
||||
--hash=sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b \
|
||||
--hash=sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6 \
|
||||
--hash=sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579 \
|
||||
--hash=sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4 \
|
||||
--hash=sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59 \
|
||||
--hash=sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0 \
|
||||
--hash=sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e \
|
||||
--hash=sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be \
|
||||
--hash=sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd \
|
||||
--hash=sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18 \
|
||||
--hash=sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22 \
|
||||
--hash=sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0 \
|
||||
--hash=sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b \
|
||||
--hash=sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b \
|
||||
--hash=sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af \
|
||||
--hash=sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745
|
||||
# via weasyprint
|
||||
identify==2.6.19 \
|
||||
--hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a \
|
||||
--hash=sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842
|
||||
# via pre-commit
|
||||
nodeenv==1.10.0 \
|
||||
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
|
||||
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
|
||||
# via pre-commit
|
||||
numpy==2.4.6 \
|
||||
--hash=sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1 \
|
||||
--hash=sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4 \
|
||||
--hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \
|
||||
--hash=sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079 \
|
||||
--hash=sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096 \
|
||||
--hash=sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47 \
|
||||
--hash=sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66 \
|
||||
--hash=sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d \
|
||||
--hash=sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1 \
|
||||
--hash=sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e \
|
||||
--hash=sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147 \
|
||||
--hash=sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd \
|
||||
--hash=sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75 \
|
||||
--hash=sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063 \
|
||||
--hash=sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73 \
|
||||
--hash=sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab \
|
||||
--hash=sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4 \
|
||||
--hash=sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41 \
|
||||
--hash=sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402 \
|
||||
--hash=sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698 \
|
||||
--hash=sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7 \
|
||||
--hash=sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8 \
|
||||
--hash=sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b \
|
||||
--hash=sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8 \
|
||||
--hash=sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0 \
|
||||
--hash=sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662 \
|
||||
--hash=sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91 \
|
||||
--hash=sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0 \
|
||||
--hash=sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f \
|
||||
--hash=sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3 \
|
||||
--hash=sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f \
|
||||
--hash=sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67 \
|
||||
--hash=sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6 \
|
||||
--hash=sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997 \
|
||||
--hash=sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b \
|
||||
--hash=sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e \
|
||||
--hash=sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538 \
|
||||
--hash=sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627 \
|
||||
--hash=sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93 \
|
||||
--hash=sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02 \
|
||||
--hash=sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853 \
|
||||
--hash=sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c \
|
||||
--hash=sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43 \
|
||||
--hash=sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd \
|
||||
--hash=sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8 \
|
||||
--hash=sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089 \
|
||||
--hash=sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778 \
|
||||
--hash=sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1 \
|
||||
--hash=sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb \
|
||||
--hash=sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261 \
|
||||
--hash=sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb \
|
||||
--hash=sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a \
|
||||
--hash=sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8 \
|
||||
--hash=sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359 \
|
||||
--hash=sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5 \
|
||||
--hash=sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7 \
|
||||
--hash=sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751 \
|
||||
--hash=sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8 \
|
||||
--hash=sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605 \
|
||||
--hash=sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e \
|
||||
--hash=sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45 \
|
||||
--hash=sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2 \
|
||||
--hash=sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895 \
|
||||
--hash=sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe \
|
||||
--hash=sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb \
|
||||
--hash=sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a \
|
||||
--hash=sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577 \
|
||||
--hash=sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d \
|
||||
--hash=sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a \
|
||||
--hash=sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda \
|
||||
--hash=sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6 \
|
||||
--hash=sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20
|
||||
# via opencv-python-headless
|
||||
opencv-python-headless==5.0.0.93 \
|
||||
--hash=sha256:030ca5e0837a2963ab36ef896baa9767eb8d2b83353fb28af5a521e40dd8756f \
|
||||
--hash=sha256:09a872a157c1376ab922a69bbf22f9a95bcc7b658a9d8b436a60212b02b2eeb4 \
|
||||
--hash=sha256:10818d91510e05c04568ae12b5cd120779c70c01bf897b001a6221fe430df80f \
|
||||
--hash=sha256:1e55af3abfb462eeeabe5c775f12bdb36216d8a93a3583d69e6bd6e1d6ba7d00 \
|
||||
--hash=sha256:829717b6a95554f273e49e357cee3b3a2a26b6f4842fbc1bed2b45bdd8f87e0e \
|
||||
--hash=sha256:840bd717c21e5c11cadadc022a823315ea417f961213d06b4df010e019eb16f4 \
|
||||
--hash=sha256:b82f9831daab90b725c7c1ee1b36cb5732c367096ac76d119e64e14eb70d5f3c \
|
||||
--hash=sha256:c6bcd96b185975ea240d22cfdb15a1f6d080cc95264cfbe2621f21bb144d89b9 \
|
||||
--hash=sha256:ed709fdf9aa0bd1f2ed8549e71d19449b03a675bb581eb292285f6861953be37
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pdf2image==1.17.0 \
|
||||
--hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \
|
||||
--hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pillow==12.3.0 \
|
||||
--hash=sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756 \
|
||||
--hash=sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a \
|
||||
--hash=sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59 \
|
||||
--hash=sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45 \
|
||||
--hash=sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3 \
|
||||
--hash=sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df \
|
||||
--hash=sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139 \
|
||||
--hash=sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b \
|
||||
--hash=sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39 \
|
||||
--hash=sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e \
|
||||
--hash=sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8 \
|
||||
--hash=sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1 \
|
||||
--hash=sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8 \
|
||||
--hash=sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89 \
|
||||
--hash=sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5 \
|
||||
--hash=sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130 \
|
||||
--hash=sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd \
|
||||
--hash=sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d \
|
||||
--hash=sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b \
|
||||
--hash=sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed \
|
||||
--hash=sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace \
|
||||
--hash=sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb \
|
||||
--hash=sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931 \
|
||||
--hash=sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510 \
|
||||
--hash=sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6 \
|
||||
--hash=sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1 \
|
||||
--hash=sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce \
|
||||
--hash=sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385 \
|
||||
--hash=sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e \
|
||||
--hash=sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c \
|
||||
--hash=sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7 \
|
||||
--hash=sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace \
|
||||
--hash=sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c \
|
||||
--hash=sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f \
|
||||
--hash=sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64 \
|
||||
--hash=sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f \
|
||||
--hash=sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a \
|
||||
--hash=sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827 \
|
||||
--hash=sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17 \
|
||||
--hash=sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4 \
|
||||
--hash=sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a \
|
||||
--hash=sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701 \
|
||||
--hash=sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e \
|
||||
--hash=sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91 \
|
||||
--hash=sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66 \
|
||||
--hash=sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468 \
|
||||
--hash=sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217 \
|
||||
--hash=sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658 \
|
||||
--hash=sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418 \
|
||||
--hash=sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a \
|
||||
--hash=sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c \
|
||||
--hash=sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330 \
|
||||
--hash=sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402 \
|
||||
--hash=sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09 \
|
||||
--hash=sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930 \
|
||||
--hash=sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f \
|
||||
--hash=sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec \
|
||||
--hash=sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a \
|
||||
--hash=sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94 \
|
||||
--hash=sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468 \
|
||||
--hash=sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b \
|
||||
--hash=sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965 \
|
||||
--hash=sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8 \
|
||||
--hash=sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd \
|
||||
--hash=sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7 \
|
||||
--hash=sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c \
|
||||
--hash=sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777 \
|
||||
--hash=sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35 \
|
||||
--hash=sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9 \
|
||||
--hash=sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f \
|
||||
--hash=sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f \
|
||||
--hash=sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0 \
|
||||
--hash=sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c \
|
||||
--hash=sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71 \
|
||||
--hash=sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3 \
|
||||
--hash=sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838 \
|
||||
--hash=sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf \
|
||||
--hash=sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321 \
|
||||
--hash=sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26 \
|
||||
--hash=sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec \
|
||||
--hash=sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9 \
|
||||
--hash=sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65 \
|
||||
--hash=sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5 \
|
||||
--hash=sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e \
|
||||
--hash=sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d \
|
||||
--hash=sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198 \
|
||||
--hash=sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7
|
||||
# via
|
||||
# -r .github/scripts/requirements_dev.in
|
||||
# pdf2image
|
||||
# weasyprint
|
||||
platformdirs==4.10.0 \
|
||||
--hash=sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7 \
|
||||
--hash=sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
pre-commit==4.6.0 \
|
||||
--hash=sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9 \
|
||||
--hash=sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pycparser==3.0 \
|
||||
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
|
||||
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
|
||||
# via cffi
|
||||
pydyf==0.12.1 \
|
||||
--hash=sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc \
|
||||
--hash=sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095
|
||||
# via weasyprint
|
||||
pyphen==0.17.2 \
|
||||
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
|
||||
--hash=sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3
|
||||
# via weasyprint
|
||||
python-discovery==1.4.4 \
|
||||
--hash=sha256:5cad33982d412c1f3ffb8f9ca4ea292c9680bca3942451d30b69c37fce53a4a3 \
|
||||
--hash=sha256:abebe9120b43453b68c908acfb1e72a19d1a959ed2cb620ad38fc57d08056dbe
|
||||
# via virtualenv
|
||||
pyyaml==6.0.3 \
|
||||
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
|
||||
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
|
||||
--hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
|
||||
--hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
|
||||
--hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
|
||||
--hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
|
||||
--hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
|
||||
--hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
|
||||
--hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
|
||||
--hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
|
||||
--hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
|
||||
--hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
|
||||
--hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
|
||||
--hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
|
||||
--hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
|
||||
--hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
|
||||
--hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
|
||||
--hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
|
||||
--hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
|
||||
--hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
|
||||
--hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
|
||||
--hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
|
||||
--hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
|
||||
--hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
|
||||
--hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
|
||||
--hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
|
||||
--hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
|
||||
--hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
|
||||
--hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
|
||||
--hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
|
||||
--hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
|
||||
--hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
|
||||
--hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
|
||||
--hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
|
||||
--hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
|
||||
--hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
|
||||
--hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
|
||||
--hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
|
||||
--hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
|
||||
--hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
|
||||
--hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
|
||||
--hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
|
||||
--hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
|
||||
--hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
|
||||
--hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
|
||||
--hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
|
||||
--hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
|
||||
--hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
|
||||
--hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
|
||||
--hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
|
||||
--hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
|
||||
--hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
|
||||
--hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
|
||||
--hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
|
||||
--hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
|
||||
--hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
|
||||
--hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
|
||||
--hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
|
||||
--hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
|
||||
--hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
|
||||
--hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
|
||||
--hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
|
||||
--hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
|
||||
--hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
|
||||
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
|
||||
--hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
|
||||
--hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
|
||||
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
|
||||
--hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
|
||||
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
|
||||
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||
# via pre-commit
|
||||
tinycss2==1.5.1 \
|
||||
--hash=sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 \
|
||||
--hash=sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957
|
||||
# via
|
||||
# cssselect2
|
||||
# weasyprint
|
||||
tinyhtml5==2.1.0 \
|
||||
--hash=sha256:60a50ec3d938a37e491efa01af895853060943dcebb5627de5b10d188b338a67 \
|
||||
--hash=sha256:6e11cfff38515834268daf89d5f85bbde0b6dd02e8d9e212d1385c2289b89f0a
|
||||
# via weasyprint
|
||||
unoserver==3.7 \
|
||||
--hash=sha256:b05f9578506ac7374ae1b314c3a79528636c542ac78220a9ce99110584ca424b \
|
||||
--hash=sha256:fc44e6808071c9d2957e705ecf1742cea8a582aa5d5cc23babf36bb332ec6e8e
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
virtualenv==21.6.1 \
|
||||
--hash=sha256:15f978b7cd329f24855ff4a0c4b4899cc7678589f49adbdcbbb4d3232e641128 \
|
||||
--hash=sha256:afe991df855715a2b2f60edfcc0107ef95a79fdfd8cb4cdaa71603d1c12e463b
|
||||
# via pre-commit
|
||||
weasyprint==69.0 \
|
||||
--hash=sha256:475951cfd917014de6d4d005caff48c6aa867e7e42b80cd5b16a0484a1609ee6 \
|
||||
--hash=sha256:a7a32f39ca16bd82ef11de99c92ea4b5f14951c9033af035e451ce4f4ee0a88c
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
webencodings==0.5.1 \
|
||||
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
|
||||
--hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
|
||||
# via
|
||||
# cssselect2
|
||||
# tinycss2
|
||||
# tinyhtml5
|
||||
zopfli==0.4.3 \
|
||||
--hash=sha256:0087c9a6f0c8a052be0f6d1a9bb71b6caffdd3e10201d6d6166e28d482cebe6d \
|
||||
--hash=sha256:47604eee5c6704bdf0e94d8391fe3b74ddb2abd84128fbcfdc3ee0fc265feaef \
|
||||
--hash=sha256:62248dbf8dbcbd588ee194b210e5be9fa80bce29641f55599d6d394bd2a9d8a3 \
|
||||
--hash=sha256:628c3e941752880b3491db8d44163d0aedb221944e22a17187ff7fc549b050f6 \
|
||||
--hash=sha256:769875152d0625c46707bcca57d4b2233fe653482067acd55fbf6ec525cb9bdc \
|
||||
--hash=sha256:7e9703ca6e7ef66c8d05e0826b6f558b680c9db8206f84f05a3ee93430a12e42 \
|
||||
--hash=sha256:7fa3c35193475290e3f007bbcdebdbae64ba2f012d75c632da0d727e1da50d5e \
|
||||
--hash=sha256:88f4fbe429aad72bc206275d81fab11a097e0f951a5848d1f51083c37ea73073 \
|
||||
--hash=sha256:921c2c9907f4364963848da5ad194b46d68865e07fdb975d04fd09bc42d47357 \
|
||||
--hash=sha256:d3a50f91a13cea9bafe025de8fd87a005eb26de02a4f0c193127ddbf23ac8ebe \
|
||||
--hash=sha256:d4f51dd1ab5312e837e2091284e0d9f1a138188f2e65812f9a5799dc02c45f94 \
|
||||
--hash=sha256:eb0c9c1d40a8cb1d58762d7e57290ccb753e0828c4d01be8acb59aae5d0ca206 \
|
||||
--hash=sha256:f2e0adcf7d36c6fd0dd36cc771ef7f0c5803a05666feafcd90d7170174a4148e
|
||||
# via fonttools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==26.1.2 \
|
||||
--hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \
|
||||
--hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
setuptools==83.0.0 \
|
||||
--hash=sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef \
|
||||
--hash=sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
@@ -1,2 +0,0 @@
|
||||
tomlkit
|
||||
tomli-w
|
||||
@@ -1,14 +0,0 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.13
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_sync_readme.txt' --strip-extras '.github\scripts\requirements_sync_readme.in'
|
||||
#
|
||||
tomli-w==1.2.0 \
|
||||
--hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \
|
||||
--hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021
|
||||
# via -r .github/scripts/requirements_sync_readme.in
|
||||
tomlkit==0.15.0 \
|
||||
--hash=sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738 \
|
||||
--hash=sha256:7d1a9ecba3086638211b13814ea79c90dd54dd11993564376f3aa92271f5c7a3
|
||||
# via -r .github/scripts/requirements_sync_readme.in
|
||||
@@ -4,19 +4,18 @@
|
||||
Usage: verify-updater-signatures.py <dir-to-scan> [tauri.conf.json]
|
||||
"""
|
||||
|
||||
import binascii
|
||||
import sys
|
||||
import json
|
||||
import base64
|
||||
import binascii
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
||||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
||||
|
||||
ART_ROOT = Path(sys.argv[1])
|
||||
CONF = Path(
|
||||
sys.argv[2] if len(sys.argv) > 2 else "frontend/editor/src-tauri/tauri.conf.json"
|
||||
)
|
||||
CONF = Path(sys.argv[2] if len(sys.argv) > 2 else "frontend/editor/src-tauri/tauri.conf.json")
|
||||
|
||||
|
||||
def load_pubkey():
|
||||
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
if: needs.check-comment.outputs.enable_prototypes == 'true'
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: ./engine
|
||||
context: .
|
||||
file: ./engine/Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-pdf-engine
|
||||
|
||||
@@ -31,6 +31,9 @@ jobs:
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
cache-suffix: ai-engine
|
||||
|
||||
- name: Install Task
|
||||
@@ -96,6 +99,14 @@ jobs:
|
||||
echo "============================================"
|
||||
exit 1
|
||||
|
||||
- name: Build engine production image
|
||||
if: always()
|
||||
run: docker build --file engine/Dockerfile --tag stirling-pdf-engine:ci .
|
||||
|
||||
- name: Build engine development image
|
||||
if: always()
|
||||
run: docker build --file engine/Dockerfile.dev --tag stirling-pdf-engine-dev:ci .
|
||||
|
||||
- name: Remove engine check comment on success
|
||||
if: steps.engine-check.outcome == 'success' && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
|
||||
@@ -192,12 +192,14 @@ jobs:
|
||||
retention-days: 3
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Install defusedxml for coverage summary
|
||||
# coverage-summary.py parses JaCoCo XML through defusedxml to
|
||||
# silence security scanners that pattern-match on the stdlib
|
||||
# xml.etree.ElementTree.parse call.
|
||||
- name: Install uv
|
||||
if: always() && matrix.flavor == 'saas'
|
||||
run: python -m pip install --quiet defusedxml
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
|
||||
- name: JaCoCo coverage step summary
|
||||
# Only the saas leg posts the JUnit summary - it's a strict
|
||||
@@ -206,7 +208,7 @@ jobs:
|
||||
# near-identical tables crowding out the aggregate report.
|
||||
if: always() && matrix.flavor == 'saas'
|
||||
run: |
|
||||
python scripts/coverage-summary.py \
|
||||
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
|
||||
--title "Backend JUnit coverage (JDK ${{ matrix.jdk-version }})" \
|
||||
--jacoco "common=app/common/build/reports/jacoco/test/jacocoTestReport.xml" \
|
||||
--jacoco "core=app/core/build/reports/jacoco/test/jacocoTestReport.xml" \
|
||||
|
||||
@@ -329,15 +329,16 @@ jobs:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: "pip"
|
||||
cache-dependency-path: ./testing/cucumber/requirements.txt
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
- name: Install behave test deps
|
||||
run: |
|
||||
pip install --require-hashes --only-binary=:all: -r ./testing/cucumber/requirements.txt
|
||||
uv sync --project engine --locked --group cucumber
|
||||
- name: Build the multi-node image
|
||||
working-directory: testing/compose
|
||||
run: docker compose -f "$MN_COMPOSE" build
|
||||
@@ -360,10 +361,10 @@ jobs:
|
||||
- name: Run multi-node regression (implemented guarantees)
|
||||
working-directory: testing/cucumber
|
||||
# -e overrides behave.ini's exclusion of features/multinode; ~@known_gap skips any tracked-gap scenarios.
|
||||
run: python -m behave features/multinode -e "features/enterprise" --tags="~@known_gap ~@destructive" --no-capture -f plain
|
||||
run: uv run --project ../../engine --locked --group cucumber python -m behave features/multinode -e "features/enterprise" --tags="~@known_gap ~@destructive" --no-capture -f plain
|
||||
- name: Run multi-node failover (destructive)
|
||||
working-directory: testing/cucumber
|
||||
run: python -m behave features/multinode -e "features/enterprise" --tags="@destructive ~@known_gap" --no-capture -f plain
|
||||
run: uv run --project ../../engine --locked --group cucumber python -m behave features/multinode -e "features/enterprise" --tags="@destructive ~@known_gap" --no-capture -f plain
|
||||
- name: Dump node logs on failure
|
||||
if: failure()
|
||||
working-directory: testing/compose
|
||||
|
||||
@@ -34,6 +34,9 @@ jobs:
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
cache-suffix: generated-models
|
||||
|
||||
- name: Set up JDK 25
|
||||
|
||||
@@ -195,20 +195,23 @@ jobs:
|
||||
console.log(`Reference file path: ${referenceFilePath}`);
|
||||
core.exportVariable("REFERENCE_FILE", referenceFilePath);
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt
|
||||
uv sync --project engine --locked --group tools
|
||||
|
||||
- name: Run Python script to check files
|
||||
id: run-check
|
||||
run: |
|
||||
echo "Running Python script to check TOML files..."
|
||||
python .github/scripts/check_language_toml.py \
|
||||
uv run --project engine --locked --group tools python .github/scripts/check_language_toml.py \
|
||||
--actor ${{ github.event.pull_request.user.login }} \
|
||||
--reference-file "${REFERENCE_FILE}" \
|
||||
--branch "pr-branch" \
|
||||
|
||||
@@ -57,15 +57,13 @@ jobs:
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install defusedxml for coverage scripts
|
||||
# Both coverage-summary.py and coverage-matrix.py parse JaCoCo
|
||||
# XML through defusedxml - see the script headers for context.
|
||||
run: python -m pip install --quiet defusedxml
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
|
||||
# Pattern matches every artifact this PR's producers might upload:
|
||||
# jacoco-exec-junit-jdk-25 (uploaded only by the saas
|
||||
@@ -158,7 +156,7 @@ jobs:
|
||||
# ("how much of the backend do real user flows cover?").
|
||||
if: steps.inventory.outputs.found_e2e == 'true'
|
||||
run: |
|
||||
python scripts/coverage-summary.py \
|
||||
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
|
||||
--title "Real user-flow backend coverage (e2e:live + cucumber)" \
|
||||
--jacoco "merged=build/reports/jacoco/aggregate-e2e/jacocoTestReport.xml" \
|
||||
--github-step-summary
|
||||
@@ -169,7 +167,7 @@ jobs:
|
||||
# is meaningless when one is a strict superset of the other.
|
||||
if: steps.inventory.outputs.found_all == 'true'
|
||||
run: |
|
||||
python scripts/coverage-summary.py \
|
||||
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
|
||||
--title "Combined backend coverage (JUnit + e2e:live + cucumber)" \
|
||||
--jacoco "merged=build/reports/jacoco/aggregate-all/jacocoTestReport.xml" \
|
||||
--github-step-summary
|
||||
@@ -224,7 +222,7 @@ jobs:
|
||||
# generated above) plus whichever frontend artifacts landed.
|
||||
# Every input is optional; missing ones render as "-".
|
||||
run: |
|
||||
python scripts/coverage-matrix.py \
|
||||
uv run --project engine --locked --group tools python scripts/coverage-matrix.py \
|
||||
${{ steps.inventory.outputs.found_all == 'true' && '--jacoco-all build/reports/jacoco/aggregate-all/jacocoTestReport.xml' || '' }} \
|
||||
${{ steps.inventory.outputs.found_e2e == 'true' && '--jacoco-e2e build/reports/jacoco/aggregate-e2e/jacocoTestReport.xml' || '' }} \
|
||||
--vitest matrix-inputs/vitest/coverage-summary.json \
|
||||
|
||||
@@ -69,16 +69,17 @@ jobs:
|
||||
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.39.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: "pip" # caching pip dependencies
|
||||
cache-dependency-path: ./testing/cucumber/requirements.txt
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
|
||||
- name: Pip requirements
|
||||
- name: Install Cucumber and coverage dependencies
|
||||
run: |
|
||||
pip install --require-hashes --only-binary=:all: -r ./testing/cucumber/requirements.txt
|
||||
uv sync --project engine --locked --group cucumber --group tools
|
||||
|
||||
- name: Extract JaCoCo agent for cucumber coverage
|
||||
# Stages build/jacoco/jacocoagent.jar where the coverage override
|
||||
@@ -121,16 +122,10 @@ jobs:
|
||||
echo "report=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Install defusedxml for coverage summary
|
||||
# coverage-summary.py parses JaCoCo XML through defusedxml -
|
||||
# see the script header for context.
|
||||
if: always() && steps.cucumber-coverage.outputs.report == 'true'
|
||||
run: python -m pip install --quiet defusedxml
|
||||
|
||||
- name: Cucumber coverage step summary
|
||||
if: always() && steps.cucumber-coverage.outputs.report == 'true'
|
||||
run: |
|
||||
python scripts/coverage-summary.py \
|
||||
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
|
||||
--title "Cucumber (docker) JaCoCo coverage" \
|
||||
--jacoco "cucumber=build/reports/jacoco/cucumber/jacocoTestReport.xml" \
|
||||
--github-step-summary
|
||||
|
||||
@@ -119,20 +119,18 @@ jobs:
|
||||
echo "::warning::No e2e:live .exec found at .test-state/playwright/jacoco.exec; skipping report"
|
||||
echo "report=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Set up Python for coverage summary
|
||||
if: always() && steps.live-coverage.outputs.report == 'true'
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
- name: Install uv
|
||||
if: always()
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install defusedxml for coverage summary
|
||||
# coverage-summary.py uses defusedxml instead of stdlib xml.etree
|
||||
# to dodge XXE / billion-laughs scanner findings.
|
||||
if: always() && steps.live-coverage.outputs.report == 'true'
|
||||
run: python -m pip install --quiet defusedxml
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
- name: e2e:live coverage step summary
|
||||
if: always() && steps.live-coverage.outputs.report == 'true'
|
||||
run: |
|
||||
python scripts/coverage-summary.py \
|
||||
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
|
||||
--title "Playwright (live backend) JaCoCo coverage" \
|
||||
--jacoco "e2e-live=build/reports/jacoco/e2e-live/jacocoTestReport.xml" \
|
||||
--github-step-summary
|
||||
@@ -155,23 +153,6 @@ jobs:
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Set up Python for frontend coverage summary
|
||||
# Separate from the backend-coverage python step because the
|
||||
# frontend path doesn't depend on a JaCoCo report - it produces
|
||||
# a summary even on backend failure, as long as some Playwright
|
||||
# tests ran far enough to dump V8 coverage.
|
||||
if: always()
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install defusedxml for frontend coverage summary
|
||||
# Idempotent re-install: the backend-coverage step may have
|
||||
# installed it already, but this leg can run on its own when the
|
||||
# backend report step skips (e.g. .exec missing).
|
||||
if: always()
|
||||
run: python -m pip install --quiet defusedxml
|
||||
|
||||
- name: Aggregate Playwright frontend (V8) coverage
|
||||
# Rolls per-test V8 dumps from the test-base fixture into one
|
||||
# vitest-shaped coverage-summary.json. Tolerates a missing dump
|
||||
@@ -182,7 +163,7 @@ jobs:
|
||||
run: |
|
||||
if [ -d .test-state/playwright/coverage-pw ] && \
|
||||
find .test-state/playwright/coverage-pw -name '*.json' -type f | grep -q .; then
|
||||
python scripts/playwright-coverage-summary.py \
|
||||
uv run --project engine --locked --group tools python scripts/playwright-coverage-summary.py \
|
||||
.test-state/playwright/coverage-pw \
|
||||
--out .test-state/playwright/coverage-pw-summary/coverage-summary.json
|
||||
echo "summary=true" >> "$GITHUB_OUTPUT"
|
||||
@@ -194,10 +175,10 @@ jobs:
|
||||
- name: Playwright frontend coverage step summary
|
||||
if: always() && steps.pw-frontend-coverage.outputs.summary == 'true'
|
||||
run: |
|
||||
python scripts/coverage-summary.py \
|
||||
--title "Playwright (live) frontend coverage" \
|
||||
--vitest .test-state/playwright/coverage-pw-summary/coverage-summary.json \
|
||||
--github-step-summary
|
||||
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
|
||||
--title "Playwright (live) frontend coverage" \
|
||||
--vitest .test-state/playwright/coverage-pw-summary/coverage-summary.json \
|
||||
--github-step-summary
|
||||
|
||||
- name: Upload Playwright frontend coverage
|
||||
# Bundle both the aggregated summary and the raw V8 dumps so
|
||||
|
||||
@@ -115,20 +115,18 @@ jobs:
|
||||
id: frontend-coverage
|
||||
continue-on-error: true
|
||||
run: task frontend:test:coverage
|
||||
- name: Set up Python for coverage summary
|
||||
- name: Install uv
|
||||
if: always()
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install defusedxml for coverage summary
|
||||
# See coverage-summary.py header - it parses XML through defusedxml
|
||||
# to dodge the stdlib parser's exposure to XXE / billion-laughs.
|
||||
if: always()
|
||||
run: python -m pip install --quiet defusedxml
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
- name: Vitest coverage step summary
|
||||
if: always()
|
||||
run: |
|
||||
python scripts/coverage-summary.py \
|
||||
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
|
||||
--title "Frontend Vitest coverage" \
|
||||
--vitest frontend/editor/coverage/coverage-summary.json \
|
||||
--github-step-summary
|
||||
|
||||
@@ -887,10 +887,16 @@ jobs:
|
||||
|
||||
# Gate publish on valid updater sigs. Runs after the review upload (so
|
||||
# artifacts survive for debugging) and before action-gh-release.
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
- name: Verify updater signatures
|
||||
run: |
|
||||
python3 -m pip install --quiet 'cryptography==44.0.0'
|
||||
python3 .github/scripts/verify-updater-signatures.py \
|
||||
uv run --project engine --locked --only-group updater-signatures python .github/scripts/verify-updater-signatures.py \
|
||||
./artifacts/tauri frontend/editor/src-tauri/tauri.conf.json
|
||||
|
||||
# workflow_dispatch path requires platform=='all' so a single-platform
|
||||
|
||||
@@ -28,6 +28,9 @@ jobs:
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
cache-suffix: pre-commit
|
||||
|
||||
- name: Install Task
|
||||
|
||||
@@ -51,28 +51,25 @@ jobs:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: "pip" # caching pip dependencies
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
cache-suffix: sync-files
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
uv sync --project engine --locked --group tools
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||
|
||||
- name: Sync translation TOML files
|
||||
run: |
|
||||
python .github/scripts/check_language_toml.py --reference-file "frontend/editor/public/locales/en-US/translation.toml" --branch main
|
||||
uv run --project engine --locked --group tools python .github/scripts/check_language_toml.py --reference-file "frontend/editor/public/locales/en-US/translation.toml" --branch main
|
||||
|
||||
- name: Sort translation TOML files
|
||||
run: |
|
||||
@@ -85,7 +82,7 @@ jobs:
|
||||
|
||||
- name: Sync README.md
|
||||
run: |
|
||||
python scripts/counter_translation_v3.py
|
||||
uv run --project engine --locked --group tools python scripts/counter_translation_v3.py
|
||||
|
||||
- name: Run git add
|
||||
run: |
|
||||
|
||||
+40
-13
@@ -2,22 +2,49 @@ version: '3'
|
||||
|
||||
tasks:
|
||||
install:
|
||||
desc: "Install engine dependencies"
|
||||
desc: "Install engine runtime and development dependencies"
|
||||
run: once
|
||||
cmds:
|
||||
- uv python install 3.13.8
|
||||
- uv sync
|
||||
- uv sync --locked --group engine --group engine-dev
|
||||
sources:
|
||||
- uv.lock
|
||||
- pyproject.toml
|
||||
status:
|
||||
- test -d .venv
|
||||
|
||||
lock:
|
||||
desc: "Update the engine lockfile from project metadata"
|
||||
cmds:
|
||||
- uv lock
|
||||
|
||||
lock:upgrade:
|
||||
desc: "Upgrade allowed engine dependencies and update the lockfile"
|
||||
cmds:
|
||||
- uv lock --upgrade
|
||||
|
||||
lock:check:
|
||||
desc: "Check whether the engine lockfile is current"
|
||||
cmds:
|
||||
- uv lock --check
|
||||
|
||||
update:
|
||||
desc: "Upgrade engine dependencies and synchronize the environment"
|
||||
cmds:
|
||||
- task: lock:upgrade
|
||||
- uv sync --locked --group engine --group engine-dev
|
||||
|
||||
update:all:
|
||||
desc: "Upgrade all Python dependency groups and synchronize the environment"
|
||||
cmds:
|
||||
- task: lock:upgrade
|
||||
- uv sync --locked --all-groups
|
||||
|
||||
prepare:
|
||||
desc: "Set up engine .env from template"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run scripts/setup_env.py
|
||||
- uv run --locked --group engine --group engine-dev scripts/setup_env.py
|
||||
sources:
|
||||
- scripts/setup_env.py
|
||||
generates:
|
||||
@@ -33,7 +60,7 @@ tasks:
|
||||
env:
|
||||
PYTHONUNBUFFERED: "1"
|
||||
cmds:
|
||||
- uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}} --workers "${STIRLING_ENGINE_WORKERS:-4}"
|
||||
- uv run --locked --group engine uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}} --workers "${STIRLING_ENGINE_WORKERS:-4}"
|
||||
|
||||
dev:
|
||||
desc: "Start engine dev server with hot reload"
|
||||
@@ -45,43 +72,43 @@ tasks:
|
||||
env:
|
||||
PYTHONUNBUFFERED: "1"
|
||||
cmds:
|
||||
- uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}} --reload
|
||||
- uv run --locked --group engine --group engine-dev uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}} --reload
|
||||
|
||||
lint:
|
||||
desc: "Run linting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run ruff check .
|
||||
- uv run --locked --group engine --group engine-dev ruff check .
|
||||
|
||||
lint:fix:
|
||||
desc: "Auto-fix lint issues"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run ruff check . --fix
|
||||
- uv run --locked --group engine --group engine-dev ruff check . --fix
|
||||
|
||||
format:
|
||||
desc: "Auto-fix code formatting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run ruff format .
|
||||
- uv run --locked --group engine --group engine-dev ruff format .
|
||||
|
||||
format:check:
|
||||
desc: "Check code formatting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run ruff format . --diff
|
||||
- uv run --locked --group engine --group engine-dev ruff format . --diff
|
||||
|
||||
typecheck:
|
||||
desc: "Run type checking"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run pyright . --warnings
|
||||
- uv run --locked --group engine --group engine-dev pyright . --warnings
|
||||
|
||||
test:
|
||||
desc: "Run tests"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- uv run pytest tests
|
||||
- uv run --locked --group engine --group engine-dev pytest tests
|
||||
|
||||
fix:
|
||||
desc: "Auto-fix lint + format"
|
||||
@@ -102,7 +129,7 @@ tasks:
|
||||
desc: "Generate tool_models.py from Java OpenAPI spec (SwaggerDoc.json)"
|
||||
deps: [install, ":backend:swagger"]
|
||||
cmds:
|
||||
- uv run python scripts/generate_tool_models.py --spec ../SwaggerDoc.json --output src/stirling/models/tool_models.py --io-output src/stirling/models/tool_io.py
|
||||
- uv run --locked --group engine --group engine-dev python scripts/generate_tool_models.py --spec ../SwaggerDoc.json --output src/stirling/models/tool_models.py --io-output src/stirling/models/tool_io.py
|
||||
sources:
|
||||
- ../SwaggerDoc.json
|
||||
- scripts/generate_tool_models.py
|
||||
@@ -114,7 +141,7 @@ tasks:
|
||||
desc: "Fail if the committed tool models are out of date"
|
||||
deps: [install, ":backend:swagger"]
|
||||
cmds:
|
||||
- uv run python scripts/generate_tool_models.py --spec ../SwaggerDoc.json --output src/stirling/models/tool_models.py --io-output src/stirling/models/tool_io.py --check
|
||||
- uv run --locked --group engine --group engine-dev python scripts/generate_tool_models.py --spec ../SwaggerDoc.json --output src/stirling/models/tool_models.py --io-output src/stirling/models/tool_io.py --check
|
||||
|
||||
clean:
|
||||
desc: "Clean build artifacts"
|
||||
|
||||
+17
-13
@@ -45,6 +45,10 @@ vars:
|
||||
# which owns the version and caches the binary here.
|
||||
GITLEAKS_BIN: '.task/bin/gitleaks{{if eq OS "windows"}}.exe{{end}}'
|
||||
|
||||
env:
|
||||
# Keep repository-wide checks isolated from the engine runtime environment.
|
||||
UV_PROJECT_ENVIRONMENT: '.venv-pre-commit'
|
||||
|
||||
tasks:
|
||||
default:
|
||||
desc: "Check formatting, spelling, and secrets across the repo"
|
||||
@@ -76,19 +80,19 @@ tasks:
|
||||
desc: "Install the pinned pre-commit Python tools"
|
||||
run: once
|
||||
cmds:
|
||||
- uv sync --project scripts/pre-commit --locked
|
||||
- uv sync --project engine --locked --group pre-commit
|
||||
sources:
|
||||
- scripts/pre-commit/uv.lock
|
||||
- scripts/pre-commit/pyproject.toml
|
||||
- engine/uv.lock
|
||||
- engine/pyproject.toml
|
||||
status:
|
||||
- test -d scripts/pre-commit/.venv
|
||||
- test -d engine/.venv-pre-commit
|
||||
|
||||
clean:
|
||||
desc: "Remove the cached gitleaks binary and the tool virtualenv"
|
||||
desc: "Remove the cached gitleaks binary and the pre-commit virtualenv"
|
||||
cmds:
|
||||
- cmd: rm -rf scripts/pre-commit/.venv .task/bin/gitleaks
|
||||
- cmd: rm -rf engine/.venv-pre-commit .task/bin/gitleaks
|
||||
platforms: [linux, darwin]
|
||||
- cmd: cmd /c "rmdir /s /q scripts\pre-commit\.venv & del /q .task\bin\gitleaks.exe"
|
||||
- cmd: cmd /c "rmdir /s /q engine\.venv-pre-commit & del /q .task\bin\gitleaks.exe"
|
||||
platforms: [windows]
|
||||
ignore_error: true
|
||||
|
||||
@@ -97,26 +101,26 @@ tasks:
|
||||
ruff:
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --project scripts/pre-commit --no-sync ruff check --line-length=127 {{if .FIX}}--fix {{end}}$(git ls-files {{.PY_FILES}})
|
||||
- uv run --project engine --locked --group pre-commit ruff check --isolated --line-length=120 {{if .FIX}}--fix {{end}}$(git ls-files {{.PY_FILES}})
|
||||
|
||||
ruff-format:
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --project scripts/pre-commit --no-sync ruff format {{if .FIX}}{{else}}--check {{end}}$(git ls-files {{.PY_FILES}})
|
||||
- uv run --project engine --locked --group pre-commit ruff format --isolated --line-length=120 {{if .FIX}}{{else}}--check {{end}}$(git ls-files {{.PY_FILES}})
|
||||
|
||||
codespell:
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --project scripts/pre-commit --no-sync codespell --ignore-words-list=thirdParty,tabEl,tabEls,Sie,ist,fulfilment --quiet-level=2 $(git ls-files {{.SPELL_FILES}})
|
||||
- uv run --project engine --locked --group pre-commit codespell --ignore-words-list=thirdParty,tabEl,tabEls,Sie,ist,fulfilment --quiet-level=2 $(git ls-files {{.SPELL_FILES}})
|
||||
|
||||
toml-sort:
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --project scripts/pre-commit --no-sync python scripts/pre-commit/sort_locale_toml.py {{if .FIX}}--fix {{end}}{{.LOCALE_TOML}}
|
||||
- uv run --project engine --locked --group pre-commit python scripts/pre-commit/sort_locale_toml.py {{if .FIX}}--fix {{end}}{{.LOCALE_TOML}}
|
||||
|
||||
whitespace:
|
||||
cmds:
|
||||
- uv run --no-project python scripts/pre-commit/whitespace.py {{if .FIX}}--fix {{end}}{{.WS_FILES}}
|
||||
- uv run --project engine --locked --group pre-commit python scripts/pre-commit/whitespace.py {{if .FIX}}--fix {{end}}{{.WS_FILES}}
|
||||
|
||||
gitleaks:
|
||||
deps: [gitleaks-bin]
|
||||
@@ -130,4 +134,4 @@ tasks:
|
||||
internal: true
|
||||
desc: "Ensure the pinned, checksum-verified gitleaks binary is cached in .task/bin"
|
||||
cmds:
|
||||
- uv run --no-project python scripts/pre-commit/install_gitleaks.py
|
||||
- uv run --project engine --locked --group pre-commit python scripts/pre-commit/install_gitleaks.py
|
||||
|
||||
Vendored
+1
-2
@@ -2,8 +2,6 @@
|
||||
"recommendations": [
|
||||
"elagil.pre-commit-helper", // Support for pre-commit hooks to enforce code quality
|
||||
"josevseb.google-java-format-for-vs-code", // Google Java code formatter to follow the Google Java Style Guide
|
||||
"ms-python.black-formatter", // Python code formatter using Black
|
||||
"ms-python.flake8", // Flake8 linter for Python to enforce code quality
|
||||
"ms-python.python", // Official Microsoft Python extension with IntelliSense, debugging, and Jupyter support
|
||||
"ms-vscode-remote.vscode-remote-extensionpack", // Remote Development Pack for SSH, WSL, and Containers
|
||||
// "Oracle.oracle-java", // Oracle Java extension with additional features for Java development
|
||||
@@ -19,6 +17,7 @@
|
||||
"yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing
|
||||
"stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting
|
||||
"redhat.vscode-yaml", // YAML extension for Visual Studio Code
|
||||
"tamasfe.even-better-toml", // TOML language support and formatting
|
||||
"oxc.oxc-vscode", // Oxc (oxlint) extension for JavaScript/TypeScript linting
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+2
-1
@@ -20,8 +20,9 @@
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"ruff.configuration": "${workspaceFolder}/engine/pyproject.toml",
|
||||
"[gradle-kotlin-dsl]": {
|
||||
"editor.defaultFormatter": "vscjava.vscode-gradle"
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ To adjust the DPI resolution for rendering PDF pages:
|
||||
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from pdf2image import convert_from_path
|
||||
from PIL import Image
|
||||
|
||||
@@ -149,9 +150,7 @@ def main(pdf_image_path, output_dir, dpi=300, single_images_flag=False):
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Convert a PDF file to WebP images.")
|
||||
parser.add_argument("pdf_path", help="The path to the input PDF file.")
|
||||
parser.add_argument(
|
||||
"output_dir", help="The directory where the WebP images should be saved."
|
||||
)
|
||||
parser.add_argument("output_dir", help="The directory where the WebP images should be saved.")
|
||||
parser.add_argument(
|
||||
"--dpi",
|
||||
type=int,
|
||||
|
||||
+4
-6
@@ -12,14 +12,12 @@ RUN apt-get update \
|
||||
|
||||
# Source under /app/engine/ to match root Taskfile's `includes.engine.dir: engine`.
|
||||
WORKDIR /app/engine
|
||||
|
||||
COPY pyproject.toml uv.lock .env ./
|
||||
COPY scripts/ ./scripts/
|
||||
COPY engine/pyproject.toml engine/uv.lock engine/.env ./
|
||||
COPY engine/scripts/ ./scripts/
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-dev
|
||||
|
||||
COPY src/ ./src/
|
||||
uv sync --frozen --no-dev --group engine
|
||||
|
||||
COPY engine/src/ ./src/
|
||||
WORKDIR /app
|
||||
COPY Taskfile.yml ./
|
||||
COPY .taskfiles/ ./.taskfiles/
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim@sha256:531f855bda2c73cd6ef67d56b733b357cea384185b3022bd09f05e002cd144ca
|
||||
WORKDIR /app
|
||||
|
||||
COPY pyproject.toml uv.lock ./
|
||||
COPY engine/pyproject.toml engine/uv.lock ./engine/
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen
|
||||
cd engine && uv sync --frozen --group engine --group engine-dev
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
ENV PATH="/app/engine/.venv/bin:$PATH"
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
EXPOSE 5001
|
||||
|
||||
CMD ["uv", "run", "uvicorn", "stirling.api.app:app", "--host", "0.0.0.0", "--port", "5001", "--reload"]
|
||||
CMD ["uv", "run", "--project", "/app/engine", "--frozen", "--group", "engine", "uvicorn", "stirling.api.app:app", "--host", "0.0.0.0", "--port", "5001", "--reload"]
|
||||
|
||||
+74
-32
@@ -1,33 +1,72 @@
|
||||
[project]
|
||||
name = "engine"
|
||||
version = "0.1.0"
|
||||
description = "AI Document Engine"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"cryptography>=50.0.0",
|
||||
"fastapi>=0.116.0",
|
||||
"pgvector>=0.3.6",
|
||||
"psycopg[binary,pool]>=3.2",
|
||||
"pydantic>=2.0.0",
|
||||
# <2 cap: 1.99.0 patches CVE-2026-46678; 2.0 is an untested major migration
|
||||
"pydantic-ai>=1.99.0,<2.0.0",
|
||||
"pydantic-ai-slim[voyageai]>=1.99.0,<2.0.0",
|
||||
"pydantic-settings>=2.0.0",
|
||||
"python-dotenv>=1.2.1",
|
||||
"sqlite-vec>=0.1.6",
|
||||
"uvicorn>=0.35.0",
|
||||
"opentelemetry-sdk>=1.39.0",
|
||||
"posthog>=3.0.0",
|
||||
]
|
||||
description = "Stirling PDF Python projects"
|
||||
requires-python = ">=3.13,<3.14"
|
||||
dependencies = []
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"anyio>=4.0.0",
|
||||
"datamodel-code-generator[ruff]>=0.26.0",
|
||||
"pytest>=8.0.0",
|
||||
"pyright>=1.1.408",
|
||||
"referencing>=0.35.0",
|
||||
"ruff>=0.14.10",
|
||||
# Runtime dependencies for the AI document engine service.
|
||||
engine = [
|
||||
"cryptography>=50.0.0",
|
||||
"fastapi>=0.141.1",
|
||||
"opentelemetry-sdk>=1.39.1",
|
||||
"pgvector>=0.5.0",
|
||||
"posthog>=7.38.3",
|
||||
"psycopg[binary,pool]>=3.3.4",
|
||||
"pydantic>=2.13.4",
|
||||
# <2 cap: 1.99.0 patches CVE-2026-46678; 2.0 is an untested major migration.
|
||||
"pydantic-ai>=1.107.2,<2.0.0",
|
||||
"pydantic-ai-slim[voyageai]>=1.107.2,<2.0.0",
|
||||
"pydantic-settings>=2.15.0",
|
||||
"python-dotenv>=1.2.2",
|
||||
"sqlite-vec>=0.1.9",
|
||||
"uvicorn>=0.52.1",
|
||||
]
|
||||
# Type checking, testing, model generation, and formatting tools for the engine.
|
||||
engine-dev = [
|
||||
"anyio>=4.14.2",
|
||||
"datamodel-code-generator[ruff]==0.64.0",
|
||||
"pyright>=1.1.411",
|
||||
"pytest>=9.1.1",
|
||||
"referencing>=0.37.0",
|
||||
"ruff==0.15.5",
|
||||
]
|
||||
# Dependencies for the Cucumber/Python integration test suite.
|
||||
cucumber = [
|
||||
"behave>=1.3.3",
|
||||
"behave-html-formatter>=0.9.10",
|
||||
"opencv-python-headless>=5.0.0.93",
|
||||
"pdf2image>=1.17.0",
|
||||
"pillow>=12.3.0",
|
||||
"pypdf[crypto]>=6.15.0",
|
||||
"qrcode[pil]>=8.2",
|
||||
"reportlab>=5.0.0",
|
||||
"requests>=2.34.2",
|
||||
]
|
||||
# Shared Python utilities used by repository scripts and CI workflows.
|
||||
tools = [
|
||||
"deep-translator>=1.11.4",
|
||||
"defusedxml>=0.7.1",
|
||||
"fonttools>=4.63.0",
|
||||
"fpdf2>=2.8.7",
|
||||
"openai>=2.53.0",
|
||||
"requests>=2.34.2",
|
||||
"tomli-w>=1.2.0",
|
||||
"tomlkit>=0.15.1",
|
||||
"urllib3>=2.7.0",
|
||||
"unoserver>=3.7",
|
||||
"weasyprint>=69.0",
|
||||
]
|
||||
# Release-signature verification dependencies.
|
||||
updater-signatures = [
|
||||
"cryptography>=50.0.0",
|
||||
]
|
||||
# Pinned repository-wide pre-commit tooling.
|
||||
pre-commit = [
|
||||
"codespell==2.4.2",
|
||||
"ruff==0.15.5",
|
||||
"tomli-w==1.2.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
@@ -36,9 +75,10 @@ build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src"]
|
||||
exclude = [
|
||||
"tests",
|
||||
]
|
||||
exclude = ["tests"]
|
||||
|
||||
[tool.uv]
|
||||
default-groups = []
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
@@ -53,14 +93,16 @@ select = [
|
||||
"W",
|
||||
"RUF100",
|
||||
"UP",
|
||||
"PYI", # flake8-pyi: flags deprecated typing constructs
|
||||
"FA", # flake8-future-annotations: flags missing future annotations imports
|
||||
"BLE", # flake8-blind-except: flags bare `except Exception`
|
||||
"PYI", # flake8-pyi: flags deprecated typing constructs
|
||||
"FA", # flake8-future-annotations: flags missing future annotations imports
|
||||
"BLE", # flake8-blind-except: flags bare `except Exception`
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["stirling", "tests"]
|
||||
|
||||
[tool.pyright]
|
||||
pythonVersion = "3.13"
|
||||
|
||||
reportImportCycles = "warning"
|
||||
reportUnnecessaryCast = "warning"
|
||||
reportUnnecessaryTypeIgnoreComment = "warning"
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import AsyncIterator
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, assert_never
|
||||
@@ -133,7 +133,7 @@ class ConcurrencyLimitedModel(WrapperModel):
|
||||
model_settings: ModelSettings | None,
|
||||
model_request_parameters: ModelRequestParameters,
|
||||
run_context: RunContext[Any] | None = None,
|
||||
) -> AsyncIterator[StreamedResponse]:
|
||||
) -> AsyncGenerator[StreamedResponse]:
|
||||
async with self._semaphore:
|
||||
async with super().request_stream(
|
||||
messages, model_settings, model_request_parameters, run_context
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
from collections.abc import Callable, Iterator
|
||||
from collections.abc import Callable, Generator
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
@@ -31,7 +31,7 @@ def _client(
|
||||
settings_factory: Callable[[], AppSettings],
|
||||
*,
|
||||
client_addr: tuple[str, int] = ("127.0.0.1", 12345),
|
||||
) -> Iterator[TestClient]:
|
||||
) -> Generator[TestClient]:
|
||||
"""Enter a TestClient whose lifespan builds app.state from ``settings_factory``."""
|
||||
previous = app.dependency_overrides.get(load_settings)
|
||||
app.dependency_overrides[load_settings] = settings_factory
|
||||
|
||||
Generated
+1099
-1348
File diff suppressed because it is too large
Load Diff
+12
-22
@@ -15,9 +15,10 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Iterable, List, Tuple
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def human_bytes(value: float) -> str:
|
||||
@@ -49,7 +50,7 @@ class FontBreakdown:
|
||||
web_program_bytes: int = 0
|
||||
pdf_program_bytes: int = 0
|
||||
metadata_bytes: int = 0
|
||||
sample_cos_ids: List[Tuple[str | None, str | None]] = None
|
||||
sample_cos_ids: list[tuple[str | None, str | None]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -82,7 +83,7 @@ def approx_struct_size(obj: Any) -> int:
|
||||
return len(json.dumps(obj, separators=(",", ":")))
|
||||
|
||||
|
||||
def analyze_fonts(fonts: Iterable[Dict[str, Any]]) -> FontBreakdown:
|
||||
def analyze_fonts(fonts: Iterable[dict[str, Any]]) -> FontBreakdown:
|
||||
total = 0
|
||||
with_cos = 0
|
||||
with_prog = 0
|
||||
@@ -92,7 +93,7 @@ def analyze_fonts(fonts: Iterable[Dict[str, Any]]) -> FontBreakdown:
|
||||
web_program_bytes = 0
|
||||
pdf_program_bytes = 0
|
||||
metadata_bytes = 0
|
||||
sample_cos_ids: List[Tuple[str | None, str | None]] = []
|
||||
sample_cos_ids: list[tuple[str | None, str | None]] = []
|
||||
|
||||
for font in fonts:
|
||||
total += 1
|
||||
@@ -105,11 +106,7 @@ def analyze_fonts(fonts: Iterable[Dict[str, Any]]) -> FontBreakdown:
|
||||
sample_cos_ids.append((font_id, uid))
|
||||
|
||||
metadata_bytes += approx_struct_size(
|
||||
{
|
||||
k: v
|
||||
for k, v in font.items()
|
||||
if k not in {"program", "webProgram", "pdfProgram"}
|
||||
}
|
||||
{k: v for k, v in font.items() if k not in {"program", "webProgram", "pdfProgram"}}
|
||||
)
|
||||
|
||||
program = font.get("program")
|
||||
@@ -140,7 +137,7 @@ def analyze_fonts(fonts: Iterable[Dict[str, Any]]) -> FontBreakdown:
|
||||
)
|
||||
|
||||
|
||||
def analyze_pages(pages: Iterable[Dict[str, Any]]) -> PageBreakdown:
|
||||
def analyze_pages(pages: Iterable[dict[str, Any]]) -> PageBreakdown:
|
||||
page_count = 0
|
||||
total_text = 0
|
||||
total_images = 0
|
||||
@@ -185,7 +182,7 @@ def analyze_pages(pages: Iterable[Dict[str, Any]]) -> PageBreakdown:
|
||||
)
|
||||
|
||||
|
||||
def analyze_document(document: Dict[str, Any], total_size: int) -> DocumentBreakdown:
|
||||
def analyze_document(document: dict[str, Any], total_size: int) -> DocumentBreakdown:
|
||||
fonts = document.get("fonts") or []
|
||||
pages = document.get("pages") or []
|
||||
metadata = document.get("metadata") or {}
|
||||
@@ -259,17 +256,10 @@ def main() -> None:
|
||||
print(f" XMP metadata bytes: {human_bytes(summary.xmp_bytes)}")
|
||||
print(f" Form fields bytes: {human_bytes(summary.form_fields_bytes)}")
|
||||
print(f" Lazy flag bytes: {summary.lazy_flag_bytes}")
|
||||
print(
|
||||
f" Text payload characters (not counting JSON overhead): "
|
||||
f"{page_stats.text_payload_chars:,}"
|
||||
)
|
||||
print(f" Text payload characters (not counting JSON overhead): {page_stats.text_payload_chars:,}")
|
||||
print(f" Approx text structure bytes: {human_bytes(page_stats.text_struct_bytes)}")
|
||||
print(
|
||||
f" Approx image structure bytes: {human_bytes(page_stats.image_struct_bytes)}"
|
||||
)
|
||||
print(
|
||||
f" Approx content stream bytes: {human_bytes(page_stats.content_stream_bytes)}"
|
||||
)
|
||||
print(f" Approx image structure bytes: {human_bytes(page_stats.image_struct_bytes)}")
|
||||
print(f" Approx content stream bytes: {human_bytes(page_stats.content_stream_bytes)}")
|
||||
print(f" Approx annotations bytes: {human_bytes(page_stats.annotations_bytes)}")
|
||||
|
||||
|
||||
|
||||
@@ -4,12 +4,13 @@ Wrap raw CFF/Type1C data (extracted from PDFs) as OpenType-CFF for web compatibi
|
||||
Builds proper Unicode cmap from PDF ToUnicode data.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from io import BytesIO
|
||||
from fontTools.ttLib import TTFont, newTable
|
||||
from pathlib import Path
|
||||
|
||||
from fontTools.cffLib import CFFFontSet
|
||||
from fontTools.ttLib import TTFont, newTable
|
||||
from fontTools.ttLib.tables._c_m_a_p import cmap_format_4, cmap_format_12
|
||||
from fontTools.ttLib.tables._n_a_m_e import NameRecord
|
||||
from fontTools.ttLib.tables.O_S_2f_2 import Panose
|
||||
@@ -117,15 +118,11 @@ def wrap_cff_as_otf(input_path, output_path, tounicode_path=None):
|
||||
|
||||
# Get glyph names
|
||||
if hasattr(cff_font, "charset") and cff_font.charset is not None:
|
||||
glyph_order = [".notdef"] + [
|
||||
name for name in cff_font.charset if name != ".notdef"
|
||||
]
|
||||
glyph_order = [".notdef"] + [name for name in cff_font.charset if name != ".notdef"]
|
||||
else:
|
||||
# Fallback to CharStrings keys
|
||||
charstrings = cff_font.CharStrings
|
||||
glyph_order = [".notdef"] + [
|
||||
name for name in charstrings.keys() if name != ".notdef"
|
||||
]
|
||||
glyph_order = [".notdef"] + [name for name in charstrings.keys() if name != ".notdef"]
|
||||
|
||||
otf.setGlyphOrder(glyph_order)
|
||||
|
||||
@@ -139,9 +136,7 @@ def wrap_cff_as_otf(input_path, output_path, tounicode_path=None):
|
||||
|
||||
# Get defaults from CFF Private dict
|
||||
private_dict = getattr(cff_font, "Private", None)
|
||||
default_width = (
|
||||
getattr(private_dict, "defaultWidthX", 500) if private_dict else 500
|
||||
)
|
||||
default_width = getattr(private_dict, "defaultWidthX", 500) if private_dict else 500
|
||||
|
||||
# Calculate bounding box, widths, and LSBs
|
||||
x_min = 0
|
||||
@@ -280,9 +275,7 @@ def wrap_cff_as_otf(input_path, output_path, tounicode_path=None):
|
||||
|
||||
# For CID fonts: glyph names are "cid00123" (5-digit zero-padded)
|
||||
# For non-CID fonts: glyph names vary but GID == array index
|
||||
is_cid_font = any(
|
||||
gn.startswith("cid") for gn in glyph_order[1:6]
|
||||
) # Check first few non-.notdef glyphs
|
||||
is_cid_font = any(gn.startswith("cid") for gn in glyph_order[1:6]) # Check first few non-.notdef glyphs
|
||||
|
||||
for gid, unicode_val in gid_to_unicode.items():
|
||||
if unicode_val > 0:
|
||||
@@ -355,14 +348,10 @@ def wrap_cff_as_otf(input_path, output_path, tounicode_path=None):
|
||||
cmap4_mac.cmap = {cp: gn for cp, gn in unicode_to_glyph.items() if cp <= 0xFFFF}
|
||||
cmap_tables.append(cmap4_mac)
|
||||
|
||||
cmap.tables = [t for t in cmap_tables if t.cmap] or [
|
||||
cmap4_win
|
||||
] # Ensure at least one
|
||||
cmap.tables = [t for t in cmap_tables if t.cmap] or [cmap4_win] # Ensure at least one
|
||||
otf["cmap"] = cmap
|
||||
|
||||
print(
|
||||
f"Built cmap with {len(unicode_to_glyph)} Unicode mappings", file=sys.stderr
|
||||
)
|
||||
print(f"Built cmap with {len(unicode_to_glyph)} Unicode mappings", file=sys.stderr)
|
||||
|
||||
# === Create OS/2 table with correct metrics ===
|
||||
os2 = newTable("OS/2")
|
||||
@@ -515,9 +504,7 @@ Examples:
|
||||
# Add named arguments
|
||||
parser.add_argument("--input", dest="input_file", help="Input CFF file path")
|
||||
parser.add_argument("--output", dest="output_file", help="Output OTF file path")
|
||||
parser.add_argument(
|
||||
"--to-unicode", dest="tounicode_file", help="ToUnicode mapping file path"
|
||||
)
|
||||
parser.add_argument("--to-unicode", dest="tounicode_file", help="ToUnicode mapping file path")
|
||||
|
||||
# Add positional arguments for backward compatibility
|
||||
parser.add_argument("input_pos", nargs="?", help="Input CFF file (positional)")
|
||||
|
||||
@@ -50,16 +50,13 @@ import glob
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from typing import Iterable
|
||||
from collections.abc import Iterable, Mapping
|
||||
|
||||
# Ensure tomlkit is installed before importing
|
||||
try:
|
||||
import tomlkit
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"The 'tomlkit' library is not installed. Please install it using 'pip install tomlkit'."
|
||||
)
|
||||
raise ImportError("The 'tomlkit' library is not installed. Please install it using 'pip install tomlkit'.")
|
||||
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
@@ -242,15 +239,11 @@ def compare_files(
|
||||
"ignore" not in sort_ignore_translation[language]
|
||||
or len(sort_ignore_translation[language].get("ignore", [])) < 1
|
||||
):
|
||||
sort_ignore_translation[language]["ignore"] = tomlkit.array(
|
||||
["language.direction"]
|
||||
)
|
||||
sort_ignore_translation[language]["ignore"] = tomlkit.array(["language.direction"])
|
||||
|
||||
# Clean up ignore list to only include keys present in reference
|
||||
sort_ignore_translation[language]["ignore"] = [
|
||||
key
|
||||
for key in sort_ignore_translation[language]["ignore"]
|
||||
if key in ref_keys or key == "language.direction"
|
||||
key for key in sort_ignore_translation[language]["ignore"] if key in ref_keys or key == "language.direction"
|
||||
]
|
||||
|
||||
translation_entries = load_translation_entries(file_path)
|
||||
@@ -264,10 +257,7 @@ def compare_files(
|
||||
continue
|
||||
|
||||
file_value = translation_entries[default_key]
|
||||
if (
|
||||
default_value == file_value
|
||||
and default_key not in sort_ignore_translation[language]["ignore"]
|
||||
):
|
||||
if default_value == file_value and default_key not in sort_ignore_translation[language]["ignore"]:
|
||||
# Missing translation (same as default and not ignored)
|
||||
fails += 1
|
||||
missing_str_keys.append(default_key)
|
||||
@@ -357,9 +347,7 @@ def main() -> None:
|
||||
lang_file = lang_input
|
||||
else:
|
||||
candidate = os.path.join(locales_dir, lang_input)
|
||||
candidate_with_file = os.path.join(
|
||||
locales_dir, lang_input, "translation.toml"
|
||||
)
|
||||
candidate_with_file = os.path.join(locales_dir, lang_input, "translation.toml")
|
||||
if os.path.exists(candidate):
|
||||
if os.path.isdir(candidate):
|
||||
lang_file = candidate_with_file
|
||||
|
||||
@@ -46,14 +46,14 @@ import json
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from defusedxml.ElementTree import ParseError as _XMLParseError
|
||||
|
||||
# defusedxml hardens the parser against XXE / billion-laughs / entity-
|
||||
# expansion attacks. JaCoCo XML on a CI runner is trusted input today,
|
||||
# but using the hardened parser is a one-line change and silences
|
||||
# scanners that pattern-match on `xml.etree.ElementTree.parse`.
|
||||
from defusedxml.ElementTree import parse as _xml_parse
|
||||
from defusedxml.ElementTree import ParseError as _XMLParseError
|
||||
|
||||
AREAS = ("core", "proprietary", "saas", "desktop")
|
||||
|
||||
@@ -69,7 +69,7 @@ class Bucket:
|
||||
def pct(self) -> float:
|
||||
return 100.0 * self.covered / self.total if self.total else 0.0
|
||||
|
||||
def add(self, other: "Bucket") -> None:
|
||||
def add(self, other: Bucket) -> None:
|
||||
self.covered += other.covered
|
||||
self.total += other.total
|
||||
|
||||
@@ -78,9 +78,7 @@ class Bucket:
|
||||
class RowBuckets:
|
||||
"""Per-area buckets for one row of the matrix."""
|
||||
|
||||
by_area: dict[str, Bucket] = field(
|
||||
default_factory=lambda: {a: Bucket() for a in AREAS}
|
||||
)
|
||||
by_area: dict[str, Bucket] = field(default_factory=lambda: {a: Bucket() for a in AREAS})
|
||||
# Some inputs (Playwright V8) don't have source-path info, so they
|
||||
# only contribute to ALL without an area attribution. Track those
|
||||
# separately so per-area cells stay honest.
|
||||
@@ -94,7 +92,7 @@ class RowBuckets:
|
||||
agg.add(self.unattributed)
|
||||
return agg
|
||||
|
||||
def merge(self, other: "RowBuckets") -> None:
|
||||
def merge(self, other: RowBuckets) -> None:
|
||||
for area in AREAS:
|
||||
self.by_area[area].add(other.by_area[area])
|
||||
self.unattributed.add(other.unattributed)
|
||||
@@ -103,7 +101,7 @@ class RowBuckets:
|
||||
# --------------------------------------------------------------------- jacoco
|
||||
|
||||
|
||||
def _classify_backend(package_name: str) -> Optional[str]:
|
||||
def _classify_backend(package_name: str) -> str | None:
|
||||
"""Map a JaCoCo package name to an area, or None to skip."""
|
||||
if not package_name:
|
||||
return None
|
||||
@@ -152,7 +150,7 @@ def parse_jacoco_methods(path: Path) -> RowBuckets:
|
||||
# --------------------------------------------------------------- vitest (frontend)
|
||||
|
||||
|
||||
def _classify_frontend(file_path: str) -> Optional[str]:
|
||||
def _classify_frontend(file_path: str) -> str | None:
|
||||
"""Map a vitest per-file path (anything containing src/<area>/) to an area."""
|
||||
if not file_path:
|
||||
return None
|
||||
@@ -170,9 +168,7 @@ def parse_vitest_per_file(path: Path) -> RowBuckets:
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
print(
|
||||
f"::warning::Failed to parse vitest summary {path}: {exc}", file=sys.stderr
|
||||
)
|
||||
print(f"::warning::Failed to parse vitest summary {path}: {exc}", file=sys.stderr)
|
||||
return row
|
||||
for file_path, metrics in data.items():
|
||||
if file_path == "total":
|
||||
@@ -267,11 +263,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
# Build each row from its source(s).
|
||||
fe_e2e = (
|
||||
parse_playwright_frontend_total(args.playwright_frontend)
|
||||
if args.playwright_frontend
|
||||
else RowBuckets()
|
||||
)
|
||||
fe_e2e = parse_playwright_frontend_total(args.playwright_frontend) if args.playwright_frontend else RowBuckets()
|
||||
fe_unit = parse_vitest_per_file(args.vitest) if args.vitest else RowBuckets()
|
||||
fe_all = RowBuckets()
|
||||
fe_all.merge(fe_unit)
|
||||
|
||||
@@ -21,16 +21,17 @@ import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
from defusedxml.ElementTree import ParseError as _XMLParseError
|
||||
|
||||
# `defusedxml` swaps out the stdlib expat parser for one that rejects the
|
||||
# usual XML attack vectors (XXE / billion laughs / entity expansion). Even
|
||||
# though JaCoCo XML on a CI runner is trusted input, swapping the parser is
|
||||
# a one-line change that silences security scanners and costs nothing.
|
||||
from defusedxml.ElementTree import parse as _xml_parse
|
||||
from defusedxml.ElementTree import ParseError as _XMLParseError
|
||||
|
||||
JACOCO_COUNTERS = ("LINE", "BRANCH", "METHOD", "CLASS", "INSTRUCTION", "COMPLEXITY")
|
||||
|
||||
@@ -48,7 +49,7 @@ class CounterTotals:
|
||||
def pct(self) -> float:
|
||||
return 100.0 * self.covered / self.total if self.total else 0.0
|
||||
|
||||
def add(self, other: "CounterTotals") -> None:
|
||||
def add(self, other: CounterTotals) -> None:
|
||||
self.covered += other.covered
|
||||
self.missed += other.missed
|
||||
|
||||
@@ -108,9 +109,7 @@ def render_jacoco(reports: Iterable[tuple[str, Path]]) -> str:
|
||||
return body
|
||||
|
||||
lines: list[str] = []
|
||||
lines.append(
|
||||
"| Metric | " + " | ".join(label for label, _ in rows) + " | **Aggregate** |"
|
||||
)
|
||||
lines.append("| Metric | " + " | ".join(label for label, _ in rows) + " | **Aggregate** |")
|
||||
lines.append("|---" * (len(rows) + 2) + "|")
|
||||
|
||||
for t in ("LINE", "BRANCH", "METHOD", "CLASS"):
|
||||
|
||||
@@ -27,7 +27,6 @@ import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Set, Tuple
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
import requests
|
||||
@@ -71,9 +70,9 @@ def parse_args() -> argparse.Namespace:
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_urls(args: argparse.Namespace) -> List[str]:
|
||||
urls: List[str] = []
|
||||
seen: Set[str] = set()
|
||||
def load_urls(args: argparse.Namespace) -> list[str]:
|
||||
urls: list[str] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
def add(url: str) -> None:
|
||||
clean = url.strip()
|
||||
@@ -125,7 +124,7 @@ def download_pdf(
|
||||
output_dir: Path,
|
||||
timeout: int,
|
||||
overwrite: bool,
|
||||
) -> Tuple[str, Optional[Path], Optional[str]]:
|
||||
) -> tuple[str, Path | None, str | None]:
|
||||
try:
|
||||
dest = build_filename(url, output_dir)
|
||||
if dest.exists() and not overwrite:
|
||||
@@ -161,20 +160,15 @@ def main() -> None:
|
||||
output_dir = Path(args.output_dir).resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(
|
||||
f"Downloading {len(urls)} PDFs to {output_dir} using {args.workers} workers..."
|
||||
)
|
||||
print(f"Downloading {len(urls)} PDFs to {output_dir} using {args.workers} workers...")
|
||||
|
||||
successes = 0
|
||||
skipped = 0
|
||||
failures: List[Tuple[str, str]] = []
|
||||
failures: list[tuple[str, str]] = []
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=args.workers) as executor:
|
||||
future_to_url = {
|
||||
executor.submit(
|
||||
download_pdf, url, output_dir, args.timeout, args.overwrite
|
||||
): url
|
||||
for url in urls
|
||||
executor.submit(download_pdf, url, output_dir, args.timeout, args.overwrite): url for url in urls
|
||||
}
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
url = future_to_url[future]
|
||||
@@ -190,9 +184,7 @@ def main() -> None:
|
||||
print(f"[OK] {url} -> {path}")
|
||||
|
||||
print()
|
||||
print(
|
||||
f"Completed. Success: {successes}, Skipped: {skipped}, Failures: {len(failures)}"
|
||||
)
|
||||
print(f"Completed. Success: {successes}, Skipped: {skipped}, Failures: {len(failures)}")
|
||||
if failures:
|
||||
print("Failures:")
|
||||
for url, error in failures:
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
@echo off
|
||||
REM --------------------------------------------------
|
||||
REM Batch script to (re-)generate all requirements
|
||||
REM with check for pip-compile and user confirmation
|
||||
REM --------------------------------------------------
|
||||
|
||||
REM Check if pip-compile is available
|
||||
pip-compile --version >nul 2>&1
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo ERROR: pip-compile was not found.
|
||||
echo Please install pip-tools:
|
||||
echo pip install pip-tools
|
||||
echo and ensure that pip-compile is in your PATH.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo pip-compile detected.
|
||||
|
||||
REM Prompt user for confirmation (default = Yes on ENTER)
|
||||
set /p confirm="Do you want to generate all requirements? [Y/n] "
|
||||
if /I "%confirm%"=="" set confirm=Y
|
||||
|
||||
if /I not "%confirm%"=="Y" (
|
||||
echo Generation cancelled by user.
|
||||
pause
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
echo Starting generation...
|
||||
|
||||
echo Generating .github\scripts\requirements_dev.txt
|
||||
pip-compile --allow-unsafe --generate-hashes --upgrade --strip-extras ^
|
||||
--output-file=".github\scripts\requirements_dev.txt" ^
|
||||
".github\scripts\requirements_dev.in"
|
||||
|
||||
echo Generating .github\scripts\requirements_sync_readme.txt
|
||||
pip-compile --generate-hashes --upgrade --strip-extras ^
|
||||
--output-file=".github\scripts\requirements_sync_readme.txt" ^
|
||||
".github\scripts\requirements_sync_readme.in"
|
||||
|
||||
echo Generating testing\cucumber\requirements.txt
|
||||
pip-compile --generate-hashes --upgrade --strip-extras ^
|
||||
--output-file="testing\cucumber\requirements.txt" ^
|
||||
"testing\cucumber\requirements.in"
|
||||
|
||||
echo All done!
|
||||
pause
|
||||
@@ -27,16 +27,14 @@ import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Sequence, Tuple
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Bulk collect Type3 font signatures from PDFs."
|
||||
)
|
||||
parser = argparse.ArgumentParser(description="Bulk collect Type3 font signatures from PDFs.")
|
||||
parser.add_argument(
|
||||
"--input",
|
||||
nargs="+",
|
||||
@@ -72,8 +70,8 @@ def parse_args() -> argparse.Namespace:
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def discover_pdfs(paths: Sequence[str]) -> List[Path]:
|
||||
pdfs: List[Path] = []
|
||||
def discover_pdfs(paths: Sequence[str]) -> list[Path]:
|
||||
pdfs: list[Path] = []
|
||||
for raw in paths:
|
||||
path = Path(raw).resolve()
|
||||
if path.is_file():
|
||||
@@ -113,8 +111,8 @@ def load_signature_file(path: Path) -> dict:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
def collect_known_signatures(signatures_dir: Path) -> Dict[str, dict]:
|
||||
known: Dict[str, dict] = {}
|
||||
def collect_known_signatures(signatures_dir: Path) -> dict[str, dict]:
|
||||
known: dict[str, dict] = {}
|
||||
if not signatures_dir.exists():
|
||||
return known
|
||||
for json_file in signatures_dir.rglob("*.json"):
|
||||
@@ -139,9 +137,7 @@ def collect_known_signatures(signatures_dir: Path) -> Dict[str, dict]:
|
||||
return known
|
||||
|
||||
|
||||
def run_signature_tool(
|
||||
gradle_cmd: str, pdf: Path, output_path: Path, pretty: bool, cwd: Path
|
||||
) -> None:
|
||||
def run_signature_tool(gradle_cmd: str, pdf: Path, output_path: Path, pretty: bool, cwd: Path) -> None:
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
args = f"--pdf {shlex.quote(str(pdf))} --output {shlex.quote(str(output_path))}"
|
||||
if pretty:
|
||||
@@ -157,12 +153,10 @@ def run_signature_tool(
|
||||
text=True,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"Gradle Type3SignatureTool failed for {pdf}:\n{completed.stderr.strip()}"
|
||||
)
|
||||
raise RuntimeError(f"Gradle Type3SignatureTool failed for {pdf}:\n{completed.stderr.strip()}")
|
||||
|
||||
|
||||
def extract_fonts_from_payload(payload: dict) -> List[dict]:
|
||||
def extract_fonts_from_payload(payload: dict) -> list[dict]:
|
||||
pdf = payload.get("pdf")
|
||||
fonts = []
|
||||
for font in payload.get("fonts", []):
|
||||
@@ -182,7 +176,7 @@ def extract_fonts_from_payload(payload: dict) -> List[dict]:
|
||||
return fonts
|
||||
|
||||
|
||||
def write_report(report_path: Path, fonts_by_signature: Dict[str, dict]) -> None:
|
||||
def write_report(report_path: Path, fonts_by_signature: dict[str, dict]) -> None:
|
||||
ordered = sorted(fonts_by_signature.values(), key=lambda entry: entry["signature"])
|
||||
report = {
|
||||
"generatedAt": dt.datetime.utcnow().isoformat(timespec="seconds") + "Z",
|
||||
@@ -201,7 +195,7 @@ def main() -> None:
|
||||
pdfs = discover_pdfs(args.input)
|
||||
|
||||
known = collect_known_signatures(signatures_dir)
|
||||
newly_added: List[Tuple[str, str]] = []
|
||||
newly_added: list[tuple[str, str]] = []
|
||||
|
||||
for pdf in pdfs:
|
||||
signature_path = derive_signature_path(pdf, signatures_dir)
|
||||
@@ -209,15 +203,11 @@ def main() -> None:
|
||||
try:
|
||||
payload = load_signature_file(signature_path)
|
||||
except Exception as exc:
|
||||
print(
|
||||
f"[WARN] Failed to parse cached signature {signature_path}: {exc}"
|
||||
)
|
||||
print(f"[WARN] Failed to parse cached signature {signature_path}: {exc}")
|
||||
payload = None
|
||||
else:
|
||||
try:
|
||||
run_signature_tool(
|
||||
args.gradle_cmd, pdf, signature_path, args.pretty, REPO_ROOT
|
||||
)
|
||||
run_signature_tool(args.gradle_cmd, pdf, signature_path, args.pretty, REPO_ROOT)
|
||||
except Exception as exc:
|
||||
print(f"[ERROR] Harvest failed for {pdf}: {exc}", file=sys.stderr)
|
||||
continue
|
||||
|
||||
@@ -157,11 +157,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
stats = aggregate(args.dump_dir)
|
||||
write_vitest_summary(stats, args.out)
|
||||
pct = (
|
||||
100.0 * stats["functions_covered"] / stats["functions_total"]
|
||||
if stats["functions_total"]
|
||||
else 0.0
|
||||
)
|
||||
pct = 100.0 * stats["functions_covered"] / stats["functions_total"] if stats["functions_total"] else 0.0
|
||||
print(
|
||||
f"Aggregated {stats['tests']} tests / {stats['scripts']} scripts: "
|
||||
f"{stats['functions_covered']}/{stats['functions_total']} functions "
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Download the pinned gitleaks binary into .task/bin, verifying its checksum.
|
||||
|
||||
gitleaks is a Go binary with no PyPI package, so it can't be locked like the
|
||||
other tools (ruff/codespell/toml-sort live in scripts/pre-commit/pyproject.toml).
|
||||
other tools (ruff/codespell/toml-sort live in engine/pyproject.toml).
|
||||
This script is the single source of truth for the gitleaks version and the
|
||||
SHA-256 of each release asset. It is cross-platform (stdlib only) and idempotent:
|
||||
if the cached binary already reports the pinned version it does nothing, so
|
||||
@@ -40,9 +40,7 @@ BIN = REPO_ROOT / ".task" / "bin" / ("gitleaks.exe" if IS_WINDOWS else "gitleaks
|
||||
|
||||
|
||||
def platform_key() -> str:
|
||||
os_name = {"Linux": "linux", "Darwin": "darwin", "Windows": "windows"}.get(
|
||||
platform.system()
|
||||
)
|
||||
os_name = {"Linux": "linux", "Darwin": "darwin", "Windows": "windows"}.get(platform.system())
|
||||
arch = {
|
||||
"x86_64": "x64",
|
||||
"amd64": "x64",
|
||||
@@ -55,9 +53,7 @@ def platform_key() -> str:
|
||||
"armv6l": "armv6",
|
||||
}.get(platform.machine().lower())
|
||||
if not os_name or not arch:
|
||||
raise SystemExit(
|
||||
f"Unsupported platform for gitleaks: {platform.system()}/{platform.machine()}"
|
||||
)
|
||||
raise SystemExit(f"Unsupported platform for gitleaks: {platform.system()}/{platform.machine()}")
|
||||
return f"{os_name}_{arch}"
|
||||
|
||||
|
||||
@@ -65,9 +61,7 @@ def cached_version() -> str | None:
|
||||
if not BIN.exists():
|
||||
return None
|
||||
try:
|
||||
return subprocess.run(
|
||||
[str(BIN), "version"], capture_output=True, text=True
|
||||
).stdout.strip()
|
||||
return subprocess.run([str(BIN), "version"], capture_output=True, text=True).stdout.strip()
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
@@ -90,9 +84,7 @@ def main() -> int:
|
||||
archive, _ = urllib.request.urlretrieve(url)
|
||||
digest = hashlib.sha256(Path(archive).read_bytes()).hexdigest()
|
||||
if digest != expected:
|
||||
raise SystemExit(
|
||||
f"gitleaks checksum mismatch: expected {expected}, got {digest}"
|
||||
)
|
||||
raise SystemExit(f"gitleaks checksum mismatch: expected {expected}, got {digest}")
|
||||
|
||||
member = "gitleaks.exe" if IS_WINDOWS else "gitleaks"
|
||||
if suffix == "zip":
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# Pinned Python lint/format tools for `task pre-commit`. uv.lock locks these
|
||||
# plus their transitive dependencies by hash, so `uv run --project
|
||||
# scripts/pre-commit --locked <tool>` is reproducible and integrity-checked.
|
||||
# This is not a packaged project - it only exists to lock the tooling.
|
||||
[project]
|
||||
name = "stirling-precommit-tools"
|
||||
version = "0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"ruff==0.15.14",
|
||||
"codespell==2.4.2",
|
||||
"tomli-w==1.2.0",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
package = false
|
||||
@@ -54,13 +54,9 @@ def sort_file(path: str, fix: bool) -> bool:
|
||||
try:
|
||||
reordered = tomllib.loads(expected)
|
||||
except tomllib.TOMLDecodeError as exc:
|
||||
raise SortError(
|
||||
f"{path}: refusing to sort, the sorted output is not valid TOML: {exc}"
|
||||
) from exc
|
||||
raise SortError(f"{path}: refusing to sort, the sorted output is not valid TOML: {exc}") from exc
|
||||
if reordered != original:
|
||||
raise SortError(
|
||||
f"{path}: refusing to sort, sorting would change the file's contents"
|
||||
)
|
||||
raise SortError(f"{path}: refusing to sort, sorting would change the file's contents")
|
||||
|
||||
if fix:
|
||||
Path(path).write_text(expected, encoding="utf-8")
|
||||
|
||||
Generated
-63
@@ -1,63 +0,0 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "codespell"
|
||||
version = "2.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2d/9d/1d0903dff693160f893ca6abcabad545088e7a2ee0a6deae7c24e958be69/codespell-2.4.2.tar.gz", hash = "sha256:3c33be9ae34543807f088aeb4832dfad8cb2dae38da61cac0a7045dd376cfdf3", size = 352058, upload-time = "2026-03-05T18:10:42.936Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/a1/52fa05533e95fe45bcc09bcf8a503874b1c08f221a4e35608017e0938f55/codespell-2.4.2-py3-none-any.whl", hash = "sha256:97e0c1060cf46bd1d5db89a936c98db8c2b804e1fdd4b5c645e82a1ec6b1f886", size = 353715, upload-time = "2026-03-05T18:10:41.398Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stirling-precommit-tools"
|
||||
version = "0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "codespell" },
|
||||
{ name = "ruff" },
|
||||
{ name = "tomli-w" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "codespell", specifier = "==2.4.2" },
|
||||
{ name = "ruff", specifier = "==0.15.14" },
|
||||
{ name = "tomli-w", specifier = "==1.2.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli-w"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" },
|
||||
]
|
||||
@@ -40,7 +40,10 @@ def normalise(data: bytes) -> bytes:
|
||||
body = b"\n".join(lines)
|
||||
# Ensure a non-empty file ends with exactly one newline.
|
||||
stripped = body.rstrip(b"\r\n")
|
||||
return stripped + b"\n" if stripped else body
|
||||
if not stripped:
|
||||
return body
|
||||
newline = b"\r\n" if data.endswith(b"\r\n") else b"\n"
|
||||
return stripped + newline
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
||||
@@ -14,13 +14,10 @@ import argparse
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Summarize Type3 signature JSON dumps."
|
||||
)
|
||||
parser = argparse.ArgumentParser(description="Summarize Type3 signature JSON dumps.")
|
||||
parser.add_argument(
|
||||
"--input",
|
||||
default="docs/type3/signatures",
|
||||
@@ -34,8 +31,8 @@ def parse_args() -> argparse.Namespace:
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_signatures(directory: Path) -> Dict[str, List[dict]]:
|
||||
inventory: Dict[str, List[dict]] = defaultdict(list)
|
||||
def load_signatures(directory: Path) -> dict[str, list[dict]]:
|
||||
inventory: dict[str, list[dict]] = defaultdict(list)
|
||||
for path in sorted(directory.glob("*.json")):
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
@@ -55,15 +52,12 @@ def load_signatures(directory: Path) -> Dict[str, List[dict]]:
|
||||
return inventory
|
||||
|
||||
|
||||
def write_markdown(
|
||||
inventory: Dict[str, List[dict]], output: Path, input_dir: Path
|
||||
) -> None:
|
||||
lines: List[str] = []
|
||||
def write_markdown(inventory: dict[str, list[dict]], output: Path, input_dir: Path) -> None:
|
||||
lines: list[str] = []
|
||||
lines.append("# Type3 Signature Inventory")
|
||||
lines.append("")
|
||||
lines.append(
|
||||
f"_Generated from `{input_dir}`. "
|
||||
"Run `scripts/summarize_type3_signatures.py` after capturing new samples._"
|
||||
f"_Generated from `{input_dir}`. Run `scripts/summarize_type3_signatures.py` after capturing new samples._"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
@@ -76,9 +70,7 @@ def write_markdown(
|
||||
for entry in entries:
|
||||
signature = entry.get("signature") or "—"
|
||||
sample = Path(entry["source"]).name
|
||||
glyph_count = (
|
||||
entry.get("glyphCount") if entry.get("glyphCount") is not None else "—"
|
||||
)
|
||||
glyph_count = entry.get("glyphCount") if entry.get("glyphCount") is not None else "—"
|
||||
coverage = entry.get("glyphCoverage") or []
|
||||
preview = ", ".join(str(code) for code in coverage[:10])
|
||||
lines.append(f"| `{signature}` | `{sample}` | {glyph_count} | {preview} |")
|
||||
|
||||
@@ -382,9 +382,7 @@ def make_converter(mapping: dict[str, str]):
|
||||
return lambda text: (text, [])
|
||||
# Longest-first so multi-word/longer forms win; \b ensures whole words.
|
||||
pattern = re.compile(
|
||||
r"\b("
|
||||
+ "|".join(re.escape(w) for w in sorted(mapping, key=len, reverse=True))
|
||||
+ r")\b",
|
||||
r"\b(" + "|".join(re.escape(w) for w in sorted(mapping, key=len, reverse=True)) + r")\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
@@ -509,9 +507,7 @@ def parse_structured(
|
||||
continue
|
||||
kv = KV_RE.match(s)
|
||||
if kv:
|
||||
(top if section == "" else sections[section]).append(
|
||||
(kv.group(1), kv.group(2))
|
||||
)
|
||||
(top if section == "" else sections[section]).append((kv.group(1), kv.group(2)))
|
||||
return top, order, sections
|
||||
|
||||
|
||||
@@ -563,9 +559,7 @@ def sync_en_us(dry_run: bool) -> int:
|
||||
# en-US-only keys that belong to this (shared) section
|
||||
for k, v in us_sections.get(name, []):
|
||||
if k not in gb_section_keys:
|
||||
_insert_ci(
|
||||
merged, (k, uk_to_us_convert(v)[0]), lambda kv: kv[0].lower()
|
||||
)
|
||||
_insert_ci(merged, (k, uk_to_us_convert(v)[0]), lambda kv: kv[0].lower())
|
||||
out_sections.append((name, merged))
|
||||
|
||||
# en-US-only sections (absent from en-GB): insert by ci header order
|
||||
@@ -595,9 +589,7 @@ def sync_en_us(dry_run: bool) -> int:
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument(
|
||||
"--dry-run", action="store_true", help="report changes without writing"
|
||||
)
|
||||
ap.add_argument("--dry-run", action="store_true", help="report changes without writing")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not EN_US.exists() or not EN_GB.exists():
|
||||
|
||||
@@ -6,14 +6,15 @@ batch processing, quality checks, and integration helpers.
|
||||
TOML format only.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any
|
||||
import argparse
|
||||
import re
|
||||
from datetime import datetime
|
||||
import csv
|
||||
import json
|
||||
import re
|
||||
import tomllib
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import tomli_w
|
||||
|
||||
|
||||
@@ -22,7 +23,7 @@ class AITranslationHelper:
|
||||
self.locales_dir = Path(locales_dir)
|
||||
self.golden_truth_file = self.locales_dir / "en-US" / "translation.toml"
|
||||
|
||||
def _load_translation_file(self, file_path: Path) -> Dict:
|
||||
def _load_translation_file(self, file_path: Path) -> dict:
|
||||
"""Load TOML translation file."""
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
@@ -31,14 +32,14 @@ class AITranslationHelper:
|
||||
print(f"Error loading {file_path}: {e}")
|
||||
return {}
|
||||
|
||||
def _save_translation_file(self, data: Dict, file_path: Path) -> None:
|
||||
def _save_translation_file(self, data: dict, file_path: Path) -> None:
|
||||
"""Save TOML translation file."""
|
||||
with open(file_path, "wb") as f:
|
||||
tomli_w.dump(data, f)
|
||||
|
||||
def create_ai_batch_file(
|
||||
self,
|
||||
languages: List[str],
|
||||
languages: list[str],
|
||||
output_file: Path,
|
||||
max_entries_per_language: int = 50,
|
||||
) -> None:
|
||||
@@ -74,14 +75,9 @@ class AITranslationHelper:
|
||||
untranslated = self._find_untranslated_entries(golden_truth, lang_data)
|
||||
|
||||
# Limit entries if specified
|
||||
if (
|
||||
max_entries_per_language
|
||||
and len(untranslated) > max_entries_per_language
|
||||
):
|
||||
if max_entries_per_language and len(untranslated) > max_entries_per_language:
|
||||
# Prioritize by key importance
|
||||
untranslated = self._prioritize_translation_keys(
|
||||
untranslated, max_entries_per_language
|
||||
)
|
||||
untranslated = self._prioritize_translation_keys(untranslated, max_entries_per_language)
|
||||
|
||||
batch_data["translations"][lang] = {}
|
||||
for key, value in untranslated.items():
|
||||
@@ -94,15 +90,11 @@ class AITranslationHelper:
|
||||
# Always save batch files as JSON for compatibility
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
json.dump(batch_data, f, indent=2, ensure_ascii=False)
|
||||
total_entries = sum(
|
||||
len(lang_data) for lang_data in batch_data["translations"].values()
|
||||
)
|
||||
total_entries = sum(len(lang_data) for lang_data in batch_data["translations"].values())
|
||||
print(f"Created AI batch file: {output_file}")
|
||||
print(f"Total entries to translate: {total_entries}")
|
||||
|
||||
def _find_untranslated_entries(
|
||||
self, golden_truth: Dict, lang_data: Dict
|
||||
) -> Dict[str, str]:
|
||||
def _find_untranslated_entries(self, golden_truth: dict, lang_data: dict) -> dict[str, str]:
|
||||
"""Find entries that need translation."""
|
||||
golden_flat = self._flatten_dict(golden_truth)
|
||||
lang_flat = self._flatten_dict(lang_data)
|
||||
@@ -112,19 +104,14 @@ class AITranslationHelper:
|
||||
if (
|
||||
key not in lang_flat
|
||||
or lang_flat[key] == value
|
||||
or (
|
||||
isinstance(lang_flat[key], str)
|
||||
and lang_flat[key].startswith("[UNTRANSLATED]")
|
||||
)
|
||||
or (isinstance(lang_flat[key], str) and lang_flat[key].startswith("[UNTRANSLATED]"))
|
||||
):
|
||||
if not self._is_expected_identical(key, value):
|
||||
untranslated[key] = value
|
||||
|
||||
return untranslated
|
||||
|
||||
def _flatten_dict(
|
||||
self, d: Dict, parent_key: str = "", separator: str = "."
|
||||
) -> Dict[str, Any]:
|
||||
def _flatten_dict(self, d: dict, parent_key: str = "", separator: str = ".") -> dict[str, Any]:
|
||||
"""Flatten nested dictionary."""
|
||||
items = []
|
||||
for k, v in d.items():
|
||||
@@ -141,9 +128,7 @@ class AITranslationHelper:
|
||||
return True
|
||||
return "language.direction" in key.lower()
|
||||
|
||||
def _prioritize_translation_keys(
|
||||
self, untranslated: Dict[str, str], max_count: int
|
||||
) -> Dict[str, str]:
|
||||
def _prioritize_translation_keys(self, untranslated: dict[str, str], max_count: int) -> dict[str, str]:
|
||||
"""Prioritize which keys to translate first based on importance."""
|
||||
# Define priority order (higher score = higher priority)
|
||||
priority_patterns = [
|
||||
@@ -191,19 +176,17 @@ class AITranslationHelper:
|
||||
|
||||
if len(parts) > 0:
|
||||
main_section = parts[0]
|
||||
context = contexts.get(
|
||||
main_section, f"Part of {main_section} functionality"
|
||||
)
|
||||
context = contexts.get(main_section, f"Part of {main_section} functionality")
|
||||
if len(parts) > 1:
|
||||
context += f", specifically for {parts[-1]}"
|
||||
return context
|
||||
|
||||
return "General application text"
|
||||
|
||||
def validate_ai_translations(self, batch_file: Path) -> Dict[str, List[str]]:
|
||||
def validate_ai_translations(self, batch_file: Path) -> dict[str, list[str]]:
|
||||
"""Validate AI translations for common issues."""
|
||||
# Batch files are always JSON
|
||||
with open(batch_file, "r", encoding="utf-8") as f:
|
||||
with open(batch_file, encoding="utf-8") as f:
|
||||
batch_data = json.load(f)
|
||||
issues = {"errors": [], "warnings": []}
|
||||
|
||||
@@ -227,29 +210,21 @@ class AITranslationHelper:
|
||||
)
|
||||
|
||||
# Check if translation is identical to original (might be untranslated)
|
||||
if translated == original and not self._is_expected_identical(
|
||||
key, original
|
||||
):
|
||||
issues["warnings"].append(
|
||||
f"{lang}.{key}: Translation identical to original"
|
||||
)
|
||||
if translated == original and not self._is_expected_identical(key, original):
|
||||
issues["warnings"].append(f"{lang}.{key}: Translation identical to original")
|
||||
|
||||
# Check for common AI translation artifacts
|
||||
artifacts = ["[TRANSLATE]", "[TODO]", "UNTRANSLATED", "{{", "}}"]
|
||||
for artifact in artifacts:
|
||||
if artifact in translated:
|
||||
issues["errors"].append(
|
||||
f"{lang}.{key}: Contains translation artifact: {artifact}"
|
||||
)
|
||||
issues["errors"].append(f"{lang}.{key}: Contains translation artifact: {artifact}")
|
||||
|
||||
return issues
|
||||
|
||||
def apply_ai_batch_translations(
|
||||
self, batch_file: Path, validate: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
def apply_ai_batch_translations(self, batch_file: Path, validate: bool = True) -> dict[str, Any]:
|
||||
"""Apply translations from AI batch file to individual language files."""
|
||||
# Batch files are always JSON
|
||||
with open(batch_file, "r", encoding="utf-8") as f:
|
||||
with open(batch_file, encoding="utf-8") as f:
|
||||
batch_data = json.load(f)
|
||||
results = {"applied": {}, "errors": [], "warnings": []}
|
||||
|
||||
@@ -291,7 +266,7 @@ class AITranslationHelper:
|
||||
|
||||
return results
|
||||
|
||||
def _set_nested_value(self, data: Dict, key_path: str, value: Any) -> None:
|
||||
def _set_nested_value(self, data: dict, key_path: str, value: Any) -> None:
|
||||
"""Set value in nested dict using dot notation."""
|
||||
keys = key_path.split(".")
|
||||
current = data
|
||||
@@ -300,24 +275,18 @@ class AITranslationHelper:
|
||||
current[key] = {}
|
||||
elif not isinstance(current[key], dict):
|
||||
# If the current value is not a dict, we can't nest into it
|
||||
print(
|
||||
f"Warning: Converting non-dict value at '{key}' to dict to allow nesting"
|
||||
)
|
||||
print(f"Warning: Converting non-dict value at '{key}' to dict to allow nesting")
|
||||
current[key] = {}
|
||||
current = current[key]
|
||||
current[keys[-1]] = value
|
||||
|
||||
def export_for_external_translation(
|
||||
self, languages: List[str], output_format: str = "csv"
|
||||
) -> None:
|
||||
def export_for_external_translation(self, languages: list[str], output_format: str = "csv") -> None:
|
||||
"""Export translations for external translation services."""
|
||||
golden_truth = self._load_translation_file(self.golden_truth_file)
|
||||
golden_flat = self._flatten_dict(golden_truth)
|
||||
|
||||
if output_format == "csv":
|
||||
output_file = Path(
|
||||
f"translations_export_{datetime.now().strftime('%Y%m%d')}.csv"
|
||||
)
|
||||
output_file = Path(f"translations_export_{datetime.now().strftime('%Y%m%d')}.csv")
|
||||
|
||||
with open(output_file, "w", newline="", encoding="utf-8") as csvfile:
|
||||
fieldnames = ["key", "context", "en_US"] + languages
|
||||
@@ -353,9 +322,7 @@ class AITranslationHelper:
|
||||
print(f"Exported to {output_file}")
|
||||
|
||||
elif output_format == "json":
|
||||
output_file = Path(
|
||||
f"translations_export_{datetime.now().strftime('%Y%m%d')}.json"
|
||||
)
|
||||
output_file = Path(f"translations_export_{datetime.now().strftime('%Y%m%d')}.json")
|
||||
export_data = {"languages": languages, "translations": {}}
|
||||
|
||||
for key, en_value in golden_flat.items():
|
||||
@@ -386,9 +353,7 @@ class AITranslationHelper:
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="AI Translation Helper", epilog="Works with TOML translation files."
|
||||
)
|
||||
parser = argparse.ArgumentParser(description="AI Translation Helper", epilog="Works with TOML translation files.")
|
||||
parser.add_argument(
|
||||
"--locales-dir",
|
||||
default="frontend/editor/public/locales",
|
||||
@@ -398,40 +363,24 @@ def main():
|
||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||
|
||||
# Create batch command
|
||||
batch_parser = subparsers.add_parser(
|
||||
"create-batch", help="Create AI translation batch file"
|
||||
)
|
||||
batch_parser.add_argument(
|
||||
"--languages", nargs="+", required=True, help="Language codes to include"
|
||||
)
|
||||
batch_parser = subparsers.add_parser("create-batch", help="Create AI translation batch file")
|
||||
batch_parser.add_argument("--languages", nargs="+", required=True, help="Language codes to include")
|
||||
batch_parser.add_argument("--output", required=True, help="Output batch file")
|
||||
batch_parser.add_argument(
|
||||
"--max-entries", type=int, default=100, help="Max entries per language"
|
||||
)
|
||||
batch_parser.add_argument("--max-entries", type=int, default=100, help="Max entries per language")
|
||||
|
||||
# Validate command
|
||||
validate_parser = subparsers.add_parser("validate", help="Validate AI translations")
|
||||
validate_parser.add_argument("batch_file", help="Batch file to validate")
|
||||
|
||||
# Apply command
|
||||
apply_parser = subparsers.add_parser(
|
||||
"apply-batch", help="Apply AI batch translations"
|
||||
)
|
||||
apply_parser = subparsers.add_parser("apply-batch", help="Apply AI batch translations")
|
||||
apply_parser.add_argument("batch_file", help="Batch file with translations")
|
||||
apply_parser.add_argument(
|
||||
"--skip-validation", action="store_true", help="Skip validation before applying"
|
||||
)
|
||||
apply_parser.add_argument("--skip-validation", action="store_true", help="Skip validation before applying")
|
||||
|
||||
# Export command
|
||||
export_parser = subparsers.add_parser(
|
||||
"export", help="Export for external translation"
|
||||
)
|
||||
export_parser.add_argument(
|
||||
"--languages", nargs="+", required=True, help="Language codes to export"
|
||||
)
|
||||
export_parser.add_argument(
|
||||
"--format", choices=["csv", "json"], default="csv", help="Export format"
|
||||
)
|
||||
export_parser = subparsers.add_parser("export", help="Export for external translation")
|
||||
export_parser.add_argument("--languages", nargs="+", required=True, help="Language codes to export")
|
||||
export_parser.add_argument("--format", choices=["csv", "json"], default="csv", help="Export format")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -464,9 +413,7 @@ def main():
|
||||
|
||||
elif args.command == "apply-batch":
|
||||
batch_file = Path(args.batch_file)
|
||||
results = helper.apply_ai_batch_translations(
|
||||
batch_file, validate=not args.skip_validation
|
||||
)
|
||||
results = helper.apply_ai_batch_translations(batch_file, validate=not args.skip_validation)
|
||||
|
||||
total_applied = sum(results["applied"].values())
|
||||
print(f"Total translations applied: {total_applied}")
|
||||
|
||||
@@ -5,16 +5,15 @@ Extracts, translates, merges, and beautifies translations for a language.
|
||||
TOML format only.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def run_command(cmd, description=""):
|
||||
@@ -50,27 +49,19 @@ def load_translation_file(file_path):
|
||||
|
||||
def extract_untranslated(language_code, batch_size=500, include_existing=False):
|
||||
"""Extract untranslated entries and split into batches."""
|
||||
mode = (
|
||||
"all untranslated (including existing)" if include_existing else "new (missing)"
|
||||
)
|
||||
mode = "all untranslated (including existing)" if include_existing else "new (missing)"
|
||||
print(f"\n🔍 Extracting {mode} entries for {language_code}...")
|
||||
|
||||
# Load files
|
||||
golden_path = find_translation_file(Path("frontend/editor/public/locales/en-US"))
|
||||
lang_path = find_translation_file(
|
||||
Path(f"frontend/editor/public/locales/{language_code}")
|
||||
)
|
||||
lang_path = find_translation_file(Path(f"frontend/editor/public/locales/{language_code}"))
|
||||
|
||||
if not golden_path:
|
||||
print(
|
||||
"Error: Golden truth file not found in frontend/editor/public/locales/en-US"
|
||||
)
|
||||
print("Error: Golden truth file not found in frontend/editor/public/locales/en-US")
|
||||
return None
|
||||
|
||||
if not lang_path:
|
||||
print(
|
||||
f"Error: Language file not found in frontend/editor/public/locales/{language_code}"
|
||||
)
|
||||
print(f"Error: Language file not found in frontend/editor/public/locales/{language_code}")
|
||||
return None
|
||||
|
||||
def flatten_dict(d, parent_key="", separator="."):
|
||||
@@ -101,10 +92,7 @@ def extract_untranslated(language_code, batch_size=500, include_existing=False):
|
||||
if (
|
||||
key not in lang_flat
|
||||
or lang_flat.get(key) == value
|
||||
or (
|
||||
isinstance(lang_flat.get(key), str)
|
||||
and lang_flat.get(key).startswith("[UNTRANSLATED]")
|
||||
)
|
||||
or (isinstance(lang_flat.get(key), str) and lang_flat.get(key).startswith("[UNTRANSLATED]"))
|
||||
):
|
||||
untranslated[key] = value
|
||||
else:
|
||||
@@ -141,9 +129,7 @@ def extract_untranslated(language_code, batch_size=500, include_existing=False):
|
||||
return batch_files
|
||||
|
||||
|
||||
def translate_batches(
|
||||
batch_files, language_code, api_key, timeout=600, model="gpt-5.5", parallel=1
|
||||
):
|
||||
def translate_batches(batch_files, language_code, api_key, timeout=600, model="gpt-5.5", parallel=1):
|
||||
"""Translate all batch files using the given OpenAI model."""
|
||||
if not batch_files:
|
||||
return []
|
||||
@@ -169,9 +155,7 @@ def translate_batches(
|
||||
cmd = f'python3 scripts/translations/batch_translator.py "{batch_file}" --language {language_code} --api-key "{api_key}" --model {model}'
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, shell=True, capture_output=True, text=True, timeout=timeout
|
||||
)
|
||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f"✗ Timed out after {timeout}s: {batch_file}", file=sys.stderr)
|
||||
return None
|
||||
@@ -222,7 +206,7 @@ def merge_translations(translated_files, language_code):
|
||||
print(f"Error: Translated file not found: {filename}")
|
||||
return None
|
||||
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
with open(filename, encoding="utf-8") as f:
|
||||
merged.update(json.load(f))
|
||||
|
||||
lang_code_safe = language_code.replace("-", "_")
|
||||
@@ -313,18 +297,10 @@ Examples:
|
||||
)
|
||||
|
||||
parser.add_argument("language", help="Language code (e.g., es-ES, de-DE, zh-CN)")
|
||||
parser.add_argument(
|
||||
"--api-key", help="OpenAI API key (or set OPENAI_API_KEY env var)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--batch-size", type=int, default=500, help="Entries per batch (default: 500)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-cleanup", action="store_true", help="Keep temporary batch files"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-verification", action="store_true", help="Skip final completion check"
|
||||
)
|
||||
parser.add_argument("--api-key", help="OpenAI API key (or set OPENAI_API_KEY env var)")
|
||||
parser.add_argument("--batch-size", type=int, default=500, help="Entries per batch (default: 500)")
|
||||
parser.add_argument("--no-cleanup", action="store_true", help="Keep temporary batch files")
|
||||
parser.add_argument("--skip-verification", action="store_true", help="Skip final completion check")
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=int,
|
||||
@@ -353,9 +329,7 @@ Examples:
|
||||
# Verify API key
|
||||
api_key = args.api_key or os.environ.get("OPENAI_API_KEY")
|
||||
if not api_key:
|
||||
print(
|
||||
"Error: OpenAI API key required. Provide via --api-key or OPENAI_API_KEY environment variable"
|
||||
)
|
||||
print("Error: OpenAI API key required. Provide via --api-key or OPENAI_API_KEY environment variable")
|
||||
sys.exit(1)
|
||||
|
||||
print("=" * 60)
|
||||
@@ -369,9 +343,7 @@ Examples:
|
||||
|
||||
try:
|
||||
# Step 1: Extract and split
|
||||
batch_files = extract_untranslated(
|
||||
args.language, args.batch_size, args.include_existing
|
||||
)
|
||||
batch_files = extract_untranslated(args.language, args.batch_size, args.include_existing)
|
||||
if batch_files is None:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ Automatically translates JSON batch files to target language while preserving:
|
||||
Note: Works with JSON batch files. Translation files can be TOML or JSON format.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from openai import OpenAI
|
||||
@@ -117,9 +117,7 @@ Return ONLY the translated JSON. No markdown, no explanations, just the JSON obj
|
||||
cost_note = f", ~${cost:.4f}" if cost else ""
|
||||
print(f" Tokens: {prompt_tokens:,} in / {completion_tokens:,} out{cost_note}")
|
||||
|
||||
def translate_batch(
|
||||
self, batch_data: dict, target_language: str, language_code: str
|
||||
) -> dict:
|
||||
def translate_batch(self, batch_data: dict, target_language: str, language_code: str) -> dict:
|
||||
"""Translate a batch file using OpenAI API."""
|
||||
# Convert batch to compact JSON for API
|
||||
input_json = json.dumps(batch_data, ensure_ascii=False, separators=(",", ":"))
|
||||
@@ -134,9 +132,7 @@ Return ONLY the translated JSON. No markdown, no explanations, just the JSON obj
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": self.get_translation_prompt(
|
||||
target_language, language_code
|
||||
),
|
||||
"content": self.get_translation_prompt(target_language, language_code),
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
@@ -198,9 +194,7 @@ Return ONLY the translated JSON. No markdown, no explanations, just the JSON obj
|
||||
trans_placeholders = set(re.findall(placeholder_pattern, trans_value))
|
||||
|
||||
if orig_placeholders != trans_placeholders:
|
||||
issues.append(
|
||||
f"Placeholder mismatch in '{key}': {orig_placeholders} vs {trans_placeholders}"
|
||||
)
|
||||
issues.append(f"Placeholder mismatch in '{key}': {orig_placeholders} vs {trans_placeholders}")
|
||||
|
||||
if issues:
|
||||
print("\n⚠ Validation warnings:")
|
||||
@@ -276,12 +270,8 @@ Examples:
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"input_files", nargs="+", help="Input batch JSON file(s) or pattern"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--api-key", help="OpenAI API key (or set OPENAI_API_KEY env var)"
|
||||
)
|
||||
parser.add_argument("input_files", nargs="+", help="Input batch JSON file(s) or pattern")
|
||||
parser.add_argument("--api-key", help="OpenAI API key (or set OPENAI_API_KEY env var)")
|
||||
parser.add_argument(
|
||||
"--language",
|
||||
"-l",
|
||||
@@ -298,9 +288,7 @@ Examples:
|
||||
default="_translated",
|
||||
help="Suffix for output files (default: _translated)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-validation", action="store_true", help="Skip validation checks"
|
||||
)
|
||||
parser.add_argument("--skip-validation", action="store_true", help="Skip validation checks")
|
||||
parser.add_argument(
|
||||
"--delay",
|
||||
type=float,
|
||||
@@ -315,9 +303,7 @@ Examples:
|
||||
|
||||
api_key = args.api_key or os.environ.get("OPENAI_API_KEY")
|
||||
if not api_key:
|
||||
print(
|
||||
"Error: OpenAI API key required. Provide via --api-key or OPENAI_API_KEY environment variable"
|
||||
)
|
||||
print("Error: OpenAI API key required. Provide via --api-key or OPENAI_API_KEY environment variable")
|
||||
sys.exit(1)
|
||||
|
||||
# Get language info
|
||||
@@ -356,13 +342,11 @@ Examples:
|
||||
|
||||
try:
|
||||
# Load input file
|
||||
with open(input_file, "r", encoding="utf-8") as f:
|
||||
with open(input_file, encoding="utf-8") as f:
|
||||
batch_data = json.load(f)
|
||||
|
||||
# Translate
|
||||
translated_data = translator.translate_batch(
|
||||
batch_data, language_name, language_code
|
||||
)
|
||||
translated_data = translator.translate_batch(batch_data, language_name, language_code)
|
||||
|
||||
# Validate
|
||||
if not args.skip_validation:
|
||||
@@ -396,10 +380,7 @@ Examples:
|
||||
|
||||
# Cost summary
|
||||
print("-" * 60)
|
||||
print(
|
||||
f"Total tokens: {translator.total_prompt_tokens:,} in / "
|
||||
f"{translator.total_completion_tokens:,} out"
|
||||
)
|
||||
print(f"Total tokens: {translator.total_prompt_tokens:,} in / {translator.total_completion_tokens:,} out")
|
||||
if translator.total_cost:
|
||||
print(f"Estimated cost ({args.model}): ${translator.total_cost:.4f}")
|
||||
|
||||
|
||||
@@ -7,16 +7,13 @@ Supports concurrent translation with configurable thread pool.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import subprocess
|
||||
from typing import List, Tuple, Optional
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import time
|
||||
import tomllib
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
|
||||
# Thread-safe print lock
|
||||
print_lock = threading.Lock()
|
||||
@@ -28,7 +25,7 @@ def safe_print(*args, **kwargs):
|
||||
print(*args, **kwargs)
|
||||
|
||||
|
||||
def get_all_languages(locales_dir: Path) -> List[str]:
|
||||
def get_all_languages(locales_dir: Path) -> list[str]:
|
||||
"""Get all language codes from locales directory."""
|
||||
languages = []
|
||||
|
||||
@@ -45,7 +42,7 @@ def get_all_languages(locales_dir: Path) -> List[str]:
|
||||
return languages
|
||||
|
||||
|
||||
def get_language_completion(locales_dir: Path, language: str) -> Optional[float]:
|
||||
def get_language_completion(locales_dir: Path, language: str) -> float | None:
|
||||
"""Get completion percentage for a language."""
|
||||
lang_dir = locales_dir / language
|
||||
toml_file = lang_dir / "translation.toml"
|
||||
@@ -77,11 +74,7 @@ def get_language_completion(locales_dir: Path, language: str) -> Optional[float]
|
||||
target_flat = flatten(target_data)
|
||||
|
||||
# Count translated (not equal to en-US)
|
||||
translated = sum(
|
||||
1
|
||||
for k in en_us_flat
|
||||
if k in target_flat and target_flat[k] != en_us_flat[k]
|
||||
)
|
||||
translated = sum(1 for k in en_us_flat if k in target_flat and target_flat[k] != en_us_flat[k])
|
||||
total = len(en_us_flat)
|
||||
|
||||
return (translated / total * 100) if total > 0 else 0.0
|
||||
@@ -99,7 +92,7 @@ def translate_language(
|
||||
skip_verification: bool,
|
||||
include_existing: bool,
|
||||
model: str,
|
||||
) -> Tuple[str, bool, str]:
|
||||
) -> tuple[str, bool, str]:
|
||||
"""
|
||||
Translate a single language.
|
||||
Returns: (language_code, success, message)
|
||||
@@ -178,9 +171,7 @@ Note: Requires OPENAI_API_KEY environment variable or --api-key argument.
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--api-key", help="OpenAI API key (or set OPENAI_API_KEY env var)"
|
||||
)
|
||||
parser.add_argument("--api-key", help="OpenAI API key (or set OPENAI_API_KEY env var)")
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default="gpt-5.5",
|
||||
@@ -241,9 +232,7 @@ Note: Requires OPENAI_API_KEY environment variable or --api-key argument.
|
||||
# Verify API key (unless dry run)
|
||||
api_key = args.api_key or os.environ.get("OPENAI_API_KEY")
|
||||
if not args.dry_run and not api_key:
|
||||
print(
|
||||
"Error: OpenAI API key required. Provide via --api-key or OPENAI_API_KEY environment variable"
|
||||
)
|
||||
print("Error: OpenAI API key required. Provide via --api-key or OPENAI_API_KEY environment variable")
|
||||
sys.exit(1)
|
||||
|
||||
locales_dir = Path(args.locales_dir)
|
||||
|
||||
@@ -5,11 +5,11 @@ Outputs untranslated entries in minimal JSON format with whitespace stripped.
|
||||
TOML format only.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import tomllib # Python 3.11+ (stdlib)
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class CompactTranslationExtractor:
|
||||
@@ -50,9 +50,7 @@ class CompactTranslationExtractor:
|
||||
try:
|
||||
with open(self.ignore_file, "rb") as f:
|
||||
ignore_data = tomllib.load(f)
|
||||
return {
|
||||
lang: set(data.get("ignore", [])) for lang, data in ignore_data.items()
|
||||
}
|
||||
return {lang: set(data.get("ignore", [])) for lang, data in ignore_data.items()}
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Warning: Could not load ignore file {self.ignore_file}: {e}",
|
||||
@@ -60,9 +58,7 @@ class CompactTranslationExtractor:
|
||||
)
|
||||
return {}
|
||||
|
||||
def _flatten_dict(
|
||||
self, d: dict, parent_key: str = "", separator: str = "."
|
||||
) -> dict:
|
||||
def _flatten_dict(self, d: dict, parent_key: str = "", separator: str = ".") -> dict:
|
||||
"""Flatten nested dictionary into dot-notation keys."""
|
||||
items = []
|
||||
for k, v in d.items():
|
||||
@@ -102,12 +98,8 @@ class CompactTranslationExtractor:
|
||||
target_value = target_flat[key]
|
||||
golden_value = golden_flat[key]
|
||||
|
||||
if (
|
||||
isinstance(target_value, str)
|
||||
and target_value.startswith("[UNTRANSLATED]")
|
||||
) or (
|
||||
golden_value == target_value
|
||||
and not self._is_expected_identical(key, golden_value)
|
||||
if (isinstance(target_value, str) and target_value.startswith("[UNTRANSLATED]")) or (
|
||||
golden_value == target_value and not self._is_expected_identical(key, golden_value)
|
||||
):
|
||||
untranslated_keys.add(key)
|
||||
|
||||
@@ -151,9 +143,7 @@ def main():
|
||||
default="scripts/ignore_translation.toml",
|
||||
help="Path to ignore patterns file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-entries", type=int, help="Maximum number of entries to output"
|
||||
)
|
||||
parser.add_argument("--max-entries", type=int, help="Maximum number of entries to output")
|
||||
parser.add_argument("--output", help="Output file (default: stdout)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -4,13 +4,13 @@ TOML Beautifier and Structure Fixer for Stirling PDF Frontend
|
||||
Restructures translation TOML files to match en-US structure and key order exactly.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
import argparse
|
||||
from collections import OrderedDict
|
||||
|
||||
import sys
|
||||
import tomllib
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import tomli_w
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class TOMLBeautifier:
|
||||
self.golden_truth_file = self.locales_dir / "en-US" / "translation.toml"
|
||||
self.golden_structure = self._load_toml(self.golden_truth_file)
|
||||
|
||||
def _load_toml(self, file_path: Path) -> Dict:
|
||||
def _load_toml(self, file_path: Path) -> dict:
|
||||
"""Load TOML file with error handling."""
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
@@ -32,7 +32,7 @@ class TOMLBeautifier:
|
||||
print(f"Error: Invalid TOML in {file_path}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def _save_toml(self, data: Dict, file_path: Path, backup: bool = False) -> None:
|
||||
def _save_toml(self, data: dict, file_path: Path, backup: bool = False) -> None:
|
||||
"""Save TOML file with proper formatting."""
|
||||
if backup and file_path.exists():
|
||||
backup_path = file_path.with_suffix(".backup.restructured.toml")
|
||||
@@ -44,9 +44,7 @@ class TOMLBeautifier:
|
||||
with open(file_path, "wb") as f:
|
||||
tomli_w.dump(data, f)
|
||||
|
||||
def _flatten_dict(
|
||||
self, d: Dict, parent_key: str = "", separator: str = "."
|
||||
) -> Dict[str, Any]:
|
||||
def _flatten_dict(self, d: dict, parent_key: str = "", separator: str = ".") -> dict[str, Any]:
|
||||
"""Flatten nested dictionary into dot-notation keys."""
|
||||
items = []
|
||||
for k, v in d.items():
|
||||
@@ -57,9 +55,7 @@ class TOMLBeautifier:
|
||||
items.append((new_key, v))
|
||||
return dict(items)
|
||||
|
||||
def _rebuild_structure(
|
||||
self, flat_dict: Dict[str, Any], reference_structure: Dict
|
||||
) -> Dict:
|
||||
def _rebuild_structure(self, flat_dict: dict[str, Any], reference_structure: dict) -> dict:
|
||||
"""Rebuild nested structure based on reference structure and available translations."""
|
||||
|
||||
def build_recursive(ref_obj: Any, current_path: str = "") -> Any:
|
||||
@@ -94,7 +90,7 @@ class TOMLBeautifier:
|
||||
|
||||
return build_recursive(reference_structure) or OrderedDict()
|
||||
|
||||
def restructure_translation_file(self, target_file: Path) -> Dict[str, Any]:
|
||||
def restructure_translation_file(self, target_file: Path) -> dict[str, Any]:
|
||||
"""Restructure a translation file to match en-US structure exactly."""
|
||||
if not target_file.exists():
|
||||
print(f"Error: Target file does not exist: {target_file}")
|
||||
@@ -111,9 +107,7 @@ class TOMLBeautifier:
|
||||
|
||||
return restructured
|
||||
|
||||
def beautify_and_restructure(
|
||||
self, target_file: Path, backup: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
def beautify_and_restructure(self, target_file: Path, backup: bool = False) -> dict[str, Any]:
|
||||
"""Main function to beautify and restructure a translation file."""
|
||||
lang_code = target_file.parent.name
|
||||
print(f"Restructuring {lang_code} translation file...")
|
||||
@@ -135,18 +129,16 @@ class TOMLBeautifier:
|
||||
"language": lang_code,
|
||||
"total_reference_keys": total_keys,
|
||||
"preserved_keys": preserved_keys,
|
||||
"structure_match": self._compare_structures(
|
||||
self.golden_structure, restructured_data
|
||||
),
|
||||
"structure_match": self._compare_structures(self.golden_structure, restructured_data),
|
||||
}
|
||||
|
||||
print(f"Restructured {lang_code}: {preserved_keys}/{total_keys} keys preserved")
|
||||
return result
|
||||
|
||||
def _compare_structures(self, ref: Dict, target: Dict) -> Dict[str, bool]:
|
||||
def _compare_structures(self, ref: dict, target: dict) -> dict[str, bool]:
|
||||
"""Compare structures between reference and target."""
|
||||
|
||||
def compare_recursive(r: Any, t: Any, path: str = "") -> List[str]:
|
||||
def compare_recursive(r: Any, t: Any, path: str = "") -> list[str]:
|
||||
issues = []
|
||||
|
||||
if isinstance(r, dict) and isinstance(t, dict):
|
||||
@@ -157,9 +149,7 @@ class TOMLBeautifier:
|
||||
missing_sections = ref_keys - target_keys
|
||||
if missing_sections:
|
||||
for section in missing_sections:
|
||||
issues.append(
|
||||
f"Missing section: {path}.{section}" if path else section
|
||||
)
|
||||
issues.append(f"Missing section: {path}.{section}" if path else section)
|
||||
|
||||
# Recurse into common sections
|
||||
for key in ref_keys & target_keys:
|
||||
@@ -176,11 +166,11 @@ class TOMLBeautifier:
|
||||
"total_issues": len(issues),
|
||||
}
|
||||
|
||||
def validate_key_order(self, target_file: Path) -> Dict[str, Any]:
|
||||
def validate_key_order(self, target_file: Path) -> dict[str, Any]:
|
||||
"""Validate that keys appear in the same order as en-US."""
|
||||
target_data = self._load_toml(target_file)
|
||||
|
||||
def get_key_order(obj: Dict, path: str = "") -> List[str]:
|
||||
def get_key_order(obj: dict, path: str = "") -> list[str]:
|
||||
keys = []
|
||||
for key in obj.keys():
|
||||
new_path = f"{path}.{key}" if path else key
|
||||
@@ -195,19 +185,14 @@ class TOMLBeautifier:
|
||||
# Find common keys and check their relative order
|
||||
common_keys = set(golden_order) & set(target_order)
|
||||
|
||||
golden_indices = {
|
||||
key: idx for idx, key in enumerate(golden_order) if key in common_keys
|
||||
}
|
||||
target_indices = {
|
||||
key: idx for idx, key in enumerate(target_order) if key in common_keys
|
||||
}
|
||||
golden_indices = {key: idx for idx, key in enumerate(golden_order) if key in common_keys}
|
||||
target_indices = {key: idx for idx, key in enumerate(target_order) if key in common_keys}
|
||||
|
||||
order_preserved = all(
|
||||
golden_indices[key1] < golden_indices[key2]
|
||||
for key1 in common_keys
|
||||
for key2 in common_keys
|
||||
if golden_indices[key1] < golden_indices[key2]
|
||||
and target_indices[key1] < target_indices[key2]
|
||||
if golden_indices[key1] < golden_indices[key2] and target_indices[key1] < target_indices[key2]
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -229,12 +214,8 @@ def main():
|
||||
help="Path to locales directory",
|
||||
)
|
||||
parser.add_argument("--language", help="Restructure specific language only")
|
||||
parser.add_argument(
|
||||
"--all-languages", action="store_true", help="Restructure all language files"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--backup", action="store_true", help="Create backup files before modifying"
|
||||
)
|
||||
parser.add_argument("--all-languages", action="store_true", help="Restructure all language files")
|
||||
parser.add_argument("--backup", action="store_true", help="Create backup files before modifying")
|
||||
parser.add_argument(
|
||||
"--validate-only",
|
||||
action="store_true",
|
||||
@@ -255,21 +236,13 @@ def main():
|
||||
order_result = beautifier.validate_key_order(target_file)
|
||||
print(f"Key order validation for {args.language}:")
|
||||
print(f" Order preserved: {order_result['order_preserved']}")
|
||||
print(
|
||||
f" Common keys: {order_result['common_keys_count']}/{order_result['golden_keys_count']}"
|
||||
)
|
||||
print(f" Common keys: {order_result['common_keys_count']}/{order_result['golden_keys_count']}")
|
||||
else:
|
||||
result = beautifier.beautify_and_restructure(
|
||||
target_file, backup=args.backup
|
||||
)
|
||||
result = beautifier.beautify_and_restructure(target_file, backup=args.backup)
|
||||
print(f"\nResults for {result['language']}:")
|
||||
print(
|
||||
f" Keys preserved: {result['preserved_keys']}/{result['total_reference_keys']}"
|
||||
)
|
||||
print(f" Keys preserved: {result['preserved_keys']}/{result['total_reference_keys']}")
|
||||
if result["structure_match"]["total_issues"] > 0:
|
||||
print(
|
||||
f" Structure issues: {result['structure_match']['total_issues']}"
|
||||
)
|
||||
print(f" Structure issues: {result['structure_match']['total_issues']}")
|
||||
for issue in result["structure_match"]["issues"]:
|
||||
print(f" - {issue}")
|
||||
|
||||
@@ -281,13 +254,9 @@ def main():
|
||||
if translation_file.exists():
|
||||
if args.validate_only:
|
||||
order_result = beautifier.validate_key_order(translation_file)
|
||||
print(
|
||||
f"{lang_dir.name}: Order preserved = {order_result['order_preserved']}"
|
||||
)
|
||||
print(f"{lang_dir.name}: Order preserved = {order_result['order_preserved']}")
|
||||
else:
|
||||
result = beautifier.beautify_and_restructure(
|
||||
translation_file, backup=args.backup
|
||||
)
|
||||
result = beautifier.beautify_and_restructure(translation_file, backup=args.backup)
|
||||
results.append(result)
|
||||
|
||||
if not args.validate_only and results:
|
||||
|
||||
@@ -12,17 +12,16 @@ Usage:
|
||||
python3 toml_validator.py --all-batches ar_AR
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import glob
|
||||
|
||||
import sys
|
||||
import tomllib
|
||||
|
||||
|
||||
def get_line_context(file_path, line_num, context_lines=3):
|
||||
"""Get lines around the error for context"""
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
start = max(0, line_num - context_lines - 1)
|
||||
@@ -41,7 +40,7 @@ def get_line_context(file_path, line_num, context_lines=3):
|
||||
def get_character_context(file_path, char_pos, context_chars=100):
|
||||
"""Get characters around the error position"""
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
start = max(0, char_pos - context_chars)
|
||||
@@ -144,12 +143,8 @@ def main():
|
||||
metavar="LANG",
|
||||
help="Validate all batch files for a language (e.g., ar_AR)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--brief", action="store_true", help="Show brief output without context"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quiet", action="store_true", help="Only show files with errors"
|
||||
)
|
||||
parser.add_argument("--brief", action="store_true", help="Show brief output without context")
|
||||
parser.add_argument("--quiet", action="store_true", help="Only show files with errors")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
@@ -4,12 +4,11 @@ Translation Analyzer for Stirling PDF Frontend
|
||||
Compares language files against en-US golden truth file.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Set
|
||||
import argparse
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TranslationAnalyzer:
|
||||
@@ -24,7 +23,7 @@ class TranslationAnalyzer:
|
||||
self.ignore_file = Path(ignore_file)
|
||||
self.ignore_patterns = self._load_ignore_patterns()
|
||||
|
||||
def _load_translation_file(self, file_path: Path) -> Dict:
|
||||
def _load_translation_file(self, file_path: Path) -> dict:
|
||||
"""Load TOML translation file with error handling."""
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
@@ -36,7 +35,7 @@ class TranslationAnalyzer:
|
||||
print(f"Error: Invalid file {file_path}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def _load_ignore_patterns(self) -> Dict[str, Set[str]]:
|
||||
def _load_ignore_patterns(self) -> dict[str, set[str]]:
|
||||
"""Load ignore patterns from TOML file."""
|
||||
if not self.ignore_file.exists():
|
||||
return {}
|
||||
@@ -56,9 +55,7 @@ class TranslationAnalyzer:
|
||||
print(f"Warning: Could not load ignore file {self.ignore_file}: {e}")
|
||||
return {}
|
||||
|
||||
def _flatten_dict(
|
||||
self, d: Dict, parent_key: str = "", separator: str = "."
|
||||
) -> Dict[str, str]:
|
||||
def _flatten_dict(self, d: dict, parent_key: str = "", separator: str = ".") -> dict[str, str]:
|
||||
"""Flatten nested dictionary into dot-notation keys."""
|
||||
items = []
|
||||
for k, v in d.items():
|
||||
@@ -69,7 +66,7 @@ class TranslationAnalyzer:
|
||||
items.append((new_key, str(v)))
|
||||
return dict(items)
|
||||
|
||||
def get_all_language_files(self) -> List[Path]:
|
||||
def get_all_language_files(self) -> list[Path]:
|
||||
"""Get all translation files except en-US."""
|
||||
files = []
|
||||
for lang_dir in self.locales_dir.iterdir():
|
||||
@@ -79,7 +76,7 @@ class TranslationAnalyzer:
|
||||
files.append(toml_file)
|
||||
return sorted(files)
|
||||
|
||||
def find_missing_translations(self, target_file: Path) -> Set[str]:
|
||||
def find_missing_translations(self, target_file: Path) -> set[str]:
|
||||
"""Find keys that exist in en-US but missing in target file."""
|
||||
target_data = self._load_translation_file(target_file)
|
||||
|
||||
@@ -93,7 +90,7 @@ class TranslationAnalyzer:
|
||||
ignore_set = self.ignore_patterns.get(lang_code, set())
|
||||
return missing - ignore_set
|
||||
|
||||
def find_untranslated_entries(self, target_file: Path) -> Set[str]:
|
||||
def find_untranslated_entries(self, target_file: Path) -> set[str]:
|
||||
"""Find entries that appear to be untranslated (identical to en-US)."""
|
||||
target_data = self._load_translation_file(target_file)
|
||||
|
||||
@@ -110,10 +107,7 @@ class TranslationAnalyzer:
|
||||
golden_value = golden_flat[key]
|
||||
|
||||
# Check if marked as [UNTRANSLATED] or identical to en-US
|
||||
if (
|
||||
isinstance(target_value, str)
|
||||
and target_value.startswith("[UNTRANSLATED]")
|
||||
) or (
|
||||
if (isinstance(target_value, str) and target_value.startswith("[UNTRANSLATED]")) or (
|
||||
golden_value == target_value
|
||||
and key not in ignore_set
|
||||
and not self._is_expected_identical(key, golden_value)
|
||||
@@ -138,7 +132,7 @@ class TranslationAnalyzer:
|
||||
|
||||
return False
|
||||
|
||||
def find_extra_translations(self, target_file: Path) -> Set[str]:
|
||||
def find_extra_translations(self, target_file: Path) -> set[str]:
|
||||
"""Find keys that exist in target file but not in en-US."""
|
||||
target_data = self._load_translation_file(target_file)
|
||||
|
||||
@@ -147,7 +141,7 @@ class TranslationAnalyzer:
|
||||
|
||||
return set(target_flat.keys()) - set(golden_flat.keys())
|
||||
|
||||
def analyze_file(self, target_file: Path) -> Dict:
|
||||
def analyze_file(self, target_file: Path) -> dict:
|
||||
"""Complete analysis of a single translation file."""
|
||||
lang_code = target_file.parent.name
|
||||
|
||||
@@ -172,14 +166,10 @@ class TranslationAnalyzer:
|
||||
if key in target_flat:
|
||||
value = target_flat[key]
|
||||
if not (isinstance(value, str) and value.startswith("[UNTRANSLATED]")):
|
||||
if (
|
||||
key not in untranslated
|
||||
): # Not identical to en-US (unless expected)
|
||||
if key not in untranslated: # Not identical to en-US (unless expected)
|
||||
properly_translated += 1
|
||||
|
||||
completion_rate = (
|
||||
(properly_translated / total_keys) * 100 if total_keys > 0 else 0
|
||||
)
|
||||
completion_rate = (properly_translated / total_keys) * 100 if total_keys > 0 else 0
|
||||
|
||||
return {
|
||||
"language": lang_code,
|
||||
@@ -194,7 +184,7 @@ class TranslationAnalyzer:
|
||||
"completion_rate": completion_rate,
|
||||
}
|
||||
|
||||
def analyze_all_files(self) -> List[Dict]:
|
||||
def analyze_all_files(self) -> list[dict]:
|
||||
"""Analyze all translation files."""
|
||||
results = []
|
||||
for file_path in self.get_all_language_files():
|
||||
@@ -203,9 +193,7 @@ class TranslationAnalyzer:
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Analyze translation files against en-US golden truth"
|
||||
)
|
||||
parser = argparse.ArgumentParser(description="Analyze translation files against en-US golden truth")
|
||||
parser.add_argument(
|
||||
"--locales-dir",
|
||||
default="frontend/editor/public/locales",
|
||||
@@ -217,20 +205,14 @@ def main():
|
||||
help="Path to ignore patterns TOML file",
|
||||
)
|
||||
parser.add_argument("--language", help="Analyze specific language only")
|
||||
parser.add_argument(
|
||||
"--missing-only", action="store_true", help="Show only missing translations"
|
||||
)
|
||||
parser.add_argument("--missing-only", action="store_true", help="Show only missing translations")
|
||||
parser.add_argument(
|
||||
"--untranslated-only",
|
||||
action="store_true",
|
||||
help="Show only untranslated entries",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--summary", action="store_true", help="Show summary statistics only"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--format", choices=["text", "json"], default="text", help="Output format"
|
||||
)
|
||||
parser.add_argument("--summary", action="store_true", help="Show summary statistics only")
|
||||
parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -287,16 +269,12 @@ def main():
|
||||
print(f"\n{'=' * 60}")
|
||||
print("SUMMARY")
|
||||
print(f"{'=' * 60}")
|
||||
avg_completion = (
|
||||
sum(r["completion_rate"] for r in results) / len(results) if results else 0
|
||||
)
|
||||
avg_completion = sum(r["completion_rate"] for r in results) / len(results) if results else 0
|
||||
print(f"Average Completion Rate: {avg_completion:.1f}%")
|
||||
print(f"Languages Analyzed: {len(results)}")
|
||||
|
||||
# Top languages by completion
|
||||
sorted_by_completion = sorted(
|
||||
results, key=lambda x: x["completion_rate"], reverse=True
|
||||
)
|
||||
sorted_by_completion = sorted(results, key=lambda x: x["completion_rate"], reverse=True)
|
||||
print("\nTop 5 Most Complete Languages:")
|
||||
for result in sorted_by_completion[:5]:
|
||||
print(f" {result['language']}: {result['completion_rate']:.1f}%")
|
||||
|
||||
@@ -6,28 +6,24 @@ Useful for AI-assisted translation workflows.
|
||||
TOML format only.
|
||||
"""
|
||||
|
||||
import os
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tomllib
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import tomllib
|
||||
import tomli_w
|
||||
|
||||
|
||||
class TranslationMerger:
|
||||
def __init__(
|
||||
self,
|
||||
locales_dir: str = os.path.join(
|
||||
os.getcwd(), "frontend", "editor", "public", "locales"
|
||||
),
|
||||
ignore_file: str = os.path.join(
|
||||
os.getcwd(), "scripts", "ignore_translation.toml"
|
||||
),
|
||||
locales_dir: str = os.path.join(os.getcwd(), "frontend", "editor", "public", "locales"),
|
||||
ignore_file: str = os.path.join(os.getcwd(), "scripts", "ignore_translation.toml"),
|
||||
):
|
||||
self.locales_dir = Path(locales_dir)
|
||||
self.golden_truth_file = self.locales_dir / "en-US" / "translation.toml"
|
||||
@@ -47,14 +43,10 @@ class TranslationMerger:
|
||||
print(f"Error: Invalid file {file_path}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def _save_translation_file(
|
||||
self, data: dict[str, Any], file_path: Path, backup: bool = False
|
||||
) -> None:
|
||||
def _save_translation_file(self, data: dict[str, Any], file_path: Path, backup: bool = False) -> None:
|
||||
"""Save TOML translation file with backup option."""
|
||||
if backup and file_path.exists():
|
||||
backup_path = file_path.with_suffix(
|
||||
f".backup.{datetime.now().strftime('%Y%m%d_%H%M%S')}.toml"
|
||||
)
|
||||
backup_path = file_path.with_suffix(f".backup.{datetime.now().strftime('%Y%m%d_%H%M%S')}.toml")
|
||||
shutil.copy2(file_path, backup_path)
|
||||
print(f"Backup created: {backup_path}")
|
||||
|
||||
@@ -71,9 +63,7 @@ class TranslationMerger:
|
||||
ignore_data = tomllib.load(f)
|
||||
|
||||
# Convert to sets for faster lookup
|
||||
return {
|
||||
lang: set(data.get("ignore", [])) for lang, data in ignore_data.items()
|
||||
}
|
||||
return {lang: set(data.get("ignore", [])) for lang, data in ignore_data.items()}
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load ignore file {self.ignore_file}: {e}")
|
||||
return {}
|
||||
@@ -89,9 +79,7 @@ class TranslationMerger:
|
||||
return None
|
||||
return current
|
||||
|
||||
def _set_nested_value(
|
||||
self, data: dict[str, Any], key_path: str, value: Any
|
||||
) -> None:
|
||||
def _set_nested_value(self, data: dict[str, Any], key_path: str, value: Any) -> None:
|
||||
"""Set value in nested dict using dot notation."""
|
||||
keys = key_path.split(".")
|
||||
current = data
|
||||
@@ -101,16 +89,12 @@ class TranslationMerger:
|
||||
elif not isinstance(current[key], dict):
|
||||
# If the current value is not a dict, we can't nest into it
|
||||
# This handles cases where a key exists as a string but we need to make it a dict
|
||||
print(
|
||||
f"Warning: Converting non-dict value at '{key}' to dict to allow nesting"
|
||||
)
|
||||
print(f"Warning: Converting non-dict value at '{key}' to dict to allow nesting")
|
||||
current[key] = {}
|
||||
current = current[key]
|
||||
current[keys[-1]] = value
|
||||
|
||||
def _flatten_dict(
|
||||
self, d: dict[str, Any], parent_key: str = "", separator: str = "."
|
||||
) -> dict[str, Any]:
|
||||
def _flatten_dict(self, d: dict[str, Any], parent_key: str = "", separator: str = ".") -> dict[str, Any]:
|
||||
"""Flatten nested dictionary into dot-notation keys."""
|
||||
items = []
|
||||
for k, v in d.items():
|
||||
@@ -207,9 +191,7 @@ class TranslationMerger:
|
||||
"data": target_data,
|
||||
}
|
||||
|
||||
def extract_untranslated_entries(
|
||||
self, target_file: Path, output_file: Path | None = None
|
||||
) -> dict[str, Any]:
|
||||
def extract_untranslated_entries(self, target_file: Path, output_file: Path | None = None) -> dict[str, Any]:
|
||||
"""Extract entries marked as untranslated or identical to en-US for AI translation."""
|
||||
if not target_file.exists():
|
||||
print(f"Error: Target file does not exist: {target_file}")
|
||||
@@ -233,9 +215,7 @@ class TranslationMerger:
|
||||
"reason": "marked_untranslated",
|
||||
}
|
||||
# Check if identical to golden (and should be translated)
|
||||
elif value == golden_value and not self._is_expected_identical(
|
||||
key, value
|
||||
):
|
||||
elif value == golden_value and not self._is_expected_identical(key, value):
|
||||
untranslated_entries[key] = {
|
||||
"original": golden_value,
|
||||
"current": value,
|
||||
@@ -279,9 +259,7 @@ class TranslationMerger:
|
||||
for key, translation in translations.items():
|
||||
try:
|
||||
# Remove [UNTRANSLATED] marker if present
|
||||
if isinstance(translation, str) and translation.startswith(
|
||||
"[UNTRANSLATED]"
|
||||
):
|
||||
if isinstance(translation, str) and translation.startswith("[UNTRANSLATED]"):
|
||||
translation = translation.replace("[UNTRANSLATED]", "").strip()
|
||||
|
||||
self._set_nested_value(target_data, key, translation)
|
||||
@@ -390,45 +368,25 @@ def main():
|
||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||
|
||||
# Add missing command
|
||||
add_parser = subparsers.add_parser(
|
||||
"add-missing", help="Add missing translations from en-US"
|
||||
)
|
||||
add_parser.add_argument(
|
||||
"--backup", action="store_true", help="Create backup before modifying files"
|
||||
)
|
||||
add_parser = subparsers.add_parser("add-missing", help="Add missing translations from en-US")
|
||||
add_parser.add_argument("--backup", action="store_true", help="Create backup before modifying files")
|
||||
|
||||
# Extract untranslated command
|
||||
extract_parser = subparsers.add_parser(
|
||||
"extract-untranslated", help="Extract untranslated entries"
|
||||
)
|
||||
extract_parser = subparsers.add_parser("extract-untranslated", help="Extract untranslated entries")
|
||||
extract_parser.add_argument("--output", help="Output file path")
|
||||
|
||||
# Create template command
|
||||
template_parser = subparsers.add_parser(
|
||||
"create-template", help="Create AI translation template"
|
||||
)
|
||||
template_parser.add_argument(
|
||||
"--output", required=True, help="Output template file path"
|
||||
)
|
||||
template_parser = subparsers.add_parser("create-template", help="Create AI translation template")
|
||||
template_parser.add_argument("--output", required=True, help="Output template file path")
|
||||
|
||||
# Apply translations command
|
||||
apply_parser = subparsers.add_parser(
|
||||
"apply-translations", help="Apply translations from JSON file"
|
||||
)
|
||||
apply_parser.add_argument(
|
||||
"--translations-file", required=True, help="JSON file with translations"
|
||||
)
|
||||
apply_parser.add_argument(
|
||||
"--backup", action="store_true", help="Create backup before modifying files"
|
||||
)
|
||||
apply_parser = subparsers.add_parser("apply-translations", help="Apply translations from JSON file")
|
||||
apply_parser.add_argument("--translations-file", required=True, help="JSON file with translations")
|
||||
apply_parser.add_argument("--backup", action="store_true", help="Create backup before modifying files")
|
||||
|
||||
# Remove unused translations command
|
||||
remove_parser = subparsers.add_parser(
|
||||
"remove-unused", help="Remove unused translations not present in en-US"
|
||||
)
|
||||
remove_parser.add_argument(
|
||||
"--backup", action="store_true", help="Create backup before modifying files"
|
||||
)
|
||||
remove_parser = subparsers.add_parser("remove-unused", help="Remove unused translations not present in en-US")
|
||||
remove_parser.add_argument("--backup", action="store_true", help="Create backup before modifying files")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -453,9 +411,7 @@ def main():
|
||||
continue
|
||||
target_file = lang_dir / "translation.toml"
|
||||
print(f"Processing {lang_dir.name}...")
|
||||
result = merger.add_missing_translations(
|
||||
target_file, backup=args.backup
|
||||
)
|
||||
result = merger.add_missing_translations(target_file, backup=args.backup)
|
||||
added = result["added_count"]
|
||||
total_added += added
|
||||
print(f"Added {added} missing translations")
|
||||
@@ -475,9 +431,7 @@ def main():
|
||||
continue
|
||||
target_file = lang_dir / "translation.toml"
|
||||
print(f"Processing {lang_dir.name}...")
|
||||
result = merger.remove_unused_translations(
|
||||
target_file, backup=args.backup
|
||||
)
|
||||
result = merger.remove_unused_translations(target_file, backup=args.backup)
|
||||
removed = result["removed_count"]
|
||||
total_removed += removed
|
||||
print(f"Removed {removed} unused translations")
|
||||
@@ -489,11 +443,7 @@ def main():
|
||||
sys.exit(1)
|
||||
lang_dir = Path(args.locales_dir) / args.language
|
||||
target_file = lang_dir / "translation.toml"
|
||||
output_file = (
|
||||
Path(args.output)
|
||||
if args.output
|
||||
else target_file.with_suffix(".untranslated.json")
|
||||
)
|
||||
output_file = Path(args.output) if args.output else target_file.with_suffix(".untranslated.json")
|
||||
untranslated = merger.extract_untranslated_entries(target_file, output_file)
|
||||
print(f"Extracted {len(untranslated)} untranslated entries to {output_file}")
|
||||
|
||||
@@ -512,22 +462,18 @@ def main():
|
||||
lang_dir = Path(args.locales_dir) / args.language
|
||||
target_file = lang_dir / "translation.toml"
|
||||
|
||||
with open(args.translations_file, "r", encoding="utf-8") as f:
|
||||
with open(args.translations_file, encoding="utf-8") as f:
|
||||
translations_data = json.load(f)
|
||||
|
||||
# Extract translations from template format or simple dict
|
||||
if "translations" in translations_data:
|
||||
translations = {
|
||||
k: v["translated"]
|
||||
for k, v in translations_data["translations"].items()
|
||||
if v.get("translated")
|
||||
k: v["translated"] for k, v in translations_data["translations"].items() if v.get("translated")
|
||||
}
|
||||
else:
|
||||
translations = translations_data
|
||||
|
||||
result = merger.apply_translations(
|
||||
target_file, translations, backup=args.backup
|
||||
)
|
||||
result = merger.apply_translations(target_file, translations, backup=args.backup)
|
||||
|
||||
if result["success"]:
|
||||
print(f"Applied {result['applied_count']} translations")
|
||||
|
||||
@@ -13,15 +13,14 @@ Usage:
|
||||
python scripts/translations/validate_json_structure.py [--language LANG]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Set
|
||||
import argparse
|
||||
import tomllib # Python 3.11+ (stdlib)
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_all_keys(d: dict, parent_key: str = "", sep: str = ".") -> Set[str]:
|
||||
def get_all_keys(d: dict, parent_key: str = "", sep: str = ".") -> set[str]:
|
||||
"""Get all keys from nested dict as dot-notation paths."""
|
||||
keys = set()
|
||||
for k, v in d.items():
|
||||
@@ -42,9 +41,7 @@ def validate_translation_file(file_path: Path) -> tuple[bool, str]:
|
||||
return False, f"Error reading file: {str(e)}"
|
||||
|
||||
|
||||
def validate_structure(
|
||||
en_us_keys: Set[str], lang_keys: Set[str], lang_code: str
|
||||
) -> Dict:
|
||||
def validate_structure(en_us_keys: set[str], lang_keys: set[str], lang_code: str) -> dict:
|
||||
"""Compare structure between en-US and target language."""
|
||||
missing_keys = en_us_keys - lang_keys
|
||||
extra_keys = lang_keys - en_us_keys
|
||||
@@ -60,7 +57,7 @@ def validate_structure(
|
||||
}
|
||||
|
||||
|
||||
def print_validation_result(result: Dict, verbose: bool = False):
|
||||
def print_validation_result(result: dict, verbose: bool = False):
|
||||
"""Print validation results in readable format."""
|
||||
lang = result["language"]
|
||||
|
||||
@@ -111,9 +108,7 @@ def main():
|
||||
help="Specific language code to validate (e.g., es-ES)",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verbose", "-v", action="store_true", help="Show all missing/extra keys"
|
||||
)
|
||||
parser.add_argument("--verbose", "-v", action="store_true", help="Show all missing/extra keys")
|
||||
parser.add_argument("--json", action="store_true", help="Output results as JSON")
|
||||
|
||||
args = parser.parse_args()
|
||||
@@ -162,9 +157,7 @@ def main():
|
||||
# First check if file is valid
|
||||
is_valid, message = validate_translation_file(lang_path)
|
||||
if not is_valid:
|
||||
json_errors.append(
|
||||
{"language": lang_code, "file": str(lang_path), "error": message}
|
||||
)
|
||||
json_errors.append({"language": lang_code, "file": str(lang_path), "error": message})
|
||||
continue
|
||||
|
||||
# Load and compare structure
|
||||
@@ -194,9 +187,7 @@ def main():
|
||||
print("\n📊 Structure Validation Summary:")
|
||||
print(f" Languages validated: {len(results)}")
|
||||
|
||||
perfect = sum(
|
||||
1 for r in results if r["missing_count"] == 0 and r["extra_count"] == 0
|
||||
)
|
||||
perfect = sum(1 for r in results if r["missing_count"] == 0 and r["extra_count"] == 0)
|
||||
print(f" Perfect matches: {perfect}/{len(results)}")
|
||||
|
||||
total_missing = sum(r["missing_count"] for r in results)
|
||||
@@ -211,9 +202,7 @@ def main():
|
||||
print("\n✅ All translations have perfect structure!")
|
||||
|
||||
# Exit with error code if issues found
|
||||
has_issues = len(json_errors) > 0 or any(
|
||||
r["missing_count"] > 0 or r["extra_count"] > 0 for r in results
|
||||
)
|
||||
has_issues = len(json_errors) > 0 or any(r["missing_count"] > 0 or r["extra_count"] > 0 for r in results)
|
||||
sys.exit(1 if has_issues else 0)
|
||||
|
||||
|
||||
|
||||
@@ -9,23 +9,22 @@ Usage:
|
||||
--fix: Automatically remove extra placeholders (use with caution)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Set
|
||||
import argparse
|
||||
import tomllib # Python 3.11+ (stdlib)
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_placeholders(text: str) -> Set[str]:
|
||||
def find_placeholders(text: str) -> set[str]:
|
||||
"""Find all placeholders in text like {n}, {{var}}, {0}, etc."""
|
||||
if not isinstance(text, str):
|
||||
return set()
|
||||
return set(re.findall(r"\{\{?[^}]+\}\}?", text))
|
||||
|
||||
|
||||
def flatten_dict(d: dict, parent_key: str = "", sep: str = ".") -> Dict[str, str]:
|
||||
def flatten_dict(d: dict, parent_key: str = "", sep: str = ".") -> dict[str, str]:
|
||||
"""Flatten nested dict to dot-notation keys."""
|
||||
items = []
|
||||
for k, v in d.items():
|
||||
@@ -37,9 +36,7 @@ def flatten_dict(d: dict, parent_key: str = "", sep: str = ".") -> Dict[str, str
|
||||
return dict(items)
|
||||
|
||||
|
||||
def validate_language(
|
||||
en_us_flat: Dict[str, str], lang_flat: Dict[str, str], lang_code: str
|
||||
) -> List[Dict]:
|
||||
def validate_language(en_us_flat: dict[str, str], lang_flat: dict[str, str], lang_code: str) -> list[dict]:
|
||||
"""Validate placeholders for a language against en-US."""
|
||||
issues = []
|
||||
|
||||
@@ -67,7 +64,7 @@ def validate_language(
|
||||
return issues
|
||||
|
||||
|
||||
def print_issues(issues: List[Dict], verbose: bool = False):
|
||||
def print_issues(issues: list[dict], verbose: bool = False):
|
||||
"""Print validation issues in a readable format."""
|
||||
if not issues:
|
||||
print("✅ No placeholder validation issues found!")
|
||||
@@ -93,9 +90,7 @@ def print_issues(issues: List[Dict], verbose: bool = False):
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate translation placeholder consistency"
|
||||
)
|
||||
parser = argparse.ArgumentParser(description="Validate translation placeholder consistency")
|
||||
parser.add_argument(
|
||||
"--language",
|
||||
help="Specific language code to validate (e.g., es-ES)",
|
||||
|
||||
+36
-53
@@ -19,9 +19,9 @@ import argparse
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from collections.abc import Iterable, Sequence
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, List, Optional, Sequence, Tuple
|
||||
|
||||
from fontTools.fontBuilder import FontBuilder
|
||||
from fontTools.misc.fixedTools import otRound
|
||||
@@ -29,17 +29,16 @@ from fontTools.pens.cu2quPen import Cu2QuPen
|
||||
from fontTools.pens.t2CharStringPen import T2CharStringPen
|
||||
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
||||
|
||||
|
||||
Command = Dict[str, object]
|
||||
Matrix = Tuple[float, float, float, float, float, float]
|
||||
Command = dict[str, object]
|
||||
Matrix = tuple[float, float, float, float, float, float]
|
||||
|
||||
|
||||
@dataclass
|
||||
class GlyphSource:
|
||||
name: str
|
||||
width: float
|
||||
unicode: Optional[int]
|
||||
char_code: Optional[int]
|
||||
unicode: int | None
|
||||
char_code: int | None
|
||||
outline: Sequence[Command]
|
||||
|
||||
|
||||
@@ -48,34 +47,20 @@ class GlyphBuildResult:
|
||||
name: str
|
||||
width: int
|
||||
charstring: object
|
||||
ttf_glyph: Optional[object]
|
||||
unicode: Optional[int]
|
||||
char_code: Optional[int]
|
||||
bounds: Optional[Tuple[float, float, float, float]]
|
||||
ttf_glyph: object | None
|
||||
unicode: int | None
|
||||
char_code: int | None
|
||||
bounds: tuple[float, float, float, float] | None
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Synthesize fonts from Type3 glyph JSON."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input", required=True, help="Path to glyph JSON emitted by the backend"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--otf-output", required=True, help="Destination path for the CFF/OTF font"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ttf-output", help="Optional destination path for a TrueType font"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--family-name", default="Type3 Synth", help="Family name for the output"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--style-name", default="Regular", help="Style name for the output"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--units-per-em", type=int, default=1000, help="Units per EM value"
|
||||
)
|
||||
parser = argparse.ArgumentParser(description="Synthesize fonts from Type3 glyph JSON.")
|
||||
parser.add_argument("--input", required=True, help="Path to glyph JSON emitted by the backend")
|
||||
parser.add_argument("--otf-output", required=True, help="Destination path for the CFF/OTF font")
|
||||
parser.add_argument("--ttf-output", help="Optional destination path for a TrueType font")
|
||||
parser.add_argument("--family-name", default="Type3 Synth", help="Family name for the output")
|
||||
parser.add_argument("--style-name", default="Regular", help="Style name for the output")
|
||||
parser.add_argument("--units-per-em", type=int, default=1000, help="Units per EM value")
|
||||
parser.add_argument(
|
||||
"--cu2qu-error",
|
||||
type=float,
|
||||
@@ -85,7 +70,7 @@ def parse_args() -> argparse.Namespace:
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_json(path: Path) -> Dict[str, object]:
|
||||
def load_json(path: Path) -> dict[str, object]:
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
@@ -94,7 +79,7 @@ def load_json(path: Path) -> Dict[str, object]:
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def parse_font_matrix(rows: Optional[Iterable[Iterable[float]]]) -> Matrix:
|
||||
def parse_font_matrix(rows: Iterable[Iterable[float]] | None) -> Matrix:
|
||||
"""
|
||||
Retrieve the raw 2×3 FontMatrix entries for diagnostics. Type3 glyph
|
||||
outlines in our extractor are emitted in their native coordinate system, so
|
||||
@@ -102,7 +87,7 @@ def parse_font_matrix(rows: Optional[Iterable[Iterable[float]]]) -> Matrix:
|
||||
"""
|
||||
if not rows:
|
||||
return (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
|
||||
values: List[List[float]] = []
|
||||
values: list[list[float]] = []
|
||||
for row in rows:
|
||||
try:
|
||||
values.append([float(col) for col in row])
|
||||
@@ -132,10 +117,10 @@ def resolve_width(raw_width: float, default: int) -> int:
|
||||
|
||||
|
||||
def quadratic_to_cubic(
|
||||
current: Tuple[float, float],
|
||||
ctrl: Tuple[float, float],
|
||||
end: Tuple[float, float],
|
||||
) -> Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float]]:
|
||||
current: tuple[float, float],
|
||||
ctrl: tuple[float, float],
|
||||
end: tuple[float, float],
|
||||
) -> tuple[tuple[float, float], tuple[float, float], tuple[float, float]]:
|
||||
"""
|
||||
Convert a quadratic Bézier segment to cubic control points.
|
||||
"""
|
||||
@@ -150,9 +135,9 @@ def quadratic_to_cubic(
|
||||
return c1, c2, end
|
||||
|
||||
|
||||
def iterate_glyphs(data: Dict[str, object]) -> List[GlyphSource]:
|
||||
def iterate_glyphs(data: dict[str, object]) -> list[GlyphSource]:
|
||||
glyph_records = data.get("glyphs") or []
|
||||
sources: List[GlyphSource] = []
|
||||
sources: list[GlyphSource] = []
|
||||
for index, record in enumerate(glyph_records, start=1):
|
||||
if not isinstance(record, dict):
|
||||
continue
|
||||
@@ -170,9 +155,7 @@ def iterate_glyphs(data: Dict[str, object]) -> List[GlyphSource]:
|
||||
char_code_value = record.get("code")
|
||||
if not isinstance(char_code_value, int):
|
||||
char_code_value = record.get("charCodeRaw")
|
||||
if not isinstance(char_code_value, int) or not (
|
||||
0 <= char_code_value <= 0x10FFFF
|
||||
):
|
||||
if not isinstance(char_code_value, int) or not (0 <= char_code_value <= 0x10FFFF):
|
||||
char_code_value = None
|
||||
outline = record.get("outline")
|
||||
if not isinstance(outline, list):
|
||||
@@ -192,19 +175,19 @@ def iterate_glyphs(data: Dict[str, object]) -> List[GlyphSource]:
|
||||
def build_cff_charstring(
|
||||
glyph: GlyphSource,
|
||||
width: int,
|
||||
) -> Tuple[object, Optional[Tuple[float, float, float, float]]]:
|
||||
) -> tuple[object, tuple[float, float, float, float] | None]:
|
||||
pen = T2CharStringPen(width=width, glyphSet=None)
|
||||
bounds = [math.inf, math.inf, -math.inf, -math.inf]
|
||||
|
||||
def update_bounds(point: Tuple[float, float]) -> None:
|
||||
def update_bounds(point: tuple[float, float]) -> None:
|
||||
x, y = point
|
||||
bounds[0] = min(bounds[0], x)
|
||||
bounds[1] = min(bounds[1], y)
|
||||
bounds[2] = max(bounds[2], x)
|
||||
bounds[3] = max(bounds[3], y)
|
||||
|
||||
current: Optional[Tuple[float, float]] = None
|
||||
start_point: Optional[Tuple[float, float]] = None
|
||||
current: tuple[float, float] | None = None
|
||||
start_point: tuple[float, float] | None = None
|
||||
open_path = False
|
||||
|
||||
for command in glyph.outline:
|
||||
@@ -278,7 +261,7 @@ def build_cff_charstring(
|
||||
return charstring, bbox
|
||||
|
||||
|
||||
def build_ttf_glyph(glyph: GlyphSource, max_error: float) -> Optional[object]:
|
||||
def build_ttf_glyph(glyph: GlyphSource, max_error: float) -> object | None:
|
||||
pen = TTGlyphPen(glyphSet=None)
|
||||
draw_pen = Cu2QuPen(pen, max_error, reverse_direction=False)
|
||||
|
||||
@@ -321,9 +304,9 @@ def build_ttf_glyph(glyph: GlyphSource, max_error: float) -> Optional[object]:
|
||||
|
||||
|
||||
def synthesise_fonts(
|
||||
data: Dict[str, object],
|
||||
data: dict[str, object],
|
||||
otf_output: Path,
|
||||
ttf_output: Optional[Path],
|
||||
ttf_output: Path | None,
|
||||
family_name: str,
|
||||
style_name: str,
|
||||
units_per_em: int,
|
||||
@@ -332,7 +315,7 @@ def synthesise_fonts(
|
||||
_font_matrix = parse_font_matrix(data.get("fontMatrix"))
|
||||
glyphs = iterate_glyphs(data)
|
||||
|
||||
results: List[GlyphBuildResult] = []
|
||||
results: list[GlyphBuildResult] = []
|
||||
global_y_min = math.inf
|
||||
global_y_max = -math.inf
|
||||
|
||||
@@ -377,7 +360,7 @@ def synthesise_fonts(
|
||||
horizontal_metrics = {result.name: (result.width, 0) for result in results}
|
||||
horizontal_metrics[".notdef"] = (default_width, 0)
|
||||
|
||||
cmap: Dict[int, str] = {}
|
||||
cmap: dict[int, str] = {}
|
||||
next_private = 0xF000
|
||||
for result in results:
|
||||
code_point = result.unicode
|
||||
@@ -433,7 +416,7 @@ def synthesise_fonts(
|
||||
if ttf_output is None:
|
||||
return
|
||||
|
||||
glyph_objects: Dict[str, object] = {}
|
||||
glyph_objects: dict[str, object] = {}
|
||||
empty_pen = TTGlyphPen(None)
|
||||
empty_pen.moveTo((0, 0))
|
||||
empty_pen.lineTo((0, 0))
|
||||
|
||||
@@ -17,25 +17,15 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_SIGNATURES = REPO_ROOT / "docs" / "type3" / "signatures"
|
||||
DEFAULT_INDEX = (
|
||||
REPO_ROOT
|
||||
/ "app"
|
||||
/ "core"
|
||||
/ "src"
|
||||
/ "main"
|
||||
/ "resources"
|
||||
/ "type3"
|
||||
/ "library"
|
||||
/ "index.json"
|
||||
)
|
||||
DEFAULT_INDEX = REPO_ROOT / "app" / "core" / "src" / "main" / "resources" / "type3" / "library" / "index.json"
|
||||
|
||||
|
||||
def normalize_alias(value: Optional[str]) -> Optional[str]:
|
||||
def normalize_alias(value: str | None) -> str | None:
|
||||
if not value:
|
||||
return None
|
||||
trimmed = value.strip()
|
||||
@@ -75,9 +65,9 @@ def iter_signature_fonts(signature_file: Path):
|
||||
}
|
||||
|
||||
|
||||
def make_alias_index(entries: List[Dict]) -> Tuple[Dict[str, Dict], Dict[str, Dict]]:
|
||||
alias_index: Dict[str, Dict] = {}
|
||||
signature_index: Dict[str, Dict] = {}
|
||||
def make_alias_index(entries: list[dict]) -> tuple[dict[str, dict], dict[str, dict]]:
|
||||
alias_index: dict[str, dict] = {}
|
||||
signature_index: dict[str, dict] = {}
|
||||
for entry in entries:
|
||||
for alias in entry.get("aliases", []) or []:
|
||||
normalized = normalize_alias(alias)
|
||||
@@ -91,7 +81,7 @@ def make_alias_index(entries: List[Dict]) -> Tuple[Dict[str, Dict], Dict[str, Di
|
||||
return alias_index, signature_index
|
||||
|
||||
|
||||
def ensure_list(container: Dict, key: str) -> List:
|
||||
def ensure_list(container: dict, key: str) -> list:
|
||||
value = container.get(key)
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
@@ -100,11 +90,11 @@ def ensure_list(container: Dict, key: str) -> List:
|
||||
return value
|
||||
|
||||
|
||||
def merge_sorted_unique(values: Iterable[int]) -> List[int]:
|
||||
def merge_sorted_unique(values: Iterable[int]) -> list[int]:
|
||||
return sorted({int(v) for v in values if isinstance(v, int)})
|
||||
|
||||
|
||||
def normalize_source_path(pdf_path: Optional[str]) -> Optional[str]:
|
||||
def normalize_source_path(pdf_path: str | None) -> str | None:
|
||||
if not pdf_path:
|
||||
return None
|
||||
try:
|
||||
@@ -117,13 +107,13 @@ def normalize_source_path(pdf_path: Optional[str]) -> Optional[str]:
|
||||
|
||||
def update_library(
|
||||
signatures_dir: Path, index_path: Path, apply_changes: bool
|
||||
) -> Tuple[int, int, List[Tuple[str, Path]]]:
|
||||
) -> tuple[int, int, list[tuple[str, Path]]]:
|
||||
entries = load_json(index_path)
|
||||
alias_index, signature_index = make_alias_index(entries)
|
||||
|
||||
modifications = 0
|
||||
updated_entries = set()
|
||||
unmatched: List[Tuple[str, Path]] = []
|
||||
unmatched: list[tuple[str, Path]] = []
|
||||
|
||||
signature_files = sorted(signatures_dir.glob("*.json"))
|
||||
if not signature_files:
|
||||
@@ -198,9 +188,7 @@ def update_library(
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Update Type3 library index using signature dumps."
|
||||
)
|
||||
parser = argparse.ArgumentParser(description="Update Type3 library index using signature dumps.")
|
||||
parser.add_argument(
|
||||
"--signatures-dir",
|
||||
type=Path,
|
||||
@@ -223,11 +211,7 @@ def parse_args() -> argparse.Namespace:
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
signatures_dir = (
|
||||
args.signatures_dir
|
||||
if args.signatures_dir.is_absolute()
|
||||
else (REPO_ROOT / args.signatures_dir)
|
||||
)
|
||||
signatures_dir = args.signatures_dir if args.signatures_dir.is_absolute() else (REPO_ROOT / args.signatures_dir)
|
||||
index_path = args.index if args.index.is_absolute() else (REPO_ROOT / args.index)
|
||||
|
||||
if not signatures_dir.exists():
|
||||
@@ -237,9 +221,7 @@ def main() -> None:
|
||||
print(f"Index file not found: {index_path}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
modifications, updated_entries, unmatched = update_library(
|
||||
signatures_dir, index_path, apply_changes=args.apply
|
||||
)
|
||||
modifications, updated_entries, unmatched = update_library(signatures_dir, index_path, apply_changes=args.apply)
|
||||
|
||||
mode = "APPLIED" if args.apply else "DRY-RUN"
|
||||
print(
|
||||
|
||||
@@ -25,12 +25,8 @@ elif [ "$SEED" = 1 ]; then
|
||||
fi
|
||||
|
||||
echo "==> Checking Python + behave..."
|
||||
PY="${PYTHON:-python}"
|
||||
command -v "$PY" >/dev/null || PY=python3
|
||||
if ! "$PY" -c "import behave" 2>/dev/null; then
|
||||
echo " installing test deps..."
|
||||
"$PY" -m pip install -q -r "$CUKE_DIR/requirements.txt" || {
|
||||
echo " could not install behave; install $CUKE_DIR/requirements.txt manually"; exit 1; }
|
||||
if ! uv run --project ../../engine --locked --group cucumber python -c "import behave" 2>/dev/null; then
|
||||
echo " could not load the central uv cucumber environment"; exit 1
|
||||
fi
|
||||
|
||||
REPORT_DIR="$(pwd)/multinode/regression-report"
|
||||
@@ -39,7 +35,7 @@ mkdir -p "$REPORT_DIR"
|
||||
run_behave() { # $1=tags $2=label
|
||||
echo "==> behave features/multinode --tags='$1' ($2)"
|
||||
# behave.ini excludes features/multinode by default; -e here overrides that while still excluding the licence-gated enterprise suite.
|
||||
( cd "$CUKE_DIR" && "$PY" -m behave features/multinode -e "features/enterprise" \
|
||||
( cd "$CUKE_DIR" && uv run --project ../../engine --locked --group cucumber python -m behave features/multinode -e "features/enterprise" \
|
||||
--tags="$1" --no-capture --format plain --format html --outfile "$REPORT_DIR/$2.html" )
|
||||
return $?
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
behave
|
||||
behave-html-formatter
|
||||
requests
|
||||
pypdf
|
||||
reportlab
|
||||
PyCryptodome
|
||||
qrcode[pil]
|
||||
@@ -1,305 +0,0 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.13
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --generate-hashes --output-file='testing\cucumber\requirements.txt' --strip-extras 'testing\cucumber\requirements.in'
|
||||
#
|
||||
behave==1.3.3 \
|
||||
--hash=sha256:2b8f4b64ed2ea756a5a2a73e23defc1c4631e9e724c499e46661778453ebaf51 \
|
||||
--hash=sha256:89bdb62af8fb9f147ce245736a5de69f025e5edfb66f1fbe16c5007493f842c0
|
||||
# via
|
||||
# -r testing/cucumber/requirements.in
|
||||
# behave-html-formatter
|
||||
behave-html-formatter==0.9.10 \
|
||||
--hash=sha256:c5a9ad3edcac7be5766b14aacce46794885c749c4741fc93f8fc3a4bf2a891aa \
|
||||
--hash=sha256:fff7ac2118463701423645ad5a12636845bfd6c8a2dd52097b524b4f290aa7c8
|
||||
# via -r testing/cucumber/requirements.in
|
||||
certifi==2026.6.17 \
|
||||
--hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \
|
||||
--hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db
|
||||
# via requests
|
||||
charset-normalizer==3.4.9 \
|
||||
--hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \
|
||||
--hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \
|
||||
--hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \
|
||||
--hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \
|
||||
--hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \
|
||||
--hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \
|
||||
--hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \
|
||||
--hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \
|
||||
--hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \
|
||||
--hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \
|
||||
--hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \
|
||||
--hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \
|
||||
--hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \
|
||||
--hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \
|
||||
--hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \
|
||||
--hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \
|
||||
--hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \
|
||||
--hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \
|
||||
--hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \
|
||||
--hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \
|
||||
--hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \
|
||||
--hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \
|
||||
--hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \
|
||||
--hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \
|
||||
--hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \
|
||||
--hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \
|
||||
--hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \
|
||||
--hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \
|
||||
--hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \
|
||||
--hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \
|
||||
--hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \
|
||||
--hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \
|
||||
--hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \
|
||||
--hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \
|
||||
--hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \
|
||||
--hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \
|
||||
--hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \
|
||||
--hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \
|
||||
--hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \
|
||||
--hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \
|
||||
--hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \
|
||||
--hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \
|
||||
--hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \
|
||||
--hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \
|
||||
--hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \
|
||||
--hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \
|
||||
--hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \
|
||||
--hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \
|
||||
--hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \
|
||||
--hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \
|
||||
--hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \
|
||||
--hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \
|
||||
--hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \
|
||||
--hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \
|
||||
--hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \
|
||||
--hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \
|
||||
--hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \
|
||||
--hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \
|
||||
--hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \
|
||||
--hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \
|
||||
--hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \
|
||||
--hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \
|
||||
--hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \
|
||||
--hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \
|
||||
--hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \
|
||||
--hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \
|
||||
--hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \
|
||||
--hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \
|
||||
--hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \
|
||||
--hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \
|
||||
--hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \
|
||||
--hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \
|
||||
--hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \
|
||||
--hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \
|
||||
--hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \
|
||||
--hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \
|
||||
--hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \
|
||||
--hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \
|
||||
--hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \
|
||||
--hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \
|
||||
--hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \
|
||||
--hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \
|
||||
--hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \
|
||||
--hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \
|
||||
--hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \
|
||||
--hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \
|
||||
--hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \
|
||||
--hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \
|
||||
--hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \
|
||||
--hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \
|
||||
--hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \
|
||||
--hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \
|
||||
--hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115
|
||||
# via
|
||||
# reportlab
|
||||
# requests
|
||||
colorama==0.4.6 \
|
||||
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
||||
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
||||
# via
|
||||
# behave
|
||||
# qrcode
|
||||
cucumber-expressions==20.0.0 \
|
||||
--hash=sha256:5cbd4012c66584aa82ada990a6e7cb131274796e132e905d27d95ae9a2ca0f48 \
|
||||
--hash=sha256:8a0434529efd7ca6e2052934ec8d677c7e24edc0fad3b1d1b1bc4bbca5e521f3
|
||||
# via behave
|
||||
cucumber-tag-expressions==10.0.0 \
|
||||
--hash=sha256:496f4a834e4e7ef9c1ae3f6f9c22f237a32f1916f96c32d8b8f900865f988f81 \
|
||||
--hash=sha256:b3ca4163660b247760031f88797ecde2c5384aac85f9d5908cb4750eddde89b7
|
||||
# via behave
|
||||
idna==3.18 \
|
||||
--hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \
|
||||
--hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848
|
||||
# via requests
|
||||
parse==1.22.1 \
|
||||
--hash=sha256:20f0925a46f06602485ac90d751764d0697fd8455aaa97489ba8953a4b66de32 \
|
||||
--hash=sha256:d3a4740ec3da338e2b258b2d69741b731eadfddca59e24a14bc4ee5fce38c911
|
||||
# via
|
||||
# behave
|
||||
# parse-type
|
||||
parse-type==0.6.6 \
|
||||
--hash=sha256:3ca79bbe71e170dfccc8ec6c341edfd1c2a0fc1e5cfd18330f93af938de2348c \
|
||||
--hash=sha256:513a3784104839770d690e04339a8b4d33439fcd5dd99f2e4580f9fc1097bfb2
|
||||
# via behave
|
||||
pillow==12.3.0 \
|
||||
--hash=sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756 \
|
||||
--hash=sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a \
|
||||
--hash=sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59 \
|
||||
--hash=sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45 \
|
||||
--hash=sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3 \
|
||||
--hash=sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df \
|
||||
--hash=sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139 \
|
||||
--hash=sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b \
|
||||
--hash=sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39 \
|
||||
--hash=sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e \
|
||||
--hash=sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8 \
|
||||
--hash=sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1 \
|
||||
--hash=sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8 \
|
||||
--hash=sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89 \
|
||||
--hash=sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5 \
|
||||
--hash=sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130 \
|
||||
--hash=sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd \
|
||||
--hash=sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d \
|
||||
--hash=sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b \
|
||||
--hash=sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed \
|
||||
--hash=sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace \
|
||||
--hash=sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb \
|
||||
--hash=sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931 \
|
||||
--hash=sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510 \
|
||||
--hash=sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6 \
|
||||
--hash=sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1 \
|
||||
--hash=sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce \
|
||||
--hash=sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385 \
|
||||
--hash=sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e \
|
||||
--hash=sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c \
|
||||
--hash=sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7 \
|
||||
--hash=sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace \
|
||||
--hash=sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c \
|
||||
--hash=sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f \
|
||||
--hash=sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64 \
|
||||
--hash=sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f \
|
||||
--hash=sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a \
|
||||
--hash=sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827 \
|
||||
--hash=sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17 \
|
||||
--hash=sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4 \
|
||||
--hash=sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a \
|
||||
--hash=sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701 \
|
||||
--hash=sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e \
|
||||
--hash=sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91 \
|
||||
--hash=sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66 \
|
||||
--hash=sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468 \
|
||||
--hash=sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217 \
|
||||
--hash=sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658 \
|
||||
--hash=sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418 \
|
||||
--hash=sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a \
|
||||
--hash=sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c \
|
||||
--hash=sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330 \
|
||||
--hash=sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402 \
|
||||
--hash=sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09 \
|
||||
--hash=sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930 \
|
||||
--hash=sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f \
|
||||
--hash=sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec \
|
||||
--hash=sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a \
|
||||
--hash=sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94 \
|
||||
--hash=sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468 \
|
||||
--hash=sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b \
|
||||
--hash=sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965 \
|
||||
--hash=sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8 \
|
||||
--hash=sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd \
|
||||
--hash=sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7 \
|
||||
--hash=sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c \
|
||||
--hash=sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777 \
|
||||
--hash=sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35 \
|
||||
--hash=sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9 \
|
||||
--hash=sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f \
|
||||
--hash=sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f \
|
||||
--hash=sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0 \
|
||||
--hash=sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c \
|
||||
--hash=sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71 \
|
||||
--hash=sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3 \
|
||||
--hash=sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838 \
|
||||
--hash=sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf \
|
||||
--hash=sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321 \
|
||||
--hash=sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26 \
|
||||
--hash=sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec \
|
||||
--hash=sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9 \
|
||||
--hash=sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65 \
|
||||
--hash=sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5 \
|
||||
--hash=sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e \
|
||||
--hash=sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d \
|
||||
--hash=sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198 \
|
||||
--hash=sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7
|
||||
# via
|
||||
# qrcode
|
||||
# reportlab
|
||||
pycryptodome==3.23.0 \
|
||||
--hash=sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4 \
|
||||
--hash=sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c \
|
||||
--hash=sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630 \
|
||||
--hash=sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f \
|
||||
--hash=sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27 \
|
||||
--hash=sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a \
|
||||
--hash=sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56 \
|
||||
--hash=sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef \
|
||||
--hash=sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5 \
|
||||
--hash=sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477 \
|
||||
--hash=sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886 \
|
||||
--hash=sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a \
|
||||
--hash=sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75 \
|
||||
--hash=sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720 \
|
||||
--hash=sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339 \
|
||||
--hash=sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625 \
|
||||
--hash=sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490 \
|
||||
--hash=sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8 \
|
||||
--hash=sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b \
|
||||
--hash=sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818 \
|
||||
--hash=sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a \
|
||||
--hash=sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002 \
|
||||
--hash=sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae \
|
||||
--hash=sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7 \
|
||||
--hash=sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d \
|
||||
--hash=sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265 \
|
||||
--hash=sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39 \
|
||||
--hash=sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566 \
|
||||
--hash=sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353 \
|
||||
--hash=sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b \
|
||||
--hash=sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4 \
|
||||
--hash=sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2 \
|
||||
--hash=sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575 \
|
||||
--hash=sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6 \
|
||||
--hash=sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843 \
|
||||
--hash=sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4 \
|
||||
--hash=sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446 \
|
||||
--hash=sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379 \
|
||||
--hash=sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa \
|
||||
--hash=sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be \
|
||||
--hash=sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7
|
||||
# via -r testing/cucumber/requirements.in
|
||||
pypdf==6.14.2 \
|
||||
--hash=sha256:3f07891af76dc002657e04993ab9b4de81de29f9013b9761d0b7968bff12e946 \
|
||||
--hash=sha256:7873f502fe4385e79539b21d872392dc0c4e3714327c15881cbc7fbfd1f95b25
|
||||
# via -r testing/cucumber/requirements.in
|
||||
qrcode==8.2 \
|
||||
--hash=sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f \
|
||||
--hash=sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c
|
||||
# via -r testing/cucumber/requirements.in
|
||||
reportlab==5.0.0 \
|
||||
--hash=sha256:9d5a3affa84919e1111ede580031266a570e93b1ce388219621347965ff1d93c \
|
||||
--hash=sha256:e4494a0c6623ae213bb856fba523171b2b54a7bf629fda02d5e525a7b899a784
|
||||
# via -r testing/cucumber/requirements.in
|
||||
requests==2.34.2 \
|
||||
--hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
|
||||
--hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
|
||||
# via -r testing/cucumber/requirements.in
|
||||
six==1.17.0 \
|
||||
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
|
||||
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
|
||||
# via
|
||||
# behave
|
||||
# parse-type
|
||||
urllib3==2.7.0 \
|
||||
--hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
|
||||
--hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
|
||||
# via requests
|
||||
+1
-1
@@ -920,7 +920,7 @@ main() {
|
||||
export TEST_REPORT_DIR="$REPORT_DIR"
|
||||
|
||||
gha_group "Test: Behave regression tests"
|
||||
if python -m behave \
|
||||
if uv run --project ../../engine --locked --group cucumber python -m behave \
|
||||
-f behave_html_formatter:HTMLFormatter -o "$CUCUMBER_REPORT" \
|
||||
-f pretty \
|
||||
--junit --junit-directory "$CUCUMBER_JUNIT_DIR"; then
|
||||
|
||||
Reference in New Issue
Block a user